diff --git a/CHANGELOG.md b/CHANGELOG.md index c8da9857..66bb27b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,596 @@ *** +## [v14.99.0] - 2026-05-22 + +### 新增 — 主题令牌审计工具增强 + +**Feature: 主题令牌自动审计引擎** +- 🆕 `theme_audit.dart` 重构为可编程审计引擎 `ThemeAuditEngine` + - `AuditCategory` 枚举 — 违规类别 (color/spacing/radius/fontSize) + - `AuditViolation` 数据类 — 记录 filePath/lineNumber/category/currentCode/suggestion + - `AuditSummary` 汇总类 — 按类别统计违规数/涉及文件数/总数 + - `ThemeAuditEngine` 审计引擎 — 扫描 lib/ 下所有 .dart 文件 + - `runAudit()` 方法 — 执行全量审计,返回 AuditSummary + - 颜色检测: `Color(0x...)` / `CupertinoColors.xxx` / `Colors.xxx` / `'#FF0000'` 十六进制字符串 + - 间距检测: `SizedBox(height: N)` / `EdgeInsets.all(N)` / `EdgeInsets.symmetric(...)` 非令牌值 + - 圆角检测: `BorderRadius.circular(N)` / `Radius.circular(N)` 非令牌值 + - 字号检测: `fontSize: N` 非令牌值 + - 白名单排除: `lib/core/theme/` / `lib/l10n/` / `*.g.dart` / `*.freezed.dart` / 主题定义文件 + - 智能建议: 自动匹配最近令牌值,输出修复建议 +- 🆕 `scripts/theme_audit.dart` CLI 审计脚本 + - `dart run scripts/theme_audit.dart` — 运行全量审计 + - `--category=color|spacing|radius|fontSize` — 按类别过滤 + - `--fix-hints` — 显示修复建议 + - `--json` — JSON 格式输出 (CI 集成) + - `--help` — 帮助信息 + - 退出码: 0=无违规, 1=有违规 (CI 集成) +- ✏️ `theme_audit.dart` — 原有文档注释保留,新增审计引擎代码 + +**举一反三**: +- 审计引擎可在 Flutter 应用内调用 `ThemeAuditEngine().runAudit()` 展示审计结果 +- CLI 脚本可集成到 CI/CD 流水线,确保新代码不引入硬编码令牌 +- 后续可扩展 `AuditCategory` 添加更多检测维度 (如 fontWeight/shadow/animation duration) + +*** + +## [v14.98.0] - 2026-05-22 + +### 新增 — 自定义页面转场动画 + 设备信息缓存刷新 + 字体下载服务提取 + +**Feature 1: 自定义页面转场动画** +- 🆕 `app_page_transitions.dart` 自定义PageRouteBuilder转场动画 + - `AppSlideTransition` — iOS风格从右滑入+轻微淡入,350ms,Curves.easeOutCubic + - `AppFadeScaleTransition` — 淡入+缩放(0.95→1.0),300ms,Curves.easeOut,适用于模态/Sheet页面 + - `instantTransition` — 减少动画模式下的即时转场(Duration.zero) + - `resolveTransition(ref)` — 根据generalSettingsProvider的reduceAnimations和pageTransitionMode自动选择转场 + - `resolveTransitionFromContext(context)` — 无WidgetRef时从ProviderScope.containerOf读取设置 +- ✏️ `app_nav_extension.dart` — `appPush` 新增可选 `transitionMode` 参数,鸿蒙端使用自定义转场 +- ✏️ `ohos_nav_bridge.dart` — `push`/`replace` 从CupertinoPageRoute改为自定义转场 + - `push` 新增 `transitionMode` 参数,支持调用方指定转场类型 + - `replace` 使用 `resolveTransitionFromContext` 自动选择转场 + - 新增 `general_settings_provider.dart` 和 `app_page_transitions.dart` 导入 + +**Feature 2: 设备信息缓存刷新** +- 🆕 `device_info_service.dart` 新增 `refresh()` 方法 + - 清除缓存 → 重新读取平台设备信息 → 更新缓存 → 如果之前已注册则重新注册到服务端 + - 解决 `initCache()` 使用 `??=` 只初始化一次的问题 +- 🆕 `device_info_service.dart` 新增 `clearCache()` 方法 + - 仅清除 `_cachedDeviceModel` 和 `_cachedDeviceName`,不重新注册 + - 适用于设备信息变更但不需要立即注册的场景 + +**Feature 3: 字体下载服务提取** +- 🆕 `font_download_service.dart` 独立字体下载服务 + - `FontDownloadService.downloadFont()` — 从URL下载字体(含重试+降级+进度回调) + - `FontDownloadService.downloadFontFromUrl()` — URL方式下载(含重试+验证) + - `FontDownloadService.importLocalFonts()` — 本地文件导入(含格式验证) + - `FontDownloadService.loadFontIntoEngine()` — 字体加载到Flutter引擎 + - `FontDownloadService.isValidFontFile()` — TTF/OTF/TTC文件头验证(公开方法) + - `FontDownloadService.getFontDirectory()` — 获取字体目录(公开方法) + - `FontDownloadResult` — 下载结果数据类(success/savePath/fileSize/errorMsg/fontFamily/displayName) + - `ProgressCallback` — 进度回调类型定义 +- ✏️ `font_management_notifier.dart` — 下载逻辑委托给FontDownloadService + - `downloadFont()` → 调用 `FontDownloadService.downloadFont()` + 进度回调更新状态 + - `downloadFontFromUrl()` → 调用 `FontDownloadService.downloadFontFromUrl()` + 进度回调 + - `importFont()` → 调用 `FontDownloadService.importLocalFonts()` + 加载引擎 + - `_loadDynamicFonts()` → 调用 `FontDownloadService.loadFontIntoEngine()` + - `_getFontDirectory()` → 委托 `FontDownloadService.getFontDirectory()` + - 移除 `_dio`、`_isValidFontFile`、`_loadFontIntoEngine` 私有方法(已提取到Service) + - Notifier仅管理状态,不直接处理下载/验证/文件IO逻辑 + +**举一反三**: +- 自定义转场可在 `appPush` 调用时通过 `transitionMode` 参数覆盖默认设置,如:`context.appPush('/settings', transitionMode: PageTransitionMode.sheet)` +- `DeviceInfoService.refresh()` 可在设备信息变更后调用(如系统更新、设备重命名),确保服务端设备信息同步 +- `FontDownloadService` 为纯静态服务类,后续可扩展为实例类+依赖注入,便于单元测试mock + +*** + +## [v14.97.0] - 2026-05-22 + +### 新增 — 工具中心弹性回弹 + BottomSheet 手势冲突修复 + +**Feature 1: 工具中心弹性回弹效果** +- 🆕 `home_tool_center.dart` 顶部滑入动画改用 `SpringSimulation` 弹簧物理 + - 参数:`SpringDescription(mass: 1, stiffness: 400, damping: 15)`,阻尼比 0.375(欠阻尼),约 5% 过冲后回弹 + - 展开动画:`_animationController.animateWith(SpringSimulation(...))`,自然弹性回弹 + - 收起动画:`_animationController.animateTo(0, curve: Curves.easeInCubic)`,平滑减速无回弹 + - `transitionDuration` 从 400ms 调整为 600ms 适配弹簧动画时长 + - 移除 `_slideAnimation` CurvedAnimation,直接使用 `_animationController` 驱动 SlideTransition + - `_backdropAnimation` 保持 `CurvedAnimation(Curves.easeOut)`,由 CurvedAnimation 内部 clamp 保证不超调 +- 🆕 新增 `import 'package:flutter/physics.dart'` + +**Feature 2: BottomSheet ScrollView 嵌套手势冲突修复** +- 🆕 `bottom_sheet.dart` 新增 `ScrollAwareSheetContent` 组件 + - `NotificationListener` 跟踪内部滚动位置 + - `NotificationListener` 非顶部时禁用 overscroll 指示器 + - `NotificationListener` 非顶部时消费向下 overscroll 通知,防止误触关闭面板 + - 配合 `stupid_simple_sheet` 的 `ScrollDragDetector`(`onlyDragWhenScrollWasAtTop: true`)双重保护 +- 🆕 `AppBottomSheet.showCustom` 新增 `scrollAware` 参数(默认 false) +- 🆕 `AppBottomSheet.showHalf` 新增 `scrollAware` 参数(默认 false) +- 使用方式:`AppBottomSheet.showCustom(context: ctx, builder: ..., scrollAware: true)` + +**举一反三**: +- 后续新增含 ListView/ScrollView 的 BottomSheet 内容时,应设置 `scrollAware: true` +- 弹簧参数可根据不同场景调整:stiffness 越大回弹越快,damping 越大回弹越小 +- `SpringSimulation` 也可应用于其他面板/弹窗的入场动画,替代 `Curves.easeOut*` + +*** + +## [v14.96.0] - 2026-05-22 + +### 新增 — 画布样式编辑器三大交互功能 + +**文件**: `lib/editor/widgets/panels/canvas_style_sheet.dart` + +**Feature 1: 样式预设模板** +- 🆕 顶部新增「样式预设」横向滚动区域 +- 提供 5 种预设风格:🃏卡片风 / ✨极简风 / 🧊毛玻璃风 / 📜复古风 / 💡霓虹风 +- 每个预设芯片含迷你预览卡片 + emoji名称 +- 点击预设一键应用全部样式值 +- 当前激活预设显示 ✓ 勾选标记 +- `_StylePreset` 数据模型 + `_buildPresets()` 工厂函数 + `_matchesPreset()` 匹配逻辑 + +**Feature 2: 拖拽调整圆角** +- 🆕 圆角区域新增「拖拽手柄」CupertinoSwitch 开关 +- 开启后预览卡片四角显示 12×12 半透明圆形手柄 +- 拖拽手柄向内增大圆角、向外减小圆角 +- 四角联动,拖拽任一手柄同步更新所有角 +- `_CornerHandle` 组件:基于对角线投影计算半径变化量 +- 手柄视觉:accent色60%透明 + 白色1.5px描边 + 阴影 + +**Feature 3: 取色器(Eyedropper)** +- 🆕 边框颜色行新增 👁 取色按钮 +- 点击进入取色模式,Header 切换为「取色中」状态标签 +- 预览区覆盖半透明遮罩 + 「👆 点击取色」提示 +- 触摸/滑动实时采样像素颜色,显示放大镜效果(圆形色块 + 十字准星 + HEX值) +- 释放手指或点击确认取色,自动应用并退出取色模式 +- `_CapturedImageData` 缓存 + `RepaintBoundary.toImage()` + RGBA字节采样 +- `_EyedropperOverlayPainter` 自定义画笔绘制放大镜 + +**设计规范**: +- 全部使用 `AppTheme.ext(context)` 主题令牌,支持动态主题 +- iOS Cupertino 风格组件(CupertinoSwitch / CupertinoIcons) +- 遵循 AppSpacing / AppRadius / AppTypography 设计系统 + +*** + +## [v14.95.0] - 2026-05-22 + +### 重构 — my_devices_page.dart 拆分子组件 + +**问题**:`my_devices_page.dart` 超过 1170 行,远超 1000 行限制,维护困难。 + +**拆分方案**: +- 🆕 `devices/device_utils.dart` — 工具函数(平台图标/颜色/名称/认证/剪贴板/确认弹窗) +- 🆕 `devices/device_overview_card.dart` — 设备概览卡片 + 文件传输入口卡片 +- 🆕 `devices/device_card.dart` — 设备卡片 + 分区标题头组件 +- 🆕 `devices/device_detail_sheet.dart` — 设备详情底部弹窗 + 信息行组件 +- ✏️ `my_devices_page.dart` — 主页面精简至 ~400 行,导入并使用子组件 + +**组件接口**: +- `DeviceOverviewCard(state)` — 接收 DeviceState 显示概览 +- `FileTransferEntry(onTap)` — 文件传输入口 +- `DeviceCard(device, isOnline, onTap, onRename)` — 单个设备卡片 +- `DeviceSectionHeader(title, count, isOnline)` — 分区标题 +- `showDeviceDetailSheet(context, device, isOnline, isCurrent, onRename, onOffline, onRemove)` — 详情弹窗 +- `DeviceInfoRow(icon, label, value, trailing)` — 信息行 +- 工具函数:`getPlatformIcon/Color/Emoji/Label`, `getDisplayDeviceName`, `getFriendlyModel`, `authenticate`, `copyToClipboard`, `showConfirmDialog` + +*** + +## [v14.94.0] - 2026-05-22 + +### 重构 — 合并 NotificationScheduler + DailyNotifyService → NotificationCenter + +**问题**:`NotificationScheduler` 和 `DailyNotifyService` 存在功能重叠,两者都管理每日通知调度,且 ID 命名空间可能冲突(DailyNotifyService 使用 ID 2000,与 NotificationScheduler 的节气 ID 2001 相邻)。 + +**合并方案**: +- 🆕 新增 `NotificationCenter`(`lib/core/services/notification/notification_center.dart`),统一管理所有本地通知调度 +- 🔴 `NotificationScheduler` 标记 `@Deprecated`,保留文件但不再使用 +- 🔴 `DailyNotifyService` 标记 `@Deprecated`,保留文件但不再使用 + +**NotificationCenter 统一管理**: +- 全局通知开关 `setNotificationsEnabled` / `isNotificationsEnabled` +- 每日推荐 `setDailyRecommendEnabled` / `setDailyRecommendTime` +- 签到提醒 `setSigninReminderEnabled` / `setSigninReminderTime` +- 节气通知 `setSolarTermEnabled` +- 每日运势 `setFortuneEnabled` / `setFortuneTime` +- 学习进度 `setStudyProgressEnabled` +- 稍后读 `isReadlaterEnabled` / `setReadlaterEnabled` +- 统一 ID 命名空间:1001(推荐) 1002(签到) 1003(运势) 1004(学习) 2001(节气) +- `configureAll()` — 一键调度所有已启用通知 +- `cancelAllManaged()` — 取消所有受管理的通知 + +**消费者更新**: +- `general_settings_provider.dart` — 替换 `NotificationScheduler`/`DailyNotifyService` → `NotificationCenter` +- `notification_settings_page.dart` — 替换所有 `NotificationScheduler` 调用 → `NotificationCenter` +- `local_notification_service.dart` — 替换 `NotificationScheduler.isNotificationsEnabled` → `NotificationCenter.isNotificationsEnabled` + +**举一反三**: +- 新增通知类型时只需在 `NotificationCenter` 中添加 ID 常量、存储键、getter/setter 和 `_configureXxx` 方法 +- ID 命名空间统一管理,避免不同服务间的 ID 冲突 +- 所有通知调度入口统一为 `configureAll()`,确保状态一致性 + +*** + +## [v14.93.0] - 2026-05-22 + +### 安全 — 移除3个高敏感权限,降低应用商店审核风险 + +**问题**:`AppPermission` 枚举包含3个高敏感/受限权限,可能导致应用商店审核被拒: +- `systemAlertWindow`(悬浮窗)— 大多数应用商店拒绝请求此权限的应用 +- `contacts`(通讯录)— 高度敏感,非核心功能不需要 +- `ignoreBatteryOptimization`(忽略电池优化)— 受限权限,Google Play要求特殊理由 + +**移除**: +- 🔴 `systemAlertWindow` — 移除枚举值及 `isPlatformRelevant` 判断 +- 🔴 `contacts` — 移除枚举值 +- 🔴 `ignoreBatteryOptimization` — 移除枚举值及 `isPlatformRelevant` 判断 + +**平台配置检查**(均无需修改): +- ✅ AndroidManifest.xml — 未声明 SYSTEM_ALERT_WINDOW / READ_CONTACTS / REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +- ✅ Info.plist — 未声明 NSContactsUsageDescription +- ✅ macOS entitlements — 未声明通讯录权限 +- ✅ module.json5 — 未声明通讯录权限 + +**剩余权限审查**(10项,均合理保留): +- camera / photos / notification — 必要权限,核心功能 +- location — 已使用粗略位置(城市级),最小化方案 +- bluetooth / nearbyDevices — 文件传输核心功能 +- microphone — 仅语音功能使用时请求,已正确门控 +- storage — 仅 Android SDK≤32 需要,已正确门控 +- network / clipboard — 虚拟权限,仅信息展示 + +**举一反三**: +- 新增权限时需评估应用商店审核风险,高敏感权限应避免或提供替代方案 +- 悬浮窗功能可改用应用内浮层(Overlay)替代系统悬浮窗 +- 通讯录功能可改用邀请码/链接分享替代直接读取通讯录 +- 电池优化可改用 WorkManager 周期任务替代前台服务+电池优化豁免 + +*** + +## [v14.92.0] - 2026-05-22 + +### 修复 — 引导页空壳功能实现 + PC端宽屏布局溢出 + +**Issue 1: 引导页空壳功能实现** + +- `welcome_page.dart`: 新增"扫一扫"功能卡片;所有功能卡片添加点击提示(AppToast),扫一扫提示"引导完成后可在首页工具中心使用扫一扫" +- `agreement_page.dart`: "权限说明"Tab从纯文本改为权限列表展示,使用`AppPermission`枚举显示图标、名称、描述、必要/系统级标签 +- `personalization_page.dart`: 功能开关同步`generalSettingsProvider`持久化 + - "摇一摇换句" → `setShakeToSwitch()` + - "特效背景" → 新增开关 + `setShaderBackground()` + - "音效反馈" → `setSfxEnabled()` +- `personalization_page.dart`: 字体大小滑块同步更新`generalSettingsProvider.fontScaleId` +- `agreement_page.dart`: "跳过引导"按钮改为`async`,先调用`completeOnboarding()`保存状态再导航 +- `onboarding_provider.dart`: 新增`shaderBackground`字段;`completeOnboarding()`同步设置到`generalSettingsProvider` +- `onboarding_constants.dart`: `coreFeatures`新增"扫一扫"条目 + +**Issue 2: PC端宽屏布局溢出修复** + +- `onboarding_page.dart`: 使用`LayoutBuilder`检测屏幕宽度;>900px时左右分栏布局(左侧角色+品牌,右侧PageView内容500px宽);≤900px保持原有纵向布局 +- `welcome_page.dart`: `Center`+`ConstrainedBox(maxWidth:600)`约束内容宽度;宽屏时功能卡片2列`Wrap`布局 +- `agreement_page.dart`: `Center`+`ConstrainedBox(maxWidth:600)`约束内容宽度 +- `personalization_page.dart`: `Center`+`ConstrainedBox(maxWidth:600)`约束内容宽度;预览卡片`maxWidth:400`居中 +- 所有子页面移除重复`SafeArea`(父级`onboarding_page.dart`已有) + +**举一反三**: +- 宽屏布局断点统一为900px,后续新增引导页也需遵循此约束 +- 功能开关需同时更新`onboardingProvider`(UI状态)和`generalSettingsProvider`(持久化),避免引导完成后设置丢失 +- 权限列表使用`AppPermission.isPlatformRelevant`过滤,自动适配不同平台 + +*** + +## [v14.91.0] - 2026-05-22 + +### 修复 — 字体大小导航 + 设备识别增强 + +**Issue 1: 字体大小按钮无响应** +- `general_settings_page.dart`: `_onNavigate` 方法新增 `font_scale` case,导航到字体管理页面 (`AppRoutes.fontManagement`) + +**Issue 2: 设备识别显示"unknown"** +- `device_info_service.dart` 大幅增强: + - 新增品牌中文名映射表 (`_brandNameMap`):华为/荣耀/小米/红米/OPPO/vivo/一加/真我/魅族/三星/中兴/联想/摩托罗拉等30+品牌 + - 新增热门型号友好名称映射表 (`_modelNameMap`):华为Mate/P系列、小米14/13/12系列、OPPO Find/Reno系列、vivo X/S系列、三星Galaxy S/Note/Z系列等100+型号 + - 新增iOS设备型号映射表 (`_iosModelMap`):iPhone 8~16 Pro Max、iPad Pro/Air/mini等40+型号 + - `getDeviceModel()` 改进:优先使用友好名称,回退到品牌中文名+型号代码,不再返回"HarmonyOS Device"或"unknown" + - `getDeviceName()` 改进:优先使用友好名称,回退到品牌中文名,不再返回"Unknown" + - `getDeviceId()` 改进:失败时生成稳定UUID并缓存到SharedPreferences,避免每次启动生成不同ID导致重复注册 + - 新增公开方法 `getBrandChineseName()`、`getModelFriendlyName()` 供其他模块调用 + +**Issue 2 续: 我的设备页面展示优化** +- `my_devices_page.dart` 改进: + - `_getPlatformIcon()`: macOS/Linux使用 `desktopcomputer`,区分桌面平台 + - 新增 `_getPlatformEmoji()`: 鸿蒙设备显示"鸿蒙"标签,Android显示🤖 + - 新增 `_getDisplayDeviceName()`: 优先使用友好型号名称,回退到deviceName + - 新增 `_getFriendlyModel()`: 详情Sheet中设备型号显示友好名称 + - `_platformLabel()`: 鸿蒙平台显示"HarmonyOS 🪶" + - 设备卡片和详情Sheet统一使用友好名称展示 + +**举一反三**: +- 新增手机型号时需同步更新 `_modelNameMap`,建议定期补充新款机型 +- iOS型号映射需跟随苹果新品发布更新 +- 品牌映射表可扩展至海外品牌(如ASUS ROG、Nothing等已包含) + +*** + +## [v14.90.0] - 2026-05-22 + +### 新增 — 权限管理页面补全7项基础权限 + +**问题**:`AppPermission` 枚举仅有6个权限(相机/相册/通知/位置/蓝牙/附近设备),缺少麦克风、存储、电池优化、悬浮窗、通讯录等基础权限,也未展示网络和剪贴板等系统级能力。 + +**新增权限**: +- 🎤 `microphone` — 麦克风:语音朗读、语音搜索、AI对话语音输入 +- 📁 `storage` — 存储空间:保存卡片、字体下载、数据导出(Android 12及以下) +- 🔋 `ignoreBatteryOptimization` — 忽略电池优化:确保通知准时推送、后台同步(Android only) +- 🪟 `systemAlertWindow` — 悬浮窗:番茄钟/阅读计时悬浮窗(Android only) +- 👥 `contacts` — 通讯录:好友发现、邀请好友 +- 🌐 `network` — 网络连接(虚拟权限):展示网络连接状态 +- 📋 `clipboard` — 剪贴板(虚拟权限):展示剪贴板访问状态 + +**架构变更**: +- `AppPermission` 枚举新增 `isVirtual` 布尔字段,区分真实权限和虚拟权限 +- 虚拟权限使用 `Permission.notification` 作为占位符(不实际请求) +- `PermissionService.checkStatus()` 对虚拟权限直接返回 `granted` +- `PermissionService.checkAllStatus()` 过滤平台不相关权限(iOS不显示storage/batteryOptimization/systemAlertWindow) +- `AppPermission.isPlatformRelevant` getter:storage仅Android SDK≤32显示,batteryOptimization/systemAlertWindow仅Android显示 +- `PermissionService.requestPermission()` 虚拟权限直接返回 `true`,不发起请求 +- `PermissionService.requestMicrophone()` 快捷方法 + +**UI变更**: +- 权限管理页面分为两个区域:「📱 应用权限」和「⚙️ 系统级能力」 +- 虚拟权限显示紫色「系统级」标签(含齿轮图标),替代「必要/可选」标签 +- 虚拟权限右侧显示灰色信息图标(不可点击),替代「请求/去设置」按钮 +- `_buildCategoryBadge()` 方法统一处理必要/可选/系统级三种标签 + +**举一反三**: +- 新增权限时需考虑平台差异:Android特有权限需在 `isPlatformRelevant` 中过滤 +- 虚拟权限模式可扩展至其他系统级能力(如NFC、生物识别等) + +*** + +## [v14.89.0] - 2026-05-22 + +### 重构 — 统一状态栏样式管理 + +**问题**: +1. 状态栏样式在多处分散设置(main.dart/app.dart/app_shell.dart/ohos_app_shell.dart/editor_system_ui.dart/pro_editor_bridge.dart),导致不同页面/设备状态栏图标颜色不一致 +2. main.dart 硬编码 `Brightness.dark` 状态栏图标,不响应主题切换,深色模式下图标不可见 +3. app_shell.dart 和 ohos_app_shell.dart 各自设置 `AnnotatedRegion`,覆盖 app 层设置 +4. 沉浸式状态栏开关(display_settings_provider)仅存储布尔值,不调用 `SystemChrome.setEnabledSystemUIMode`,开关无实际效果 +5. 编辑器退出时 `_applyDefaultOverlayStyle` 硬编码深色图标,与当前主题不匹配 + +**修复**: +- 新增 `lib/core/services/ui/status_bar_service.dart`: + - `StatusBarService` 集中管理类:`resolveStyle(isDark)` 统一生成 `SystemUiOverlayStyle`,`applyStyle(isDark)` 直接应用,`setImmersive(bool)` 切换沉浸式/edgeToEdge 模式 + - `StatusBarStyleRegion` Widget:封装 `AnnotatedRegion`,供 app 层使用 +- `app.dart`: + - 两处 `AnnotatedRegion` 替换为 `StatusBarStyleRegion(isDark: settings.isDark)` + - 移除 `flutter/services.dart` 导入 +- `app_shell.dart`: + - 移除 `AnnotatedRegion` 包裹,让 app 层统一管理 + - 移除 `flutter/services.dart` 导入 +- `ohos_app_shell.dart`: + - 同上,移除 `AnnotatedRegion` 包裹和 `flutter/services.dart` 导入 +- `main.dart`: + - 移除 `SystemChrome.setSystemUIOverlayStyle()` 硬编码调用,仅保留 `setEnabledSystemUIMode(edgeToEdge)` +- `editor_system_ui.dart`: + - `enterEditor`/`exitEditor`/`updateForTheme` 改用 `StatusBarService.applyStyle()` 和 `StatusBarService.enterEdgeToEdge()` + - 移除 `_applyOverlayStyle`/`_applyDefaultOverlayStyle` 私有方法 +- `pro_editor_bridge.dart`: + - `MainEditorStyle.uiOverlayStyle` 改用 `StatusBarService.resolveStyle(isDark:)` + - 移除 `flutter/services.dart` 导入 +- `display_settings_provider.dart` + `general_settings_provider.dart`: + - `setImmersiveStatusBar` 新增 `StatusBarService.setImmersive(v)` 调用,开关实际生效 + +**举一反三**: +- 所有 `SystemUiOverlayStyle` 使用现在集中在 `StatusBarService`,后续修改状态栏行为只需改一处 +- `StatusBarStyleRegion` 作为 Widget 可响应主题变化,确保状态栏图标颜色始终与当前主题匹配 +- 沉浸式模式使用 `SystemUiMode.immersiveSticky`(用户滑出后自动隐藏),退出沉浸式恢复 `edgeToEdge` + +*** + +## [v14.88.0] - 2026-05-22 + +### 新增 — 桌面端返回按钮 + 键盘快捷键 + +**功能**: +1. 桌面端(Windows/macOS/Linux) CupertinoNavigationBar 自动显示返回按钮 +2. 键盘快捷键返回:Alt+Left(Windows/Linux) / Cmd+Left(macOS) +3. 移动端(Android/iOS/鸿蒙)不显示返回按钮,依赖系统手势 + +**实现**: +- `adaptive_back_button.dart`: + - 检测 `pu.isDesktop`,桌面端且可 pop 时显示 `CupertinoIcons.chevron_left` 返回按钮 + - 移动端返回 `SizedBox.shrink()`,不影响系统手势返回 + - 使用 `AppTheme.ext(context).accent` 主题色 +- `keyboard_back_handler.dart`: + - 使用 `Shortcuts` + `Actions` 注册 Alt+Left / Cmd+Left 快捷键 + - 通过 `rootNavigatorKey.currentState` 访问 Navigator,无需在 Navigator 子树内 + - 仅桌面端激活 +- `app.dart`: + - 在 `ScreenUtilInit` builder 外层包裹 `KeyboardBackHandler` +- 79个页面文件: + - 所有无 `leading:` 的 CupertinoNavigationBar 添加 `leading: const AdaptiveBackButton()` + - 已有 `leading:` 的页面不重复添加(如 crash_log_page 选择模式切换) + +**举一反三**: +- 桌面端 UX 需额外考虑键盘快捷键和鼠标交互,后续可扩展更多快捷键 +- `AdaptiveBackButton` 模式可推广到其他平台差异化 UI 组件 + +*** + +## [v14.87.0] - 2026-05-22 + +### 修复 — 引导页在鸿蒙端不显示 + +**问题**: +1. 鸿蒙端使用 `MaterialApp(home: OhosAppShell())` 而非 `MaterialApp.router(routerConfig: appRouter)`,GoRouter 的 `_resolveInitialLocation()` 引导页判断逻辑从未执行 +2. `OhosAppShell` 直接渲染 Tab 页面,完全跳过引导页检查 +3. `OhosNavBridge._routes` 路由注册表缺少 `/onboarding` 路由 +4. `onboarding_provider.dart` 中 `completeOnboarding()` 写入键名 `'show_on_next_launch'`,但 `KvStorage.shouldShowOnboarding` 读取键名 `'show_onboarding'`,键名不匹配导致引导页状态无法正确持久化(影响所有平台) + +**修复**: +- `ohos_app_shell.dart`: + - `initState` 新增 `_checkOnboarding()` 方法,检查 `KvStorage.isFirstLaunch` 和 `KvStorage.shouldShowOnboarding` + - 首次启动或需要显示引导页时,通过 `Navigator.push()` 推送 `OnboardingPage` + - 引导页完成后 `context.appGo(AppRoutes.home)` 自动 pop 回 Shell 并切换 Tab + - 新增 `KvStorage` 和 `OnboardingPage` 导入 +- `ohos_nav_bridge.dart`: + - 路由注册表新增 `/onboarding` → `OnboardingPage` 条目 + - 新增 `OnboardingPage` 导入 +- `onboarding_provider.dart`: + - `completeOnboarding()` 中 `setBool('show_on_next_launch', ...)` → `KvStorage.setShowOnboarding(...)`,修复键名不匹配 Bug +- `app_router.dart`: + - `_resolveInitialLocation()` 鸿蒙端引导页路径增加详细日志 + +**举一反三**: +- 鸿蒙端因不使用 GoRouter,所有依赖 GoRouter `initialLocation`/`redirect` 的逻辑都需要在 `OhosAppShell` 中单独实现 +- 新增路由时必须双写:`app_router.dart`(GoRouter)+ `ohos_nav_bridge.dart`(鸿蒙注册表) +- `KvStorage` 便捷方法(如 `setShowOnboarding`)应优先使用,避免直接 `setBool(硬编码键名)` + +*** + +## [v14.86.0] - 2026-05-22 + +### 修复 — 编辑器画布样式圆角+图标+中间件 + +**问题**: +1. 顶部导航栏「工具抽屉」和「画布样式」按钮显示问号图标(`sidebar_left`/`rectangle` 未注册到 `EditorIconData.cupertinoMapping`) +2. 画布样式面板圆角设置仅影响预览卡片,不作用于实际画布 +3. `canvas_style_sheet.dart` 中硬编码间距/圆角值,未使用主题令牌 +4. 画布样式面板头部图标和边框区域图标不够语义化 + +**修复**: +- `editor_icons.dart`: + - `cupertinoMapping` 新增 `sidebar_left`、`rectangle`、`paintbrush_fill` 三个缺失映射 + - 修复 `EditorIcon.cupertino('sidebar_left')` 和 `EditorIcon.cupertino('rectangle')` 回退到问号图标的问题 +- `canvas_style_middleware.dart`: + - `ClipRRect` 新增 `clipBehavior: Clip.hardEdge` 确保圆角裁剪生效 + - 新增 `CanvasStyleWrapper` 组件:自动从 `EditorSettingsService` 读取/持久化画布样式 + - 新增 `CanvasStyleScope` 便捷访问类:子组件可通过 InheritedWidget 读取/更新样式 + - 新增 `_CanvasStyleInherited` InheritedWidget 实现跨组件样式共享 +- `canvas_style_sheet.dart`: + - 头部图标 `CupertinoIcons.rectangle` → `CupertinoIcons.paintbrush_fill`(更语义化) + - 圆角区域图标 → `CupertinoIcons.rectangle`,边框区域图标 → `CupertinoIcons.square` + - 硬编码间距/圆角替换为主题令牌 `AppSpacing`、`AppRadius` + - 颜色选择器弹窗圆角使用 `AppRadius.xl` +- `editor_top_nav.dart`: + - 画布样式按钮图标 `EditorIcon.cupertino('rectangle')` → `EditorIcon.svg('square_dashed')`(虚线矩形更语义化) +- `pro_editor_page.dart`: + - 更新文件头部注释 + +*** + +## [v14.85.0] - 2026-05-22 + +### 修复 — 字体下载失败(CDN不可靠+逻辑Bug+无重试) + +**问题**: +1. `cdn.jsdelivr.net` 在中国不稳定,6个在线字体中有5个使用该CDN,下载经常失败 +2. `downloadFont()` 无重试机制,一次网络波动即失败 +3. `downloadFontFromUrl()` 同样无重试,且无文件格式验证 +4. `_isValidFontFile()` 不支持 TTC 格式(文泉驿微米黑为 .ttc 文件) +5. `setActiveFont()` 逻辑Bug:`if (fontInfo.path.isEmpty && !fontInfo.path.startsWith('google_fonts://'))` 中 `path.isEmpty` 时 `startsWith` 必为 false,`!false=true`,条件永远为 true,导致 Google Font 切换也被拦截 +6. 无备用URL降级机制,主URL失败后直接报错 + +**修复**: +- `font_models.dart`: + - 替换5个 `cdn.jsdelivr.net` URL 为 `raw.githubusercontent.com`(更稳定) + - 替换阿里巴巴普惠体 URL 为阿里 OSS 直链 + - 新增2个字体:更纱黑体(SarasaGothicSC) 🎯、文泉驿微米黑(WenQuanYiMicroHei) ✒️ + - 新增 `fontFallbackUrls` 备用URL映射,每个字体1-2个备用CDN +- `font_management_notifier.dart`: + - `downloadFont()` 新增3次指数退避重试(1s/2s/4s延迟) + - `downloadFont()` 新增URL降级:主URL失败后依次尝试 `fontFallbackUrls` 中的备用URL + - `downloadFont()` 新增下载后文件头验证:验证 TTF/OTF/TTC 魔数,无效文件自动删除 + - `downloadFontFromUrl()` 同步增加3次重试+文件头验证 + - `_isValidFontFile()` 新增 TTC 格式支持(魔数 `ttcf`)和 TTF `true` 变体 + - `setActiveFont()` 修复逻辑Bug:改为 `if (fontInfo.path.isEmpty && !fontInfo.isBuiltIn)` + 额外检查 Google Font 路径 +- `Scripts/verify_fonts.dart`:新增字体验证脚本,验证所有主URL+备用URL的可访问性和文件格式,`dart run Scripts/verify_fonts.dart` 运行 + +*** + +## [v14.84.0] - 2026-05-22 + +### Bug修复 — 通用设置页面通知系统多项缺陷 + +**问题**: +1. `notificationEnabledProvider` 使用静态同步调用,通知状态不响应式更新,始终显示"已关闭" +2. `general_settings_provider` 和 `notification_settings_page` 各自维护独立的通知状态系统(`dailyNotification` vs `dailyRecommend`),互不同步 +3. 关闭单个通知类型时调用 `NotificationService.cancelAll()` / `LocalNotificationService.cancelAll()`,误杀所有通知 +4. `studyProgress` 仅保存布尔值,无实际通知调度;`chargingReadLater` 充电监听未在启动时恢复 +5. `clearCache` 仅清临时目录,但 `calculateCacheSize` 计算3个目录,显示与实际清理不一致 +6. `resetAll` 重置后不取消已调度通知 +7. 通知文本不统一:'拾光为你选了一句' vs '闲言每日一句' +8. `DailyNotifyService`(ID 2000) 和 `NotificationScheduler`(ID 1001) 双重调度同一每日通知 + +**修复**: +- `notification_scheduler.dart`: + - 新增 `_cancelAllManaged()` 替代 `cancelAll()`,仅取消已管理的通知ID(1001-1004, 2001) + - 新增学习进度通知调度(ID 1004, 20:00, payload: study_progress) + - 统一每日一句文本为 '闲言每日一句' / '今天的句子已准备好,来看看吧 ✨' +- `general_settings_provider.dart`: + - `notificationEnabledProvider` 改为 watch `generalSettingsProvider.select((s) => s.dailyNotification)` 实现响应式 + - `setDailyNotification`/`setNotifyTimeHour`/`setNotifyTimeMinute` 统一使用 `NotificationScheduler` 调度,移除 `DailyNotifyService` + - 新增 `setNotifyTime(hour, minute)` 合并方法避免双重 configureAll + - `clearCache` 扩展为清除临时/文档/支持3个目录,与 `calculateCacheSize` 一致 + - `resetAll` 增加 `DailyNotifyService.cancelAll()` + `NotificationScheduler.setNotificationsEnabled(false)` + `ReadlaterReminderService.stopMonitoring()` +- `notification_settings_page.dart`: + - `loadFromPrefs` 改为从 `NotificationScheduler`/`ReadlaterReminderService` 读取状态,移除 SharedPreferences 依赖 + - `setDailyRecommend` 同步更新 `generalSettingsProvider.dailyNotification` + - `setDailyRecommendTime` 同步更新 `generalSettingsProvider.setNotifyTime` + - 移除所有 `NotificationService.cancelAll()` 调用,统一由 `NotificationScheduler.configureAll()` 管理 + - `setStudyProgress` 改为调用 `NotificationScheduler.setStudyProgressEnabled` 实现实际调度 + - `loadFromPrefs` 中若 `chargingReadLater` 已启用则自动启动 `ReadlaterReminderService.startMonitoring()` +- `general_settings_pickers.dart`:移除 `DailyNotifyService` 引用,使用 `setNotifyTime` 统一调度 +- `general_settings_page.dart`:移除 `_onToggle` 中 `daily_notification` 的重复调度代码 +- `local_notification_service.dart`:新增 `study_progress` payload 路由处理 + +*** + +## [v14.83.0] - 2026-05-22 + +### 修复 — 首页工具中心下拉无法打开 + +**问题**: +1. 下拉阈值过大(stage1=80px,stage2需160px),导致工具中心几乎无法触发 +2. 工具中心使用底部弹窗(AppBottomSheet.showCustom),不符合"从顶部滑入"的交互预期 +3. 松手后无顶部滑入动画 + +**修复**: +- `home_refresh_indicator.dart`:降低二段式阈值 + - stage1 阈值从 80→55px + - stage2 触发从 `_pullProgress > 1.0` 改为 `_pullProgress > 0.7`(总拖拽约90px即可触发) + - 刷新触发从 0.8 降至 0.5 + - 进度显示适配新阈值:stage2 显示"松手打开工具中心" +- `home_tool_center.dart`:从底部弹窗改为顶部滑入面板 + - 使用自定义 `_ToolCenterRoute`(PageRoute)实现全屏覆盖 + - AnimationController 驱动 SlideTransition(从顶部滑入)+ FadeTransition(背景遮罩) + - 毛玻璃效果(BackdropFilter blur 30) + - 点击背景遮罩或"收起"按钮关闭,带反向滑出动画 + - 网格从3列改为4列,适配顶部面板布局 +- `home_page.dart`:`_showToolCenter` 改用 `HomeToolCenter.show(context)` + +*** + +## [v14.82.1] - 2026-05-22 + +### Bug修复 — ShaderCardBackground 重复生命周期监听 + +**问题**:`ShaderCardBackground` 同时通过 `WidgetsBindingObserver` 和 `PerformanceOrchestrator` 注册前后台回调,导致 `_pauseTicker()`/`_resumeTicker()` 被重复调用。 + +**修复**: +- 移除 `WidgetsBindingObserver` mixin 及相关代码(`addObserver`/`removeObserver`/`didChangeAppLifecycleState`) +- 统一使用 `PerformanceOrchestrator` 的 `onForeground`/`onBackground` 回调机制 + +**Issue验证**: +- ❌ Issue1(switch缺少break)— **虚假问题**。Dart 3.0+ switch case 不再隐式 fall-through,当前代码正确无需修改 +- ✅ Issue2(重复生命周期监听)— **确认存在并已修复** + +*** + ## [v14.82.0] - 2026-05-22 ### 移动端发热与资源占用优化 — 智能节流方案 diff --git a/android/.kotlin/sessions/kotlin-compiler-8131190623881858600.salive b/android/.kotlin/sessions/kotlin-compiler-8131190623881858600.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/fix_imports.ps1 b/fix_imports.ps1 new file mode 100644 index 00000000..0c7bd1c0 --- /dev/null +++ b/fix_imports.ps1 @@ -0,0 +1,52 @@ +$root = "e:\project\flutter\f\xianyan" +$missingImportFiles = @( + 'lib\features\countdown\presentation\countdown_page.dart', + 'lib\features\daily_card\presentation\daily_card_page.dart', + 'lib\features\home\presentation\cache_management_page.dart', + 'lib\features\home\presentation\providers\offline_page.dart', + 'lib\features\knowledge_graph\presentation\knowledge_graph_page.dart', + 'lib\features\mine\achievement\presentation\checkin_page.dart', + 'lib\features\mine\settings\presentation\data_management_page.dart', + 'lib\features\mine\signin\presentation\signin_page.dart', + 'lib\features\mine\user_center\presentation\coin_log_page.dart', + 'lib\features\mine\user_center\presentation\public_profile_page.dart', + 'lib\features\note\presentation\note_list_page.dart', + 'lib\features\pomodoro\presentation\pomodoro_page.dart', + 'lib\features\progress\presentation\progress_page.dart', + 'lib\features\reading_report\presentation\reading_report_page.dart', + 'lib\features\solar_term\presentation\solar_term_page.dart', + 'lib\features\statistics\presentation\statistics_page.dart', + 'lib\features\study_plan\presentation\study_plan_page.dart', + 'lib\features\template\presentation\template_gallery_page.dart', + 'lib\features\tool_center\inspiration\presentation\pages\readlater_stats_page.dart', + 'lib\features\tool_center\inspiration\presentation\pages\chat\hidden_sessions_page.dart' +) + +foreach ($rel in $missingImportFiles) { + $fullPath = Join-Path $root $rel + $content = Get-Content $fullPath -Raw -Encoding UTF8 + + $parts = $rel.Split('\') + $depth = 0 + for ($i = 1; $i -lt $parts.Count; $i++) { + if ($parts[$i] -ne 'lib') { $depth++ } else { break } + } + $prefix = '../' * $depth + $importPath = $prefix + 'shared/widgets/adaptive_back_button.dart' + $importLine = "import '$importPath';" + + $lastImportIdx = $content.LastIndexOf("import '") + if ($lastImportIdx -eq -1) { + $lastImportIdx = $content.LastIndexOf('import "') + } + + if ($lastImportIdx -ge 0) { + $endOfLine = $content.IndexOf("`n", $lastImportIdx) + if ($endOfLine -eq -1) { $endOfLine = $content.Length } + $newContent = $content.Substring(0, $endOfLine) + "`n$importLine" + $content.Substring($endOfLine) + [System.IO.File]::WriteAllText($fullPath, $newContent, [System.Text.Encoding]::UTF8) + Write-Output "FIXED: $rel" + } else { + Write-Output "NO IMPORTS FOUND: $rel" + } +} diff --git a/lib/app/app.dart b/lib/app/app.dart index 6c1ff15e..36cc5bf4 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -3,7 +3,7 @@ /// 创建时间: 2026-04-20 /// 更新时间: 2026-05-22 /// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate -/// 上次更新: 集成AppLifecycleGate前后台统一管理 +/// 上次更新: 集成KeyboardBackHandler桌面端快捷键返回 /// ============================================================ import 'dart:async'; @@ -17,11 +17,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; -import 'package:flutter/services.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter_quill/flutter_quill.dart' show FlutterQuillLocalizations; +import '../core/services/ui/status_bar_service.dart'; import '../core/router/app_router.dart' show appRouter, rootNavigatorKey; import '../core/layout/ohos_app_shell.dart'; import '../core/services/device/app_lock_service.dart'; @@ -29,6 +29,7 @@ import '../core/services/performance/app_lifecycle_gate.dart'; import '../core/theme/app_theme.dart'; import '../core/utils/logger.dart'; import '../core/utils/platform_utils.dart' as pu; +import '../shared/widgets/keyboard_back_handler.dart'; import '../features/mine/settings/providers/theme_settings_provider.dart'; import '../features/mine/settings/presentation/font_management_notifier.dart'; import '../l10n/app_locale.dart'; @@ -173,12 +174,73 @@ class _XianyanAppState extends ConsumerState minTextAdapt: true, splitScreenMode: true, builder: (context, child) { - if (pu.isOhos) { - Log.i( - '🟢 [OHOS] 使用 MaterialApp(home:) + OhosAppShell (liquidGlass=$liquidGlassReady)', - ); - final ohosMaterialApp = MaterialApp( - navigatorKey: rootNavigatorKey, + Widget buildApp() { + if (pu.isOhos) { + Log.i( + '🟢 [OHOS] 使用 MaterialApp(home:) + OhosAppShell (liquidGlass=$liquidGlassReady)', + ); + final ohosMaterialApp = MaterialApp( + navigatorKey: rootNavigatorKey, + title: '闲言', + debugShowCheckedModeBanner: false, + scrollBehavior: const AppScrollBehavior(), + locale: appLocale, + supportedLocales: supportedLocales, + localizationsDelegates: _localizationsDelegates, + theme: theme, + darkTheme: darkTheme, + themeMode: themeMode, + home: const OhosAppShell(), + builder: (context, widget) { + final botToastBuilder = BotToastInit(); + final botWidget = botToastBuilder(context, widget); + return StatusBarStyleRegion( + isDark: settings.isDark, + child: MediaQuery( + data: MediaQuery.of( + context, + ).copyWith(textScaler: TextScaler.noScaling), + child: DefaultTextStyle( + style: const TextStyle(), + child: botWidget, + ), + ), + ); + }, + ); + + return InheritedGoRouter( + goRouter: appRouter, + child: liquidGlassReady + ? GlassTheme( + data: GlassThemeData( + light: GlassThemeVariant( + settings: GlassThemeSettings( + thickness: 30.0, + blur: settings.glassEnabled ? 3.0 : 0.0, + refractiveIndex: 1.65, + lightIntensity: 1.2, + ambientStrength: 0.6, + saturation: 1.2, + ), + ), + dark: GlassThemeVariant( + settings: GlassThemeSettings( + thickness: 40.0, + blur: settings.glassEnabled ? 5.0 : 0.0, + lightIntensity: 1.5, + refractiveIndex: 1.2, + saturation: 1.1, + ), + ), + ), + child: ohosMaterialApp, + ) + : ohosMaterialApp, + ); + } + + final materialApp = MaterialApp.router( title: '闲言', debugShowCheckedModeBanner: false, scrollBehavior: const AppScrollBehavior(), @@ -188,25 +250,13 @@ class _XianyanAppState extends ConsumerState theme: theme, darkTheme: darkTheme, themeMode: themeMode, - home: const OhosAppShell(), + routerConfig: appRouter, builder: (context, widget) { final botToastBuilder = BotToastInit(); final botWidget = botToastBuilder(context, widget); - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: settings.isDark - ? Brightness.light - : Brightness.dark, - statusBarBrightness: settings.isDark - ? Brightness.dark - : Brightness.light, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: settings.isDark - ? Brightness.light - : Brightness.dark, - systemNavigationBarDividerColor: Colors.transparent, - ), + + return StatusBarStyleRegion( + isDark: settings.isDark, child: MediaQuery( data: MediaQuery.of( context, @@ -220,107 +270,33 @@ class _XianyanAppState extends ConsumerState }, ); - return InheritedGoRouter( - goRouter: appRouter, - child: liquidGlassReady - ? GlassTheme( - data: GlassThemeData( - light: GlassThemeVariant( - settings: GlassThemeSettings( - thickness: 30.0, - blur: settings.glassEnabled ? 3.0 : 0.0, - refractiveIndex: 1.65, - lightIntensity: 1.2, - ambientStrength: 0.6, - saturation: 1.2, - ), - ), - dark: GlassThemeVariant( - settings: GlassThemeSettings( - thickness: 40.0, - blur: settings.glassEnabled ? 5.0 : 0.0, - lightIntensity: 1.5, - refractiveIndex: 1.2, - saturation: 1.1, - ), - ), - ), - child: ohosMaterialApp, - ) - : ohosMaterialApp, + return GlassTheme( + data: GlassThemeData( + light: GlassThemeVariant( + settings: GlassThemeSettings( + thickness: 30.0, + blur: settings.glassEnabled ? 3.0 : 0.0, + refractiveIndex: 1.65, + lightIntensity: 1.2, + ambientStrength: 0.6, + saturation: 1.2, + ), + ), + dark: GlassThemeVariant( + settings: GlassThemeSettings( + thickness: 40.0, + blur: settings.glassEnabled ? 5.0 : 0.0, + lightIntensity: 1.5, + refractiveIndex: 1.2, + saturation: 1.1, + ), + ), + ), + child: materialApp, ); } - final materialApp = MaterialApp.router( - title: '闲言', - debugShowCheckedModeBanner: false, - scrollBehavior: const AppScrollBehavior(), - locale: appLocale, - supportedLocales: supportedLocales, - localizationsDelegates: _localizationsDelegates, - theme: theme, - darkTheme: darkTheme, - themeMode: themeMode, - routerConfig: appRouter, - builder: (context, widget) { - final botToastBuilder = BotToastInit(); - final botWidget = botToastBuilder(context, widget); - - final isDark = settings.isDark; - final wrappedWidget = AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: isDark - ? Brightness.light - : Brightness.dark, - statusBarBrightness: isDark - ? Brightness.dark - : Brightness.light, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: isDark - ? Brightness.light - : Brightness.dark, - systemNavigationBarDividerColor: Colors.transparent, - ), - child: MediaQuery( - data: MediaQuery.of( - context, - ).copyWith(textScaler: TextScaler.noScaling), - child: DefaultTextStyle( - style: const TextStyle(), - child: botWidget, - ), - ), - ); - - return wrappedWidget; - }, - ); - - return GlassTheme( - data: GlassThemeData( - light: GlassThemeVariant( - settings: GlassThemeSettings( - thickness: 30.0, - blur: settings.glassEnabled ? 3.0 : 0.0, - refractiveIndex: 1.65, - lightIntensity: 1.2, - ambientStrength: 0.6, - saturation: 1.2, - ), - ), - dark: GlassThemeVariant( - settings: GlassThemeSettings( - thickness: 40.0, - blur: settings.glassEnabled ? 5.0 : 0.0, - lightIntensity: 1.5, - refractiveIndex: 1.2, - saturation: 1.1, - ), - ), - ), - child: materialApp, - ); + return KeyboardBackHandler(child: buildApp()); }, ), ), diff --git a/lib/core/layout/app_shell.dart b/lib/core/layout/app_shell.dart index 53513adb..08b537f1 100644 --- a/lib/core/layout/app_shell.dart +++ b/lib/core/layout/app_shell.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 应用布局壳 // 创建时间: 2026-04-20 // 更新时间: 2026-05-18 @@ -9,7 +9,6 @@ import 'package:badges/badges.dart' as badges; 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/utils/platform_utils.dart' as pu; import 'package:go_router/go_router.dart'; @@ -167,29 +166,15 @@ class AppShell extends ConsumerWidget { ); return CelebrationOverlay( - child: AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: ext.isDark - ? Brightness.light - : Brightness.dark, - statusBarBrightness: ext.isDark ? Brightness.dark : Brightness.light, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: ext.isDark - ? Brightness.light - : Brightness.dark, - systemNavigationBarDividerColor: Colors.transparent, - ), - child: PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, _) { - if (didPop) return; - }, - child: Scaffold( - extendBody: true, - body: Stack(children: [child]), - bottomNavigationBar: bottomBar, - ), + child: PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (didPop) return; + }, + child: Scaffold( + extendBody: true, + body: Stack(children: [child]), + bottomNavigationBar: bottomBar, ), ), ); diff --git a/lib/core/layout/ohos_app_shell.dart b/lib/core/layout/ohos_app_shell.dart index 22b19ec8..3d10d910 100644 --- a/lib/core/layout/ohos_app_shell.dart +++ b/lib/core/layout/ohos_app_shell.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 鸿蒙端专用布局壳 /// 创建时间: 2026-05-18 -/// 更新时间: 2026-05-18 +/// 更新时间: 2026-05-22 /// 作用: 鸿蒙端使用 Scaffold+GlassBottomBar 替代 GoRouter+StatefulShellRoute -/// 上次更新: 恢复液态玻璃效果+TabIconSprite精灵动画,仅阉割路由 +/// 上次更新: 修复引导页在鸿蒙端不显示的问题,initState检查onboarding状态 /// ============================================================ /// /// 根因: 鸿蒙端 Flutter 引擎中 MaterialApp.router + 额外包导入 = 白屏 @@ -12,14 +12,15 @@ import 'package:badges/badges.dart' as badges; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; +import '../../core/storage/kv_storage.dart'; import '../../core/theme/app_theme.dart'; import '../../core/utils/interaction_animations.dart'; import '../../core/utils/logger.dart'; import '../../core/utils/platform_utils.dart' show OhosDeviceCapabilities; +import '../../features/onboarding/presentation/onboarding_page.dart'; import '../../features/tool_center/inspiration/providers/chat_provider.dart'; import '../../features/tool_center/inspiration/presentation/pages/home/inspiration_page.dart'; import '../../features/home/presentation/home_page.dart'; @@ -57,8 +58,30 @@ class _OhosAppShellState extends ConsumerState { void initState() { super.initState(); OhosAppShell._instance = this; + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkOnboarding(); + }); } + void _checkOnboarding() { + if (!mounted) return; + final shouldShow = + KvStorage.isFirstLaunch || KvStorage.shouldShowOnboarding; + if (shouldShow) { + Log.i('🟢 [OHOS] 引导页检查: 首次启动=$isFirstLaunch, 应显示引导=$shouldShow → 推送引导页'); + Navigator.of(context).push( + CupertinoPageRoute( + fullscreenDialog: true, + builder: (_) => const OnboardingPage(), + ), + ); + } else { + Log.i('🟢 [OHOS] 引导页检查: 已完成引导,直接进入主页'); + } + } + + bool get isFirstLaunch => KvStorage.isFirstLaunch; + @override void dispose() { OhosAppShell._instance = null; @@ -104,29 +127,15 @@ class _OhosAppShellState extends ConsumerState { ); return CelebrationOverlay( - child: AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: ext.isDark - ? Brightness.light - : Brightness.dark, - statusBarBrightness: ext.isDark ? Brightness.dark : Brightness.light, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: ext.isDark - ? Brightness.light - : Brightness.dark, - systemNavigationBarDividerColor: Colors.transparent, - ), - child: PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, _) { - if (didPop) return; - }, - child: Scaffold( - extendBody: true, - body: IndexedStack(index: _currentIndex, children: _tabPages), - bottomNavigationBar: bottomBar, - ), + child: PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (didPop) return; + }, + child: Scaffold( + extendBody: true, + body: IndexedStack(index: _currentIndex, children: _tabPages), + bottomNavigationBar: bottomBar, ), ), ); @@ -249,7 +258,10 @@ class _OhosAppShellState extends ConsumerState { label: t.navDiscover, badgeCount: unreadCount, ), - GlassBottomNavBarItem(spriteType: TabSpriteType.profile, label: t.navProfile), + GlassBottomNavBarItem( + spriteType: TabSpriteType.profile, + label: t.navProfile, + ), ], selectedIndex: _currentIndex, onTabSelected: (index) => setState(() => _currentIndex = index), diff --git a/lib/core/registry/page_registry.dart b/lib/core/registry/page_registry.dart index 6cbb08ee..458c5fa4 100644 --- a/lib/core/registry/page_registry.dart +++ b/lib/core/registry/page_registry.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 页面注册表 /// 创建时间: 2026-04-20 /// 更新时间: 2026-04-20 @@ -8,7 +8,7 @@ import '../theme/app_spacing.dart'; import '../theme/app_radius.dart'; -import '../router/app_router.dart'; +import '../router/app_routes.dart'; // ============================================================ // 页面注册条目 diff --git a/lib/core/router/app_nav_extension.dart b/lib/core/router/app_nav_extension.dart index 5d819a10..08101637 100644 --- a/lib/core/router/app_nav_extension.dart +++ b/lib/core/router/app_nav_extension.dart @@ -1,24 +1,34 @@ /// ============================================================ /// 闲言APP — 跨平台导航扩展 /// 创建时间: 2026-05-18 -/// 更新时间: 2026-05-18 +/// 更新时间: 2026-05-22 /// 作用: 统一导航API,鸿蒙端使用OhosNavBridge,其他端使用GoRouter -/// 上次更新: 支持extra参数传递,补全appGo导航逻辑 +/// 上次更新: 集成自定义转场动画,根据pageTransitionMode设置切换AppSlide/AppFadeScale /// ============================================================ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:xianyan/core/utils/platform_utils.dart' as pu; import 'package:xianyan/core/router/ohos_nav_bridge.dart'; +import 'package:xianyan/features/mine/settings/providers/general_settings_provider.dart'; extension AppNavExtension on BuildContext { /// 跨平台导航推送 /// - /// 鸿蒙端: 使用OhosNavBridge → Navigator.push(CupertinoPageRoute) + /// 鸿蒙端: 使用OhosNavBridge → 自定义转场PageRouteBuilder /// 其他端: 使用GoRouter → context.push() - Future appPush(String route, {Object? extra}) { + Future appPush( + String route, { + Object? extra, + PageTransitionMode? transitionMode, + }) { if (pu.isOhos) { - return OhosNavBridge.push(this, route, extra: extra); + return OhosNavBridge.push( + this, + route, + extra: extra, + transitionMode: transitionMode, + ); } return push(route, extra: extra); } diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 83f04590..b8bf3b1d 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,12 +1,10 @@ -// ============================================================ -// 闲言APP — 路由配置 -// 创建时间: 2026-04-20 -// 更新时间: 2026-05-21 -// 作用: go_router 路由表 + ShellRoute 布局壳 + iOS 风格转场 -// 上次更新: v14.64.0 新增onboarding引导页路由,首次启动判断跳转引导页 // ============================================================ - -import 'dart:typed_data'; +// 闲言APP — 路由配置(主入口) +// 创建时间: 2026-04-20 +// 更新时间: 2026-05-22 +// 作用: go_router 路由表组装 + ShellRoute 布局壳 + iOS 风格转场 + 引导页重定向 +// 上次更新: 拆分为模块化路由文件,本文件仅负责组装和全局配置 +// ============================================================ import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; @@ -16,255 +14,38 @@ import '../utils/platform_utils.dart' as pu; import '../../features/onboarding/presentation/onboarding_page.dart'; import '../../features/home/presentation/home_page.dart'; -import '../../features/home/presentation/favorite_page.dart'; -import '../../features/home/presentation/history_page.dart'; -import '../../features/home/presentation/providers/likes_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/home/footprint_page.dart'; import '../../features/tool_center/inspiration/presentation/pages/home/inspiration_page.dart'; import '../../features/mine/profile/presentation/profile_page.dart'; -import '../../features/mine/profile/presentation/about_page.dart'; -import '../../features/auth/presentation/login_page.dart'; -import '../../features/mine/signin/presentation/signin_page.dart'; -import '../../features/mine/user_center/presentation/my_devices_page.dart'; -import '../../features/auth/presentation/qrcode_login_page.dart'; -import '../../features/note/presentation/note_list_page.dart'; -import '../../features/note/presentation/note_edit_page.dart'; -import '../../features/statistics/presentation/statistics_page.dart'; -import '../../features/correction/presentation/correction_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/tool/hanzi_tool_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/tool/calc_tool_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/tool/china_colors_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/tool_list_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/tool/ocr_tool_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/tool/pinyin_tool_page.dart'; -import '../../editor/pages/editor/editor_page.dart'; -import '../../editor/pages/tools/image_preview_page.dart'; -import '../../editor/pages/tools/image_crop_page.dart'; -import '../../editor/pages/tools/draft_list_page.dart'; -import '../../editor/pages/tools/model_3d_preview_page.dart'; -import '../../editor/services/core/model_catalog_service.dart'; -import '../../features/search/presentation/search_page.dart'; -import '../../features/source/presentation/source_page.dart'; -import '../../features/mine/member/presentation/member_page.dart'; -import '../../features/widget/presentation/widget_management_page.dart'; -import '../../features/mine/settings/presentation/theme/theme_settings_page.dart'; -import '../../features/mine/settings/presentation/general/general_settings_page.dart'; -import '../../features/mine/settings/presentation/language_settings_page.dart'; -import '../../features/mine/settings/presentation/account/account_settings_page.dart'; -import '../../features/mine/settings/presentation/account/account_deletion_page.dart'; -import '../../features/mine/settings/presentation/data_management_page.dart'; -import '../../features/mine/settings/presentation/account/change_password_page.dart'; -import '../../features/mine/settings/presentation/account/security_question_page.dart'; -import '../../features/mine/settings/presentation/font_management_page.dart'; -import '../../features/home/presentation/providers/offline_page.dart'; -import '../../features/home/presentation/cache_management_page.dart'; -import '../../features/home/presentation/providers/readlater_page.dart'; -import '../../features/discover/presentation/discover_page.dart'; -import '../../features/discover/presentation/category_detail_page.dart'; -import '../../features/search/presentation/hot_search_page.dart'; -import '../../features/search/presentation/user_preference_page.dart'; -import '../../features/mine/user_center/presentation/learning_center_page.dart'; -import '../../features/classics/presentation/classics_page.dart'; -import '../../features/health/presentation/health_page.dart'; -import '../../features/tool_center/game/presentation/game_center_page.dart'; -import '../../features/mine/achievement/presentation/achievement_page.dart'; -import '../../features/mine/achievement/presentation/badge_wall_page.dart'; -import '../../features/task/presentation/daily_task_page.dart'; -import '../../features/rank/presentation/rank_page.dart'; -import '../../features/article/presentation/article_list_page.dart'; -import '../../features/article/presentation/article_detail_page.dart'; -import '../../features/article/presentation/article_edit_page.dart'; -import '../../features/article/presentation/my_articles_page.dart'; -import '../../features/check/presentation/check_page.dart'; -import '../../features/mine/user_center/presentation/coin_log_page.dart'; -import '../../features/mine/user_center/presentation/public_profile_page.dart'; -import '../../features/mine/user_center/presentation/user_center_page.dart'; -import '../../features/mine/user_center/presentation/tag_cloud_page.dart'; -import '../../features/mine/user_center/presentation/user_debug_page.dart'; -import '../../features/statistics/presentation/user_stats_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/chat/chat_flow_page.dart'; -import '../../features/tool_center/inspiration/models/chat_session.dart'; -import '../../features/tool_center/inspiration/presentation/pages/chat/chat_settings_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/chat/hidden_sessions_page.dart'; -import '../../features/file_transfer/presentation/pages/file_transfer_page.dart'; -import '../../features/file_transfer/presentation/pages/transfer_chat_page.dart'; -import '../../features/file_transfer/presentation/pages/device_pairing_page.dart'; -import '../../features/file_transfer/models/models.dart'; -import '../../features/tool_center/inspiration/presentation/pages/translate/translate_page.dart'; -import '../../features/tool_center/inspiration/presentation/pages/translate/translate_settings_page.dart'; -import '../../features/file_transfer/collaboration/canvas/pages/canvas_page.dart'; -import '../../features/file_transfer/collaboration/clipboard/pages/clipboard_flow_page.dart'; -import '../../features/file_transfer/collaboration/screen_share/pages/screen_share_page.dart'; -import '../../features/daily_card/presentation/daily_card_page.dart'; -import '../../features/template/presentation/template_gallery_page.dart'; -import '../../features/reading_report/presentation/reading_report_page.dart'; -import '../../features/poetry/presentation/poetry_page.dart'; -import '../../features/poetry/presentation/poetry_settings_page.dart'; -import '../../features/weather/presentation/weather_page.dart'; -import '../../features/weather/presentation/weather_settings_page.dart'; -import '../../features/pomodoro/presentation/pomodoro_page.dart'; -import '../../features/countdown/presentation/countdown_page.dart'; -import '../../features/solar_term/presentation/solar_term_page.dart'; -import '../../features/mine/settings/presentation/notification_settings_page.dart'; -import '../../features/mine/settings/presentation/smart_mode_settings_page.dart'; -import '../../features/mine/settings/presentation/more_settings_page.dart'; -import '../../features/mine/settings/presentation/privacy/permission_management_page.dart'; -import '../../features/mine/settings/presentation/privacy/privacy_policy_page.dart'; -import '../../features/agreements/presentation/agreement_list_page.dart'; -import '../../features/agreements/presentation/agreement_page.dart'; -import '../../features/agreements/data/agreement_types.dart'; -import '../../features/mine/settings/presentation/privacy/log_viewer_page.dart'; -import '../../features/mine/settings/presentation/privacy/crash_log_page.dart'; -import '../../features/knowledge_graph/presentation/knowledge_graph_page.dart'; -import '../../features/study_plan/presentation/study_plan_page.dart'; -import '../../features/daily_fortune/presentation/daily_fortune_page.dart'; -import '../../features/daily_fortune/presentation/fortune_settings_page.dart'; -import '../../features/progress/presentation/progress_page.dart'; -import '../../features/share/presentation/share_history_page.dart'; -import '../../features/share/presentation/share_target_edit_page.dart'; import '../layout/app_shell.dart'; import '../storage/kv_storage.dart'; import '../utils/page_transitions.dart'; + +import 'app_routes.dart'; +import 'settings_routes.dart'; +import 'tool_routes.dart'; import 'editor_router.dart'; +import 'user_routes.dart'; +import 'content_routes.dart'; +import 'feature_routes.dart'; -// ============================================================ -// 路由名称常量 -// ============================================================ +export 'app_routes.dart'; -/// 路由路径常量 -class AppRoutes { - AppRoutes._(); - - static const String home = '/home'; - static const String inspiration = '/inspiration'; - static const String profile = '/profile'; - static const String editor = '/editor'; - static const String search = '/search'; - static const String source = '/source'; - static const String member = '/member'; - static const String widgetManagement = '/widget-management'; - static const String themeSettings = '/settings/theme'; - static const String generalSettings = '/settings/general'; - static const String languageSettings = '/settings/language'; - static const String accountSettings = '/settings/account'; - static const String passwordSettings = '/settings/password'; - static const String securityQuestion = '/settings/security-question'; - static const String dataManagement = '/settings/data'; - static const String accountDeletion = '/settings/account/deletion'; - static const String fontManagement = '/settings/fonts'; - static const String favorites = '/favorites'; - static const String history = '/history'; - static const String likes = '/likes'; - static const String quickCard = '/quick-card'; - static const String login = '/login'; - static const String signin = '/signin'; - static const String myDevices = '/my-devices'; - static const String qrcodeLogin = '/qrcode-login'; - static const String noteList = '/notes'; - static const String noteEdit = '/notes/edit'; - static const String statistics = '/statistics'; - static const String correction = '/correction'; - static const String hanziTool = '/hanzi-tool'; - static const String offline = '/offline'; - static const String cacheManagement = '/cache'; - static const String readLater = '/readlater'; - static const String discover = '/discover'; - static const String categoryDetail = '/category/:type'; - static const String hotSearch = '/hot-search'; - static const String learning = '/learning'; - static const String classics = '/classics'; - static const String health = '/health'; - static const String game = '/game'; - static const String achievement = '/achievement'; - static const String checkin = '/achievement/checkin'; - static const String badgeWall = '/badge-wall'; - static const String dailyTask = '/daily-task'; - static const String rank = '/rank'; - static const String articles = '/articles'; - static const String articleDetail = '/article/:id'; - static const String articleEdit = '/article/edit'; - static const String myArticles = '/article/mine'; - static const String check = '/check'; - static const String coinLog = '/coin-log'; - static const String publicProfile = '/user/:uid'; - static const String userPreference = '/user-preference'; - static const String shareHistory = '/share/history'; - static const String shareTargetEdit = '/share/targets'; - static const String userCenter = '/user-center'; - static const String userDebug = '/user-debug'; - static const String chatFlow = '/chat-flow'; - static const String chatSettings = '/chat-settings'; - static const String hiddenSessions = '/hidden-sessions'; - static const String footprint = '/footprint'; - static const String dailyCard = '/daily-card'; - static const String templateGallery = '/template-gallery'; - static const String readingReport = '/reading-report'; - static const String weather = '/weather'; - static const String weatherSettings = '/weather/settings'; - static const String poetry = '/poetry'; - static const String poetrySettings = '/poetry/settings'; - static const String pomodoro = '/pomodoro'; - static const String countdown = '/countdown'; - static const String solarTerm = '/solar-term'; - static const String notificationSettings = '/notification-settings'; - static const String smartModeSettings = '/smart-mode-settings'; - static const String moreSettings = '/more-settings'; - static const String permissionManagement = '/permission-management'; - static const String privacyPolicy = '/privacy-policy'; - static const String logViewer = '/log-viewer'; - static const String crashLog = '/crash-log'; - static const String knowledgeGraph = '/knowledge-graph'; - static const String studyPlan = '/study-plan'; - static const String dailyFortune = '/daily-fortune'; - static const String dailyFortuneSettings = '/daily-fortune/settings'; - static const String progress = '/progress'; - static const String learningProgress = '/learning-progress'; - static const String tagCloud = '/tag-cloud'; - static const String userStats = '/user-stats'; - static const String about = '/about'; - static const String agreements = '/agreements'; - static const String agreement = '/agreement'; - static const String fileTransfer = '/file-transfer'; - static const String transferChat = '/transfer-chat'; - static const String devicePairing = '/device-pairing'; - static const String canvas = '/canvas'; - static const String clipboard = '/clipboard'; - static const String screenShare = '/screen-share'; - static const String readlaterChat = '/readlater-chat'; - static const String translate = '/translate'; - static const String translateSettings = '/translate-settings'; - static const String onboarding = '/onboarding'; -} - -// ============================================================ -// 路由 Key -// ============================================================ - -/// 全局 NavigatorKey (ShellRoute 内部导航) final rootNavigatorKey = GlobalKey(); -/// Shell 内部 NavigatorKey (Tab 页面级导航) final shellNavigatorKey = GlobalKey(); -/// Tab 分支 NavigatorKeys final _homeNavigatorKey = GlobalKey(debugLabel: 'home'); final _inspirationNavigatorKey = GlobalKey( debugLabel: 'inspiration', ); final _profileNavigatorKey = GlobalKey(debugLabel: 'profile'); -// ============================================================ -// Router 实例 -// ============================================================ - -/// 全局路由器 final GoRouter appRouter = GoRouter( navigatorKey: rootNavigatorKey, initialLocation: _resolveInitialLocation(), debugLogDiagnostics: true, observers: pu.isOhos ? [_OhosRouteObserver()] : null, routes: [ - // ---- 引导页 — 首次启动展示 ---- GoRoute( path: AppRoutes.onboarding, name: 'onboarding', @@ -273,7 +54,6 @@ final GoRouter appRouter = GoRouter( iosSlideTransition(state: state, child: const OnboardingPage()), ), - // ---- Shell 布局 (底部 TabBar) ---- StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => AppShell(child: navigationShell), @@ -316,1112 +96,23 @@ final GoRouter appRouter = GoRouter( ], ), - // ---- 全屏页面 (iOS 右滑入) ---- - - // 编辑器 — 全屏 Heroine 共享元素动画 - GoRoute( - path: AppRoutes.editor, - name: 'editor', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final text = state.uri.queryParameters['text']; - return heroineTransition( - state: state, - child: EditorPage(initialText: text), - ); - }, - ), - - // ---- 编辑器子页面 (iOS 滑入) ---- - - // 图片预览 - GoRoute( - path: EditorRoutes.imagePreview, - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final extra = state.extra as ImagePreviewExtra?; - if (extra?.bytes != null) { - return iosSlideTransition( - state: state, - child: ImagePreviewPage.fromBytes(extra!.bytes!), - ); - } - return iosSlideTransition( - state: state, - child: ImagePreviewPage( - imageProvider: extra!.provider!, - heroTag: state.uri.queryParameters['heroTag'] ?? '', - ), - ); - }, - ), - - // 图片裁剪 - GoRoute( - path: EditorRoutes.imageCrop, - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final extra = state.extra as ImageCropExtra?; - return iosSlideTransition( - state: state, - child: ImageCropPage(imageBytes: extra?.bytes ?? Uint8List(0)), - ); - }, - ), - - // 草稿列表 - GoRoute( - path: EditorRoutes.draftList, - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const DraftListPage()), - ), - - // 图片画廊 - GoRoute( - path: EditorRoutes.imageGallery, - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final extra = state.extra as ImageGalleryExtra?; - return iosSlideTransition( - state: state, - child: ImageGalleryPage( - images: extra?.images ?? [], - initialIndex: extra?.initialIndex ?? 0, - ), - ); - }, - ), - - // 3D模型预览 - GoRoute( - path: EditorRoutes.model3dPreview, - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final model = state.extra as Model3DItem?; - if (model == null) { - return iosSlideTransition(state: state, child: const _NotFoundPage()); - } - return iosSlideTransition( - state: state, - child: Model3DPreviewPage(model: model), - ); - }, - ), - - // 搜索 — iOS 滑入 - GoRoute( - path: AppRoutes.search, - name: 'search', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const SearchPage()), - ), - - // 句子来源 — iOS 滑入 - GoRoute( - path: AppRoutes.source, - name: 'source', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const SourcePage()), - ), - - // 会员 — 液态玻璃底部面板 (iOS 26 Glass Sheet) - GoRoute( - path: AppRoutes.member, - name: 'member', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - glassSheetTransition(state: state, child: const MemberPage()), - ), - - GoRoute( - path: AppRoutes.widgetManagement, - name: 'widget-management', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const WidgetManagementPage()), - ), - - // 主题个性化 — iOS 滑入 - GoRoute( - path: AppRoutes.themeSettings, - name: 'theme-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ThemeSettingsPage()), - ), - - // 通用设置 — iOS 滑入 - GoRoute( - path: AppRoutes.generalSettings, - name: 'general-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const GeneralSettingsPage()), - ), - - // 语言设置 — iOS 滑入 - GoRoute( - path: AppRoutes.languageSettings, - name: 'language-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const LanguageSettingsPage()), - ), - - // 账户设置 — iOS 滑入 - GoRoute( - path: AppRoutes.accountSettings, - name: 'account-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const AccountSettingsPage()), - ), - - // 修改密码 — iOS 滑入 - GoRoute( - path: AppRoutes.passwordSettings, - name: 'password-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ChangePasswordPage()), - ), - - // 密保问题管理 — iOS 滑入 - GoRoute( - path: AppRoutes.securityQuestion, - name: 'security-question', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const SecurityQuestionPage()), - ), - - // 数据管理 — iOS 滑入 - GoRoute( - path: AppRoutes.dataManagement, - name: 'data-management', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const DataManagementPage()), - ), - - // 注销删除账号 — iOS 滑入 - GoRoute( - path: AppRoutes.accountDeletion, - name: 'account-deletion', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const AccountDeletionPage()), - ), - - // 字体管理 — iOS 滑入 - GoRoute( - path: AppRoutes.fontManagement, - name: 'font-management', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const FontManagementPage()), - ), - - // 收藏 — iOS 滑入 - GoRoute( - path: AppRoutes.favorites, - name: 'favorites', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const FavoritePage()), - ), - - // 阅读历史 — iOS 滑入 - GoRoute( - path: AppRoutes.history, - name: 'history', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const HistoryPage()), - ), - - // 点赞历史 — iOS 滑入 - GoRoute( - path: AppRoutes.likes, - name: 'likes', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const LikesPage()), - ), - - // 登录 — iOS 滑入 - GoRoute( - path: AppRoutes.login, - name: 'login', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const LoginPage()), - ), - - // 签到 — iOS 滑入 - GoRoute( - path: AppRoutes.signin, - name: 'signin', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const SigninPage()), - ), - - // 我的设备 — iOS 滑入 - GoRoute( - path: AppRoutes.myDevices, - name: 'my-devices', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const MyDevicesPage()), - ), - - // 扫码登录 — iOS 滑入 - GoRoute( - path: AppRoutes.qrcodeLogin, - name: 'qrcode-login', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const QrcodeLoginPage()), - ), - - // 笔记列表 — iOS 滑入 - GoRoute( - path: AppRoutes.noteList, - name: 'note-list', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const NoteListPage()), - ), - - // 笔记编辑 — iOS 滑入 - GoRoute( - path: AppRoutes.noteEdit, - name: 'note-edit', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final noteId = state.uri.queryParameters['id']; - return iosSlideTransition( - state: state, - child: NoteEditPage( - noteId: noteId != null ? int.tryParse(noteId) : null, - ), - ); - }, - ), - - // 统计 — iOS 滑入 - GoRoute( - path: AppRoutes.statistics, - name: 'statistics', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const StatisticsPage()), - ), - - // 纠错 — iOS 滑入 - GoRoute( - path: AppRoutes.correction, - name: 'correction', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const CorrectionPage()), - ), - - // 汉语工具 — iOS 滑入 - GoRoute( - path: AppRoutes.hanziTool, - name: 'hanzi-tool', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final config = state.extra as HanziToolConfig?; - if (config == null) { - return iosSlideTransition(state: state, child: const _NotFoundPage()); - } - return iosSlideTransition( - state: state, - child: HanziToolPage(config: config), - ); - }, - ), - - // 计算型工具 — iOS 滑入 - GoRoute( - path: '/tool/calc', - name: 'calc-tool', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final config = state.extra as CalcToolConfig?; - if (config == null) { - return iosSlideTransition(state: state, child: const _NotFoundPage()); - } - return iosSlideTransition( - state: state, - child: CalcToolPage(config: config), - ); - }, - ), - - // 中国传统色 — iOS 滑入 - GoRoute( - path: '/tool/china_colors', - name: 'china-colors', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ChinaColorsPage()), - ), - - // 列表型工具 — iOS 滑入(热搜/一言/古诗词/酒方等) - GoRoute( - path: '/tool/list', - name: 'tool-list', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final extra = state.extra as Map?; - if (extra == null) { - return iosSlideTransition(state: state, child: const _NotFoundPage()); - } - return iosSlideTransition( - state: state, - child: ToolListPage( - toolId: extra['toolId'] as String? ?? '', - title: extra['title'] as String? ?? '', - emoji: extra['emoji'] as String? ?? '', - listType: extra['listType'] as String? ?? 'hanzi_search', - searchType: extra['searchType'] as String?, - showSearch: extra['showSearch'] as bool? ?? false, - detailDesc: extra['detailDesc'] as String?, - ), - ); - }, - ), - - // OCR图片识别 - GoRoute( - path: '/tool/ocr', - name: 'ocr-tool', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const OcrToolPage()), - ), - - // 拼音转换 - GoRoute( - path: '/tool/pinyin', - name: 'pinyin-tool', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const PinyinToolPage()), - ), - - // 每日推荐 - GoRoute( - path: '/tool/daily-recommend', - name: 'daily-recommend-tool', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => iosSlideTransition( - state: state, - child: const ToolListPage( - toolId: 'daily_recommend', - title: '每日推荐', - emoji: '🌟', - listType: 'daily_recommend', - ), - ), - ), - - // 离线模式 — iOS 滑入 - GoRoute( - path: AppRoutes.offline, - name: 'offline', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const OfflinePage()), - ), - - // 缓存管理 — iOS 滑入 - GoRoute( - path: AppRoutes.cacheManagement, - name: 'cache-management', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const CacheManagementPage()), - ), - - // 稍后阅读 — iOS 滑入 - GoRoute( - path: AppRoutes.readLater, - name: 'read-later', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ReadLaterPage()), - ), - - // 发现页 — iOS 滑入 - GoRoute( - path: AppRoutes.discover, - name: 'discover', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const DiscoverPage()), - ), - - // 会话流 — iOS 滑入 - GoRoute( - path: '${AppRoutes.chatFlow}/:conversationId', - name: 'chat-flow', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final convId = state.pathParameters['conversationId'] ?? 'default'; - return iosSlideTransition( - state: state, - child: ChatFlowPage(conversationId: convId), - ); - }, - ), - - // 聊天设置 — iOS 滑入 - GoRoute( - path: '${AppRoutes.chatSettings}/:conversationId', - name: 'chat-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final convId = state.pathParameters['conversationId'] ?? 'default'; - return iosSlideTransition( - state: state, - child: ChatSettingsPage(conversationId: convId), - ); - }, - ), - - // 隐藏会话管理 — iOS 滑入 - GoRoute( - path: AppRoutes.hiddenSessions, - name: 'hidden-sessions', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const HiddenSessionsPage()), - ), - - // 稍后读会话 — iOS 滑入 - GoRoute( - path: AppRoutes.readlaterChat, - name: 'readlater-chat', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => iosSlideTransition( - state: state, - child: const ChatFlowPage( - conversationId: 'readlater', - sessionType: ChatSessionType.readlater, - ), - ), - ), - - // 文件传输助手 — iOS 滑入 - GoRoute( - path: AppRoutes.fileTransfer, - name: 'file-transfer', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const FileTransferPage()), - ), - - // 翻译助手 — iOS 滑入 - GoRoute( - path: AppRoutes.translate, - name: 'translate', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const TranslatePage()), - ), - - // 翻译设置 — iOS 滑入 - GoRoute( - path: AppRoutes.translateSettings, - name: 'translate-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => iosSlideTransition( - state: state, - child: const TranslateSettingsPage(), - ), - ), - - // 传输聊天 — iOS 滑入 - GoRoute( - path: AppRoutes.transferChat, - name: 'transfer-chat', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final device = state.extra as TransferDevice?; - return iosSlideTransition( - state: state, - child: TransferChatPage( - peerDevice: - device ?? - TransferDevice( - id: 'unknown', - alias: '未知设备', - deviceType: DeviceType.mobile, - port: 53317, - pairingMethod: PairingMethod.lan, - preferredTransport: TransportType.localsendHttp, - lastSeen: DateTime.now(), - isOnline: false, - isVerified: false, - ), - ), - ); - }, - ), - - // 设备配对 — iOS 滑入 - GoRoute( - path: AppRoutes.devicePairing, - name: 'device-pairing', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const DevicePairingPage()), - ), - - // 协作画布 — iOS 滑入 - GoRoute( - path: '${AppRoutes.canvas}/:id', - name: 'canvas', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final canvasId = state.pathParameters['id'] ?? ''; - final peerDeviceId = state.uri.queryParameters['peerDeviceId']; - final userId = state.uri.queryParameters['userId'] ?? ''; - return iosSlideTransition( - state: state, - child: CanvasPage( - canvasId: canvasId, - peerDeviceId: peerDeviceId, - userId: userId, - ), - ); - }, - ), - - // 剪贴板同步 — iOS 滑入 - GoRoute( - path: AppRoutes.clipboard, - name: 'clipboard', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ClipboardFlowPage()), - ), - - // 屏幕共享 — iOS 滑入 - GoRoute( - path: '${AppRoutes.screenShare}/:id', - name: 'screen-share', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final sessionId = state.pathParameters['id'] ?? ''; - final peerDeviceId = state.uri.queryParameters['peerDeviceId']; - return iosSlideTransition( - state: state, - child: ScreenSharePage( - sessionId: sessionId, - peerDeviceId: peerDeviceId, - ), - ); - }, - ), - - // 足迹 — iOS 滑入 - GoRoute( - path: AppRoutes.footprint, - name: 'footprint', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const FootprintPage()), - ), - - // 日签卡片 — iOS 滑入 - GoRoute( - path: AppRoutes.dailyCard, - name: 'daily-card', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const DailyCardPage()), - ), - - // 壁纸模板广场 — iOS 滑入 - GoRoute( - path: AppRoutes.templateGallery, - name: 'template-gallery', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const TemplateGalleryPage()), - ), - - // 阅读报告 — iOS 滑入 - GoRoute( - path: AppRoutes.readingReport, - name: 'reading-report', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ReadingReportPage()), - ), - - // 天气诗词 — iOS 滑入 - GoRoute( - path: AppRoutes.weather, - name: 'weather', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const WeatherPage()), - ), - - GoRoute( - path: AppRoutes.weatherSettings, - name: 'weather-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const WeatherSettingsPage()), - ), - - // 今日诗词 — iOS 滑入 - GoRoute( - path: AppRoutes.poetry, - name: 'poetry', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const PoetryPage()), - ), - - GoRoute( - path: AppRoutes.poetrySettings, - name: 'poetry-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const PoetrySettingsPage()), - ), - - // 番茄钟 — iOS 滑入 - GoRoute( - path: AppRoutes.pomodoro, - name: 'pomodoro', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const PomodoroPage()), - ), - - // 倒计时 — iOS 滑入 - GoRoute( - path: AppRoutes.countdown, - name: 'countdown', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const CountdownPage()), - ), - GoRoute( - path: AppRoutes.solarTerm, - name: 'solar-term', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const SolarTermPage()), - ), - GoRoute( - path: AppRoutes.notificationSettings, - name: 'notification-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => iosSlideTransition( - state: state, - child: const NotificationSettingsPage(), - ), - ), - GoRoute( - path: AppRoutes.smartModeSettings, - name: 'smart-mode-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => iosSlideTransition( - state: state, - child: const SmartModeSettingsPage(), - ), - ), - GoRoute( - path: AppRoutes.moreSettings, - name: 'more-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const MoreSettingsPage()), - ), - GoRoute( - path: AppRoutes.permissionManagement, - name: 'permission-management', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => iosSlideTransition( - state: state, - child: const PermissionManagementPage(), - ), - ), - GoRoute( - path: AppRoutes.privacyPolicy, - name: 'privacy-policy', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const PrivacyPolicyPage()), - ), - GoRoute( - path: AppRoutes.logViewer, - name: 'log-viewer', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const LogViewerPage()), - ), - GoRoute( - path: AppRoutes.crashLog, - name: 'crash-log', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const CrashLogPage()), - ), - GoRoute( - path: AppRoutes.knowledgeGraph, - name: 'knowledge-graph', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const KnowledgeGraphPage()), - ), - GoRoute( - path: AppRoutes.studyPlan, - name: 'study-plan', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const StudyPlanPage()), - ), - - // 每日运势 — iOS 滑入 - GoRoute( - path: AppRoutes.dailyFortune, - name: 'daily-fortune', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const DailyFortunePage()), - ), - - // 运势设置 — iOS 滑入 - GoRoute( - path: AppRoutes.dailyFortuneSettings, - name: 'daily-fortune-settings', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const FortuneSettingsPage()), - ), - - // 进度会话 — iOS 滑入 - GoRoute( - path: AppRoutes.progress, - name: 'progress', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ProgressPage()), - ), - - // 学习进度 — 已合并到学习中心,旧路径重定向 - GoRoute( - path: AppRoutes.learningProgress, - name: 'learning-progress', - parentNavigatorKey: rootNavigatorKey, - redirect: (context, state) => AppRoutes.learning, - ), - - // 关于 — iOS 滑入 - GoRoute( - path: AppRoutes.about, - name: 'about', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const AboutPage()), - ), - - // 软件协议列表 — iOS 滑入 - GoRoute( - path: AppRoutes.agreements, - name: 'agreements', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const AgreementListPage()), - ), - - // 协议详情 — iOS 滑入 - GoRoute( - path: '${AppRoutes.agreement}/:type', - name: 'agreement', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final typeStr = state.pathParameters['type'] ?? 'privacy-policy'; - final type = AgreementType.fromRoute(typeStr); - return iosSlideTransition( - state: state, - child: AgreementPage(type: type), - ); - }, - ), - - // 分类详情 — iOS 滑入 - GoRoute( - path: AppRoutes.categoryDetail, - name: 'category-detail', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final extra = state.extra as Map?; - final type = - state.pathParameters['type'] ?? extra?['type'] as String? ?? ''; - final name = extra?['name'] as String? ?? ''; - final icon = extra?['icon'] as String? ?? ''; - return iosSlideTransition( - state: state, - child: CategoryDetailPage(type: type, name: name, icon: icon), - ); - }, - ), - - // 热搜榜 — iOS 滑入 - GoRoute( - path: AppRoutes.hotSearch, - name: 'hot-search', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const HotSearchPage()), - ), - - // 用户偏好 — iOS 滑入 - GoRoute( - path: AppRoutes.userPreference, - name: 'user-preference', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const UserPreferencePage()), - ), - - // 分享历史 — iOS 滑入 - GoRoute( - path: AppRoutes.shareHistory, - name: 'share-history', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ShareHistoryPage()), - ), - - // 分享目标编辑 — iOS 滑入 - GoRoute( - path: AppRoutes.shareTargetEdit, - name: 'share-target-edit', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ShareTargetEditPage()), - ), - - // 学习中心 — iOS 滑入 - GoRoute( - path: AppRoutes.learning, - name: 'learning', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const LearningCenterPage()), - ), - - // 国学经典 — iOS 滑入 - GoRoute( - path: AppRoutes.classics, - name: 'classics', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ClassicsPage()), - ), - - // 健康生活 — iOS 滑入 - GoRoute( - path: AppRoutes.health, - name: 'health', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const HealthPage()), - ), - - // 游戏中心 — iOS 滑入 - GoRoute( - path: AppRoutes.game, - name: 'game', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const GameCenterPage()), - ), - - // 成就中心 — iOS 滑入 - GoRoute( - path: AppRoutes.achievement, - name: 'achievement', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const AchievementPage()), - ), - - // 学习打卡 — 已合并到成就中心,旧路径重定向 - GoRoute( - path: AppRoutes.checkin, - name: 'checkin', - parentNavigatorKey: rootNavigatorKey, - redirect: (context, state) => AppRoutes.achievement, - ), - - // 勋章墙 — iOS 滑入 - GoRoute( - path: AppRoutes.badgeWall, - name: 'badge-wall', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const BadgeWallPage()), - ), - - // 每日任务 — iOS 滑入 - GoRoute( - path: AppRoutes.dailyTask, - name: 'daily-task', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const DailyTaskPage()), - ), - - // 赛季排行榜 — iOS 滑入 - GoRoute( - path: AppRoutes.rank, - name: 'rank', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const RankPage()), - ), - - // 文章广场 — iOS 滑入 - GoRoute( - path: AppRoutes.articles, - name: 'articles', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const ArticleListPage()), - ), - - // 文章详情 — iOS 滑入 - GoRoute( - path: AppRoutes.articleDetail, - name: 'article-detail', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final id = int.tryParse(state.pathParameters['id'] ?? '') ?? 0; - return iosSlideTransition( - state: state, - child: ArticleDetailPage(articleId: id), - ); - }, - ), - - // 文章编辑 — iOS 滑入 - GoRoute( - path: AppRoutes.articleEdit, - name: 'article-edit', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final extra = state.extra as Map?; - return iosSlideTransition( - state: state, - child: ArticleEditPage(articleId: extra?['articleId'] as int?), - ); - }, - ), - - // 我的文章 — iOS 滑入 - GoRoute( - path: AppRoutes.myArticles, - name: 'my-articles', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const MyArticlesPage()), - ), - - // 内容查重 — iOS 滑入 - GoRoute( - path: AppRoutes.check, - name: 'check', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const CheckPage()), - ), - - // 金币记录 — iOS 滑入 - GoRoute( - path: AppRoutes.coinLog, - name: 'coin-log', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const CoinLogPage()), - ), - - // 公开用户主页 — iOS 滑入 - GoRoute( - path: AppRoutes.publicProfile, - name: 'public-profile', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) { - final uid = int.tryParse(state.pathParameters['uid'] ?? '') ?? 0; - return iosSlideTransition( - state: state, - child: PublicProfilePage(uid: uid), - ); - }, - ), - - // 个人中心 — iOS 滑入 - GoRoute( - path: AppRoutes.userCenter, - name: 'user-center', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const UserCenterPage()), - ), - - // 标签云 — iOS 滑入 - GoRoute( - path: AppRoutes.tagCloud, - name: 'tag-cloud', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const TagCloudPage()), - ), - - // 用户数据统计 — iOS 滑入 - GoRoute( - path: AppRoutes.userStats, - name: 'user-stats', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const UserStatsPage()), - ), - - // 调试信息 — iOS 滑入 - GoRoute( - path: AppRoutes.userDebug, - name: 'user-debug', - parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => - iosSlideTransition(state: state, child: const UserDebugPage()), - ), + ...buildEditorRoutes(rootNavigatorKey), + ...buildSettingsRoutes(rootNavigatorKey), + ...buildToolRoutes(rootNavigatorKey), + ...buildUserRoutes(rootNavigatorKey), + ...buildContentRoutes(rootNavigatorKey), + ...buildFeatureRoutes(rootNavigatorKey), ], - errorBuilder: (context, state) => const _NotFoundPage(), + errorBuilder: (context, state) => const NotFoundPage(), ); -// ============================================================ -// 404 页面 -// ============================================================ - String _resolveInitialLocation() { if (KvStorage.isFirstLaunch || KvStorage.shouldShowOnboarding) { + if (pu.isOhos) { + Log.i( + '🟢 [OHOS] _resolveInitialLocation: isFirstLaunch=${KvStorage.isFirstLaunch}, shouldShowOnboarding=${KvStorage.shouldShowOnboarding} → /onboarding', + ); + } return AppRoutes.onboarding; } final savedPage = KvStorage.getString('general_startup_page'); @@ -1467,34 +158,3 @@ class _OhosRouteObserver extends NavigatorObserver { Log.i('🟢 [OHOS] Route didStartUserGesture: ${route.settings.name}'); } } - -class _NotFoundPage extends StatelessWidget { - const _NotFoundPage(); - - @override - Widget build(BuildContext context) { - return CupertinoPageScaffold( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('🔍', style: TextStyle(fontSize: 64)), - const SizedBox(height: 16), - Text( - '页面不存在', - style: TextStyle( - fontSize: 18, - color: CupertinoColors.secondaryLabel.resolveFrom(context), - ), - ), - const SizedBox(height: 24), - CupertinoButton( - onPressed: () => context.go(AppRoutes.home), - child: const Text('返回首页'), - ), - ], - ), - ), - ); - } -} diff --git a/lib/core/router/app_routes.dart b/lib/core/router/app_routes.dart new file mode 100644 index 00000000..af639aeb --- /dev/null +++ b/lib/core/router/app_routes.dart @@ -0,0 +1,145 @@ +// ============================================================ +// 闲言APP — 路由路径常量 + 路由辅助组件 +// 创建时间: 2026-04-20 +// 更新时间: 2026-05-22 +// 作用: 集中管理所有路由路径字符串,供各路由模块和页面统一引用 +// 上次更新: 从 app_router.dart 拆分出独立常量文件 +// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +class AppRoutes { + AppRoutes._(); + + static const String home = '/home'; + static const String inspiration = '/inspiration'; + static const String profile = '/profile'; + static const String editor = '/editor'; + static const String search = '/search'; + static const String source = '/source'; + static const String member = '/member'; + static const String widgetManagement = '/widget-management'; + static const String themeSettings = '/settings/theme'; + static const String generalSettings = '/settings/general'; + static const String languageSettings = '/settings/language'; + static const String accountSettings = '/settings/account'; + static const String passwordSettings = '/settings/password'; + static const String securityQuestion = '/settings/security-question'; + static const String dataManagement = '/settings/data'; + static const String accountDeletion = '/settings/account/deletion'; + static const String fontManagement = '/settings/fonts'; + static const String favorites = '/favorites'; + static const String history = '/history'; + static const String likes = '/likes'; + static const String quickCard = '/quick-card'; + static const String login = '/login'; + static const String signin = '/signin'; + static const String myDevices = '/my-devices'; + static const String qrcodeLogin = '/qrcode-login'; + static const String noteList = '/notes'; + static const String noteEdit = '/notes/edit'; + static const String statistics = '/statistics'; + static const String correction = '/correction'; + static const String hanziTool = '/hanzi-tool'; + static const String offline = '/offline'; + static const String cacheManagement = '/cache'; + static const String readLater = '/readlater'; + static const String discover = '/discover'; + static const String categoryDetail = '/category/:type'; + static const String hotSearch = '/hot-search'; + static const String learning = '/learning'; + static const String classics = '/classics'; + static const String health = '/health'; + static const String game = '/game'; + static const String achievement = '/achievement'; + static const String checkin = '/achievement/checkin'; + static const String badgeWall = '/badge-wall'; + static const String dailyTask = '/daily-task'; + static const String rank = '/rank'; + static const String articles = '/articles'; + static const String articleDetail = '/article/:id'; + static const String articleEdit = '/article/edit'; + static const String myArticles = '/article/mine'; + static const String check = '/check'; + static const String coinLog = '/coin-log'; + static const String publicProfile = '/user/:uid'; + static const String userPreference = '/user-preference'; + static const String shareHistory = '/share/history'; + static const String shareTargetEdit = '/share/targets'; + static const String userCenter = '/user-center'; + static const String userDebug = '/user-debug'; + static const String chatFlow = '/chat-flow'; + static const String chatSettings = '/chat-settings'; + static const String hiddenSessions = '/hidden-sessions'; + static const String footprint = '/footprint'; + static const String dailyCard = '/daily-card'; + static const String templateGallery = '/template-gallery'; + static const String readingReport = '/reading-report'; + static const String weather = '/weather'; + static const String weatherSettings = '/weather/settings'; + static const String poetry = '/poetry'; + static const String poetrySettings = '/poetry/settings'; + static const String pomodoro = '/pomodoro'; + static const String countdown = '/countdown'; + static const String solarTerm = '/solar-term'; + static const String notificationSettings = '/notification-settings'; + static const String smartModeSettings = '/smart-mode-settings'; + static const String moreSettings = '/more-settings'; + static const String permissionManagement = '/permission-management'; + static const String privacyPolicy = '/privacy-policy'; + static const String logViewer = '/log-viewer'; + static const String crashLog = '/crash-log'; + static const String knowledgeGraph = '/knowledge-graph'; + static const String studyPlan = '/study-plan'; + static const String dailyFortune = '/daily-fortune'; + static const String dailyFortuneSettings = '/daily-fortune/settings'; + static const String progress = '/progress'; + static const String learningProgress = '/learning-progress'; + static const String tagCloud = '/tag-cloud'; + static const String userStats = '/user-stats'; + static const String about = '/about'; + static const String agreements = '/agreements'; + static const String agreement = '/agreement'; + static const String fileTransfer = '/file-transfer'; + static const String transferChat = '/transfer-chat'; + static const String devicePairing = '/device-pairing'; + static const String canvas = '/canvas'; + static const String clipboard = '/clipboard'; + static const String screenShare = '/screen-share'; + static const String readlaterChat = '/readlater-chat'; + static const String translate = '/translate'; + static const String translateSettings = '/translate-settings'; + static const String onboarding = '/onboarding'; +} + +class NotFoundPage extends StatelessWidget { + const NotFoundPage({super.key}); + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('🔍', style: TextStyle(fontSize: 64)), + const SizedBox(height: 16), + Text( + '页面不存在', + style: TextStyle( + fontSize: 18, + color: CupertinoColors.secondaryLabel.resolveFrom(context), + ), + ), + const SizedBox(height: 24), + CupertinoButton( + onPressed: () => context.go(AppRoutes.home), + child: const Text('返回首页'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/core/router/content_routes.dart b/lib/core/router/content_routes.dart new file mode 100644 index 00000000..748008e1 --- /dev/null +++ b/lib/core/router/content_routes.dart @@ -0,0 +1,259 @@ +// ============================================================ +// 闲言APP — 内容模块路由 +// 创建时间: 2026-05-22 +// 更新时间: 2026-05-22 +// 作用: 笔记、统计、纠错、发现、搜索、文章、诗词、运势等 GoRoute 定义 +// 上次更新: 从 app_router.dart 拆分 +// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +import '../../features/note/presentation/note_list_page.dart'; +import '../../features/note/presentation/note_edit_page.dart'; +import '../../features/statistics/presentation/statistics_page.dart'; +import '../../features/correction/presentation/correction_page.dart'; +import '../../features/discover/presentation/discover_page.dart'; +import '../../features/discover/presentation/category_detail_page.dart'; +import '../../features/search/presentation/search_page.dart'; +import '../../features/search/presentation/hot_search_page.dart'; +import '../../features/classics/presentation/classics_page.dart'; +import '../../features/article/presentation/article_list_page.dart'; +import '../../features/article/presentation/article_detail_page.dart'; +import '../../features/article/presentation/article_edit_page.dart'; +import '../../features/article/presentation/my_articles_page.dart'; +import '../../features/check/presentation/check_page.dart'; +import '../../features/daily_card/presentation/daily_card_page.dart'; +import '../../features/template/presentation/template_gallery_page.dart'; +import '../../features/reading_report/presentation/reading_report_page.dart'; +import '../../features/poetry/presentation/poetry_page.dart'; +import '../../features/poetry/presentation/poetry_settings_page.dart'; +import '../../features/knowledge_graph/presentation/knowledge_graph_page.dart'; +import '../../features/study_plan/presentation/study_plan_page.dart'; +import '../../features/daily_fortune/presentation/daily_fortune_page.dart'; +import '../../features/daily_fortune/presentation/fortune_settings_page.dart'; +import '../../features/progress/presentation/progress_page.dart'; +import '../../features/agreements/presentation/agreement_list_page.dart'; +import '../../features/agreements/presentation/agreement_page.dart'; +import '../../features/agreements/data/agreement_types.dart'; +import '../utils/page_transitions.dart'; +import 'app_routes.dart'; + +List buildContentRoutes( + GlobalKey rootNavigatorKey, +) => [ + GoRoute( + path: AppRoutes.search, + name: 'search', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const SearchPage()), + ), + GoRoute( + path: AppRoutes.noteList, + name: 'note-list', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const NoteListPage()), + ), + GoRoute( + path: AppRoutes.noteEdit, + name: 'note-edit', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final noteId = state.uri.queryParameters['id']; + return iosSlideTransition( + state: state, + child: NoteEditPage( + noteId: noteId != null ? int.tryParse(noteId) : null, + ), + ); + }, + ), + GoRoute( + path: AppRoutes.statistics, + name: 'statistics', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const StatisticsPage()), + ), + GoRoute( + path: AppRoutes.correction, + name: 'correction', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const CorrectionPage()), + ), + GoRoute( + path: AppRoutes.discover, + name: 'discover', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const DiscoverPage()), + ), + GoRoute( + path: AppRoutes.categoryDetail, + name: 'category-detail', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final extra = state.extra as Map?; + final type = + state.pathParameters['type'] ?? extra?['type'] as String? ?? ''; + final name = extra?['name'] as String? ?? ''; + final icon = extra?['icon'] as String? ?? ''; + return iosSlideTransition( + state: state, + child: CategoryDetailPage(type: type, name: name, icon: icon), + ); + }, + ), + GoRoute( + path: AppRoutes.hotSearch, + name: 'hot-search', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const HotSearchPage()), + ), + GoRoute( + path: AppRoutes.classics, + name: 'classics', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ClassicsPage()), + ), + GoRoute( + path: AppRoutes.articles, + name: 'articles', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ArticleListPage()), + ), + GoRoute( + path: AppRoutes.articleDetail, + name: 'article-detail', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final id = int.tryParse(state.pathParameters['id'] ?? '') ?? 0; + return iosSlideTransition( + state: state, + child: ArticleDetailPage(articleId: id), + ); + }, + ), + GoRoute( + path: AppRoutes.articleEdit, + name: 'article-edit', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final extra = state.extra as Map?; + return iosSlideTransition( + state: state, + child: ArticleEditPage(articleId: extra?['articleId'] as int?), + ); + }, + ), + GoRoute( + path: AppRoutes.myArticles, + name: 'my-articles', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const MyArticlesPage()), + ), + GoRoute( + path: AppRoutes.check, + name: 'check', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const CheckPage()), + ), + GoRoute( + path: AppRoutes.dailyCard, + name: 'daily-card', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const DailyCardPage()), + ), + GoRoute( + path: AppRoutes.templateGallery, + name: 'template-gallery', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const TemplateGalleryPage()), + ), + GoRoute( + path: AppRoutes.readingReport, + name: 'reading-report', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ReadingReportPage()), + ), + GoRoute( + path: AppRoutes.poetry, + name: 'poetry', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const PoetryPage()), + ), + GoRoute( + path: AppRoutes.poetrySettings, + name: 'poetry-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const PoetrySettingsPage()), + ), + GoRoute( + path: AppRoutes.knowledgeGraph, + name: 'knowledge-graph', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const KnowledgeGraphPage()), + ), + GoRoute( + path: AppRoutes.studyPlan, + name: 'study-plan', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const StudyPlanPage()), + ), + GoRoute( + path: AppRoutes.dailyFortune, + name: 'daily-fortune', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const DailyFortunePage()), + ), + GoRoute( + path: AppRoutes.dailyFortuneSettings, + name: 'daily-fortune-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const FortuneSettingsPage()), + ), + GoRoute( + path: AppRoutes.progress, + name: 'progress', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ProgressPage()), + ), + GoRoute( + path: AppRoutes.agreements, + name: 'agreements', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const AgreementListPage()), + ), + GoRoute( + path: '${AppRoutes.agreement}/:type', + name: 'agreement', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final typeStr = state.pathParameters['type'] ?? 'privacy-policy'; + final type = AgreementType.fromRoute(typeStr); + return iosSlideTransition( + state: state, + child: AgreementPage(type: type), + ); + }, + ), +]; diff --git a/lib/core/router/editor_router.dart b/lib/core/router/editor_router.dart index 94aa4ab2..50e66669 100644 --- a/lib/core/router/editor_router.dart +++ b/lib/core/router/editor_router.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — 编辑器路由管理 // 创建时间: 2026-04-25 -// 更新时间: 2026-04-25 +// 更新时间: 2026-05-22 // 作用: 统一管理编辑器内所有页面跳转和弹窗,基于 go_router + Navigator -// 上次更新: 初始创建 — 从散落的 Navigator.push 迁移到统一管理 +// 上次更新: 从 app_router.dart 拆分编辑器 GoRoute 定义到 buildEditorRoutes() // ============================================================ import 'dart:typed_data'; @@ -11,8 +11,14 @@ import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; -import '../../../core/router/app_router.dart'; -import '../../../editor/services/core/model_catalog_service.dart'; +import '../../editor/pages/editor/editor_page.dart'; +import '../../editor/pages/tools/image_preview_page.dart'; +import '../../editor/pages/tools/image_crop_page.dart'; +import '../../editor/pages/tools/draft_list_page.dart'; +import '../../editor/pages/tools/model_3d_preview_page.dart'; +import '../../editor/services/core/model_catalog_service.dart'; +import '../utils/page_transitions.dart'; +import 'app_routes.dart'; class EditorRoutes { EditorRoutes._(); @@ -24,6 +30,86 @@ class EditorRoutes { static const String model3dPreview = '/editor/3d-preview'; } +List buildEditorRoutes(GlobalKey rootNavigatorKey) => [ + GoRoute( + path: AppRoutes.editor, + name: 'editor', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final text = state.uri.queryParameters['text']; + return heroineTransition( + state: state, + child: EditorPage(initialText: text), + ); + }, + ), + GoRoute( + path: EditorRoutes.imagePreview, + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final extra = state.extra as ImagePreviewExtra?; + if (extra?.bytes != null) { + return iosSlideTransition( + state: state, + child: ImagePreviewPage.fromBytes(extra!.bytes!), + ); + } + return iosSlideTransition( + state: state, + child: ImagePreviewPage( + imageProvider: extra!.provider!, + heroTag: state.uri.queryParameters['heroTag'] ?? '', + ), + ); + }, + ), + GoRoute( + path: EditorRoutes.imageCrop, + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final extra = state.extra as ImageCropExtra?; + return iosSlideTransition( + state: state, + child: ImageCropPage(imageBytes: extra?.bytes ?? Uint8List(0)), + ); + }, + ), + GoRoute( + path: EditorRoutes.draftList, + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const DraftListPage()), + ), + GoRoute( + path: EditorRoutes.imageGallery, + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final extra = state.extra as ImageGalleryExtra?; + return iosSlideTransition( + state: state, + child: ImageGalleryPage( + images: extra?.images ?? [], + initialIndex: extra?.initialIndex ?? 0, + ), + ); + }, + ), + GoRoute( + path: EditorRoutes.model3dPreview, + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final model = state.extra as Model3DItem?; + if (model == null) { + return iosSlideTransition(state: state, child: const NotFoundPage()); + } + return iosSlideTransition( + state: state, + child: Model3DPreviewPage(model: model), + ); + }, + ), +]; + extension EditorNav on BuildContext { void goToImagePreview({ required ImageProvider provider, diff --git a/lib/core/router/feature_routes.dart b/lib/core/router/feature_routes.dart new file mode 100644 index 00000000..0668964a --- /dev/null +++ b/lib/core/router/feature_routes.dart @@ -0,0 +1,72 @@ +// ============================================================ +// 闲言APP — 功能模块路由 +// 创建时间: 2026-05-22 +// 更新时间: 2026-05-22 +// 作用: 天气、番茄钟、倒计时、节气、健康、游戏等独立功能 GoRoute 定义 +// 上次更新: 从 app_router.dart 拆分 +// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +import '../../features/weather/presentation/weather_page.dart'; +import '../../features/weather/presentation/weather_settings_page.dart'; +import '../../features/pomodoro/presentation/pomodoro_page.dart'; +import '../../features/countdown/presentation/countdown_page.dart'; +import '../../features/solar_term/presentation/solar_term_page.dart'; +import '../../features/health/presentation/health_page.dart'; +import '../../features/tool_center/game/presentation/game_center_page.dart'; +import '../utils/page_transitions.dart'; +import 'app_routes.dart'; + +List buildFeatureRoutes(GlobalKey rootNavigatorKey) => [ + GoRoute( + path: AppRoutes.weather, + name: 'weather', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const WeatherPage()), + ), + GoRoute( + path: AppRoutes.weatherSettings, + name: 'weather-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const WeatherSettingsPage()), + ), + GoRoute( + path: AppRoutes.pomodoro, + name: 'pomodoro', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const PomodoroPage()), + ), + GoRoute( + path: AppRoutes.countdown, + name: 'countdown', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const CountdownPage()), + ), + GoRoute( + path: AppRoutes.solarTerm, + name: 'solar-term', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const SolarTermPage()), + ), + GoRoute( + path: AppRoutes.health, + name: 'health', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const HealthPage()), + ), + GoRoute( + path: AppRoutes.game, + name: 'game', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const GameCenterPage()), + ), +]; diff --git a/lib/core/router/ohos_nav_bridge.dart b/lib/core/router/ohos_nav_bridge.dart index 760ddd21..e5ecad10 100644 --- a/lib/core/router/ohos_nav_bridge.dart +++ b/lib/core/router/ohos_nav_bridge.dart @@ -1,11 +1,17 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 鸿蒙端导航桥接 /// 创建时间: 2026-05-18 -/// 更新时间: 2026-05-21 +/// 更新时间: 2026-05-22 /// 作用: 鸿蒙端注册表+中间件路由架构,桥接GoRouter路由到Navigator.push -/// 上次更新: v14.65.0 重构为注册表+中间件架构,自动参数解析,AuthMiddleware,增强replace/go +/// 上次更新: 注册/onboarding引导页路由,修复引导页在鸿蒙端不可导航的问题 /// ============================================================ /// +/// ⚠️ 同步提醒:新增路由时,必须同时更新以下文件: +/// 1. app_routes.dart — 路由路径常量 +/// 2. 对应的模块路由文件 (settings_routes / tool_routes / editor_router / user_routes / content_routes / feature_routes) +/// 3. 本文件 (ohos_nav_bridge.dart) — 鸿蒙端路由注册表 +/// 4. CHANGELOG.md — 变更日志 +/// /// 鸿蒙端不使用GoRouter(因MaterialApp.router白屏), /// 二级页面通过Navigator.of(context).push()跳转, /// 本工具使用注册表+中间件架构,自动解析路径参数/查询参数/extra, @@ -17,6 +23,8 @@ import 'package:xianyan/core/utils/platform_utils.dart' as pu; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/core/layout/ohos_app_shell.dart'; import 'package:xianyan/features/auth/providers/auth_provider.dart'; +import 'package:xianyan/features/mine/settings/providers/general_settings_provider.dart'; +import 'package:xianyan/shared/widgets/app_page_transitions.dart'; import 'package:xianyan/features/search/presentation/search_page.dart'; import 'package:xianyan/features/home/presentation/favorite_page.dart'; @@ -109,6 +117,7 @@ import 'package:xianyan/features/file_transfer/collaboration/clipboard/pages/cli import 'package:xianyan/features/tool_center/inspiration/presentation/pages/translate/translate_page.dart'; import 'package:xianyan/features/tool_center/inspiration/presentation/pages/translate/translate_settings_page.dart'; import 'package:xianyan/features/widget/presentation/widget_management_page.dart'; +import 'package:xianyan/features/onboarding/presentation/onboarding_page.dart'; /// 路由参数解析结果 — 自动从 URL 提取路径参数和查询参数 class OhosRouteParams { @@ -246,6 +255,12 @@ class OhosNavBridge { /// 路由注册表 — 统一管理所有路由(含参数路由和中间件) static final List _routes = [ + // ---- 引导页 ---- + OhosRouteEntry( + pattern: '/onboarding', + builder: (_) => const OnboardingPage(), + ), + // ---- 无参数路由 ---- OhosRouteEntry(pattern: '/search', builder: (_) => const SearchPage()), OhosRouteEntry(pattern: '/favorites', builder: (_) => const FavoritePage()), @@ -686,6 +701,7 @@ class OhosNavBridge { BuildContext context, String route, { Object? extra, + PageTransitionMode? transitionMode, }) { if (!pu.isOhos) { return _goRouterPush(context, route); @@ -697,9 +713,18 @@ class OhosNavBridge { return _pushNotFound(context, Uri.parse(route).path); } - return Navigator.of( - context, - ).push(CupertinoPageRoute(builder: (_) => widget)); + final transition = transitionMode != null + ? (transitionMode == PageTransitionMode.navigate + ? appSlideTransition + : appFadeScaleTransition) + : resolveTransitionFromContext(context); + + return Navigator.of(context).push( + transition( + builder: (_) => widget, + settings: RouteSettings(name: route), + ), + ); } /// 鸿蒙端导航替换 @@ -721,9 +746,14 @@ class OhosNavBridge { return Future.value(); } - return Navigator.of( - context, - ).pushReplacement(CupertinoPageRoute(builder: (_) => widget)); + final transition = resolveTransitionFromContext(context); + + return Navigator.of(context).pushReplacement( + transition( + builder: (_) => widget, + settings: RouteSettings(name: route), + ), + ); } /// 鸿蒙端导航回到根/切换Tab diff --git a/lib/core/router/settings_routes.dart b/lib/core/router/settings_routes.dart new file mode 100644 index 00000000..af110f1a --- /dev/null +++ b/lib/core/router/settings_routes.dart @@ -0,0 +1,150 @@ +// ============================================================ +// 闲言APP — 设置模块路由 +// 创建时间: 2026-05-22 +// 更新时间: 2026-05-22 +// 作用: 设置相关页面的 GoRoute 定义 +// 上次更新: 从 app_router.dart 拆分 +// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +import '../../features/mine/settings/presentation/theme/theme_settings_page.dart'; +import '../../features/mine/settings/presentation/general/general_settings_page.dart'; +import '../../features/mine/settings/presentation/language_settings_page.dart'; +import '../../features/mine/settings/presentation/account/account_settings_page.dart'; +import '../../features/mine/settings/presentation/account/account_deletion_page.dart'; +import '../../features/mine/settings/presentation/data_management_page.dart'; +import '../../features/mine/settings/presentation/account/change_password_page.dart'; +import '../../features/mine/settings/presentation/account/security_question_page.dart'; +import '../../features/mine/settings/presentation/font_management_page.dart'; +import '../../features/mine/settings/presentation/notification_settings_page.dart'; +import '../../features/mine/settings/presentation/smart_mode_settings_page.dart'; +import '../../features/mine/settings/presentation/more_settings_page.dart'; +import '../../features/mine/settings/presentation/privacy/permission_management_page.dart'; +import '../../features/mine/settings/presentation/privacy/privacy_policy_page.dart'; +import '../../features/mine/settings/presentation/privacy/log_viewer_page.dart'; +import '../../features/mine/settings/presentation/privacy/crash_log_page.dart'; +import '../utils/page_transitions.dart'; +import 'app_routes.dart'; + +List buildSettingsRoutes( + GlobalKey rootNavigatorKey, +) => [ + GoRoute( + path: AppRoutes.themeSettings, + name: 'theme-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ThemeSettingsPage()), + ), + GoRoute( + path: AppRoutes.generalSettings, + name: 'general-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const GeneralSettingsPage()), + ), + GoRoute( + path: AppRoutes.languageSettings, + name: 'language-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const LanguageSettingsPage()), + ), + GoRoute( + path: AppRoutes.accountSettings, + name: 'account-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const AccountSettingsPage()), + ), + GoRoute( + path: AppRoutes.passwordSettings, + name: 'password-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ChangePasswordPage()), + ), + GoRoute( + path: AppRoutes.securityQuestion, + name: 'security-question', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const SecurityQuestionPage()), + ), + GoRoute( + path: AppRoutes.dataManagement, + name: 'data-management', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const DataManagementPage()), + ), + GoRoute( + path: AppRoutes.accountDeletion, + name: 'account-deletion', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const AccountDeletionPage()), + ), + GoRoute( + path: AppRoutes.fontManagement, + name: 'font-management', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const FontManagementPage()), + ), + GoRoute( + path: AppRoutes.notificationSettings, + name: 'notification-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => iosSlideTransition( + state: state, + child: const NotificationSettingsPage(), + ), + ), + GoRoute( + path: AppRoutes.smartModeSettings, + name: 'smart-mode-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const SmartModeSettingsPage()), + ), + GoRoute( + path: AppRoutes.moreSettings, + name: 'more-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const MoreSettingsPage()), + ), + GoRoute( + path: AppRoutes.permissionManagement, + name: 'permission-management', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => iosSlideTransition( + state: state, + child: const PermissionManagementPage(), + ), + ), + GoRoute( + path: AppRoutes.privacyPolicy, + name: 'privacy-policy', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const PrivacyPolicyPage()), + ), + GoRoute( + path: AppRoutes.logViewer, + name: 'log-viewer', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const LogViewerPage()), + ), + GoRoute( + path: AppRoutes.crashLog, + name: 'crash-log', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const CrashLogPage()), + ), +]; diff --git a/lib/core/router/tool_routes.dart b/lib/core/router/tool_routes.dart new file mode 100644 index 00000000..0ffca463 --- /dev/null +++ b/lib/core/router/tool_routes.dart @@ -0,0 +1,261 @@ +// ============================================================ +// 闲言APP — 工具中心路由 +// 创建时间: 2026-05-22 +// 更新时间: 2026-05-22 +// 作用: 工具中心、AI聊天、翻译、文件传输、协作相关 GoRoute 定义 +// 上次更新: 从 app_router.dart 拆分 +// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +import '../../features/tool_center/inspiration/presentation/pages/tool/hanzi_tool_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/tool/calc_tool_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/tool/china_colors_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/tool_list_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/tool/ocr_tool_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/tool/pinyin_tool_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/chat/chat_flow_page.dart'; +import '../../features/tool_center/inspiration/models/chat_session.dart'; +import '../../features/tool_center/inspiration/presentation/pages/chat/chat_settings_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/chat/hidden_sessions_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/translate/translate_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/translate/translate_settings_page.dart'; +import '../../features/file_transfer/presentation/pages/file_transfer_page.dart'; +import '../../features/file_transfer/presentation/pages/transfer_chat_page.dart'; +import '../../features/file_transfer/presentation/pages/device_pairing_page.dart'; +import '../../features/file_transfer/models/models.dart'; +import '../../features/file_transfer/collaboration/canvas/pages/canvas_page.dart'; +import '../../features/file_transfer/collaboration/clipboard/pages/clipboard_flow_page.dart'; +import '../../features/file_transfer/collaboration/screen_share/pages/screen_share_page.dart'; +import '../utils/page_transitions.dart'; +import 'app_routes.dart'; + +List buildToolRoutes(GlobalKey rootNavigatorKey) => [ + GoRoute( + path: AppRoutes.hanziTool, + name: 'hanzi-tool', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final config = state.extra as HanziToolConfig?; + if (config == null) { + return iosSlideTransition(state: state, child: const NotFoundPage()); + } + return iosSlideTransition( + state: state, + child: HanziToolPage(config: config), + ); + }, + ), + GoRoute( + path: '/tool/calc', + name: 'calc-tool', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final config = state.extra as CalcToolConfig?; + if (config == null) { + return iosSlideTransition(state: state, child: const NotFoundPage()); + } + return iosSlideTransition( + state: state, + child: CalcToolPage(config: config), + ); + }, + ), + GoRoute( + path: '/tool/china_colors', + name: 'china-colors', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ChinaColorsPage()), + ), + GoRoute( + path: '/tool/list', + name: 'tool-list', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final extra = state.extra as Map?; + if (extra == null) { + return iosSlideTransition(state: state, child: const NotFoundPage()); + } + return iosSlideTransition( + state: state, + child: ToolListPage( + toolId: extra['toolId'] as String? ?? '', + title: extra['title'] as String? ?? '', + emoji: extra['emoji'] as String? ?? '', + listType: extra['listType'] as String? ?? 'hanzi_search', + searchType: extra['searchType'] as String?, + showSearch: extra['showSearch'] as bool? ?? false, + detailDesc: extra['detailDesc'] as String?, + ), + ); + }, + ), + GoRoute( + path: '/tool/ocr', + name: 'ocr-tool', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const OcrToolPage()), + ), + GoRoute( + path: '/tool/pinyin', + name: 'pinyin-tool', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const PinyinToolPage()), + ), + GoRoute( + path: '/tool/daily-recommend', + name: 'daily-recommend-tool', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => iosSlideTransition( + state: state, + child: const ToolListPage( + toolId: 'daily_recommend', + title: '每日推荐', + emoji: '🌟', + listType: 'daily_recommend', + ), + ), + ), + GoRoute( + path: '${AppRoutes.chatFlow}/:conversationId', + name: 'chat-flow', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final convId = state.pathParameters['conversationId'] ?? 'default'; + return iosSlideTransition( + state: state, + child: ChatFlowPage(conversationId: convId), + ); + }, + ), + GoRoute( + path: '${AppRoutes.chatSettings}/:conversationId', + name: 'chat-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final convId = state.pathParameters['conversationId'] ?? 'default'; + return iosSlideTransition( + state: state, + child: ChatSettingsPage(conversationId: convId), + ); + }, + ), + GoRoute( + path: AppRoutes.hiddenSessions, + name: 'hidden-sessions', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const HiddenSessionsPage()), + ), + GoRoute( + path: AppRoutes.readlaterChat, + name: 'readlater-chat', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => iosSlideTransition( + state: state, + child: const ChatFlowPage( + conversationId: 'readlater', + sessionType: ChatSessionType.readlater, + ), + ), + ), + GoRoute( + path: AppRoutes.translate, + name: 'translate', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const TranslatePage()), + ), + GoRoute( + path: AppRoutes.translateSettings, + name: 'translate-settings', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const TranslateSettingsPage()), + ), + GoRoute( + path: AppRoutes.fileTransfer, + name: 'file-transfer', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const FileTransferPage()), + ), + GoRoute( + path: AppRoutes.transferChat, + name: 'transfer-chat', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final device = state.extra as TransferDevice?; + return iosSlideTransition( + state: state, + child: TransferChatPage( + peerDevice: + device ?? + TransferDevice( + id: 'unknown', + alias: '未知设备', + deviceType: DeviceType.mobile, + port: 53317, + pairingMethod: PairingMethod.lan, + preferredTransport: TransportType.localsendHttp, + lastSeen: DateTime.now(), + isOnline: false, + isVerified: false, + ), + ), + ); + }, + ), + GoRoute( + path: AppRoutes.devicePairing, + name: 'device-pairing', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const DevicePairingPage()), + ), + GoRoute( + path: '${AppRoutes.canvas}/:id', + name: 'canvas', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final canvasId = state.pathParameters['id'] ?? ''; + final peerDeviceId = state.uri.queryParameters['peerDeviceId']; + final userId = state.uri.queryParameters['userId'] ?? ''; + return iosSlideTransition( + state: state, + child: CanvasPage( + canvasId: canvasId, + peerDeviceId: peerDeviceId, + userId: userId, + ), + ); + }, + ), + GoRoute( + path: AppRoutes.clipboard, + name: 'clipboard', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ClipboardFlowPage()), + ), + GoRoute( + path: '${AppRoutes.screenShare}/:id', + name: 'screen-share', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final sessionId = state.pathParameters['id'] ?? ''; + final peerDeviceId = state.uri.queryParameters['peerDeviceId']; + return iosSlideTransition( + state: state, + child: ScreenSharePage( + sessionId: sessionId, + peerDeviceId: peerDeviceId, + ), + ); + }, + ), +]; diff --git a/lib/core/router/user_routes.dart b/lib/core/router/user_routes.dart new file mode 100644 index 00000000..45abced4 --- /dev/null +++ b/lib/core/router/user_routes.dart @@ -0,0 +1,265 @@ +// ============================================================ +// 闲言APP — 用户/认证/个人中心路由 +// 创建时间: 2026-05-22 +// 更新时间: 2026-05-22 +// 作用: 登录、签到、设备、收藏、历史、会员、个人中心等 GoRoute 定义 +// 上次更新: 从 app_router.dart 拆分 +// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +import '../../features/auth/presentation/login_page.dart'; +import '../../features/auth/presentation/qrcode_login_page.dart'; +import '../../features/mine/signin/presentation/signin_page.dart'; +import '../../features/mine/user_center/presentation/my_devices_page.dart'; +import '../../features/home/presentation/favorite_page.dart'; +import '../../features/home/presentation/history_page.dart'; +import '../../features/home/presentation/providers/likes_page.dart'; +import '../../features/home/presentation/providers/offline_page.dart'; +import '../../features/home/presentation/cache_management_page.dart'; +import '../../features/home/presentation/providers/readlater_page.dart'; +import '../../features/tool_center/inspiration/presentation/pages/home/footprint_page.dart'; +import '../../features/mine/member/presentation/member_page.dart'; +import '../../features/widget/presentation/widget_management_page.dart'; +import '../../features/mine/profile/presentation/about_page.dart'; +import '../../features/mine/user_center/presentation/user_center_page.dart'; +import '../../features/mine/user_center/presentation/public_profile_page.dart'; +import '../../features/mine/user_center/presentation/tag_cloud_page.dart'; +import '../../features/statistics/presentation/user_stats_page.dart'; +import '../../features/mine/user_center/presentation/user_debug_page.dart'; +import '../../features/mine/user_center/presentation/coin_log_page.dart'; +import '../../features/search/presentation/user_preference_page.dart'; +import '../../features/share/presentation/share_history_page.dart'; +import '../../features/share/presentation/share_target_edit_page.dart'; +import '../../features/mine/user_center/presentation/learning_center_page.dart'; +import '../../features/mine/achievement/presentation/achievement_page.dart'; +import '../../features/mine/achievement/presentation/badge_wall_page.dart'; +import '../../features/task/presentation/daily_task_page.dart'; +import '../../features/rank/presentation/rank_page.dart'; +import '../../features/source/presentation/source_page.dart'; +import '../utils/page_transitions.dart'; +import 'app_routes.dart'; + +List buildUserRoutes(GlobalKey rootNavigatorKey) => [ + GoRoute( + path: AppRoutes.login, + name: 'login', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const LoginPage()), + ), + GoRoute( + path: AppRoutes.signin, + name: 'signin', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const SigninPage()), + ), + GoRoute( + path: AppRoutes.myDevices, + name: 'my-devices', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const MyDevicesPage()), + ), + GoRoute( + path: AppRoutes.qrcodeLogin, + name: 'qrcode-login', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const QrcodeLoginPage()), + ), + GoRoute( + path: AppRoutes.favorites, + name: 'favorites', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const FavoritePage()), + ), + GoRoute( + path: AppRoutes.history, + name: 'history', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const HistoryPage()), + ), + GoRoute( + path: AppRoutes.likes, + name: 'likes', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const LikesPage()), + ), + GoRoute( + path: AppRoutes.offline, + name: 'offline', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const OfflinePage()), + ), + GoRoute( + path: AppRoutes.cacheManagement, + name: 'cache-management', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const CacheManagementPage()), + ), + GoRoute( + path: AppRoutes.readLater, + name: 'read-later', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ReadLaterPage()), + ), + GoRoute( + path: AppRoutes.footprint, + name: 'footprint', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const FootprintPage()), + ), + GoRoute( + path: AppRoutes.member, + name: 'member', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + glassSheetTransition(state: state, child: const MemberPage()), + ), + GoRoute( + path: AppRoutes.widgetManagement, + name: 'widget-management', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const WidgetManagementPage()), + ), + GoRoute( + path: AppRoutes.about, + name: 'about', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const AboutPage()), + ), + GoRoute( + path: AppRoutes.userCenter, + name: 'user-center', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const UserCenterPage()), + ), + GoRoute( + path: AppRoutes.publicProfile, + name: 'public-profile', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + final uid = int.tryParse(state.pathParameters['uid'] ?? '') ?? 0; + return iosSlideTransition( + state: state, + child: PublicProfilePage(uid: uid), + ); + }, + ), + GoRoute( + path: AppRoutes.tagCloud, + name: 'tag-cloud', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const TagCloudPage()), + ), + GoRoute( + path: AppRoutes.userStats, + name: 'user-stats', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const UserStatsPage()), + ), + GoRoute( + path: AppRoutes.userDebug, + name: 'user-debug', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const UserDebugPage()), + ), + GoRoute( + path: AppRoutes.coinLog, + name: 'coin-log', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const CoinLogPage()), + ), + GoRoute( + path: AppRoutes.userPreference, + name: 'user-preference', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const UserPreferencePage()), + ), + GoRoute( + path: AppRoutes.shareHistory, + name: 'share-history', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ShareHistoryPage()), + ), + GoRoute( + path: AppRoutes.shareTargetEdit, + name: 'share-target-edit', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const ShareTargetEditPage()), + ), + GoRoute( + path: AppRoutes.learning, + name: 'learning', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const LearningCenterPage()), + ), + GoRoute( + path: AppRoutes.learningProgress, + name: 'learning-progress', + parentNavigatorKey: rootNavigatorKey, + redirect: (context, state) => AppRoutes.learning, + ), + GoRoute( + path: AppRoutes.achievement, + name: 'achievement', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const AchievementPage()), + ), + GoRoute( + path: AppRoutes.checkin, + name: 'checkin', + parentNavigatorKey: rootNavigatorKey, + redirect: (context, state) => AppRoutes.achievement, + ), + GoRoute( + path: AppRoutes.badgeWall, + name: 'badge-wall', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const BadgeWallPage()), + ), + GoRoute( + path: AppRoutes.dailyTask, + name: 'daily-task', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const DailyTaskPage()), + ), + GoRoute( + path: AppRoutes.rank, + name: 'rank', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const RankPage()), + ), + GoRoute( + path: AppRoutes.source, + name: 'source', + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => + iosSlideTransition(state: state, child: const SourcePage()), + ), +]; diff --git a/lib/core/services/auth/permission_service.dart b/lib/core/services/auth/permission_service.dart index 0c17a429..02ae4ff7 100644 --- a/lib/core/services/auth/permission_service.dart +++ b/lib/core/services/auth/permission_service.dart @@ -1,11 +1,13 @@ /// ============================================================ /// 闲言APP — 权限管理服务 /// 创建时间: 2026-04-23 -/// 更新时间: 2026-05-21 -/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备权限 -/// 上次更新: 重写AppPermission枚举,移除废弃storage权限,新增isRequired和usageScenes字段 +/// 更新时间: 2026-05-22 +/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板权限 +/// 上次更新: 移除3个高敏感权限(悬浮窗/通讯录/忽略电池优化),降低应用商店审核风险 /// ============================================================ +import 'dart:io'; + import 'package:flutter/cupertino.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -87,7 +89,41 @@ enum AppPermission { Color(0xFF64D2FF), usageScenes: ['文件传输 — 局域网发现', '设备连接 — WiFi直连'], ), - ; + microphone( + '麦克风', + Permission.microphone, + CupertinoIcons.mic_fill, + '用于语音朗读句子、语音搜索、AI对话语音输入。仅在您主动使用语音功能时请求,不会后台录音。', + Color(0xFFFF3B30), + usageScenes: ['语音朗读 — 朗读句子', '语音搜索 — 语音输入关键词', 'AI对话 — 语音输入消息'], + ), + storage( + '存储空间', + Permission.storage, + CupertinoIcons.folder_fill, + '用于保存编辑的卡片、壁纸到本地,导出字体文件和数据。Android 12及以下版本需要此权限。', + Color(0xFFFF9500), + usageScenes: ['保存卡片 — 导出到本地', '字体管理 — 下载字体文件', '数据导出 — 导出用户数据'], + ), + network( + '网络连接', + Permission.notification, + CupertinoIcons.wifi, + '闲言需要网络连接来获取句子、同步数据和推送通知。请在系统设置中确保网络权限已开启。', + Color(0xFF007AFF), + isRequired: true, + isVirtual: true, + usageScenes: ['句子获取 — 加载每日推荐', '数据同步 — 云端同步', '推送通知 — 接收消息'], + ), + clipboard( + '剪贴板', + Permission.notification, + CupertinoIcons.doc_on_clipboard_fill, + '用于复制句子到剪贴板、粘贴文本到编辑器。应用仅在您主动操作时访问剪贴板,不会自动读取。', + Color(0xFFAF52DE), + isVirtual: true, + usageScenes: ['复制句子 — 一键复制', '编辑器 — 粘贴文本', '搜索 — 粘贴关键词'], + ); const AppPermission( this.label, @@ -96,6 +132,7 @@ enum AppPermission { this.description, this.color, { this.isRequired = false, + this.isVirtual = false, this.usageScenes = const [], }); @@ -105,7 +142,26 @@ enum AppPermission { final String description; final Color color; final bool isRequired; + final bool isVirtual; final List usageScenes; + + /// Android 13+ 不需要 storage 权限(由 photos 替代) + bool get isPlatformRelevant { + if (this == AppPermission.storage) { + if (!Platform.isAndroid) return false; + final sdkInt = _androidSdkInt; + return sdkInt != null && sdkInt <= 32; + } + return true; + } + + static int? get _androidSdkInt { + try { + return int.tryParse(Platform.version.split('.').first); + } catch (_) { + return null; + } + } } /// 权限管理服务 — iOS 风格权限请求 @@ -114,6 +170,9 @@ class PermissionService { /// 检查单个权限状态 static Future checkStatus(AppPermission perm) async { + if (perm.isVirtual) { + return _checkVirtualStatus(perm); + } try { final status = await perm.permission.status; return AppPermissionStatus.fromPermissionStatus(status); @@ -123,11 +182,26 @@ class PermissionService { } } - /// 批量查询所有权限状态 + /// 虚拟权限状态检查 + static Future _checkVirtualStatus( + AppPermission perm, + ) async { + switch (perm) { + case AppPermission.network: + return AppPermissionStatus.granted; + case AppPermission.clipboard: + return AppPermissionStatus.granted; + default: + return AppPermissionStatus.granted; + } + } + + /// 批量查询所有权限状态(过滤平台不相关权限) static Future> checkAllStatus() async { final results = {}; for (final perm in AppPermission.values) { + if (!perm.isPlatformRelevant) continue; results[perm] = await checkStatus(perm); } return results; @@ -139,6 +213,7 @@ class PermissionService { AppPermission perm, { String? rationale, }) async { + if (perm.isVirtual) return true; try { final status = await perm.permission.status; @@ -177,6 +252,10 @@ class PermissionService { }) async { final results = {}; for (final perm in permissions) { + if (perm.isVirtual) { + results[perm] = true; + continue; + } results[perm] = await requestPermission( context, perm, @@ -216,6 +295,14 @@ class PermissionService { rationale: '需要位置权限才能提供天气和节气信息', ); + /// 快捷方法: 请求麦克风权限 + static Future requestMicrophone(BuildContext context) => + requestPermission( + context, + AppPermission.microphone, + rationale: '需要麦克风权限才能使用语音功能', + ); + /// 打开系统设置 static Future openSettings() => openAppSettings(); diff --git a/lib/core/services/catcher2_config_service.dart b/lib/core/services/catcher2_config_service.dart index 69393a39..c87370f8 100644 --- a/lib/core/services/catcher2_config_service.dart +++ b/lib/core/services/catcher2_config_service.dart @@ -8,8 +8,6 @@ import 'package:catcher_2/catcher_2.dart'; import 'package:catcher_2/model/platform_type.dart'; -import 'package:catcher_2/model/report.dart'; -import 'package:catcher_2/model/report_mode.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' show SelectableText; diff --git a/lib/core/services/data/home_widget_service.dart b/lib/core/services/data/home_widget_service.dart index 4593384c..c22464b9 100644 --- a/lib/core/services/data/home_widget_service.dart +++ b/lib/core/services/data/home_widget_service.dart @@ -303,7 +303,7 @@ class HomeWidgetService { // 官方SDK下不支持,使用 dynamic 调用绕过编译检查 if (pu.isOhos) { try { - final dynamic updateWidget = HomeWidget.updateWidget; + const dynamic updateWidget = HomeWidget.updateWidget; await updateWidget( qualifiedAndroidName: type.qualifiedAndroidName, androidName: type.androidProviderName, diff --git a/lib/core/services/device/device_info_service.dart b/lib/core/services/device/device_info_service.dart index e48fad2d..76424c45 100644 --- a/lib/core/services/device/device_info_service.dart +++ b/lib/core/services/device/device_info_service.dart @@ -1,12 +1,13 @@ // ============================================================ // 闲言APP — 设备信息服务 // 创建时间: 2026-05-10 -// 更新时间: 2026-05-20 +// 更新时间: 2026-05-22 // 作用: 采集设备信息并自动注册到服务端 -// 上次更新: v14.31.0 新增cachedDeviceModel/cachedDeviceName缓存+initCache()方法 +// 上次更新: 新增refresh()/clearCache()方法,支持强制刷新设备缓存 // ============================================================ import 'dart:io'; +import 'dart:math'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; @@ -24,6 +25,7 @@ class DeviceInfoService { static final _deviceInfoPlugin = DeviceInfoPlugin(); static const _prefKeyDeviceRegistered = 'device_registered_v2'; + static const _prefKeyFallbackDeviceId = 'fallback_device_id'; static String? _cachedDeviceModel; static String? _cachedDeviceName; @@ -36,7 +38,357 @@ class DeviceInfoService { _cachedDeviceName ??= await getDeviceName(); } - /// 获取设备唯一标识 + // ============================================================ + // 品牌中文名映射 + // ============================================================ + + static const Map _brandNameMap = { + 'huawei': '华为', + 'honor': '荣耀', + 'xiaomi': '小米', + 'redmi': '红米', + 'oppo': 'OPPO', + 'vivo': 'vivo', + 'oneplus': '一加', + 'realme': '真我', + 'meizu': '魅族', + 'samsung': '三星', + 'zte': '中兴', + 'lenovo': '联想', + 'motorola': '摩托罗拉', + 'nubia': '努比亚', + 'iqoo': 'iQOO', + 'pixel': 'Google Pixel', + 'apple': 'Apple', + 'sony': '索尼', + 'asus': '华硕', + 'rog': 'ROG', + 'nothing': 'Nothing', + 'transsion': '传音', + 'tecno': 'TECNO', + 'infinix': 'Infinix', + 'poco': 'POCO', + 'sharp': '夏普', + 'htc': 'HTC', + 'lg': 'LG', + 'nokia': '诺基亚', + 'google': 'Google', + }; + + // ============================================================ + // 热门型号友好名称映射 + // ============================================================ + + static const Map _modelNameMap = { + // 华为 + 'ELE-AL00': '华为 P40', + 'ELE-TL00': '华为 P40', + 'ANA-AL00': '华为 P40 Pro', + 'ANA-TL00': '华为 P40 Pro', + 'ELS-AN00': '华为 P40 Pro+', + 'JAD-AL00': '华为 P50', + 'JAD-AN00': '华为 P50', + 'JAD-LX9': '华为 P50', + 'JAD-AL50': '华为 P50 Pro', + 'ABR-AL00': '华为 P50 Pro', + 'ALT-AL00': '华为 P50 Pocket', + 'MATE-AL00': '华为 Mate 50', + 'MATE-AN00': '华为 Mate 50', + 'CET-AL00': '华为 Mate 50 Pro', + 'CET-AN00': '华为 Mate 50 Pro', + 'TAH-AN00': '华为 Mate 60', + 'TAH-AN00m': '华为 Mate 60', + 'ALT-AN00': '华为 Mate 60 Pro', + 'BRA-AN00': '华为 Mate 60 Pro+', + 'ALN-AL00': '华为 Mate 30', + 'ALN-AN00': '华为 Mate 30', + 'LIO-AL00': '华为 Mate 30 Pro', + 'LIO-AN00': '华为 Mate 30 Pro', + 'OCE-AN10': '华为 Mate 40', + 'NOH-AN00': '华为 Mate 40 Pro', + 'NOH-AN01': '华为 Mate 40 Pro', + 'NOP-AN00': '华为 Mate 40 Pro+', + 'TET-AN00': '华为 Mate X', + 'TET-AN10': '华为 Mate X', + 'TAH-N1H': '华为 Mate X3', + 'GEM-AN00': '华为 Mate X5', + 'WGR-AN00': '华为 nova 12', + 'ADA-AL00': '华为 nova 7', + 'ANG-AN00': '华为 nova 8', + 'RTE-AL00': '华为 nova 9', + 'NAM-AL00': '华为 nova 10', + 'BZT-AN00': '华为 nova 11', + 'FIG-AL00': '华为 Mate 20', + 'FIG-TL00': '华为 Mate 20', + 'LYA-AL00': '华为 Mate 20 Pro', + 'LYA-AN00': '华为 Mate 20 Pro', + 'EVR-AL00': '华为 Mate 20 X', + 'EVR-AN00': '华为 Mate 20 X', + 'VOG-AL00': '华为 P30', + 'VOG-AN00': '华为 P30 Pro', + 'VCE-AL00': '华为 P30', + 'MAR-AL00': '华为 P30 lite', + // 荣耀 + 'ANY-AN00': '荣耀 30', + 'BMH-AN10': '荣耀 30S', + 'EBG-AN00': '荣耀 50', + 'FNE-AN00': '荣耀 60', + 'FRI-AN00': '荣耀 70', + 'VMA-AN00': '荣耀 80', + 'MGI-AN00': '荣耀 90', + 'MAA-AN00': '荣耀 Magic5', + 'BVL-AN00': '荣耀 Magic6', + 'BVL-N49': '荣耀 Magic6 Pro', + // 小米 + '23127PN0CG': '小米 14', + '23116PN5BC': '小米 14 Pro', + '23117RA68G': '小米 14 Ultra', + '2211133C': '小米 13', + '2210132C': '小米 13 Pro', + '2304FPN6DG': '小米 13 Ultra', + '2201123C': '小米 12', + '2201122C': '小米 12 Pro', + '2203121C': '小米 12S Ultra', + 'M2011K2C': '小米 11', + 'M2012K11AC': '小米 11 Pro', + 'M2102K1C': '小米 11 Ultra', + 'M2007J22C': '小米 10', + 'M2001J2C': '小米 10 Pro', + 'M2004J11C': '小米 10 Ultra', + 'M1902F1A': '小米 9', + 'M1803E1A': '小米 8', + 'M2101K9C': '小米 11i', + '23013RK75C': '小米 Civi 3', + '23076MO4BC': '小米 Civi 4 Pro', + '24030PN60G': '小米 14 CIVI', + '24053PN09G': '小米 15', + '24129PN74G': '小米 15 Pro', + // 红米 + '2312DRA50G': '红米 Note 13 Pro', + '23090RA98G': '红米 Note 13', + '23076RA4DG': '红米 12', + '22101316G': '红米 Note 12 Pro', + '22111319G': '红米 Note 12', + '22071212AG': '红米 Note 11 Pro', + '2201117TG': '红米 Note 11', + 'M2010J19SY': '红米 Note 9 Pro', + 'M2003J15SC': '红米 Note 9', + '23122RAA0G': '红米 K70', + '23113RKC6G': '红米 K70 Pro', + '23078PND5G': '红米 K60', + '22127RK46C': '红米 K60 Pro', + '22081212C': '红米 K50 Ultra', + '22041211AC': '红米 K50 Pro', + '22041216C': '红米 K50', + '21081111RG': '红米 K40 Pro', + 'M2012K11C': '红米 K40', + 'M2007J22B': '红米 K30 Pro', + '24122RKC7C': '红米 K80', + '24117RKV7C': '红米 K80 Pro', + // OPPO + 'CPH2591': 'OPPO Find X7', + 'PHZ110': 'OPPO Find X7 Ultra', + 'CPH2505': 'OPPO Find X6 Pro', + 'CPH2449': 'OPPO Find X5 Pro', + 'CPH2375': 'OPPO Find X3 Pro', + 'CPH2581': 'OPPO Reno 11 Pro', + 'CPH2521': 'OPPO Reno 10 Pro', + 'CPH2441': 'OPPO Reno 9 Pro', + 'PFDM00': 'OPPO Find N3', + 'PEQM00': 'OPPO Find N2', + 'PGBM10': 'OPPO Reno 12 Pro', + 'PJC110': 'OPPO Find X8', + 'PJC1': 'OPPO Find X8 Pro', + // vivo + 'V2324A': 'vivo X100', + 'V2324HA': 'vivo X100 Pro', + 'V2231A': 'vivo X90 Pro', + 'V2219A': 'vivo X90', + 'V2145A': 'vivo X80 Pro', + 'V2171A': 'vivo X Fold3', + 'V2307A': 'vivo X Fold3 Pro', + 'V2338A': 'vivo S18 Pro', + 'V2355A': 'vivo S19 Pro', + 'V2408A': 'vivo X200', + 'V2408CA': 'vivo X200 Pro', + // iQOO + 'I2302': 'iQOO 12', + 'I2219': 'iQOO 11', + 'I2123': 'iQOO 10', + 'I2012': 'iQOO 9 Pro', + 'V2339A': 'iQOO Neo9', + 'V2403A': 'iQOO Neo9S Pro', + 'V2352A': 'iQOO Z9 Turbo', + // 一加 + 'CPH2583': '一加 12', + 'CPH2415': '一加 10 Pro', + 'PJD110': '一加 13', + 'PGB110': '一加 Ace 3', + 'PHB110': '一加 Ace 2 Pro', + 'PJE110': '一加 Ace 3V', + // 真我 + 'RMX3901': '真我 GT5 Pro', + 'RMX3700': '真我 GT5', + 'RMX3708': '真我 GT Neo5', + 'RMX3881': '真我 GT Neo6', + 'RMX3921': '真我 GT Neo6 SE', + 'RMX3999': '真我 12 Pro+', + 'RMX3951': '真我 13 Pro+', + // 魅族 + 'MEIZU 21': '魅族 21', + 'MEIZU 21 Pro': '魅族 21 Pro', + 'MEIZU 20': '魅族 20', + 'MEIZU 20 Pro': '魅族 20 Pro', + 'MEIZU 20 Infinity': '魅族 20 Infinity', + 'M1916Q': '魅族 18', + 'M1917Q': '魅族 18 Pro', + // 三星 + 'SM-S928B': '三星 Galaxy S25 Ultra', + 'SM-S926B': '三星 Galaxy S25+', + 'SM-S921B': '三星 Galaxy S25', + 'SM-S928U': '三星 Galaxy S25 Ultra', + 'SM-S928N': '三星 Galaxy S25 Ultra', + 'SM-S918B': '三星 Galaxy S24 Ultra', + 'SM-S916B': '三星 Galaxy S24+', + 'SM-S924B': '三星 Galaxy S24', + 'SM-S908B': '三星 Galaxy S23 Ultra', + 'SM-S906B': '三星 Galaxy S23+', + 'SM-S901B': '三星 Galaxy S23', + 'SM-S911B': '三星 Galaxy S22 Ultra', + 'SM-S901E': '三星 Galaxy S22', + 'SM-G998B': '三星 Galaxy S21 Ultra', + 'SM-G996B': '三星 Galaxy S21+', + 'SM-G991B': '三星 Galaxy S21', + 'SM-N986B': '三星 Galaxy Note20 Ultra', + 'SM-N981B': '三星 Galaxy Note20', + 'SM-F731B': '三星 Galaxy Z Flip5', + 'SM-F946B': '三星 Galaxy Z Fold5', + 'SM-F721B': '三星 Galaxy Z Flip4', + 'SM-F936B': '三星 Galaxy Z Fold4', + // 中兴 + 'ZTE A2023': '中兴 Axon 40 Ultra', + 'ZTE V8020': '中兴 Axon 50 Ultra', + // 努比亚 + 'NX712J': '努比亚 Z50 Ultra', + 'NX769J': '努比亚 Z60 Ultra', + // 联想 + 'L78032': '联想 拯救者 Y70', + 'L38111': '联想 拯救者 Y90', + // 摩托罗拉 + 'XT2301-5': '摩托罗拉 edge 40 Pro', + 'XT2401-1': '摩托罗拉 edge 50 Ultra', + }; + + // ============================================================ + // iOS 设备型号映射 (utsname.machine → 友好名称) + // ============================================================ + + static const Map _iosModelMap = { + 'iPhone17,1': 'iPhone 16 Pro', + 'iPhone17,2': 'iPhone 16 Pro Max', + 'iPhone17,3': 'iPhone 16', + 'iPhone17,4': 'iPhone 16 Plus', + 'iPhone17,5': 'iPhone 16e', + 'iPhone16,1': 'iPhone 15 Pro', + 'iPhone16,2': 'iPhone 15 Pro Max', + 'iPhone16,3': 'iPhone 15', + 'iPhone16,4': 'iPhone 15 Plus', + 'iPhone15,2': 'iPhone 14 Pro', + 'iPhone15,3': 'iPhone 14 Pro Max', + 'iPhone15,4': 'iPhone 14', + 'iPhone15,5': 'iPhone 14 Plus', + 'iPhone14,2': 'iPhone 13 Pro', + 'iPhone14,3': 'iPhone 13 Pro Max', + 'iPhone14,4': 'iPhone 13 mini', + 'iPhone14,5': 'iPhone 13', + 'iPhone14,6': 'iPhone SE (3rd gen)', + 'iPhone14,7': 'iPhone 13', + 'iPhone14,8': 'iPhone 13', + 'iPhone13,2': 'iPhone 12', + 'iPhone13,3': 'iPhone 12', + 'iPhone13,4': 'iPhone 12 Pro', + 'iPhone13,5': 'iPhone 12 Pro', + 'iPhone13,6': 'iPhone 12 mini', + 'iPhone13,7': 'iPhone 12 mini', + 'iPhone12,1': 'iPhone 11', + 'iPhone12,3': 'iPhone 11 Pro', + 'iPhone12,5': 'iPhone 11 Pro Max', + 'iPhone12,8': 'iPhone SE (2nd gen)', + 'iPhone11,2': 'iPhone XS', + 'iPhone11,4': 'iPhone XS Max', + 'iPhone11,8': 'iPhone XR', + 'iPhone10,3': 'iPhone X', + 'iPhone10,6': 'iPhone X', + 'iPad16,1': 'iPad Pro 13" (M4)', + 'iPad16,2': 'iPad Pro 13" (M4)', + 'iPad16,3': 'iPad Pro 11" (M4)', + 'iPad16,4': 'iPad Pro 11" (M4)', + 'iPad14,1': 'iPad Air (M2)', + 'iPad14,2': 'iPad Air (M2)', + 'iPad14,3': 'iPad Pro 12.9" (M2)', + 'iPad14,4': 'iPad Pro 12.9" (M2)', + 'iPad14,5': 'iPad Pro 11" (M2)', + 'iPad14,6': 'iPad Pro 11" (M2)', + 'iPad13,1': 'iPad Air (5th gen)', + 'iPad13,2': 'iPad Air (5th gen)', + 'iPad13,4': 'iPad Pro 12.9" (M1)', + 'iPad13,5': 'iPad Pro 12.9" (M1)', + 'iPad13,8': 'iPad Pro 11" (M1)', + 'iPad13,9': 'iPad Pro 11" (M1)', + 'iPad13,16': 'iPad (10th gen)', + 'iPad13,17': 'iPad (10th gen)', + 'iPad13,18': 'iPad mini (6th gen)', + 'iPad13,19': 'iPad mini (6th gen)', + }; + + // ============================================================ + // 品牌中文名查询 + // ============================================================ + + static String getBrandChineseName(String brand) { + final key = brand.toLowerCase().trim(); + return _brandNameMap[key] ?? brand; + } + + // ============================================================ + // 型号友好名称查询 + // ============================================================ + + static String? getModelFriendlyName(String model) { + final trimmed = model.trim(); + if (trimmed.isEmpty) return null; + if (_modelNameMap.containsKey(trimmed)) return _modelNameMap[trimmed]; + if (_iosModelMap.containsKey(trimmed)) return _iosModelMap[trimmed]; + for (final entry in _modelNameMap.entries) { + if (trimmed.toUpperCase().contains(entry.key.toUpperCase())) { + return entry.value; + } + } + return null; + } + + // ============================================================ + // 生成稳定UUID + // ============================================================ + + static String _generateStableUuid() { + final r = Random(); + const chars = '0123456789abcdef'; + const segments = [8, 4, 4, 4, 12]; + final buf = StringBuffer(); + for (var i = 0; i < segments.length; i++) { + if (i > 0) buf.write('-'); + for (var j = 0; j < segments[i]; j++) { + buf.write(chars[r.nextInt(16)]); + } + } + return buf.toString(); + } + + // ============================================================ + // 获取设备唯一标识 + // ============================================================ + static Future getDeviceId() async { try { if (pu.isOhos) { @@ -52,54 +404,87 @@ class DeviceInfoService { } catch (e) { Log.w('获取设备ID失败: $e'); } - return 'unknown_${DateTime.now().millisecondsSinceEpoch}'; + final prefs = await SharedPreferences.getInstance(); + var fallbackId = prefs.getString(_prefKeyFallbackDeviceId); + if (fallbackId != null && fallbackId.isNotEmpty) + return 'fallback_$fallbackId'; + fallbackId = _generateStableUuid(); + await prefs.setString(_prefKeyFallbackDeviceId, fallbackId); + Log.i('生成稳定回退设备ID: $fallbackId'); + return 'fallback_$fallbackId'; } - /// 获取设备名称 + // ============================================================ + // 获取设备名称 (友好名称) + // ============================================================ + static Future getDeviceName() async { if (_cachedDeviceName != null) return _cachedDeviceName!; try { if (pu.isOhos) { final android = await _deviceInfoPlugin.androidInfo; + final friendly = _buildFriendlyName(android.brand, android.model); + if (friendly != null) return friendly; if (android.model.isNotEmpty) return android.model; - if (android.brand.isNotEmpty) return android.brand; + if (android.brand.isNotEmpty) return getBrandChineseName(android.brand); if (android.display.isNotEmpty) return android.display; if (android.product.isNotEmpty) return android.product; - return 'HarmonyOS Device'; + return '鸿蒙设备'; } else if (Platform.isAndroid) { final android = await _deviceInfoPlugin.androidInfo; - return android.model.isNotEmpty - ? android.model - : (android.brand.isNotEmpty ? android.brand : 'Android'); + final friendly = _buildFriendlyName(android.brand, android.model); + if (friendly != null) return friendly; + if (android.model.isNotEmpty) return android.model; + if (android.brand.isNotEmpty) return getBrandChineseName(android.brand); + return 'Android 设备'; } else if (Platform.isIOS) { final ios = await _deviceInfoPlugin.iosInfo; - return ios.utsname.machine.isNotEmpty ? ios.utsname.machine : 'iPhone'; + final machine = ios.utsname.machine; + if (machine.isNotEmpty) { + final friendly = _iosModelMap[machine]; + if (friendly != null) return friendly; + return machine; + } + return 'iPhone'; } } catch (e) { Log.w('获取设备名称失败: $e'); } - return 'Unknown'; + return '未知设备'; } + // ============================================================ + // 获取设备型号 (品牌+型号) + // ============================================================ + static Future getDeviceModel() async { if (_cachedDeviceModel != null) return _cachedDeviceModel!; try { if (pu.isOhos) { final android = await _deviceInfoPlugin.androidInfo; - final brand = android.brand.isNotEmpty ? android.brand : 'HarmonyOS'; - final model = android.model.isNotEmpty - ? android.model - : (android.display.isNotEmpty ? android.display : (android.product.isNotEmpty ? android.product : 'Device')); - final result = '$brand $model'.trim(); - return result == 'HarmonyOS Device' ? 'HarmonyOS Device' : result; + return _buildDeviceModel( + android.brand, + android.model, + android.display, + android.product, + isOhos: true, + ); } else if (Platform.isAndroid) { final android = await _deviceInfoPlugin.androidInfo; - return '${android.brand} ${android.model}'.trim(); + return _buildDeviceModel( + android.brand, + android.model, + android.display, + android.product, + ); } else if (Platform.isIOS) { final ios = await _deviceInfoPlugin.iosInfo; - return ios.utsname.machine.isNotEmpty - ? ios.utsname.machine - : 'iOS Device'; + final machine = ios.utsname.machine; + if (machine.isNotEmpty) { + final friendly = _iosModelMap[machine]; + return friendly ?? machine; + } + return 'iOS Device'; } } catch (e) { Log.w('获取设备型号失败: $e'); @@ -107,7 +492,57 @@ class DeviceInfoService { return 'Unknown'; } - /// 获取平台标识 + // ============================================================ + // 构建友好设备名称 + // ============================================================ + + static String? _buildFriendlyName(String brand, String model) { + if (model.isNotEmpty) { + final friendly = getModelFriendlyName(model); + if (friendly != null) return friendly; + } + if (brand.isNotEmpty && model.isNotEmpty) { + final brandCn = getBrandChineseName(brand); + if (brandCn != brand) return '$brandCn $model'; + } + return null; + } + + // ============================================================ + // 构建设备型号字符串 + // ============================================================ + + static String _buildDeviceModel( + String brand, + String model, + String display, + String product, { + bool isOhos = false, + }) { + final brandCn = brand.isNotEmpty + ? getBrandChineseName(brand) + : (isOhos ? '鸿蒙' : ''); + final modelFallback = model.isNotEmpty + ? model + : (display.isNotEmpty ? display : (product.isNotEmpty ? product : '')); + + if (model.isNotEmpty) { + final friendly = getModelFriendlyName(model); + if (friendly != null) return friendly; + } + + if (brandCn.isNotEmpty && modelFallback.isNotEmpty) { + return '$brandCn $modelFallback'; + } + if (brandCn.isNotEmpty) return brandCn; + if (modelFallback.isNotEmpty) return modelFallback; + return isOhos ? '鸿蒙设备' : 'Android 设备'; + } + + // ============================================================ + // 获取平台标识 + // ============================================================ + static String getPlatform() { if (kIsWeb) return 'web'; if (pu.isOhos) return 'ohos'; @@ -119,7 +554,10 @@ class DeviceInfoService { return 'other'; } - /// 获取APP名称+版本 + // ============================================================ + // 获取APP名称+版本 + // ============================================================ + static Future getAppName() async { try { final packageInfo = await PackageInfo.fromPlatform(); @@ -129,7 +567,10 @@ class DeviceInfoService { } } - /// 注册设备到服务端(登录后调用) + // ============================================================ + // 注册设备到服务端(登录后调用) + // ============================================================ + static Future registerDeviceIfNeeded() async { try { final deviceId = await getDeviceId(); @@ -138,7 +579,6 @@ class DeviceInfoService { final platform = getPlatform(); final appName = await getAppName(); - // 查询IP归属地(不阻塞注册,失败时降级) String? ipCity; String? ipRange; try { @@ -185,11 +625,46 @@ class DeviceInfoService { } } - /// 重置注册状态(退出登录时调用) + // ============================================================ + // 重置注册状态(退出登录时调用) + // ============================================================ + static Future resetRegistration() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_prefKeyDeviceRegistered); } catch (_) {} } + + // ============================================================ + // 强制刷新设备缓存(重新读取平台信息+重新注册) + // ============================================================ + + static Future refresh() async { + clearCache(); + _cachedDeviceModel = await getDeviceModel(); + _cachedDeviceName = await getDeviceName(); + Log.i('设备缓存已刷新: model=$_cachedDeviceModel, name=$_cachedDeviceName'); + + try { + final prefs = await SharedPreferences.getInstance(); + final wasRegistered = prefs.getBool(_prefKeyDeviceRegistered) ?? false; + if (wasRegistered) { + await registerDeviceIfNeeded(); + Log.i('设备信息已重新注册到服务端'); + } + } catch (e) { + Log.w('设备重新注册失败: $e'); + } + } + + // ============================================================ + // 仅清除本地缓存(不重新注册) + // ============================================================ + + static void clearCache() { + _cachedDeviceModel = null; + _cachedDeviceName = null; + Log.d('设备缓存已清除'); + } } diff --git a/lib/core/services/notification/daily_notify_service.dart b/lib/core/services/notification/daily_notify_service.dart index 053b3694..901b197d 100644 --- a/lib/core/services/notification/daily_notify_service.dart +++ b/lib/core/services/notification/daily_notify_service.dart @@ -1,14 +1,15 @@ // ============================================================ // 闲言APP — 每日定时通知服务 // 创建时间: 2026-05-20 -// 更新时间: 2026-05-20 +// 更新时间: 2026-05-22 // 作用: 每日定时推送一句好句 -// 上次更新: 初始版本 +// 上次更新: @Deprecated 已合并至NotificationCenter,请使用NotificationCenter替代 // ============================================================ import '../../utils/logger.dart'; import 'local_notification_service.dart'; +@Deprecated('已合并至NotificationCenter,请使用NotificationCenter替代') class DailyNotifyService { DailyNotifyService._(); static final DailyNotifyService instance = DailyNotifyService._(); diff --git a/lib/core/services/notification/local_notification_service.dart b/lib/core/services/notification/local_notification_service.dart index 52e861a4..2a4d2fdf 100644 --- a/lib/core/services/notification/local_notification_service.dart +++ b/lib/core/services/notification/local_notification_service.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 本地通知服务 /// 创建时间: 2026-05-02 -/// 更新时间: 2026-05-17 +/// 更新时间: 2026-05-22 /// 作用: 本地推送通知管理 (初始化/调度/取消/点击处理) -/// 上次更新: 鸿蒙适配-使用桥接方法隔离OhosInitializationSettings +/// 上次更新: 添加study_progress通知点击路由 /// ============================================================ import 'dart:io'; @@ -15,7 +15,7 @@ import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; import '../../router/app_router.dart'; -import 'notification_scheduler.dart'; +import 'notification_center.dart'; import '../../storage/app_kv_store.dart'; import '../../utils/logger.dart'; import '../../utils/platform_utils.dart' as pu; @@ -121,6 +121,8 @@ class LocalNotificationService { context.appGo('/countdown'); case 'daily_fortune': context.appGo('/daily-fortune'); + case 'study_progress': + context.appGo('/home'); case 'readlater': context.appGo('/readlater-chat'); default: @@ -292,7 +294,7 @@ class LocalNotificationService { } static Future setupDailyNotifications(WidgetRef ref) async { - if (!NotificationScheduler.isNotificationsEnabled) { + if (!NotificationCenter.isNotificationsEnabled) { await cancelAll(); return; } diff --git a/lib/core/services/notification/notification_center.dart b/lib/core/services/notification/notification_center.dart new file mode 100644 index 00000000..e5b920bb --- /dev/null +++ b/lib/core/services/notification/notification_center.dart @@ -0,0 +1,303 @@ +/// ============================================================ +/// 闲言APP — 统一通知中心 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 合并 NotificationScheduler + DailyNotifyService,统一管理所有本地通知调度 +/// 上次更新: 初始版本,合并每日推荐/签到/节气/运势/学习进度/稍后读通知 +/// ============================================================ + +import 'local_notification_service.dart'; +import '../../storage/app_kv_store.dart'; +import '../../utils/logger.dart'; + +class NotificationCenter { + NotificationCenter._(); + + // ── 通知ID命名空间 ── + + static const int _idDailyRecommend = 1001; + static const int _idSigninReminder = 1002; + static const int _idFortune = 1003; + static const int _idStudyProgress = 1004; + static const int _idSolarTerm = 2001; + + // ── 存储键 ── + + static const _keyNotificationsEnabled = 'notifications_enabled'; + static const _keyDailyRecommendEnabled = 'daily_sentence_enabled'; + static const _keyDailyRecommendHour = 'daily_sentence_hour'; + static const _keyDailyRecommendMinute = 'daily_sentence_minute'; + static const _keySigninReminderEnabled = 'signin_reminder_enabled'; + static const _keySigninReminderHour = 'signin_reminder_hour'; + static const _keySigninReminderMinute = 'signin_reminder_minute'; + static const _keySolarTermEnabled = 'solar_term_enabled'; + static const _keyFortuneEnabled = 'fortune_enabled'; + static const _keyFortuneHour = 'fortune_hour'; + static const _keyFortuneMinute = 'fortune_minute'; + static const _keyStudyProgressEnabled = 'study_progress_enabled'; + static const _keyReadlaterEnabled = 'notif_charging_readlater'; + + // ── 全局通知开关 ── + + static bool get isNotificationsEnabled => + AppKVStore.getBool(_keyNotificationsEnabled) ?? false; + + static Future setNotificationsEnabled(bool v) async { + await AppKVStore.setBool(_keyNotificationsEnabled, v); + await configureAll(); + } + + // ── 每日推荐 ── + + static bool get isDailyRecommendEnabled => + AppKVStore.getBool(_keyDailyRecommendEnabled) ?? true; + + static int get dailyRecommendHour => + AppKVStore.getInt(_keyDailyRecommendHour) ?? 8; + + static int get dailyRecommendMinute => + AppKVStore.getInt(_keyDailyRecommendMinute) ?? 0; + + static Future setDailyRecommendEnabled(bool v) async { + await AppKVStore.setBool(_keyDailyRecommendEnabled, v); + await configureAll(); + } + + static Future setDailyRecommendTime(int hour, int minute) async { + await AppKVStore.setInt(_keyDailyRecommendHour, hour); + await AppKVStore.setInt(_keyDailyRecommendMinute, minute); + await configureAll(); + } + + // ── 签到提醒 ── + + static bool get isSigninReminderEnabled => + AppKVStore.getBool(_keySigninReminderEnabled) ?? true; + + static int get signinReminderHour => + AppKVStore.getInt(_keySigninReminderHour) ?? 20; + + static int get signinReminderMinute => + AppKVStore.getInt(_keySigninReminderMinute) ?? 0; + + static Future setSigninReminderEnabled(bool v) async { + await AppKVStore.setBool(_keySigninReminderEnabled, v); + await configureAll(); + } + + static Future setSigninReminderTime(int hour, int minute) async { + await AppKVStore.setInt(_keySigninReminderHour, hour); + await AppKVStore.setInt(_keySigninReminderMinute, minute); + await configureAll(); + } + + // ── 节气通知 ── + + static bool get isSolarTermEnabled => + AppKVStore.getBool(_keySolarTermEnabled) ?? true; + + static Future setSolarTermEnabled(bool v) async { + await AppKVStore.setBool(_keySolarTermEnabled, v); + await configureAll(); + } + + // ── 每日运势 ── + + static bool get isFortuneEnabled => + AppKVStore.getBool(_keyFortuneEnabled) ?? false; + + static int get fortuneHour => AppKVStore.getInt(_keyFortuneHour) ?? 8; + + static int get fortuneMinute => AppKVStore.getInt(_keyFortuneMinute) ?? 0; + + static Future setFortuneEnabled(bool v) async { + await AppKVStore.setBool(_keyFortuneEnabled, v); + await configureAll(); + } + + static Future setFortuneTime(int hour, int minute) async { + await AppKVStore.setInt(_keyFortuneHour, hour); + await AppKVStore.setInt(_keyFortuneMinute, minute); + await configureAll(); + } + + // ── 学习进度 ── + + static bool get isStudyProgressEnabled => + AppKVStore.getBool(_keyStudyProgressEnabled) ?? false; + + static Future setStudyProgressEnabled(bool v) async { + await AppKVStore.setBool(_keyStudyProgressEnabled, v); + await configureAll(); + } + + // ── 稍后读提醒 ── + + static bool get isReadlaterEnabled => + AppKVStore.getBool(_keyReadlaterEnabled) ?? false; + + static Future setReadlaterEnabled(bool v) async { + await AppKVStore.setBool(_keyReadlaterEnabled, v); + } + + // ── 核心调度 ── + + static Future configureAll() async { + final enabled = AppKVStore.getBool(_keyNotificationsEnabled) ?? false; + if (!enabled) { + await cancelAllManaged(); + Log.i('NotificationCenter: 通知已关闭,取消所有调度'); + return; + } + + await cancelAllManaged(); + + await _configureDailyRecommend(); + await _configureSigninReminder(); + await _configureSolarTerm(); + await _configureFortune(); + await _configureStudyProgress(); + + Log.i('NotificationCenter: 所有通知已配置'); + } + + static Future cancelAllManaged() async { + await LocalNotificationService.cancel(_idDailyRecommend); + await LocalNotificationService.cancel(_idSigninReminder); + await LocalNotificationService.cancel(_idFortune); + await LocalNotificationService.cancel(_idStudyProgress); + await LocalNotificationService.cancel(_idSolarTerm); + } + + // ── 各通知调度 ── + + static Future _configureDailyRecommend() async { + final enabled = AppKVStore.getBool(_keyDailyRecommendEnabled) ?? true; + if (!enabled) return; + + final hour = AppKVStore.getInt(_keyDailyRecommendHour) ?? 8; + final minute = AppKVStore.getInt(_keyDailyRecommendMinute) ?? 0; + + await LocalNotificationService.scheduleDaily( + id: _idDailyRecommend, + title: '闲言每日一句', + body: '今天的句子已准备好,来看看吧 ✨', + hour: hour, + minute: minute, + payload: 'daily_sentence', + ); + } + + static Future _configureSigninReminder() async { + final enabled = AppKVStore.getBool(_keySigninReminderEnabled) ?? true; + if (!enabled) return; + + final hour = AppKVStore.getInt(_keySigninReminderHour) ?? 20; + final minute = AppKVStore.getInt(_keySigninReminderMinute) ?? 0; + + await LocalNotificationService.scheduleDaily( + id: _idSigninReminder, + title: '闲言 · 签到提醒', + body: '别忘了今日签到哦 📝', + hour: hour, + minute: minute, + payload: 'signin_reminder', + ); + } + + static Future _configureSolarTerm() async { + final enabled = AppKVStore.getBool(_keySolarTermEnabled) ?? true; + if (!enabled) return; + + final nextTerm = _getNextSolarTerm(); + if (nextTerm == null) return; + + final scheduledTime = DateTime( + nextTerm['year'] as int, + nextTerm['month'] as int, + nextTerm['day'] as int, + 8, + ); + + await LocalNotificationService.scheduleOnce( + id: _idSolarTerm, + title: '闲言 · ${nextTerm['emoji']} ${nextTerm['name']}', + body: '今日${nextTerm['name']},${nextTerm['poem']}', + scheduledTime: scheduledTime, + payload: 'solar_term', + ); + } + + static Future _configureFortune() async { + final enabled = AppKVStore.getBool(_keyFortuneEnabled) ?? false; + if (!enabled) return; + + final hour = AppKVStore.getInt(_keyFortuneHour) ?? 8; + final minute = AppKVStore.getInt(_keyFortuneMinute) ?? 0; + + await LocalNotificationService.scheduleDaily( + id: _idFortune, + title: '闲言 · 🔮 每日运势', + body: '今日运势已生成,快来看看你的运势吧 ✨', + hour: hour, + minute: minute, + payload: 'daily_fortune', + ); + } + + static Future _configureStudyProgress() async { + final enabled = AppKVStore.getBool(_keyStudyProgressEnabled) ?? false; + if (!enabled) return; + + await LocalNotificationService.scheduleDaily( + id: _idStudyProgress, + title: '闲言 · 学习进度', + body: '该复习今天的学习内容了 📊', + hour: 20, + payload: 'study_progress', + ); + } + + // ── 节气数据 ── + + static Map? _getNextSolarTerm() { + final now = DateTime.now(); + final terms = _solarTerms2026; + for (final term in terms) { + final date = DateTime( + term['year'] as int, + term['month'] as int, + term['day'] as int, + ); + if (date.isAfter(now)) return term; + } + return terms.isNotEmpty ? terms.first : null; + } + + static final List> _solarTerms2026 = [ + {'year': 2026, 'month': 1, 'day': 5, 'name': '小寒', 'emoji': '❄️', 'poem': '小寒连大吕,欢鹊垒新巢'}, + {'year': 2026, 'month': 1, 'day': 20, 'name': '大寒', 'emoji': '🧊', 'poem': '大寒须守火,无事不出门'}, + {'year': 2026, 'month': 2, 'day': 4, 'name': '立春', 'emoji': '🌱', 'poem': '春风如贵客,一到便繁华'}, + {'year': 2026, 'month': 2, 'day': 18, 'name': '雨水', 'emoji': '🌧️', 'poem': '好雨知时节,当春乃发生'}, + {'year': 2026, 'month': 3, 'day': 5, 'name': '惊蛰', 'emoji': '⚡', 'poem': '微雨众卉新,一雷惊蛰始'}, + {'year': 2026, 'month': 3, 'day': 20, 'name': '春分', 'emoji': '🌸', 'poem': '雪入春分省见稀,半开桃李不胜威'}, + {'year': 2026, 'month': 4, 'day': 5, 'name': '清明', 'emoji': '🍃', 'poem': '清明时节雨纷纷,路上行人欲断魂'}, + {'year': 2026, 'month': 4, 'day': 20, 'name': '谷雨', 'emoji': '🌾', 'poem': '谷雨如丝复似尘,煮瓶浮蜡正尝新'}, + {'year': 2026, 'month': 5, 'day': 5, 'name': '立夏', 'emoji': '☀️', 'poem': '绿树阴浓夏日长,楼台倒影入池塘'}, + {'year': 2026, 'month': 5, 'day': 21, 'name': '小满', 'emoji': '🌿', 'poem': '夜莺啼绿柳,皓月醒长空'}, + {'year': 2026, 'month': 6, 'day': 5, 'name': '芒种', 'emoji': '🌻', 'poem': '时雨及芒种,四野皆插秧'}, + {'year': 2026, 'month': 6, 'day': 21, 'name': '夏至', 'emoji': '🌞', 'poem': '昼晷已云极,宵漏自此长'}, + {'year': 2026, 'month': 7, 'day': 7, 'name': '小暑', 'emoji': '🌡️', 'poem': '倏忽温风至,因循小暑来'}, + {'year': 2026, 'month': 7, 'day': 22, 'name': '大暑', 'emoji': '🔥', 'poem': '大暑三秋近,林钟九夏移'}, + {'year': 2026, 'month': 8, 'day': 7, 'name': '立秋', 'emoji': '🍂', 'poem': '乳鸦啼散玉屏空,一枕新凉一扇风'}, + {'year': 2026, 'month': 8, 'day': 23, 'name': '处暑', 'emoji': '🎐', 'poem': '处暑无三日,新凉直万金'}, + {'year': 2026, 'month': 9, 'day': 7, 'name': '白露', 'emoji': '💎', 'poem': '露从今夜白,月是故乡明'}, + {'year': 2026, 'month': 9, 'day': 23, 'name': '秋分', 'emoji': '🍁', 'poem': '金气秋分,风清露冷秋期半'}, + {'year': 2026, 'month': 10, 'day': 8, 'name': '寒露', 'emoji': '💧', 'poem': '袅袅凉风动,凄凄寒露零'}, + {'year': 2026, 'month': 10, 'day': 23, 'name': '霜降', 'emoji': '🧊', 'poem': '霜降碧天静,秋事促西风'}, + {'year': 2026, 'month': 11, 'day': 7, 'name': '立冬', 'emoji': '🧣', 'poem': '冻笔新诗懒写,寒炉美酒时温'}, + {'year': 2026, 'month': 11, 'day': 22, 'name': '小雪', 'emoji': '🌨️', 'poem': '片片互玲珑,飞扬玉漏终'}, + {'year': 2026, 'month': 12, 'day': 7, 'name': '大雪', 'emoji': '❄️', 'poem': '大雪江南见未曾,今年方始是严凝'}, + {'year': 2026, 'month': 12, 'day': 21, 'name': '冬至', 'emoji': '🥟', 'poem': '天时人事日相催,冬至阳生春又来'}, + ]; +} diff --git a/lib/core/services/notification/notification_scheduler.dart b/lib/core/services/notification/notification_scheduler.dart index d2842380..62db95a0 100644 --- a/lib/core/services/notification/notification_scheduler.dart +++ b/lib/core/services/notification/notification_scheduler.dart @@ -1,15 +1,16 @@ /// ============================================================ /// 闲言APP — 通知调度器 /// 创建时间: 2026-05-02 -/// 更新时间: 2026-05-13 -/// 作用: 统一管理本地通知调度 (每日推荐/签到/节气/番茄钟/运势) -/// 上次更新: 增加运势推送调度 +/// 更新时间: 2026-05-22 +/// 作用: 统一管理本地通知调度 (每日推荐/签到/节气/番茄钟/运势/学习进度) +/// 上次更新: @Deprecated 已合并至NotificationCenter,请使用NotificationCenter替代 /// ============================================================ import 'local_notification_service.dart'; import '../../storage/app_kv_store.dart'; import '../../utils/logger.dart'; +@Deprecated('已合并至NotificationCenter,请使用NotificationCenter替代') class NotificationScheduler { NotificationScheduler._(); @@ -24,16 +25,25 @@ class NotificationScheduler { static const _keyFortuneEnabled = 'fortune_enabled'; static const _keyFortuneHour = 'fortune_hour'; static const _keyFortuneMinute = 'fortune_minute'; + static const _keyStudyProgressEnabled = 'study_progress_enabled'; + + static Future _cancelAllManaged() async { + await LocalNotificationService.cancel(1001); + await LocalNotificationService.cancel(1002); + await LocalNotificationService.cancel(1003); + await LocalNotificationService.cancel(1004); + await LocalNotificationService.cancel(2001); + } static Future configureAll() async { final enabled = AppKVStore.getBool(_keyNotificationsEnabled) ?? false; if (!enabled) { - await LocalNotificationService.cancelAll(); + await _cancelAllManaged(); Log.i('通知调度: 通知已关闭,取消所有调度'); return; } - await LocalNotificationService.cancelAll(); + await _cancelAllManaged(); final dailySentence = AppKVStore.getBool(_keyDailySentenceEnabled) ?? true; if (dailySentence) { @@ -41,8 +51,8 @@ class NotificationScheduler { final minute = AppKVStore.getInt(_keyDailySentenceMinute) ?? 0; await LocalNotificationService.scheduleDaily( id: 1001, - title: '闲言 · 每日一句', - body: '新的一天,送你一句好话 ✨', + title: '闲言每日一句', + body: '今天的句子已准备好,来看看吧 ✨', hour: hour, minute: minute, payload: 'daily_sentence', @@ -83,6 +93,17 @@ class NotificationScheduler { ); } + final studyProgress = AppKVStore.getBool(_keyStudyProgressEnabled) ?? false; + if (studyProgress) { + await LocalNotificationService.scheduleDaily( + id: 1004, + title: '闲言 · 学习进度', + body: '该复习今天的学习内容了 📊', + hour: 20, + payload: 'study_progress', + ); + } + Log.i('通知调度: 所有通知已配置'); } @@ -151,6 +172,9 @@ class NotificationScheduler { static int get fortuneMinute => AppKVStore.getInt(_keyFortuneMinute) ?? 0; + static bool get isStudyProgressEnabled => + AppKVStore.getBool(_keyStudyProgressEnabled) ?? false; + static Future setNotificationsEnabled(bool v) async { await AppKVStore.setBool(_keyNotificationsEnabled, v); await configureAll(); @@ -194,6 +218,11 @@ class NotificationScheduler { await configureAll(); } + static Future setStudyProgressEnabled(bool v) async { + await AppKVStore.setBool(_keyStudyProgressEnabled, v); + await configureAll(); + } + static final List> _solarTerms2026 = [ { 'year': 2026, diff --git a/lib/core/services/ui/status_bar_service.dart b/lib/core/services/ui/status_bar_service.dart new file mode 100644 index 00000000..11fba32f --- /dev/null +++ b/lib/core/services/ui/status_bar_service.dart @@ -0,0 +1,65 @@ +/// ============================================================ +/// 闲言APP — 状态栏统一管理服务 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 集中管理状态栏样式,响应主题变化,支持沉浸式模式 +/// 上次更新: 初始创建,统一所有 SystemUiOverlayStyle 设置 +/// ============================================================ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class StatusBarService { + StatusBarService._(); + + static SystemUiOverlayStyle resolveStyle({required bool isDark}) { + return SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, + statusBarBrightness: isDark ? Brightness.dark : Brightness.light, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: + isDark ? Brightness.light : Brightness.dark, + systemNavigationBarDividerColor: Colors.transparent, + ); + } + + static void applyStyle({required bool isDark}) { + SystemChrome.setSystemUIOverlayStyle(resolveStyle(isDark: isDark)); + } + + static void setImmersive(bool immersive) { + if (immersive) { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky, + ); + } else { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + ); + } + } + + static void enterEdgeToEdge() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } +} + +class StatusBarStyleRegion extends StatelessWidget { + const StatusBarStyleRegion({ + super.key, + required this.isDark, + required this.child, + }); + + final bool isDark; + final Widget child; + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: StatusBarService.resolveStyle(isDark: isDark), + child: child, + ); + } +} diff --git a/lib/core/storage/secure_storage.dart b/lib/core/storage/secure_storage.dart index 896f2dcc..d2edb5d5 100644 --- a/lib/core/storage/secure_storage.dart +++ b/lib/core/storage/secure_storage.dart @@ -39,7 +39,6 @@ class SecureStorage { static const _macosOptions = MacOsOptions( usesDataProtectionKeychain: false, accessibility: KeychainAccessibility.first_unlock_this_device, - groupId: null, ); static const FlutterSecureStorage _storage = FlutterSecureStorage( diff --git a/lib/editor/pages/editor/pro_editor_page.dart b/lib/editor/pages/editor/pro_editor_page.dart index f1ae281b..eedc3797 100644 --- a/lib/editor/pages/editor/pro_editor_page.dart +++ b/lib/editor/pages/editor/pro_editor_page.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — ProImageEditor 包装页 // 创建时间: 2026-04-23 -// 更新时间: 2026-05-20 +// 更新时间: 2026-05-22 // 作用: ProImageEditor 的闲言APP包装页,全新iOS 26风格 -// 上次更新: 画布背景改透明+ColoredBox提供背景色,配合CanvasStyleMiddleware裁剪修复 +// 上次更新: 修复画布样式图标+中间件包裹层级优化+CanvasStyleWrapper集成 // ============================================================ import 'dart:convert'; diff --git a/lib/editor/pages/tools/draft_list_page.dart b/lib/editor/pages/tools/draft_list_page.dart index a856adc7..d14edc96 100644 --- a/lib/editor/pages/tools/draft_list_page.dart +++ b/lib/editor/pages/tools/draft_list_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 草稿箱列表页 // 创建时间: 2026-04-20 // 更新时间: 2026-04-20 @@ -17,6 +17,7 @@ import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_radius.dart'; import 'package:xianyan/editor/models/editor_models.dart'; import 'package:xianyan/editor/services/export/draft_service.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; /// 草稿箱页面 class DraftListPage extends StatefulWidget { @@ -137,6 +138,7 @@ class _DraftListPageState extends State { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '📝 草稿箱', style: AppTypography.headline.copyWith(color: ext.textPrimary), diff --git a/lib/editor/pages/tools/model_3d_preview_page.dart b/lib/editor/pages/tools/model_3d_preview_page.dart index 7593df91..e1f83f0d 100644 --- a/lib/editor/pages/tools/model_3d_preview_page.dart +++ b/lib/editor/pages/tools/model_3d_preview_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 3D模型预览页面 // 创建时间: 2026-04-25 // 更新时间: 2026-04-26 @@ -12,6 +12,7 @@ import 'package:flutter_3d_controller/flutter_3d_controller.dart'; import 'package:xianyan/editor/services/core/model_catalog_service.dart'; import 'package:xianyan/editor/services/3d/platform_3d_service.dart'; import 'package:xianyan/core/utils/logger.dart' as app_log; +import '../../../shared/widgets/adaptive_back_button.dart'; class Model3DPreviewPage extends StatefulWidget { const Model3DPreviewPage({super.key, required this.model}); @@ -297,6 +298,7 @@ class _Model3DPreviewPageState extends State { Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text('🧊 ${widget.model.name}'), trailing: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/editor/services/core/canvas_style_middleware.dart b/lib/editor/services/core/canvas_style_middleware.dart index 647caca8..dbe92a66 100644 --- a/lib/editor/services/core/canvas_style_middleware.dart +++ b/lib/editor/services/core/canvas_style_middleware.dart @@ -1,10 +1,11 @@ // ============================================================ // 闲言APP — 画布样式中间件 // 创建时间: 2026-05-20 -// 更新时间: 2026-05-20 +// 更新时间: 2026-05-22 // 作用: 通过包裹ProImageEditor实现画布样式实时预览 // 圆角/边框/阴影/叠层/外边距,无需魔改三方库 -// 上次更新: 重构渲染层级 — ClipRRect直接裁剪child,样式作用于画布本身 +// CanvasStyleWrapper: 自动从EditorSettingsService读取样式 +// 上次更新: 新增CanvasStyleWrapper自动持久化+动画过渡+圆角裁剪修复 // ============================================================ // // 设计原理: @@ -34,6 +35,7 @@ import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; import 'package:pro_image_editor/core/models/canvas_style_model.dart'; +import 'package:xianyan/editor/services/core/editor_settings_service.dart'; class CanvasStyleMiddleware extends StatelessWidget { const CanvasStyleMiddleware({ @@ -62,7 +64,11 @@ class CanvasStyleMiddleware extends StatelessWidget { Widget result = child; if (radius > 0) { - result = ClipRRect(borderRadius: borderRad, child: result); + result = ClipRRect( + borderRadius: borderRad, + clipBehavior: Clip.hardEdge, + child: result, + ); } if (style.hasBorder) { @@ -70,20 +76,7 @@ class CanvasStyleMiddleware extends StatelessWidget { } if (style.hasShadow) { - result = Container( - decoration: BoxDecoration( - borderRadius: borderRad, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: style.shadowOpacity), - blurRadius: style.shadowBlur, - spreadRadius: style.shadowSpread, - offset: Offset(style.shadowOffsetX, style.shadowOffsetY), - ), - ], - ), - child: result, - ); + result = _buildShadow(result, borderRad); } if (style.hasStack && style.stackLayers > 1) { @@ -100,6 +93,23 @@ class CanvasStyleMiddleware extends StatelessWidget { return result; } + Widget _buildShadow(Widget content, BorderRadius borderRad) { + return Container( + decoration: BoxDecoration( + borderRadius: borderRad, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: style.shadowOpacity), + blurRadius: style.shadowBlur, + spreadRadius: style.shadowSpread, + offset: Offset(style.shadowOffsetX, style.shadowOffsetY), + ), + ], + ), + child: content, + ); + } + Widget _buildStackLayers(Widget content, BorderRadius borderRad) { final children = []; @@ -198,3 +208,99 @@ class CanvasStyleMiddleware extends StatelessWidget { ); } } + +// ============================================================ +// CanvasStyleWrapper — 自动读取+持久化画布样式的包裹组件 +// ============================================================ + +class CanvasStyleWrapper extends StatefulWidget { + const CanvasStyleWrapper({ + super.key, + required this.child, + this.onStyleChanged, + }); + + final Widget child; + final ValueChanged? onStyleChanged; + + @override + State createState() => _CanvasStyleWrapperState(); +} + +class _CanvasStyleWrapperState extends State { + CanvasStyleModel _style = CanvasStyleModel.defaults(); + bool _loaded = false; + + @override + void initState() { + super.initState(); + _loadStyle(); + } + + void _loadStyle() async { + final style = await EditorSettingsService.loadCanvasStyle(); + if (mounted) { + setState(() { + _style = style; + _loaded = true; + }); + widget.onStyleChanged?.call(style); + } + } + + void _updateStyle(CanvasStyleModel newStyle) { + setState(() => _style = newStyle); + EditorSettingsService.saveCanvasStyle(newStyle); + widget.onStyleChanged?.call(newStyle); + } + + @override + Widget build(BuildContext context) { + if (!_loaded) return widget.child; + + return _CanvasStyleInherited( + style: _style, + onUpdate: _updateStyle, + child: CanvasStyleMiddleware( + style: _style, + child: widget.child, + ), + ); + } +} + +// ============================================================ +// InheritedWidget — 子组件可读取/更新画布样式 +// ============================================================ + +class _CanvasStyleInherited extends InheritedWidget { + const _CanvasStyleInherited({ + required this.style, + required this.onUpdate, + required super.child, + }); + + final CanvasStyleModel style; + final ValueChanged onUpdate; + + @override + bool updateShouldNotify(_CanvasStyleInherited old) => style != old.style; +} + +// ============================================================ +// CanvasStyleScope — 便捷访问画布样式 +// ============================================================ + +class CanvasStyleScope { + CanvasStyleScope._(); + + static CanvasStyleModel read(BuildContext context) { + final inherited = context.dependOnInheritedWidgetOfExactType<_CanvasStyleInherited>(); + return inherited?.style ?? CanvasStyleModel.defaults(); + } + + static void update(BuildContext context, CanvasStyleModel style) { + final inherited = context.dependOnInheritedWidgetOfExactType<_CanvasStyleInherited>(); + inherited?.onUpdate(style); + } +} diff --git a/lib/editor/services/core/editor_icons.dart b/lib/editor/services/core/editor_icons.dart index ac7c65d1..c22cf722 100644 --- a/lib/editor/services/core/editor_icons.dart +++ b/lib/editor/services/core/editor_icons.dart @@ -1,10 +1,10 @@ /// ============================================================ /// 闲言APP — 编辑器图标统一管理 /// 创建时间: 2026-05-04 -/// 更新时间: 2026-05-04 +/// 更新时间: 2026-05-22 /// 作用: 编辑器所有图标的统一入口,降级策略: Cupertino→SVG→Emoji /// 不存在的CupertinoIcons使用本地SVG自绘资源 -/// 上次更新: 修复不存在的CupertinoIcons,改用SVG资源 +/// 上次更新: 补充sidebar_left/rectangle/paintbrush_fill等缺失映射 /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -36,6 +36,7 @@ class EditorIconData { static const Map cupertinoMapping = { 'paintbrush': CupertinoIcons.paintbrush, + 'paintbrush_fill': CupertinoIcons.paintbrush_fill, 'textformat': CupertinoIcons.textformat, 'scissors': CupertinoIcons.scissors, 'cloud': CupertinoIcons.cloud, @@ -51,6 +52,8 @@ class EditorIconData { 'cube': CupertinoIcons.cube, 'circle': CupertinoIcons.circle, 'xmark': CupertinoIcons.xmark, + 'sidebar_left': CupertinoIcons.sidebar_left, + 'rectangle': CupertinoIcons.rectangle, }; static IconData? cupertino(String name) => cupertinoMapping[name]; diff --git a/lib/editor/services/core/pro_editor_bridge.dart b/lib/editor/services/core/pro_editor_bridge.dart index d189e30e..761cb028 100644 --- a/lib/editor/services/core/pro_editor_bridge.dart +++ b/lib/editor/services/core/pro_editor_bridge.dart @@ -10,11 +10,11 @@ import 'dart:ui' as ui; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:pro_image_editor/pro_image_editor.dart' as pro; import 'package:pro_image_editor/designs/frosted_glass/frosted_glass.dart'; +import 'package:xianyan/core/services/ui/status_bar_service.dart'; import 'package:xianyan/core/theme/app_radius.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/editor/models/editor_models.dart'; @@ -168,20 +168,8 @@ class ProEditorBridge { style: pro.MainEditorStyle( background: canvasBackground ?? EditorThemeNotifier.instance.palette.bgCanvas, - uiOverlayStyle: SystemUiOverlayStyle( - statusBarColor: const Color(0x00000000), - statusBarIconBrightness: EditorThemeNotifier.instance.isDark - ? Brightness.light - : Brightness.dark, - statusBarBrightness: EditorThemeNotifier.instance.isDark - ? Brightness.dark - : Brightness.light, - systemNavigationBarColor: const Color(0x00000000), - systemNavigationBarIconBrightness: - EditorThemeNotifier.instance.isDark - ? Brightness.light - : Brightness.dark, - systemNavigationBarDividerColor: const Color(0x00000000), + uiOverlayStyle: StatusBarService.resolveStyle( + isDark: EditorThemeNotifier.instance.isDark, ), ), ), diff --git a/lib/editor/widgets/controls/editor_system_ui.dart b/lib/editor/widgets/controls/editor_system_ui.dart index 99761deb..539e8f70 100644 --- a/lib/editor/widgets/controls/editor_system_ui.dart +++ b/lib/editor/widgets/controls/editor_system_ui.dart @@ -1,13 +1,14 @@ -// ============================================================ -// 闲言APP — 编辑器系统UI管理 -// 创建时间: 2026-05-03 -// 更新时间: 2026-05-03 -// 作用: 管理编辑器的系统状态栏、导航栏、屏幕方向 -// 上次更新: enterEditor读取编辑器主题偏好,浅色模式状态栏图标暗色 -// ============================================================ +/// ============================================================ +/// 闲言APP — 编辑器系统UI管理 +/// 创建时间: 2026-05-03 +/// 更新时间: 2026-05-22 +/// 作用: 管理编辑器的系统状态栏、导航栏、屏幕方向 +/// 上次更新: 统一使用StatusBarService,移除直接SystemChrome调用 +/// ============================================================ import 'package:flutter/services.dart'; +import 'package:xianyan/core/services/ui/status_bar_service.dart'; import 'package:xianyan/core/utils/device_detection.dart'; import 'package:xianyan/editor/services/core/editor_theme_notifier.dart'; @@ -16,7 +17,7 @@ class EditorSystemUI { static void enterEditor() { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - _applyOverlayStyle(isDark: EditorThemeNotifier.instance.isDark); + StatusBarService.applyStyle(isDark: EditorThemeNotifier.instance.isDark); if (AppDevice.isMobile) { SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, @@ -27,42 +28,13 @@ class EditorSystemUI { } static void exitEditor() { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - _applyDefaultOverlayStyle(); + StatusBarService.enterEdgeToEdge(); if (AppDevice.isMobile) { SystemChrome.setPreferredOrientations(DeviceOrientation.values); } } static void updateForTheme(bool isDark) { - _applyOverlayStyle(isDark: isDark); - } - - static void _applyOverlayStyle({required bool isDark}) { - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - statusBarColor: const Color(0x00000000), - statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, - statusBarBrightness: isDark ? Brightness.dark : Brightness.light, - systemNavigationBarColor: const Color(0x00000000), - systemNavigationBarIconBrightness: isDark - ? Brightness.light - : Brightness.dark, - systemNavigationBarDividerColor: const Color(0x00000000), - ), - ); - } - - static void _applyDefaultOverlayStyle() { - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Color(0x00000000), - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - systemNavigationBarColor: Color(0x00000000), - systemNavigationBarIconBrightness: Brightness.light, - systemNavigationBarDividerColor: Color(0x00000000), - ), - ); + StatusBarService.applyStyle(isDark: isDark); } } diff --git a/lib/editor/widgets/controls/editor_top_nav.dart b/lib/editor/widgets/controls/editor_top_nav.dart index a4e98279..82c590ab 100644 --- a/lib/editor/widgets/controls/editor_top_nav.dart +++ b/lib/editor/widgets/controls/editor_top_nav.dart @@ -171,8 +171,8 @@ class _EditorTopNavState extends State { if (widget.onToolDrawer != null) _PillDivider(palette: p), if (widget.onCanvasStyle != null) _NavBtn( - icon: EditorIcon.cupertino( - 'rectangle', + icon: EditorIcon.svg( + 'square_dashed', size: 16, color: p.textPrimary, ), diff --git a/lib/editor/widgets/panels/canvas_style_sheet.dart b/lib/editor/widgets/panels/canvas_style_sheet.dart index 9e5db5c3..7d67fb9a 100644 --- a/lib/editor/widgets/panels/canvas_style_sheet.dart +++ b/lib/editor/widgets/panels/canvas_style_sheet.dart @@ -1,20 +1,129 @@ // ============================================================ // 闲言APP — 画布样式编辑面板 // 创建时间: 2026-05-05 -// 更新时间: 2026-05-05 +// 更新时间: 2026-05-22 // 作用: 画布边缘样式编辑 — 圆角+边框+阴影+叠层效果 +// 样式预设模板 / 拖拽圆角手柄 / 取色器 // 独立文件,后续扩展开发 -// 上次更新: 初始创建 — 4大编辑区域+实时预览 +// 上次更新: 新增样式预设模板/拖拽圆角手柄/取色器功能 // ============================================================ +import 'dart:typed_data'; +import 'dart:ui' as ui; + import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show Colors, showModalBottomSheet, DraggableScrollableSheet; +import 'package:flutter/rendering.dart' show RenderRepaintBoundary; import 'package:xianyan/core/theme/app_theme.dart'; +import 'package:xianyan/core/theme/app_radius.dart'; +import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:pro_image_editor/core/models/canvas_style_model.dart'; +// ============================================================ +// 样式预设数据模型 +// ============================================================ + +class _StylePreset { + const _StylePreset({ + required this.name, + required this.emoji, + required this.style, + }); + + final String name; + final String emoji; + final CanvasStyleModel style; +} + +List<_StylePreset> _buildPresets() => [ + _StylePreset( + name: '卡片风', + emoji: '🃏', + style: CanvasStyleModel.defaults().copyWith( + borderRadius: 16, + borderStyle: CanvasBorderStyle.none, + borderWidth: 0, + shadowBlur: 20, + shadowOpacity: 0.15, + shadowOffsetY: 8, + ), + ), + _StylePreset( + name: '极简风', + emoji: '✨', + style: CanvasStyleModel.defaults().copyWith( + borderRadius: 4, + borderStyle: CanvasBorderStyle.none, + borderWidth: 0, + shadowBlur: 10, + shadowOpacity: 0.05, + shadowOffsetY: 2, + ), + ), + _StylePreset( + name: '毛玻璃风', + emoji: '🧊', + style: CanvasStyleModel.defaults().copyWith( + borderRadius: 24, + borderStyle: CanvasBorderStyle.solid, + borderWidth: 0.5, + borderColor: const Color(0xFFFFFFFF).withValues(alpha: 0.2), + shadowBlur: 30, + shadowOpacity: 0.1, + shadowOffsetY: 12, + ), + ), + _StylePreset( + name: '复古风', + emoji: '📜', + style: CanvasStyleModel.defaults().copyWith( + borderRadius: 8, + borderStyle: CanvasBorderStyle.solid, + borderWidth: 2, + borderColor: const Color(0xFF8B7355), + shadowBlur: 15, + shadowOpacity: 0.2, + shadowOffsetY: 5, + ), + ), + _StylePreset( + name: '霓虹风', + emoji: '💡', + style: CanvasStyleModel.defaults().copyWith( + borderRadius: 20, + borderStyle: CanvasBorderStyle.solid, + borderWidth: 1.5, + borderColor: const Color(0xFF00FFFF), + shadowBlur: 25, + shadowOpacity: 0.4, + shadowOffsetY: 0, + ), + ), + ]; + +// ============================================================ +// 取色器缓存数据 +// ============================================================ + +class _CapturedImageData { + const _CapturedImageData({ + required this.byteData, + required this.width, + required this.height, + }); + + final ByteData byteData; + final int width; + final int height; +} + +// ============================================================ +// CanvasStyleSheet — 画布样式编辑面板 +// ============================================================ + class CanvasStyleSheet extends StatefulWidget { const CanvasStyleSheet({ super.key, @@ -45,7 +154,9 @@ class CanvasStyleSheet extends StatefulWidget { builder: (context, scrollController) => Container( decoration: BoxDecoration( color: AppTheme.ext(context).bgPrimary, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppRadius.xl), + ), ), child: CanvasStyleSheet(style: style, onChanged: onChanged), ), @@ -57,17 +168,141 @@ class CanvasStyleSheet extends StatefulWidget { class _CanvasStyleSheetState extends State { late CanvasStyleModel _style; + bool _isEyedropperMode = false; + String? _eyedropperTarget; + Offset? _eyedropperPosition; + Color? _eyedropperPreviewColor; + _CapturedImageData? _capturedImageData; + bool _isRadiusHandleActive = false; + final _previewKey = GlobalKey(); + @override void initState() { super.initState(); _style = widget.style; } + @override + void dispose() { + _capturedImageData = null; + super.dispose(); + } + void _update(CanvasStyleModel newStyle) { setState(() => _style = newStyle); widget.onChanged(newStyle); } + // ─── 预设匹配 ─── + + bool _matchesPreset(_StylePreset preset) { + final p = preset.style; + return _style.borderRadius == p.borderRadius && + _style.borderStyle == p.borderStyle && + _style.borderWidth == p.borderWidth && + _style.borderColor == p.borderColor && + _style.shadowBlur == p.shadowBlur && + _style.shadowOpacity == p.shadowOpacity && + _style.shadowSpread == p.shadowSpread && + _style.shadowOffsetX == p.shadowOffsetX && + _style.shadowOffsetY == p.shadowOffsetY && + _style.stackLayers == p.stackLayers && + _style.stackDistance == p.stackDistance && + _style.stackPosition == p.stackPosition && + _style.outerMargin == p.outerMargin; + } + + // ─── 取色器 ─── + + Future _enterEyedropperMode(String target) async { + setState(() { + _isEyedropperMode = true; + _eyedropperTarget = target; + _eyedropperPosition = null; + _eyedropperPreviewColor = null; + }); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + final boundary = _previewKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) return; + + final image = await boundary.toImage(pixelRatio: 2.0); + final byteData = + await image.toByteData(); + if (mounted && byteData != null) { + setState(() { + _capturedImageData = _CapturedImageData( + byteData: byteData, + width: image.width, + height: image.height, + ); + }); + } + }); + } + + void _exitEyedropperMode() { + setState(() { + _isEyedropperMode = false; + _eyedropperTarget = null; + _eyedropperPosition = null; + _eyedropperPreviewColor = null; + _capturedImageData = null; + }); + } + + Color? _sampleColorAt(Offset localPosition) { + if (_capturedImageData == null) return null; + final data = _capturedImageData!; + const pixelRatio = 2.0; + final x = + (localPosition.dx * pixelRatio).toInt().clamp(0, data.width - 1); + final y = + (localPosition.dy * pixelRatio).toInt().clamp(0, data.height - 1); + + final offset = (y * data.width + x) * 4; + final bytes = data.byteData.buffer.asUint8List(); + if (offset + 3 >= bytes.length) return null; + + return Color.fromARGB( + bytes[offset + 3], + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + ); + } + + void _onEyedropperPanUpdate(DragUpdateDetails details) { + final color = _sampleColorAt(details.localPosition); + setState(() { + _eyedropperPosition = details.localPosition; + _eyedropperPreviewColor = color; + }); + } + + void _onEyedropperTapUp(TapUpDetails details) { + final color = _sampleColorAt(details.localPosition); + if (color != null) { + if (_eyedropperTarget == 'border') { + _update(_style.copyWith(borderColor: color)); + } + _exitEyedropperMode(); + } + } + + void _onEyedropperPanEnd(DragEndDetails details) { + if (_eyedropperPreviewColor != null) { + if (_eyedropperTarget == 'border') { + _update(_style.copyWith(borderColor: _eyedropperPreviewColor!)); + } + _exitEyedropperMode(); + } + } + + // ─── Build ─── + @override Widget build(BuildContext context) { final ext = AppTheme.ext(context); @@ -78,11 +313,13 @@ class _CanvasStyleSheetState extends State { _buildHeader(ext), Expanded( child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), children: [ - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), + _buildPresetSection(ext), + const SizedBox(height: AppSpacing.md), _buildPreview(ext), - const SizedBox(height: 16), + const SizedBox(height: AppSpacing.md), _buildRadiusSection(ext), const SizedBox(height: 20), _buildBorderSection(ext), @@ -116,11 +353,16 @@ class _CanvasStyleSheetState extends State { Widget _buildHeader(AppThemeExtension ext) { return Padding( - padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + 4, + AppSpacing.md, + AppSpacing.sm, + ), child: Row( children: [ - Icon(CupertinoIcons.rectangle, size: 20, color: ext.accent), - const SizedBox(width: 8), + const Icon(CupertinoIcons.paintbrush_fill, size: 20), + const SizedBox(width: AppSpacing.sm), Text( '画布样式', style: TextStyle( @@ -130,34 +372,141 @@ class _CanvasStyleSheetState extends State { ), ), const Spacer(), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: ext.bgCard.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(14), + if (_isEyedropperMode) + GestureDetector( + onTap: _exitEyedropperMode, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.12), + borderRadius: AppRadius.mdBorder, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(CupertinoIcons.eye_fill, + size: 12, color: ext.accent), + const SizedBox(width: 4), + Text( + '取色中', + style: TextStyle( + color: ext.accent, + fontSize: 11, + fontWeight: FontWeight.w600), + ), + ], + ), ), - child: Icon( - CupertinoIcons.xmark, - color: ext.textSecondary, - size: 14, + ) + else + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: ext.bgCard.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + CupertinoIcons.xmark, + color: ext.textSecondary, + size: 14, + ), ), ), - ), ], ), ); } - // ─── 实时预览 ─── + // ─── 样式预设 ─── + + Widget _buildPresetSection(AppThemeExtension ext) { + final presets = _buildPresets(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + const Icon(CupertinoIcons.square_grid_2x2_fill, size: 14), + const SizedBox(width: 4), + Text( + '样式预设', + style: TextStyle( + color: ext.textSecondary, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + SizedBox( + height: 72, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: presets.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (_, index) { + final preset = presets[index]; + final isActive = _matchesPreset(preset); + return _StylePresetChip( + ext: ext, + preset: preset, + isActive: isActive, + onTap: () => _update(preset.style), + ); + }, + ), + ), + ], + ); + } + + // ─── 预览 ─── Widget _buildPreview(AppThemeExtension ext) { return Container( - height: 160, + height: 180, alignment: Alignment.center, - child: _CanvasPreviewCard(style: _style, ext: ext), + child: RepaintBoundary( + key: _previewKey, + child: Stack( + alignment: Alignment.center, + children: [ + _CanvasPreviewCard( + style: _style, + ext: ext, + showCornerHandles: _isRadiusHandleActive && !_isEyedropperMode, + onRadiusChanged: (v) => + _update(_style.copyWith(borderRadius: v)), + ), + if (_isEyedropperMode) _buildEyedropperOverlay(ext), + ], + ), + ), + ); + } + + Widget _buildEyedropperOverlay(AppThemeExtension ext) { + return Positioned.fill( + child: GestureDetector( + onPanUpdate: _onEyedropperPanUpdate, + onPanEnd: _onEyedropperPanEnd, + onTapUp: _onEyedropperTapUp, + behavior: HitTestBehavior.opaque, + child: CustomPaint( + painter: _EyedropperOverlayPainter( + position: _eyedropperPosition, + previewColor: _eyedropperPreviewColor, + ), + ), + ), ); } @@ -171,6 +520,29 @@ class _CanvasStyleSheetState extends State { value: '${_style.borderRadius.round()}px', child: Column( children: [ + Row( + children: [ + Expanded( + child: Row( + children: [ + Text( + '拖拽手柄', + style: TextStyle( + color: ext.textSecondary, fontSize: 11), + ), + const SizedBox(width: 6), + CupertinoSwitch( + value: _isRadiusHandleActive, + onChanged: (v) => + setState(() => _isRadiusHandleActive = v), + activeTrackColor: ext.accent, + ), + ], + ), + ), + ], + ), + const SizedBox(height: AppSpacing.sm), Row( children: [ _RadiusPreset( @@ -214,7 +586,7 @@ class _CanvasStyleSheetState extends State { ), ], ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), CupertinoSlider( value: _style.borderRadius, max: 80, @@ -265,11 +637,12 @@ class _CanvasStyleSheetState extends State { suffix: '${_style.borderWidth.toStringAsFixed(1)}px', ), const SizedBox(height: 10), - _buildColorRow( + _buildColorRowWithEyedropper( ext, '颜色', _style.borderColor, (c) => _update(_style.copyWith(borderColor: c)), + 'border', ), ], ], @@ -290,8 +663,8 @@ class _CanvasStyleSheetState extends State { children: [ Expanded( child: GestureDetector( - onTap: () => - _update(_style.copyWith(shadowBlur: 0, shadowOpacity: 0)), + onTap: () => _update( + _style.copyWith(shadowBlur: 0, shadowOpacity: 0)), child: _PresetChip( ext: ext, label: '无', @@ -339,7 +712,7 @@ class _CanvasStyleSheetState extends State { (v) => _update(_style.copyWith(shadowBlur: v)), suffix: '${_style.shadowBlur.round()}px', ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), _buildSliderRow( ext, '浓度', @@ -350,7 +723,7 @@ class _CanvasStyleSheetState extends State { (v) => _update(_style.copyWith(shadowOpacity: v)), suffix: '${(_style.shadowOpacity * 100).round()}%', ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), _buildSliderRow( ext, '扩散', @@ -361,7 +734,7 @@ class _CanvasStyleSheetState extends State { (v) => _update(_style.copyWith(shadowSpread: v)), suffix: '${_style.shadowSpread.round()}px', ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), _buildSliderRow( ext, '↔ 偏移', @@ -371,12 +744,13 @@ class _CanvasStyleSheetState extends State { 30, (v) => _update( _style.copyWith( - shadowOffsetX: _style.shadowOffsetX < 0 ? -v : v, + shadowOffsetX: + _style.shadowOffsetX < 0 ? -v : v, ), ), suffix: '${_style.shadowOffsetX.round()}px', ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), _buildSliderRow( ext, '↕ 偏移', @@ -386,12 +760,13 @@ class _CanvasStyleSheetState extends State { 30, (v) => _update( _style.copyWith( - shadowOffsetY: _style.shadowOffsetY < 0 ? -v : v, + shadowOffsetY: + _style.shadowOffsetY < 0 ? -v : v, ), ), suffix: '${_style.shadowOffsetY.round()}px', ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), Row( children: [ _buildOffsetDirectionButton( @@ -400,10 +775,8 @@ class _CanvasStyleSheetState extends State { label: '左', onTap: () => _update( _style.copyWith( - shadowOffsetX: (_style.shadowOffsetX - 2).clamp( - -30.0, - 30.0, - ), + shadowOffsetX: + (_style.shadowOffsetX - 2).clamp(-30.0, 30.0), ), ), ), @@ -414,10 +787,8 @@ class _CanvasStyleSheetState extends State { label: '右', onTap: () => _update( _style.copyWith( - shadowOffsetX: (_style.shadowOffsetX + 2).clamp( - -30.0, - 30.0, - ), + shadowOffsetX: + (_style.shadowOffsetX + 2).clamp(-30.0, 30.0), ), ), ), @@ -428,10 +799,8 @@ class _CanvasStyleSheetState extends State { label: '上', onTap: () => _update( _style.copyWith( - shadowOffsetY: (_style.shadowOffsetY - 2).clamp( - -30.0, - 30.0, - ), + shadowOffsetY: + (_style.shadowOffsetY - 2).clamp(-30.0, 30.0), ), ), ), @@ -442,10 +811,8 @@ class _CanvasStyleSheetState extends State { label: '下', onTap: () => _update( _style.copyWith( - shadowOffsetY: (_style.shadowOffsetY + 2).clamp( - -30.0, - 30.0, - ), + shadowOffsetY: + (_style.shadowOffsetY + 2).clamp(-30.0, 30.0), ), ), ), @@ -466,8 +833,6 @@ class _CanvasStyleSheetState extends State { ); } - // ─── 叠层 ─── - Widget _buildStackSection(AppThemeExtension ext) { return _SectionCard( ext: ext, @@ -507,13 +872,14 @@ class _CanvasStyleSheetState extends State { fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.sm), Row( children: CanvasStackPosition.values.map((p) { final isActive = _style.stackPosition == p; return Expanded( child: GestureDetector( - onTap: () => _update(_style.copyWith(stackPosition: p)), + onTap: () => + _update(_style.copyWith(stackPosition: p)), child: _PresetChip( ext: ext, label: p.label, @@ -529,14 +895,11 @@ class _CanvasStyleSheetState extends State { ); } - // ─── 外边距 ─── - Widget _buildOuterMarginSection(AppThemeExtension ext) { return _SectionCard( ext: ext, icon: CupertinoIcons.resize, title: '外边距', - // subtitle: '控制画布与最外层视图的距离', child: Column( children: [ Row( @@ -605,8 +968,6 @@ class _CanvasStyleSheetState extends State { ); } - // ─── 通用组件 ─── - Widget _buildSliderRow( AppThemeExtension ext, String label, @@ -666,7 +1027,7 @@ class _CanvasStyleSheetState extends State { padding: const EdgeInsets.symmetric(vertical: 6), decoration: BoxDecoration( color: ext.bgSecondary, - borderRadius: BorderRadius.circular(8), + borderRadius: AppRadius.mdBorder, border: Border.all( color: ext.bgCard.withValues(alpha: 0.3), width: 0.5, @@ -688,11 +1049,12 @@ class _CanvasStyleSheetState extends State { ); } - Widget _buildColorRow( + Widget _buildColorRowWithEyedropper( AppThemeExtension ext, String label, Color color, ValueChanged onChanged, + String target, ) { return Row( children: [ @@ -703,7 +1065,7 @@ class _CanvasStyleSheetState extends State { style: TextStyle(color: ext.textSecondary, fontSize: 12), ), ), - const SizedBox(width: 8), + const SizedBox(width: AppSpacing.sm), GestureDetector( onTap: () => _showColorPicker(ext, color, onChanged), child: Container( @@ -711,7 +1073,7 @@ class _CanvasStyleSheetState extends State { height: 28, decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(6), + borderRadius: AppRadius.smBorder, border: Border.all( color: ext.textHint.withValues(alpha: 0.3), width: 0.5, @@ -719,13 +1081,42 @@ class _CanvasStyleSheetState extends State { ), ), ), - const SizedBox(width: 8), - Text( - '#${color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}', - style: TextStyle( - color: ext.textSecondary, - fontSize: 11, - fontFeatures: const [FontFeature.tabularFigures()], + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + '#${color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}', + style: TextStyle( + color: ext.textSecondary, + fontSize: 11, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + const SizedBox(width: AppSpacing.sm), + GestureDetector( + onTap: () => _enterEyedropperMode(target), + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: _isEyedropperMode && _eyedropperTarget == target + ? ext.accent.withValues(alpha: 0.15) + : ext.bgSecondary, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _isEyedropperMode && _eyedropperTarget == target + ? ext.accent + : ext.textHint.withValues(alpha: 0.3), + width: 0.5, + ), + ), + child: Icon( + CupertinoIcons.eye_fill, + size: 13, + color: _isEyedropperMode && _eyedropperTarget == target + ? ext.accent + : ext.textSecondary, + ), ), ), ], @@ -745,62 +1136,82 @@ class _CanvasStyleSheetState extends State { constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), - decoration: BoxDecoration( - color: ext.bgPrimary, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + decoration: const BoxDecoration( + color: Color(0x00000000), + borderRadius: BorderRadius.vertical( + top: Radius.circular(AppRadius.xl), + ), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - child: Row( - children: [ - Text( - '选择颜色', - style: TextStyle( - color: ext.textPrimary, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - const Spacer(), - CupertinoButton( - padding: EdgeInsets.zero, - minimumSize: const Size(32, 32), - onPressed: () => Navigator.pop(sheetContext), - child: Text( - '完成', - style: TextStyle(color: ext.accent, fontSize: 14), - ), - ), - ], - ), + child: Container( + decoration: BoxDecoration( + color: ext.bgPrimary, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppRadius.xl), ), - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), - child: ColorPicker( - color: current, - onColorChanged: onChanged, - width: 36, - height: 36, - borderRadius: 8, - spacing: 6, - runSpacing: 6, - wheelDiameter: 200, - showColorCode: true, - colorCodeHasColor: true, - pickersEnabled: const { - ColorPickerType.wheel: true, - ColorPickerType.accent: false, - ColorPickerType.primary: false, - ColorPickerType.both: false, - }, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + 12, + AppSpacing.md, + 0, + ), + child: Row( + children: [ + Text( + '选择颜色', + style: TextStyle( + color: ext.textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + CupertinoButton( + padding: EdgeInsets.zero, + minimumSize: const Size(32, 32), + onPressed: () => Navigator.pop(sheetContext), + child: Text( + '完成', + style: TextStyle(color: ext.accent, fontSize: 14), + ), + ), + ], ), ), - ), - ], + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + 0, + AppSpacing.md, + AppSpacing.lg, + ), + child: ColorPicker( + color: current, + onColorChanged: onChanged, + width: 36, + height: 36, + borderRadius: 8, + spacing: 6, + runSpacing: 6, + wheelDiameter: 200, + showColorCode: true, + colorCodeHasColor: true, + pickersEnabled: const { + ColorPickerType.wheel: true, + ColorPickerType.accent: false, + ColorPickerType.primary: false, + ColorPickerType.both: false, + }, + ), + ), + ), + ], + ), ), ), ); @@ -832,7 +1243,7 @@ class _SectionCard extends StatelessWidget { padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: ext.bgSecondary.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(14), + borderRadius: AppRadius.lgBorder, border: Border.all( color: ext.bgCard.withValues(alpha: 0.3), width: 0.5, @@ -966,7 +1377,8 @@ class _RadiusPreset extends StatelessWidget { height: 24, decoration: BoxDecoration( color: ext.bgCard, - borderRadius: BorderRadius.circular(radius.clamp(0, 12)), + borderRadius: + BorderRadius.circular(radius.clamp(0, 12)), border: Border.all( color: isActive ? ext.accent : ext.textHint, ), @@ -990,14 +1402,116 @@ class _RadiusPreset extends StatelessWidget { } // ============================================================ -// 实时预览卡片 +// 样式预设芯片 +// ============================================================ + +class _StylePresetChip extends StatelessWidget { + const _StylePresetChip({ + required this.ext, + required this.preset, + required this.isActive, + required this.onTap, + }); + + final AppThemeExtension ext; + final _StylePreset preset; + final bool isActive; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final s = preset.style; + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 80, + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), + decoration: BoxDecoration( + color: isActive + ? ext.accent.withValues(alpha: 0.12) + : ext.bgSecondary.withValues(alpha: 0.6), + borderRadius: AppRadius.mdBorder, + border: Border.all( + color: isActive + ? ext.accent.withValues(alpha: 0.5) + : ext.bgCard.withValues(alpha: 0.3), + width: isActive ? 1.2 : 0.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 44, + height: 28, + decoration: BoxDecoration( + color: ext.bgElevated, + borderRadius: BorderRadius.circular( + s.borderRadius.clamp(0.0, 14.0)), + border: s.hasBorder + ? Border.all( + color: s.borderColor, + width: s.borderWidth.clamp(0.5, 2.0), + ) + : null, + boxShadow: s.hasShadow + ? [ + BoxShadow( + color: Colors.black + .withValues(alpha: s.shadowOpacity * 0.6), + blurRadius: s.shadowBlur * 0.2, + offset: Offset(0, s.shadowOffsetY * 0.15), + ), + ] + : null, + ), + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (isActive) + Padding( + padding: const EdgeInsets.only(right: 2), + child: Icon(CupertinoIcons.checkmark_circle_fill, + size: 10, color: ext.accent), + ), + Text( + '${preset.emoji} ${preset.name}', + style: TextStyle( + fontSize: 9, + fontWeight: + isActive ? FontWeight.w600 : FontWeight.w400, + color: isActive ? ext.accent : ext.textSecondary, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +// ============================================================ +// 实时预览卡片(含圆角拖拽手柄) // ============================================================ class _CanvasPreviewCard extends StatelessWidget { - const _CanvasPreviewCard({required this.style, required this.ext}); + const _CanvasPreviewCard({ + required this.style, + required this.ext, + this.showCornerHandles = false, + this.onRadiusChanged, + }); final CanvasStyleModel style; final AppThemeExtension ext; + final bool showCornerHandles; + final ValueChanged? onRadiusChanged; @override Widget build(BuildContext context) { @@ -1023,13 +1537,13 @@ class _CanvasPreviewCard extends StatelessWidget { final dx = style.stackPosition == CanvasStackPosition.left ? -offset : style.stackPosition == CanvasStackPosition.right - ? offset - : 0.0; + ? offset + : 0.0; final dy = style.stackPosition == CanvasStackPosition.top ? -offset : style.stackPosition == CanvasStackPosition.bottom - ? offset - : 0.0; + ? offset + : 0.0; return Transform.translate( offset: Offset(dx, dy), @@ -1038,7 +1552,8 @@ class _CanvasPreviewCard extends StatelessWidget { height: 80, decoration: BoxDecoration( color: ext.bgCard.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(style.borderRadius.clamp(0, 40)), + borderRadius: + BorderRadius.circular(style.borderRadius.clamp(0, 40)), border: style.hasBorder ? Border.all( color: style.borderColor.withValues(alpha: 0.2), @@ -1061,7 +1576,8 @@ class _CanvasPreviewCard extends StatelessWidget { boxShadow: style.hasShadow ? [ BoxShadow( - color: Colors.black.withValues(alpha: style.shadowOpacity), + color: + Colors.black.withValues(alpha: style.shadowOpacity), blurRadius: (style.shadowBlur * 0.3).toDouble(), spreadRadius: (style.shadowSpread * 0.3).toDouble(), offset: Offset( @@ -1107,10 +1623,239 @@ class _CanvasPreviewCard extends StatelessWidget { } } + if (showCornerHandles && onRadiusChanged != null) { + card = SizedBox( + width: 120, + height: 80, + child: Stack( + clipBehavior: Clip.none, + children: [ + card, + _CornerHandle( + alignment: Alignment.topLeft, + borderRadius: style.borderRadius, + ext: ext, + onRadiusChanged: onRadiusChanged!, + ), + _CornerHandle( + alignment: Alignment.topRight, + borderRadius: style.borderRadius, + ext: ext, + onRadiusChanged: onRadiusChanged!, + ), + _CornerHandle( + alignment: Alignment.bottomLeft, + borderRadius: style.borderRadius, + ext: ext, + onRadiusChanged: onRadiusChanged!, + ), + _CornerHandle( + alignment: Alignment.bottomRight, + borderRadius: style.borderRadius, + ext: ext, + onRadiusChanged: onRadiusChanged!, + ), + ], + ), + ); + } + return card; } } +// ============================================================ +// 圆角拖拽手柄 +// ============================================================ + +class _CornerHandle extends StatelessWidget { + const _CornerHandle({ + required this.alignment, + required this.borderRadius, + required this.ext, + required this.onRadiusChanged, + }); + + final Alignment alignment; + final double borderRadius; + final AppThemeExtension ext; + final ValueChanged onRadiusChanged; + + @override + Widget build(BuildContext context) { + double? left, top, right, bottom; + const handleSize = 12.0; + const half = handleSize / 2; + + if (alignment == Alignment.topLeft) { + left = -half; + top = -half; + } else if (alignment == Alignment.topRight) { + top = -half; + right = -half; + } else if (alignment == Alignment.bottomLeft) { + left = -half; + bottom = -half; + } else { + right = -half; + bottom = -half; + } + + return Positioned( + left: left, + top: top, + right: right, + bottom: bottom, + child: GestureDetector( + onPanUpdate: (details) { + final delta = details.delta; + double projected; + if (alignment == Alignment.topLeft) { + projected = (delta.dx + delta.dy) / 1.414; + } else if (alignment == Alignment.topRight) { + projected = (-delta.dx + delta.dy) / 1.414; + } else if (alignment == Alignment.bottomLeft) { + projected = (delta.dx - delta.dy) / 1.414; + } else { + projected = (-delta.dx - delta.dy) / 1.414; + } + final newRadius = + (borderRadius + projected * 2.0).clamp(0.0, 80.0); + onRadiusChanged(newRadius); + }, + child: Container( + width: handleSize, + height: handleSize, + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.6), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withValues(alpha: 0.9), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 4, + spreadRadius: 0.5, + ), + ], + ), + ), + ), + ); + } +} + +// ============================================================ +// 取色器遮罩画笔 +// ============================================================ + +class _EyedropperOverlayPainter extends CustomPainter { + const _EyedropperOverlayPainter({ + required this.position, + required this.previewColor, + }); + + final Offset? position; + final Color? previewColor; + + @override + void paint(Canvas canvas, Size size) { + if (position == null) { + final paint = Paint() + ..color = Colors.black.withValues(alpha: 0.08); + canvas.drawRect( + Rect.fromLTWH(0, 0, size.width, size.height), + paint, + ); + + final textStyle = ui.TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 12, + ); + final paragraphBuilder = ui.ParagraphBuilder( + ui.ParagraphStyle(textAlign: ui.TextAlign.center), + )..pushStyle(textStyle)..addText('👆 点击取色'); + final paragraph = paragraphBuilder.build() + ..layout(ui.ParagraphConstraints(width: size.width)); + canvas.drawParagraph( + paragraph, + Offset(0, (size.height - paragraph.height) / 2), + ); + return; + } + + final center = position!; + const magnifierRadius = 32.0; + + final bgPaint = Paint() + ..color = Colors.black.withValues(alpha: 0.06); + canvas.drawRect( + Rect.fromLTWH(0, 0, size.width, size.height), + bgPaint, + ); + + if (previewColor != null) { + canvas.drawCircle( + center, + magnifierRadius + 3, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 2.5, + ); + + canvas.drawCircle( + center, + magnifierRadius, + Paint()..color = previewColor!, + ); + + final brightness = previewColor!.computeLuminance(); + final crossColor = + brightness > 0.5 ? Colors.black54 : Colors.white70; + final crossPaint = Paint() + ..color = crossColor + ..strokeWidth = 1.2; + canvas.drawLine( + Offset(center.dx - 8, center.dy), + Offset(center.dx + 8, center.dy), + crossPaint, + ); + canvas.drawLine( + Offset(center.dx, center.dy - 8), + Offset(center.dx, center.dy + 8), + crossPaint, + ); + + final hex = '#${previewColor!.toARGB32().toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}'; + final textStyle = ui.TextStyle( + color: brightness > 0.5 ? Colors.black87 : Colors.white, + fontSize: 9, + fontWeight: ui.FontWeight.w600, + ); + final paragraphBuilder = ui.ParagraphBuilder( + ui.ParagraphStyle(textAlign: ui.TextAlign.center), + )..pushStyle(textStyle)..addText(hex); + final paragraph = paragraphBuilder.build() + ..layout(const ui.ParagraphConstraints(width: 80)); + canvas.drawParagraph( + paragraph, + Offset(center.dx - 40, center.dy + magnifierRadius + 6), + ); + } + } + + @override + bool shouldRepaint(covariant _EyedropperOverlayPainter old) => + old.position != position || old.previewColor != previewColor; +} + +// ============================================================ +// 虚线边框画笔 +// ============================================================ + class _PreviewDashedPainter extends CustomPainter { _PreviewDashedPainter({ required this.borderColor, @@ -1148,7 +1893,8 @@ class _PreviewDashedPainter extends CustomPainter { for (final metric in metrics) { double distance = 0; while (distance < metric.length) { - final end = (distance + dashWidth).clamp(0.0, metric.length).toDouble(); + final end = + (distance + dashWidth).clamp(0.0, metric.length).toDouble(); canvas.drawPath(metric.extractPath(distance, end), paint); distance += dashWidth + dashGap; } diff --git a/lib/features/agreements/presentation/agreement_list_page.dart b/lib/features/agreements/presentation/agreement_list_page.dart index 69ea6637..69323e50 100644 --- a/lib/features/agreements/presentation/agreement_list_page.dart +++ b/lib/features/agreements/presentation/agreement_list_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 软件协议列表页 /// 创建时间: 2026-05-19 /// 更新时间: 2026-05-19 @@ -20,6 +20,7 @@ import '../../../core/router/app_nav_extension.dart'; import '../../../shared/widgets/glass_container.dart'; import '../data/agreement_types.dart'; import '../data/agreement_data.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class AgreementListPage extends ConsumerWidget { const AgreementListPage({super.key}); @@ -34,6 +35,7 @@ class AgreementListPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( languageId == 'en' ? 'Agreements' : '软件协议', style: AppTypography.title3.copyWith( diff --git a/lib/features/agreements/presentation/agreement_page.dart b/lib/features/agreements/presentation/agreement_page.dart index 196afeca..e05cb2c4 100644 --- a/lib/features/agreements/presentation/agreement_page.dart +++ b/lib/features/agreements/presentation/agreement_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 通用协议展示页 /// 创建时间: 2026-05-19 /// 更新时间: 2026-05-19 @@ -19,6 +19,7 @@ import '../../../l10n/app_locale.dart'; import '../../../shared/widgets/glass_container.dart'; import '../data/agreement_types.dart'; import '../data/agreement_data.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class AgreementPage extends ConsumerWidget { const AgreementPage({super.key, required this.type}); @@ -42,6 +43,7 @@ class AgreementPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( type.titleFor(languageId), style: AppTypography.title3.copyWith( diff --git a/lib/features/article/presentation/article_detail_page.dart b/lib/features/article/presentation/article_detail_page.dart index 29c756af..b95cbf0c 100644 --- a/lib/features/article/presentation/article_detail_page.dart +++ b/lib/features/article/presentation/article_detail_page.dart @@ -17,6 +17,7 @@ import '../../../core/theme/app_theme.dart'; import '../../../shared/widgets/glass_container.dart'; import '../models/article_models.dart'; import '../providers/article_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class ArticleDetailPage extends ConsumerStatefulWidget { const ArticleDetailPage({super.key, required this.articleId}); @@ -45,6 +46,7 @@ class _ArticleDetailPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('📖 文章详情'), previousPageTitle: '返回', ), diff --git a/lib/features/article/presentation/article_edit_page.dart b/lib/features/article/presentation/article_edit_page.dart index c0b09485..fccc0e2d 100644 --- a/lib/features/article/presentation/article_edit_page.dart +++ b/lib/features/article/presentation/article_edit_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 文章编辑/投稿页面 (增强版) // 创建时间: 2026-04-29 // 更新时间: 2026-04-30 @@ -18,6 +18,7 @@ import '../../../core/theme/app_theme.dart'; import '../../../shared/widgets/glass_container.dart'; import '../models/article_models.dart'; import '../providers/article_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class ArticleEditPage extends ConsumerStatefulWidget { const ArticleEditPage({super.key, this.articleId}); @@ -72,6 +73,7 @@ class _ArticleEditPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text(widget.articleId != null ? '✏️ 编辑文章' : '📝 写文章'), previousPageTitle: '取消', trailing: Row( diff --git a/lib/features/article/presentation/article_list_page.dart b/lib/features/article/presentation/article_list_page.dart index 2944f5c3..4fe4a97c 100644 --- a/lib/features/article/presentation/article_list_page.dart +++ b/lib/features/article/presentation/article_list_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 文章广场页面 (增强版) // 创建时间: 2026-04-29 // 更新时间: 2026-04-30 @@ -20,6 +20,7 @@ import '../../../core/theme/app_theme.dart'; import '../../../shared/widgets/glass_container.dart'; import '../models/article_models.dart'; import '../providers/article_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class ArticleListPage extends ConsumerStatefulWidget { const ArticleListPage({super.key}); @@ -59,6 +60,7 @@ class _ArticleListPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('📝 文章广场'), previousPageTitle: '返回', trailing: Row( diff --git a/lib/features/article/presentation/my_articles_page.dart b/lib/features/article/presentation/my_articles_page.dart index 984acd02..1b9e05db 100644 --- a/lib/features/article/presentation/my_articles_page.dart +++ b/lib/features/article/presentation/my_articles_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 我的文章页面 // 创建时间: 2026-04-29 // 更新时间: 2026-04-30 @@ -20,6 +20,7 @@ import '../../../core/theme/app_theme.dart'; import '../../../shared/widgets/glass_container.dart'; import '../models/article_models.dart'; import '../providers/article_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class MyArticlesPage extends ConsumerStatefulWidget { const MyArticlesPage({super.key}); @@ -53,6 +54,7 @@ class _MyArticlesPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('📄 我的文章'), previousPageTitle: '返回', trailing: CupertinoButton( diff --git a/lib/features/auth/presentation/login_page.dart b/lib/features/auth/presentation/login_page.dart index 80bf7b86..83cf63df 100644 --- a/lib/features/auth/presentation/login_page.dart +++ b/lib/features/auth/presentation/login_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 登录页面 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-19 @@ -14,7 +14,7 @@ import 'package:xianyan/core/router/app_nav_extension.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart' as url_launcher; -import '../../../core/router/app_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../core/network/api_exception.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_spacing.dart'; diff --git a/lib/features/check/presentation/check_page.dart b/lib/features/check/presentation/check_page.dart index fae88ba1..8760454a 100644 --- a/lib/features/check/presentation/check_page.dart +++ b/lib/features/check/presentation/check_page.dart @@ -19,6 +19,7 @@ import '../../../core/theme/app_theme.dart'; import '../../../shared/widgets/glass_container.dart'; import '../models/check_models.dart'; import '../providers/check_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class CheckPage extends ConsumerStatefulWidget { const CheckPage({super.key}); @@ -43,6 +44,7 @@ class _CheckPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('🔍 内容查重'), previousPageTitle: '返回', ), diff --git a/lib/features/classics/presentation/classics_list_page.dart b/lib/features/classics/presentation/classics_list_page.dart index d582ac22..a28b21c8 100644 --- a/lib/features/classics/presentation/classics_list_page.dart +++ b/lib/features/classics/presentation/classics_list_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 国学经典列表页 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -17,6 +17,7 @@ import '../../../shared/widgets/glass_container.dart'; import '../../home/models/feed_model.dart'; import '../providers/classics_provider.dart'; import '../services/classics_service.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class ClassicsListPage extends ConsumerStatefulWidget { const ClassicsListPage({super.key, required this.category}); @@ -52,6 +53,7 @@ class _ClassicsListPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text('${widget.category.emoji} ${widget.category.name}'), previousPageTitle: '国学', ), diff --git a/lib/features/classics/presentation/classics_page.dart b/lib/features/classics/presentation/classics_page.dart index 7ac76e0f..268fbb03 100644 --- a/lib/features/classics/presentation/classics_page.dart +++ b/lib/features/classics/presentation/classics_page.dart @@ -16,6 +16,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/glass_container.dart'; import '../services/classics_service.dart'; import 'classics_list_page.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class ClassicsPage extends ConsumerWidget { const ClassicsPage({super.key}); @@ -26,6 +27,7 @@ class ClassicsPage extends ConsumerWidget { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('📖 国学经典'), previousPageTitle: '返回', ), diff --git a/lib/features/correction/presentation/correction_page.dart b/lib/features/correction/presentation/correction_page.dart index d819eb46..4498e8e4 100644 --- a/lib/features/correction/presentation/correction_page.dart +++ b/lib/features/correction/presentation/correction_page.dart @@ -13,6 +13,7 @@ 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 '../../../shared/widgets/adaptive_back_button.dart'; import '../../../shared/widgets/glass_container.dart'; import '../providers/correction_provider.dart'; @@ -58,7 +59,10 @@ class _CorrectionPageState extends ConsumerState { final state = ref.watch(correctionProvider); return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(middle: Text('🔍 内容纠错')), + navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), + middle: Text('🔍 内容纠错'), + ), child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.md), diff --git a/lib/features/countdown/presentation/countdown_page.dart b/lib/features/countdown/presentation/countdown_page.dart index 792b0514..df15f126 100644 --- a/lib/features/countdown/presentation/countdown_page.dart +++ b/lib/features/countdown/presentation/countdown_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 倒计时页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -17,6 +17,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/keyboard_safe_sheet.dart'; import '../models/countdown_models.dart'; import '../providers/countdown_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class CountdownPage extends ConsumerStatefulWidget { const CountdownPage({super.key}); @@ -33,6 +34,7 @@ class _CountdownPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '⏰ 倒计时', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/daily_card/presentation/daily_card_page.dart b/lib/features/daily_card/presentation/daily_card_page.dart index ab4c2f24..35580353 100644 --- a/lib/features/daily_card/presentation/daily_card_page.dart +++ b/lib/features/daily_card/presentation/daily_card_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 日签卡片主页面 /// 创建时间: 2026-05-01 /// 更新时间: 2026-05-14 @@ -13,7 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; import '../../../core/theme/app_theme.dart'; -import '../../../core/router/app_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../editor/services/export/export_service.dart'; import '../../../shared/widgets/app_toast.dart'; import '../models/daily_card_models.dart'; @@ -22,6 +22,7 @@ import 'widgets/card_action_bar.dart'; import 'widgets/card_renderer.dart'; import 'widgets/card_style_selector.dart'; import 'widgets/content_type_selector.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class DailyCardPage extends ConsumerStatefulWidget { const DailyCardPage({super.key}); @@ -40,6 +41,7 @@ class _DailyCardPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('📅 日签卡片'), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, diff --git a/lib/features/daily_fortune/presentation/daily_fortune_page.dart b/lib/features/daily_fortune/presentation/daily_fortune_page.dart index b5f48a8e..9817a0dd 100644 --- a/lib/features/daily_fortune/presentation/daily_fortune_page.dart +++ b/lib/features/daily_fortune/presentation/daily_fortune_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 每日运势主页面 /// 创建时间: 2026-05-13 /// 更新时间: 2026-05-14 @@ -13,7 +13,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; -import '../../../core/router/app_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; @@ -24,6 +24,7 @@ import '../../auth/providers/auth_provider.dart'; import '../models/fortune_models.dart'; import '../providers/fortune_provider.dart'; import 'widgets/fortune_card_widget.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class DailyFortunePage extends ConsumerStatefulWidget { const DailyFortunePage({super.key}); @@ -50,6 +51,7 @@ class _DailyFortunePageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '🔮 每日运势', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/daily_fortune/presentation/fortune_settings_page.dart b/lib/features/daily_fortune/presentation/fortune_settings_page.dart index 0410e144..b1d3b84e 100644 --- a/lib/features/daily_fortune/presentation/fortune_settings_page.dart +++ b/lib/features/daily_fortune/presentation/fortune_settings_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 运势设置页面 /// 创建时间: 2026-05-13 /// 更新时间: 2026-05-13 @@ -18,6 +18,7 @@ import '../../../core/theme/app_typography.dart'; import '../models/fortune_models.dart'; import '../providers/fortune_provider.dart'; import 'widgets/fortune_card_widget.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class FortuneSettingsPage extends ConsumerStatefulWidget { const FortuneSettingsPage({super.key}); @@ -72,6 +73,7 @@ class _FortuneSettingsPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Text( diff --git a/lib/features/file_transfer/collaboration/screen_share/pages/screen_share_page.dart b/lib/features/file_transfer/collaboration/screen_share/pages/screen_share_page.dart index 117fcbe4..85432b06 100644 --- a/lib/features/file_transfer/collaboration/screen_share/pages/screen_share_page.dart +++ b/lib/features/file_transfer/collaboration/screen_share/pages/screen_share_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 屏幕共享观看页面 // 创建时间: 2026-05-14 // 更新时间: 2026-05-20 @@ -18,6 +18,7 @@ import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/features/file_transfer/collaboration/screen_share/models/input_action.dart'; import 'package:xianyan/features/file_transfer/collaboration/screen_share/providers/screen_share_provider.dart'; import 'package:xianyan/features/file_transfer/collaboration/screen_share/services/in_app_screen_capture_service.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class ScreenSharePage extends ConsumerStatefulWidget { const ScreenSharePage({ @@ -63,6 +64,7 @@ class _ScreenSharePageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Row( diff --git a/lib/features/file_transfer/presentation/pages/device_pairing_page.dart b/lib/features/file_transfer/presentation/pages/device_pairing_page.dart index afc5d8f1..3c47b8d3 100644 --- a/lib/features/file_transfer/presentation/pages/device_pairing_page.dart +++ b/lib/features/file_transfer/presentation/pages/device_pairing_page.dart @@ -23,6 +23,7 @@ import 'package:xianyan/features/file_transfer/presentation/pages/pairing_code_t import 'package:xianyan/features/file_transfer/presentation/pages/qr_code_tab.dart'; import 'package:xianyan/features/file_transfer/presentation/pages/radar_scan_tab.dart'; import 'package:xianyan/features/file_transfer/services/degradation_manager.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class DevicePairingPage extends ConsumerStatefulWidget { const DevicePairingPage({super.key}); @@ -59,6 +60,7 @@ class _DevicePairingPageState extends ConsumerState return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Row( diff --git a/lib/features/file_transfer/presentation/pages/file_transfer_page.dart b/lib/features/file_transfer/presentation/pages/file_transfer_page.dart index 7f4dfb0e..2b7b4fe7 100644 --- a/lib/features/file_transfer/presentation/pages/file_transfer_page.dart +++ b/lib/features/file_transfer/presentation/pages/file_transfer_page.dart @@ -23,6 +23,7 @@ import 'package:xianyan/features/file_transfer/presentation/pages/file_transfer_ import 'package:xianyan/features/file_transfer/presentation/pages/file_transfer_my_devices_tab.dart'; import 'package:xianyan/features/file_transfer/presentation/pages/file_transfer_records_tab.dart'; import 'package:xianyan/features/file_transfer/presentation/pages/file_transfer_debug_panel.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class FileTransferPage extends ConsumerStatefulWidget { final int initialTab; @@ -73,6 +74,7 @@ class _FileTransferPageState extends ConsumerState return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Row( diff --git a/lib/features/file_transfer/presentation/pages/transfer_chat_page.dart b/lib/features/file_transfer/presentation/pages/transfer_chat_page.dart index e3eca44d..4bd407ba 100644 --- a/lib/features/file_transfer/presentation/pages/transfer_chat_page.dart +++ b/lib/features/file_transfer/presentation/pages/transfer_chat_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 传输聊天页面 // 创建时间: 2026-05-09 // 更新时间: 2026-05-20 @@ -28,6 +28,7 @@ import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/core/utils/platform_helper.dart'; import 'package:xianyan/core/services/device/haptic_service.dart'; +import 'package:xianyan/shared/widgets/adaptive_back_button.dart'; import 'package:xianyan/shared/widgets/app_toast.dart'; import 'package:xianyan/features/file_transfer/models/models.dart'; import 'package:xianyan/features/file_transfer/providers/providers.dart'; @@ -364,6 +365,7 @@ class _TransferChatPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Row( diff --git a/lib/features/file_transfer/presentation/pages/transfer_settings_page.dart b/lib/features/file_transfer/presentation/pages/transfer_settings_page.dart index 4fd5f5fd..1746f41a 100644 --- a/lib/features/file_transfer/presentation/pages/transfer_settings_page.dart +++ b/lib/features/file_transfer/presentation/pages/transfer_settings_page.dart @@ -19,6 +19,7 @@ import 'package:xianyan/features/file_transfer/models/models.dart'; import 'package:xianyan/features/file_transfer/presentation/pages/transfer_stats_page.dart'; import 'package:xianyan/features/file_transfer/presentation/pages/file_transfer_page.dart'; import 'package:xianyan/features/file_transfer/presentation/pages/file_transfer_debug_panel.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class TransferSettingsPage extends ConsumerStatefulWidget { const TransferSettingsPage({super.key}); @@ -38,6 +39,7 @@ class _TransferSettingsPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Text( @@ -155,17 +157,25 @@ class _TransferSettingsPageState extends ConsumerState { ), child: Row( children: [ - Icon(CupertinoIcons.number, size: 20, color: ext.iconSecondary), + Icon( + CupertinoIcons.number, + size: 20, + color: ext.iconSecondary, + ), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( '加密算法', - style: AppTypography.subhead.copyWith(color: ext.textPrimary), + style: AppTypography.subhead.copyWith( + color: ext.textPrimary, + ), ), ), Text( settings.encryptionAlgorithm, - style: AppTypography.caption1.copyWith(color: ext.textHint), + style: AppTypography.caption1.copyWith( + color: ext.textHint, + ), ), ], ), @@ -242,7 +252,8 @@ class _TransferSettingsPageState extends ConsumerState { ext, icon: CupertinoIcons.globe, title: '信令服务器', - value: ref + value: + ref .read(transferProvider.notifier) .pairingService .signalingService @@ -985,6 +996,7 @@ class _DebugPanelPageState extends ConsumerState<_DebugPanelPage> return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Text( diff --git a/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart b/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart index d8a79f72..b4939aac 100644 --- a/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart +++ b/lib/features/file_transfer/presentation/pages/transfer_stats_page.dart @@ -17,6 +17,7 @@ import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; import 'package:xianyan/features/file_transfer/providers/transfer_stats_provider.dart'; import 'package:xianyan/features/file_transfer/services/transfer_stats_service.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class TransferStatsPage extends ConsumerStatefulWidget { const TransferStatsPage({super.key}); @@ -44,6 +45,7 @@ class _TransferStatsPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, middle: Text( diff --git a/lib/features/health/presentation/health_page.dart b/lib/features/health/presentation/health_page.dart index 5736e531..a630972b 100644 --- a/lib/features/health/presentation/health_page.dart +++ b/lib/features/health/presentation/health_page.dart @@ -16,6 +16,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/glass_container.dart'; import '../services/health_service.dart'; import 'health_search_page.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class HealthPage extends ConsumerWidget { const HealthPage({super.key}); @@ -26,6 +27,7 @@ class HealthPage extends ConsumerWidget { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('🏥 健康生活'), previousPageTitle: '返回', ), diff --git a/lib/features/health/presentation/health_search_page.dart b/lib/features/health/presentation/health_search_page.dart index 944f2a7f..468b2fa3 100644 --- a/lib/features/health/presentation/health_search_page.dart +++ b/lib/features/health/presentation/health_search_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 健康生活搜索页 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -16,6 +16,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/glass_container.dart'; import '../providers/health_provider.dart'; import '../services/health_service.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class HealthSearchPage extends ConsumerStatefulWidget { const HealthSearchPage({super.key, required this.category}); @@ -51,6 +52,7 @@ class _HealthSearchPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text('${widget.category.emoji} ${widget.category.name}'), previousPageTitle: '健康', ), diff --git a/lib/features/home/presentation/cache_management_page.dart b/lib/features/home/presentation/cache_management_page.dart index a0fd3967..5a2e8bc7 100644 --- a/lib/features/home/presentation/cache_management_page.dart +++ b/lib/features/home/presentation/cache_management_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 缓存管理页面 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-15 @@ -23,6 +23,7 @@ import '../../../shared/widgets/responsive_layout.dart'; import '../../file_transfer/database/transfer_database.dart'; import '../providers/cache_provider.dart'; import '../services/cache_service.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class CacheManagementPage extends ConsumerStatefulWidget { const CacheManagementPage({super.key}); @@ -51,6 +52,7 @@ class _CacheManagementPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/home/presentation/favorite_page.dart b/lib/features/home/presentation/favorite_page.dart index 43b37dee..99fe0364 100644 --- a/lib/features/home/presentation/favorite_page.dart +++ b/lib/features/home/presentation/favorite_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 收藏页面 /// 创建时间: 2026-04-24 /// 更新时间: 2026-05-14 @@ -20,7 +20,7 @@ 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_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../shared/widgets/glass_container.dart'; import '../../../shared/widgets/app_slidable.dart'; import '../../../shared/widgets/app_popup_menu.dart'; diff --git a/lib/features/home/presentation/home_page.dart b/lib/features/home/presentation/home_page.dart index 28ffba96..84d24ef7 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-21 +// 更新时间: 2026-05-22 // 作用: 句子阅读主页面,展示每日推荐 + 分类筛选 + 句子流 -// 上次更新: 修复句子卡片空白+拾光栏改用showHalf+空卡片占位 +// 上次更新: 工具中心改为顶部滑入面板 // ============================================================ import 'dart:async'; @@ -24,7 +24,7 @@ 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_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../core/services/audio/sfx_service.dart'; import '../../../core/services/device/shake_detector.dart'; import '../../../core/services/device/battery_info_service.dart'; @@ -825,10 +825,7 @@ class _HomePageState extends ConsumerState { } void _showToolCenter(BuildContext context) { - AppBottomSheet.showCustom( - context: context, - builder: (_) => const HomeToolCenter(), - ); + HomeToolCenter.show(context); } } diff --git a/lib/features/home/presentation/home_refresh_indicator.dart b/lib/features/home/presentation/home_refresh_indicator.dart index 9df32fe7..8b1c801c 100644 --- a/lib/features/home/presentation/home_refresh_indicator.dart +++ b/lib/features/home/presentation/home_refresh_indicator.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — 主页自定义下拉刷新指示器(二段式) // 创建时间: 2026-05-20 -// 更新时间: 2026-05-21 -// 作用: 拾光角色下拉刷新动画,1-50%松手刷新/51-100%松手打开工具中心 -// 上次更新: 修复二段式松手后工具中心未打开,使用microtask延迟回调 +// 更新时间: 2026-05-22 +// 作用: 拾光角色下拉刷新动画,1-70%松手刷新/70%+松手打开工具中心 +// 上次更新: 降低二段式阈值,stage1从80→55,stage2从1.0→0.7 // ============================================================ import 'dart:math'; @@ -41,9 +41,9 @@ class _HomeRefreshIndicatorState extends ConsumerState bool _isAtTop = true; double _accumulatedDrag = 0.0; - static const double _stage1Threshold = 80.0; + static const double _stage1Threshold = 55.0; - bool get _isStage2 => _pullProgress > 1.0; + bool get _isStage2 => _pullProgress > 0.7; late final AnimationController _resetController; double _resetBeginValue = 0.0; @@ -118,7 +118,7 @@ class _HomeRefreshIndicatorState extends ConsumerState Transform.translate( offset: Offset( 0, - progress.clamp(0.0, 1.5) * _stage1Threshold * 0.5, + progress.clamp(0.0, 1.0) * _stage1Threshold * 0.5, ), child: widget.child, ), @@ -190,7 +190,7 @@ class _HomeRefreshIndicatorState extends ConsumerState if (onToolCenter != null) { Future.microtask(() => onToolCenter()); } - } else if (_pullProgress >= 0.8) { + } else if (_pullProgress >= 0.5) { setState(() => _isRefreshing = true); widget.onRefresh().whenComplete(() { if (mounted) { @@ -212,19 +212,19 @@ class _HomeRefreshIndicatorState extends ConsumerState final expression = _getExpression(); final String text; final String stageLabel; - final isStage2 = progress > 1.0; + final isStage2 = progress > 0.7; if (_isRefreshing) { text = '刷新中...'; stageLabel = ''; } else if (isStage2) { - final v = ((progress - 1.0) * 100).round().clamp(0, 100); + final v = ((progress - 0.7) / 0.3 * 100).round().clamp(0, 100); text = '$v%'; - stageLabel = '继续下拉打开工具中心'; + stageLabel = '松手打开工具中心'; } else { - final v = (progress * 100).round().clamp(0, 100); + final v = (progress / 0.7 * 100).round().clamp(0, 100); text = '$v%'; - stageLabel = progress >= 0.8 ? '松手刷新' : '下拉刷新'; + stageLabel = progress >= 0.5 ? '松手刷新' : '下拉刷新'; } return Opacity( @@ -274,7 +274,7 @@ class _HomeRefreshIndicatorState extends ConsumerState ), child: FractionallySizedBox( alignment: Alignment.centerLeft, - widthFactor: (progress - 1.0).clamp(0.0, 1.0), + widthFactor: ((progress - 0.7) / 0.3).clamp(0.0, 1.0), child: Container( decoration: BoxDecoration( color: ext.accent, @@ -357,13 +357,13 @@ class _HomeRefreshIndicatorState extends ConsumerState CharacterExpression _getExpression() { if (_isRefreshing) return CharacterExpression.think; if (_isStage2) { - final v = _pullProgress - 1.0; + final v = (_pullProgress - 0.7) / 0.3; if (v < 0.3) return CharacterExpression.surprise; if (v < 0.7) return CharacterExpression.think; return CharacterExpression.love; } - if (_pullProgress < 0.3) return CharacterExpression.idle; - if (_pullProgress < 0.6) return CharacterExpression.surprise; + if (_pullProgress < 0.2) return CharacterExpression.idle; + if (_pullProgress < 0.4) return CharacterExpression.surprise; return CharacterExpression.pout; } } diff --git a/lib/features/home/presentation/home_tool_center.dart b/lib/features/home/presentation/home_tool_center.dart index 57e36195..b752e32c 100644 --- a/lib/features/home/presentation/home_tool_center.dart +++ b/lib/features/home/presentation/home_tool_center.dart @@ -1,24 +1,28 @@ // ============================================================ -// 闲言APP — 主页工具中心面板 +// 闲言APP — 主页工具中心面板(顶部滑入) // 创建时间: 2026-05-20 -// 更新时间: 2026-05-20 -// 作用: 下拉展开的快捷工具面板 -// 上次更新: 初始版本 +// 更新时间: 2026-05-22 +// 作用: 下拉展开的快捷工具面板,从顶部弹性滑入动画 +// 上次更新: 添加 SpringSimulation 弹性回弹效果,收起使用平滑减速 // ============================================================ +import 'dart:ui'; + import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/features/mine/settings/providers/theme_settings_provider.dart'; import 'package:xianyan/shared/widgets/app_toast.dart'; -import 'package:xianyan/shared/widgets/bottom_sheet.dart'; import 'package:xianyan/features/home/presentation/nearby_users_sheet.dart'; +import 'package:xianyan/shared/widgets/bottom_sheet.dart'; class _ToolItem { const _ToolItem({ @@ -39,11 +43,68 @@ class _ToolItem { class HomeToolCenter extends ConsumerStatefulWidget { const HomeToolCenter({super.key}); + static void show(BuildContext context) { + Navigator.of(context).push( + PageRouteBuilder( + opaque: false, + barrierDismissible: true, + barrierColor: Colors.transparent, + transitionDuration: const Duration(milliseconds: 600), + reverseTransitionDuration: const Duration(milliseconds: 350), + pageBuilder: (_, __, ___) => const HomeToolCenter(), + transitionsBuilder: (_, animation, __, child) => child, + ), + ); + } + @override ConsumerState createState() => _HomeToolCenterState(); } -class _HomeToolCenterState extends ConsumerState { +class _HomeToolCenterState extends ConsumerState + with SingleTickerProviderStateMixin { + static const _springDescription = SpringDescription( + mass: 1, + stiffness: 400, + damping: 15, + ); + + late final AnimationController _animationController; + late final Animation _backdropAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + _backdropAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + ); + _animationController.animateWith( + SpringSimulation(_springDescription, 0.0, 1.0, 0.0), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _dismiss() async { + await _animationController.animateTo( + 0.0, + duration: const Duration(milliseconds: 350), + curve: Curves.easeInCubic, + ); + if (mounted) { + Navigator.of(context).pop(); + } + } + void _onTts() { AppToast.showInfo('🔊 朗读模式开发中'); Log.i('工具中心: 朗读模式'); @@ -69,16 +130,20 @@ class _HomeToolCenterState extends ConsumerState { } void _onNearby() { - Navigator.pop(context); - AppBottomSheet.showCustom( - context: context, - builder: (_) => const NearbyUsersSheet(), - ); + _dismiss().then((_) { + if (mounted) { + AppBottomSheet.showCustom( + context: context, + builder: (_) => const NearbyUsersSheet(), + ); + } + }); } @override Widget build(BuildContext context) { final ext = AppTheme.ext(context); + final topPadding = MediaQuery.of(context).padding.top; final tools = [ _ToolItem( @@ -94,7 +159,7 @@ class _HomeToolCenterState extends ConsumerState { action: _onNearby, ), const _ToolItem( - emoji: '📡', + emoji: '📁', title: '传输助手', subtitle: '文件互传', route: AppRoutes.fileTransfer, @@ -115,49 +180,126 @@ class _HomeToolCenterState extends ConsumerState { ), ]; - return Container( - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: ext.bgPrimary.withValues(alpha: 0.95), - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - border: Border(top: BorderSide(color: ext.overlaySubtle, width: 0.5)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 36, - height: 5, + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Stack( + children: [ + GestureDetector( + onTap: _dismiss, + child: FadeTransition( + opacity: _backdropAnimation, + child: Container(color: Colors.black.withValues(alpha: 0.35)), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: SlideTransition( + position: Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(_animationController), + child: child, + ), + ), + ], + ); + }, + child: ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + margin: EdgeInsets.only(top: topPadding), + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + AppSpacing.sm, + AppSpacing.md, + AppSpacing.lg, + ), decoration: BoxDecoration( - color: ext.textHint.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(3), + color: ext.bgPrimary.withValues(alpha: 0.88), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(20), + ), + border: Border( + bottom: BorderSide(color: ext.overlaySubtle, width: 0.5), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: _dismiss, + child: Container( + width: 36, + height: 5, + decoration: BoxDecoration( + color: ext.textHint.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(3), + ), + ), + ), + const SizedBox(height: AppSpacing.md), + Row( + children: [ + Text( + '🧰 工具中心', + style: AppTypography.headline.copyWith( + color: ext.textPrimary, + ), + ), + const Spacer(), + GestureDetector( + onTap: _dismiss, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: ext.bgSecondary, + borderRadius: AppRadius.fullBorder, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.chevron_up, + size: 12, + color: ext.textHint, + ), + const SizedBox(width: 2), + Text( + '收起', + style: AppTypography.caption2.copyWith( + color: ext.textHint, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: AppSpacing.md), + GridView.count( + crossAxisCount: 4, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: AppSpacing.sm, + crossAxisSpacing: AppSpacing.sm, + childAspectRatio: 0.85, + children: tools + .map((tool) => _buildToolCard(tool, ext)) + .toList(), + ), + ], ), ), - const SizedBox(height: AppSpacing.md), - Row( - children: [ - Text( - '🧰 工具中心', - style: AppTypography.headline.copyWith(color: ext.textPrimary), - ), - const Spacer(), - Text( - '下滑收起', - style: AppTypography.caption2.copyWith(color: ext.textHint), - ), - ], - ), - const SizedBox(height: AppSpacing.md), - GridView.count( - crossAxisCount: 3, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - mainAxisSpacing: AppSpacing.sm, - crossAxisSpacing: AppSpacing.sm, - childAspectRatio: 1.1, - children: tools.map((tool) => _buildToolCard(tool, ext)).toList(), - ), - ], + ), ), ); } @@ -166,16 +308,16 @@ class _HomeToolCenterState extends ConsumerState { return GestureDetector( onTap: () { if (tool.route != null) { - Navigator.of(context).pop(); - context.appPush(tool.route!); + _dismiss().then((_) { + if (mounted) context.appPush(tool.route!); + }); return; } tool.action?.call(); }, child: Container( - padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( - color: ext.bgSecondary, + color: ext.bgSecondary.withValues(alpha: 0.6), borderRadius: AppRadius.lgBorder, ), child: Column( diff --git a/lib/features/home/presentation/home_type_chip.dart b/lib/features/home/presentation/home_type_chip.dart index 5c0dc121..7437745d 100644 --- a/lib/features/home/presentation/home_type_chip.dart +++ b/lib/features/home/presentation/home_type_chip.dart @@ -92,7 +92,7 @@ class _TypeChipState extends State : widget.ext.bgSecondary, borderRadius: AppRadius.fullBorder, border: widget.isSelected - ? Border.all(color: widget.ext.accent, width: 1.0) + ? Border.all(color: widget.ext.accent) : Border.all( color: widget.ext.textHint.withValues(alpha: 0.15), width: 0.5, diff --git a/lib/features/home/presentation/providers/likes_page.dart b/lib/features/home/presentation/providers/likes_page.dart index 7336b8cc..f2dd4bd3 100644 --- a/lib/features/home/presentation/providers/likes_page.dart +++ b/lib/features/home/presentation/providers/likes_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 点赞历史页面 /// 创建时间: 2026-04-29 /// 更新时间: 2026-05-04 @@ -19,7 +19,7 @@ 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_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../../../../shared/widgets/app_toast.dart'; import '../../../../core/utils/logger.dart'; diff --git a/lib/features/home/presentation/providers/offline_page.dart b/lib/features/home/presentation/providers/offline_page.dart index 32a65f2a..be333b2f 100644 --- a/lib/features/home/presentation/providers/offline_page.dart +++ b/lib/features/home/presentation/providers/offline_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 离线模式页面 /// 创建时间: 2026-04-28 /// 更新时间: 2026-04-28 @@ -19,6 +19,7 @@ import '../../../../core/storage/cache_config.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../../providers/offline_provider.dart'; import '../../services/offline_manager.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class OfflinePage extends ConsumerStatefulWidget { const OfflinePage({super.key}); @@ -44,6 +45,7 @@ class _OfflinePageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '📡 离线模式', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/home/presentation/widgets/quick_card_sheet.dart b/lib/features/home/presentation/widgets/quick_card_sheet.dart index 74e1fcb8..c5b18c49 100644 --- a/lib/features/home/presentation/widgets/quick_card_sheet.dart +++ b/lib/features/home/presentation/widgets/quick_card_sheet.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 快速卡片创作 Sheet // 创建时间: 2026-04-26 // 更新时间: 2026-05-18 @@ -17,7 +17,7 @@ import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/editor/models/editor_models.dart'; import 'package:xianyan/editor/services/export/export_service.dart'; diff --git a/lib/features/knowledge_graph/presentation/knowledge_graph_page.dart b/lib/features/knowledge_graph/presentation/knowledge_graph_page.dart index 723f936f..a6fac4df 100644 --- a/lib/features/knowledge_graph/presentation/knowledge_graph_page.dart +++ b/lib/features/knowledge_graph/presentation/knowledge_graph_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 知识图谱页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -20,6 +20,7 @@ import '../models/knowledge_graph_models.dart'; import '../providers/knowledge_graph_provider.dart'; import '../services/knowledge_graph_service.dart'; import 'widgets/knowledge_graph_canvas.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class KnowledgeGraphPage extends ConsumerStatefulWidget { const KnowledgeGraphPage({super.key}); @@ -47,6 +48,7 @@ class _KnowledgeGraphPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '🕸️ 知识图谱', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/mine/achievement/presentation/achievement_page.dart b/lib/features/mine/achievement/presentation/achievement_page.dart index 4aaec7ce..43ef4164 100644 --- a/lib/features/mine/achievement/presentation/achievement_page.dart +++ b/lib/features/mine/achievement/presentation/achievement_page.dart @@ -30,6 +30,7 @@ import '../../../auth/providers/auth_provider.dart'; import '../models/achievement_models.dart'; import '../providers/achievement_provider.dart'; import '../providers/checkin_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class AchievementPage extends ConsumerStatefulWidget { const AchievementPage({super.key}); @@ -78,6 +79,7 @@ class _AchievementPageState extends ConsumerState return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/achievement/presentation/badge_wall_page.dart b/lib/features/mine/achievement/presentation/badge_wall_page.dart index 40dce9ca..3e41450a 100644 --- a/lib/features/mine/achievement/presentation/badge_wall_page.dart +++ b/lib/features/mine/achievement/presentation/badge_wall_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 勋章墙页面 // 创建时间: 2026-05-14 // 更新时间: 2026-05-14 @@ -18,6 +18,7 @@ import '../../../../core/theme/app_typography.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../providers/badge_provider.dart'; import '../shared/widgets/badge_icon.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class BadgeWallPage extends ConsumerStatefulWidget { const BadgeWallPage({super.key}); @@ -42,6 +43,7 @@ class _BadgeWallPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('🎖️ 勋章墙'), previousPageTitle: '返回', ), diff --git a/lib/features/mine/achievement/presentation/checkin_page.dart b/lib/features/mine/achievement/presentation/checkin_page.dart index b753a280..81e10a85 100644 --- a/lib/features/mine/achievement/presentation/checkin_page.dart +++ b/lib/features/mine/achievement/presentation/checkin_page.dart @@ -25,6 +25,7 @@ import '../../../../shared/widgets/offline_banner.dart'; import '../../../../shared/widgets/responsive_layout.dart'; import '../models/achievement_models.dart'; import '../providers/checkin_provider.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class CheckinPage extends ConsumerStatefulWidget { const CheckinPage({super.key}); @@ -69,6 +70,7 @@ class _CheckinPageState extends ConsumerState return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/profile/presentation/about_page.dart b/lib/features/mine/profile/presentation/about_page.dart index 1886c22a..2e6f16a4 100644 --- a/lib/features/mine/profile/presentation/about_page.dart +++ b/lib/features/mine/profile/presentation/about_page.dart @@ -20,6 +20,7 @@ import '../../../../core/theme/app_typography.dart'; import '../../../../core/theme/app_radius.dart'; import '../../../../core/constants/app_constants.dart'; import '../../../../shared/widgets/glass_container.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class AboutPage extends ConsumerWidget { const AboutPage({super.key}); @@ -31,6 +32,7 @@ class AboutPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '关于', style: AppTypography.title3.copyWith( diff --git a/lib/features/mine/profile/presentation/profile_page.dart b/lib/features/mine/profile/presentation/profile_page.dart index d77fd4ee..e42face4 100644 --- a/lib/features/mine/profile/presentation/profile_page.dart +++ b/lib/features/mine/profile/presentation/profile_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 个人中心页面 /// 创建时间: 2026-04-20 /// 更新时间: 2026-05-21 @@ -17,7 +17,7 @@ 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_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/storage/kv_storage.dart'; import '../../../../l10n/app_locale.dart'; import '../../../../l10n/translations.dart'; diff --git a/lib/features/mine/settings/presentation/account/account_deletion_page.dart b/lib/features/mine/settings/presentation/account/account_deletion_page.dart index 091f1702..8064656e 100644 --- a/lib/features/mine/settings/presentation/account/account_deletion_page.dart +++ b/lib/features/mine/settings/presentation/account/account_deletion_page.dart @@ -20,6 +20,7 @@ import '../../../../../../shared/widgets/glass_container.dart'; import '../../../../../../shared/widgets/app_toast.dart'; import '../../../../auth/providers/auth_provider.dart'; import '../../../../auth/services/user_security_service.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class AccountDeletionPage extends ConsumerStatefulWidget { const AccountDeletionPage({super.key}); @@ -253,6 +254,7 @@ class _AccountDeletionPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '🗑️ 注销账号', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/mine/settings/presentation/account/account_settings_page.dart b/lib/features/mine/settings/presentation/account/account_settings_page.dart index b47c6cc9..20f41cb6 100644 --- a/lib/features/mine/settings/presentation/account/account_settings_page.dart +++ b/lib/features/mine/settings/presentation/account/account_settings_page.dart @@ -12,7 +12,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import '../../../../../../core/router/app_router.dart'; +import '../../../../../../core/router/app_routes.dart'; import '../../../../../../core/theme/app_theme.dart'; import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; @@ -25,6 +25,7 @@ import '../../../../auth/models/user_model.dart'; import '../../../../auth/providers/auth_provider.dart'; import '../../../../auth/services/auth_service.dart'; import '../../../../auth/services/user_security_service.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class AccountSettingsPage extends ConsumerWidget { const AccountSettingsPage({super.key}); @@ -38,6 +39,7 @@ class AccountSettingsPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/settings/presentation/data_management_page.dart b/lib/features/mine/settings/presentation/data_management_page.dart index 7fb8b971..af9babfc 100644 --- a/lib/features/mine/settings/presentation/data_management_page.dart +++ b/lib/features/mine/settings/presentation/data_management_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 数据管理页面 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-20 @@ -33,6 +33,7 @@ import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../../../../shared/widgets/responsive_layout.dart'; import '../providers/general_settings_provider.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class _DataCategory { const _DataCategory({ @@ -120,6 +121,7 @@ class _DataManagementPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '💾 数据管理', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/mine/settings/presentation/font_management_notifier.dart b/lib/features/mine/settings/presentation/font_management_notifier.dart index 251932c5..64c44a34 100644 --- a/lib/features/mine/settings/presentation/font_management_notifier.dart +++ b/lib/features/mine/settings/presentation/font_management_notifier.dart @@ -1,9 +1,9 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 字体管理 Notifier /// 创建时间: 2026-04-28 -/// 更新时间: 2026-05-20 +/// 更新时间: 2026-05-22 /// 作用: 字体导入/下载/删除/切换/动态加载等业务逻辑 -/// 上次更新: 修复审计问题(downloadUrl/hasDownloadError/收藏恢复/空指针防护/持久化/重复检查) +/// 上次更新: 下载逻辑委托给FontDownloadService,Notifier仅管理状态 /// ============================================================ import 'dart:convert'; @@ -11,9 +11,7 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:audioplayers/audioplayers.dart'; -import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:path_provider/path_provider.dart'; @@ -23,6 +21,7 @@ import '../../../../core/storage/app_kv_store.dart'; import '../../../../core/utils/logger.dart'; import '../../../../core/utils/platform_utils.dart' as pu; import '../../../../shared/widgets/app_toast.dart'; +import '../services/font_download_service.dart'; import '../services/font_sync_service.dart'; import '../providers/theme_settings_provider.dart'; import 'font_models.dart'; @@ -73,19 +72,6 @@ class FontManagementNotifier extends Notifier { static const _kvKeyInstalledFonts = 'font_installed_list'; static const _kvKeyDeletedFonts = 'font_deleted_families'; - static final _dio = Dio( - BaseOptions( - connectTimeout: const Duration(seconds: 30), - receiveTimeout: const Duration(minutes: 5), - sendTimeout: const Duration(seconds: 30), - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - 'Accept': '*/*', - }, - ), - ); - /// 字体切换音效播放器 static final _audioPlayer = AudioPlayer(); @@ -316,64 +302,20 @@ class FontManagementNotifier extends Notifier { if (font.path.isNotEmpty && font.isDownloaded && !deletedFamilies.contains(font.fontFamily)) { - await _loadFontIntoEngine(font.fontFamily, font.path); + await FontDownloadService.loadFontIntoEngine( + font.fontFamily, + font.path, + ); } } } - /// 校验字体文件头是否为有效 TTF/OTF - static bool _isValidFontFile(Uint8List bytes) { - if (bytes.length < 4) return false; - final h = bytes; - return (h[0] == 0x00 && h[1] == 0x01 && h[2] == 0x00 && h[3] == 0x00) || - (h[0] == 0x4F && h[1] == 0x54 && h[2] == 0x54 && h[3] == 0x4F); - } - - /// 将字体文件加载到 Flutter 引擎 - Future _loadFontIntoEngine(String fontFamily, String path) async { - try { - final file = File(path); - if (!await file.exists()) return false; - - final bytes = await file.readAsBytes(); - if (!_isValidFontFile(bytes)) { - Log.e('字体格式无效 [$fontFamily]: 文件头非 TTF/OTF 格式'); - return false; - } - final loader = FontLoader(fontFamily); - loader.addFont( - Future.value(ByteData.sublistView(Uint8List.fromList(bytes))), - ); - await loader.load(); - Log.i('字体加载成功: $fontFamily ($path)'); - return true; - } on FileSystemException catch (e) { - Log.e('字体文件系统错误 [$fontFamily]: ${e.message}', e); - return false; - } on OutOfMemoryError catch (e) { - Log.e('字体内存不足 [$fontFamily]: 文件可能过大', e); - return false; - } on FormatException catch (e) { - Log.e('字体格式无效 [$fontFamily]: ${e.message}', e); - return false; - } catch (e) { - Log.e('字体加载未知异常 [$fontFamily]', e); - return false; - } - } - /// 统一获取字体目录(自动创建) Future _getFontDirectory() async { - if (pu.isWeb) throw UnsupportedError('Web端不支持字体管理'); - final dir = await getApplicationDocumentsDirectory(); - final fontDir = Directory('${dir.path}/fonts'); - if (!await fontDir.exists()) { - await fontDir.create(recursive: true); - } - return fontDir; + return FontDownloadService.getFontDirectory(); } - /// 下载在线字体 + /// 下载在线字体(委托给FontDownloadService) Future downloadFont(int index) async { if (index < 0 || index >= state.onlineFonts.length) return; if (pu.isWeb) { @@ -407,41 +349,43 @@ class FontManagementNotifier extends Notifier { ); try { - final fontDir = await _getFontDirectory(); - final downloadUrl = state.onlineFonts[index].downloadUrl; - final urlPath = downloadUrl.isNotEmpty + final primaryUrl = downloadUrl.isNotEmpty ? downloadUrl - : onlineFontData[index].$3; - final urlExt = urlPath.contains('.') ? urlPath.split('.').last : 'ttf'; - final fileName = '${onlineFont.fontFamily}.$urlExt'; - final savePath = '${fontDir.path}${Platform.pathSeparator}$fileName'; + : (index < onlineFontData.length ? onlineFontData[index].$3 : ''); + final fallbacks = fontFallbackUrls[onlineFont.fontFamily] ?? []; - await _dio.download( - urlPath, - savePath, - onReceiveProgress: (received, total) { - if (total > 0) { - final progress = received / total; - final updated = List.from(state.onlineFonts); - updated[index] = state.onlineFonts[index].copyWith( - downloadProgress: progress, - ); - state = state.copyWith(onlineFonts: updated); - } + final result = await FontDownloadService.downloadFont( + fontFamily: onlineFont.fontFamily, + displayName: onlineFont.name, + primaryUrl: primaryUrl, + fallbackUrls: fallbacks, + onProgress: (progress) { + final updated = List.from(state.onlineFonts); + updated[index] = state.onlineFonts[index].copyWith( + downloadProgress: progress, + ); + state = state.copyWith(onlineFonts: updated); }, ); - final loaded = await _loadFontIntoEngine(onlineFont.fontFamily, savePath); + if (!result.success) { + _resetDownloadState(index, result.errorMsg ?? '下载失败'); + return; + } + + final loaded = await FontDownloadService.loadFontIntoEngine( + onlineFont.fontFamily, + result.savePath!, + ); if (loaded) { - final fileSize = await File(savePath).length(); final newFont = FontInfo( name: onlineFont.name, fontFamily: onlineFont.fontFamily, - path: savePath, + path: result.savePath!, isDownloaded: true, - fileSize: fileSize, + fileSize: result.fileSize, ); final updatedOnlineFonts = List.from(state.onlineFonts); @@ -468,9 +412,6 @@ class FontManagementNotifier extends Notifier { } else { _resetDownloadState(index, '${onlineFont.name} 加载失败'); } - } on DioException catch (e) { - Log.e('字体下载失败: ${onlineFont.name}', e); - _resetDownloadState(index, '下载失败: ${e.message ?? '网络错误'}'); } catch (e) { Log.e('字体安装异常: ${onlineFont.name}', e); _resetDownloadState(index, '安装失败: $e'); @@ -504,54 +445,39 @@ class FontManagementNotifier extends Notifier { downloadFont(index); } - /// 从文件选择器导入字体 + /// 从文件选择器导入字体(委托给FontDownloadService) Future importFont() async { if (pu.isWeb) { AppToast.showInfo('Web端暂不支持字体导入'); return; } try { - final result = await FilePicker.pickFiles( - type: FileType.custom, - allowedExtensions: ['ttf', 'otf'], - allowMultiple: true, - ); - - if (result == null || result.files.isEmpty) return; - - final fontDir = await _getFontDirectory(); + final results = await FontDownloadService.importLocalFonts(); int successCount = 0; - for (final platformFile in result.files) { - final filePath = platformFile.path; - if (filePath == null) continue; - - final sourceFile = File(filePath); - if (!await sourceFile.exists()) continue; - - final fileName = platformFile.name; - final name = fileName.replaceAll(RegExp(r'\.(ttf|otf)$'), ''); - final fontFamily = name.replaceAll(' ', ''); - final destPath = '${fontDir.path}${Platform.pathSeparator}$fileName'; - - if (await File(destPath).exists()) { - await File(destPath).delete(); + for (final result in results) { + if (!result.success) { + if (result.errorMsg != null) { + AppToast.showError(result.errorMsg!); + } + continue; } - await sourceFile.copy(destPath); - final loaded = await _loadFontIntoEngine(fontFamily, destPath); + final loaded = await FontDownloadService.loadFontIntoEngine( + result.fontFamily!, + result.savePath!, + ); if (loaded) { - final fileSize = await File(destPath).length(); final newFont = FontInfo( - name: name, - fontFamily: fontFamily, - path: destPath, + name: result.displayName!, + fontFamily: result.fontFamily!, + path: result.savePath!, isDownloaded: true, - fileSize: fileSize, + fileSize: result.fileSize, ); final existingIndex = state.fonts.indexWhere( - (f) => f.fontFamily == fontFamily, + (f) => f.fontFamily == result.fontFamily, ); final updatedFonts = List.from(state.fonts); if (existingIndex >= 0) { @@ -564,14 +490,14 @@ class FontManagementNotifier extends Notifier { _saveInstalledFontsToKV( updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList(), ); - _removeFromDeletedList(fontFamily); + _removeFromDeletedList(result.fontFamily!); successCount++; } } if (successCount > 0) { AppToast.showSuccess('成功导入 $successCount 个字体 ✅'); - } else { + } else if (results.isNotEmpty) { AppToast.showError('字体导入失败'); } } catch (e) { @@ -580,32 +506,14 @@ class FontManagementNotifier extends Notifier { } } - /// 从URL下载字体 + /// 从URL下载字体(委托给FontDownloadService) Future downloadFontFromUrl(String url, {String? name}) async { if (pu.isWeb) { AppToast.showInfo('Web端暂不支持字体下载'); return; } try { - final fontDir = await _getFontDirectory(); - - final uri = Uri.parse(url); - final urlFileName = uri.pathSegments.isNotEmpty - ? uri.pathSegments.last - : 'custom_font.ttf'; - final ext = urlFileName.contains('.') - ? urlFileName.split('.').last - : 'ttf'; - - String fontFamily = name?.isNotEmpty == true - ? name! - : urlFileName - .replaceAll('.$ext', '') - .replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); - if (fontFamily.isEmpty) - fontFamily = 'CustomFont_${DateTime.now().millisecondsSinceEpoch}'; - - final displayName = name?.isNotEmpty == true ? name! : fontFamily; + final displayName = name?.isNotEmpty == true ? name! : url; state = state.copyWith( isUrlDownloading: true, @@ -613,35 +521,40 @@ class FontManagementNotifier extends Notifier { urlDownloadName: displayName, ); - final fileName = '$fontFamily.$ext'; - final savePath = '${fontDir.path}${Platform.pathSeparator}$fileName'; - - await _dio.download( - url, - savePath, - onReceiveProgress: (received, total) { - if (total > 0) { - final progress = received / total; - state = state.copyWith(urlDownloadProgress: progress); - } + final result = await FontDownloadService.downloadFontFromUrl( + url: url, + name: name, + onProgress: (progress) { + state = state.copyWith(urlDownloadProgress: progress); }, ); - final loaded = await _loadFontIntoEngine(fontFamily, savePath); + if (!result.success) { + state = state.copyWith( + isUrlDownloading: false, + urlDownloadProgress: 0.0, + ); + AppToast.showError(result.errorMsg ?? '下载失败'); + return; + } + + final loaded = await FontDownloadService.loadFontIntoEngine( + result.fontFamily!, + result.savePath!, + ); if (loaded) { - final fileSize = await File(savePath).length(); final newFont = FontInfo( - name: displayName, - fontFamily: fontFamily, - path: savePath, + name: result.displayName!, + fontFamily: result.fontFamily!, + path: result.savePath!, isDownloaded: true, - fileSize: fileSize, + fileSize: result.fileSize, ); final updatedFonts = List.from(state.fonts); final existingIndex = updatedFonts.indexWhere( - (f) => f.fontFamily == fontFamily, + (f) => f.fontFamily == result.fontFamily, ); if (existingIndex >= 0) { updatedFonts[existingIndex] = newFont; @@ -658,11 +571,11 @@ class FontManagementNotifier extends Notifier { updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList(), ); - _removeFromDeletedList(fontFamily); + _removeFromDeletedList(result.fontFamily!); - AppToast.showSuccess('$displayName 下载安装成功 ✅'); + AppToast.showSuccess('${result.displayName} 下载安装成功 ✅'); } else { - final file = File(savePath); + final file = File(result.savePath!); if (await file.exists()) await file.delete(); state = state.copyWith( isUrlDownloading: false, @@ -670,10 +583,6 @@ class FontManagementNotifier extends Notifier { ); AppToast.showError('字体加载失败,文件可能不是有效的字体格式'); } - } on DioException catch (e) { - Log.e('URL字体下载失败', e); - state = state.copyWith(isUrlDownloading: false, urlDownloadProgress: 0.0); - AppToast.showError('下载失败: ${e.message ?? '网络错误'}'); } catch (e) { Log.e('URL字体下载异常', e); state = state.copyWith(isUrlDownloading: false, urlDownloadProgress: 0.0); @@ -771,7 +680,10 @@ class FontManagementNotifier extends Notifier { await File(outputPath).writeAsBytes(file.content as List); final fontFamily = fileName.replaceAll(RegExp(r'\.(ttf|otf)$'), ''); - final loaded = await _loadFontIntoEngine(fontFamily, outputPath); + final loaded = await FontDownloadService.loadFontIntoEngine( + fontFamily, + outputPath, + ); if (!loaded) continue; _removeFromDeletedList(fontFamily); @@ -858,13 +770,18 @@ class FontManagementNotifier extends Notifier { (f) => f.fontFamily == fontFamily, orElse: () => const FontInfo(name: '', fontFamily: '', path: ''), ); - if (fontInfo.path.isEmpty && - !fontInfo.path.startsWith('google_fonts://')) { - AppToast.showError('字体文件不存在,切换失败'); - return; + if (fontInfo.path.isEmpty && !fontInfo.isBuiltIn) { + final isGoogleFont = fontInfo.path.startsWith('google_fonts://') || + state.fonts.any( + (f) => f.fontFamily == fontFamily && f.path.startsWith('google_fonts://'), + ); + if (!isGoogleFont) { + AppToast.showError('字体文件不存在,切换失败'); + return; + } } if (fontInfo.path.isNotEmpty) { - await _loadFontIntoEngine(fontFamily, fontInfo.path); + await FontDownloadService.loadFontIntoEngine(fontFamily, fontInfo.path); } _applyCustomFont(fontFamily); } diff --git a/lib/features/mine/settings/presentation/font_models.dart b/lib/features/mine/settings/presentation/font_models.dart index 04ad6706..1a763586 100644 --- a/lib/features/mine/settings/presentation/font_models.dart +++ b/lib/features/mine/settings/presentation/font_models.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 字体管理数据模型 /// 创建时间: 2026-04-28 -/// 更新时间: 2026-05-20 +/// 更新时间: 2026-05-22 /// 作用: 字体信息模型、管理状态、在线字体数据、工具函数 -/// 上次更新: 增加downloadUrl/hasDownloadError字段+在线字体搜索过滤 +/// 上次更新: 替换不可靠CDN URL为raw.githubusercontent.com+阿里OSS,新增更纱黑体/文泉驿微米黑,增加备用URL /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -211,46 +211,88 @@ String formatFileSize(int bytes) { return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } -/// 在线字体数据 (名称, 字体族, 下载URL, emoji) +/// 在线字体数据 (名称, 字体族, 主下载URL, emoji) const onlineFontData = [ ( '霞鹜文楷', 'LXGWWenKai', - 'https://cdn.jsdelivr.net/gh/lxgw/LxgwWenKai/fonts/LXGWWenKai-Regular.ttf', + 'https://raw.githubusercontent.com/lxgw/LxgwWenKai/main/fonts/LXGWWenKai-Regular.ttf', '🖋️', ), ( '阿里巴巴普惠体', 'AlibabaPuHuiTi', - 'https://fonts.alicdn.com/font/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular.ttf', + 'https://puhuiti.oss-cn-hangzhou.aliyuncs.com/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular/AlibabaPuHuiTi-3-55-Regular.ttf', '💼', ), ( '站酷快乐体', 'ZCOOLKuaiLe', - 'https://cdn.jsdelivr.net/gh/googlefonts/zcool-kuaile/fonts/ttf/ZCOOLKuaiLe-Regular.ttf', + 'https://raw.githubusercontent.com/googlefonts/zcool-kuaile/main/fonts/ttf/ZCOOLKuaiLe-Regular.ttf', '🎉', ), ( '站酷小薇', 'ZCOOLXiaoWei', - 'https://cdn.jsdelivr.net/gh/googlefonts/zcool-xiaowei/fonts/ZCOOLXiaoWei-Regular.ttf', + 'https://raw.githubusercontent.com/googlefonts/zcool-xiaowei/main/fonts/ZCOOLXiaoWei-Regular.ttf', '🌸', ), ( '思源黑体', 'NotoSansSC', - 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf', + 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf', '📐', ), ( '思源宋体', 'NotoSerifSC', - 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk/Serif/OTF/SimplifiedChinese/NotoSerifCJKsc-Regular.otf', + 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Serif/OTF/SimplifiedChinese/NotoSerifCJKsc-Regular.otf', '📜', ), + ( + '更纱黑体', + 'SarasaGothicSC', + 'https://raw.githubusercontent.com/be5invis/Sarasa-Gothic/main/fonts/SarasaGothicSC-Regular.ttf', + '🎯', + ), + ( + '文泉驿微米黑', + 'WenQuanYiMicroHei', + 'https://raw.githubusercontent.com/niclas/wqy-microhei-font/master/wqy-microhei.ttc', + '✒️', + ), ]; +/// 字体备用下载URL映射(主URL失败时依次尝试备用URL) +const fontFallbackUrls = >{ + 'LXGWWenKai': [ + 'https://cdn.jsdelivr.net/gh/lxgw/LxgwWenKai@v1.501/fonts/LXGWWenKai-Regular.ttf', + 'https://github.com/lxgw/LxgwWenKai/releases/download/v1.501/LXGWWenKai-Regular.ttf', + ], + 'AlibabaPuHuiTi': [ + 'https://fonts.alicdn.com/font/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular.ttf', + ], + 'ZCOOLKuaiLe': [ + 'https://cdn.jsdelivr.net/gh/googlefonts/zcool-kuaile@main/fonts/ttf/ZCOOLKuaiLe-Regular.ttf', + ], + 'ZCOOLXiaoWei': [ + 'https://cdn.jsdelivr.net/gh/googlefonts/zcool-xiaowei@main/fonts/ZCOOLXiaoWei-Regular.ttf', + ], + 'NotoSansSC': [ + 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf', + 'https://github.com/notofonts/noto-cjk/releases/download/Sans2.004/08_NotoSansCJKsc.zip', + ], + 'NotoSerifSC': [ + 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Serif/OTF/SimplifiedChinese/NotoSerifCJKsc-Regular.otf', + ], + 'SarasaGothicSC': [ + 'https://github.com/be5invis/Sarasa-Gothic/releases/download/v1.0.19/SarasaGothicSC-Regular.ttf', + ], + 'WenQuanYiMicroHei': [ + 'https://cdn.jsdelivr.net/gh/niclas/wqy-microhei-font@master/wqy-microhei.ttc', + ], +}; + /// 内置字体配置 class BuiltInFontConfig { const BuiltInFontConfig({ diff --git a/lib/features/mine/settings/presentation/general/general_settings_page.dart b/lib/features/mine/settings/presentation/general/general_settings_page.dart index fdbb56c1..e09eb620 100644 --- a/lib/features/mine/settings/presentation/general/general_settings_page.dart +++ b/lib/features/mine/settings/presentation/general/general_settings_page.dart @@ -1,9 +1,9 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 通用设置页面 /// 创建时间: 2026-04-28 -/// 更新时间: 2026-05-20 +/// 更新时间: 2026-05-22 /// 作用: 声音/震动/通知/显示/性能/隐私/高级等设置 -/// 上次更新: 新增重新打开引导页action处理 +/// 上次更新: 新增font_scale导航到字体管理页面 /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -11,14 +11,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import '../../../../../../core/router/app_router.dart'; +import '../../../../../../core/router/app_routes.dart'; import '../../../../../../core/services/device/haptic_service.dart'; -import '../../../../../../core/services/notification/daily_notify_service.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 '../../../../../../l10n/translations.dart'; +import '../../../../../../shared/widgets/adaptive_back_button.dart'; import '../../../../../../shared/widgets/glass_container.dart'; import '../../providers/general_settings_provider.dart'; import 'general_settings_sections.dart'; @@ -94,6 +94,7 @@ class _GeneralSettingsPageState extends ConsumerState return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -469,16 +470,6 @@ class _GeneralSettingsPageState extends ConsumerState notifier.setShakeToSwitch(value); case 'daily_notification': notifier.setDailyNotification(value); - if (value) { - DailyNotifyService.instance.scheduleDailyNotification( - hour: ref.read(generalSettingsProvider).notifyTimeHour, - minute: ref.read(generalSettingsProvider).notifyTimeMinute, - title: '拾光为你选了一句', - body: '打开看看今天的句子吧 ✨', - ); - } else { - DailyNotifyService.instance.cancelAll(); - } case 'shader_background': notifier.setShaderBackground(value); case 'nearby_discovery': @@ -535,6 +526,8 @@ class _GeneralSettingsPageState extends ConsumerState context.appPush(AppRoutes.privacyPolicy); case 'log_viewer': context.appPush(AppRoutes.logViewer); + case 'font_scale': + context.appPush(AppRoutes.fontManagement); } } diff --git a/lib/features/mine/settings/presentation/general/general_settings_pickers.dart b/lib/features/mine/settings/presentation/general/general_settings_pickers.dart index ab26e7b2..fcdfbdfa 100644 --- a/lib/features/mine/settings/presentation/general/general_settings_pickers.dart +++ b/lib/features/mine/settings/presentation/general/general_settings_pickers.dart @@ -1,9 +1,9 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 通用设置选择器弹窗 /// 创建时间: 2026-05-19 -/// 更新时间: 2026-05-20 +/// 更新时间: 2026-05-22 /// 作用: 提供各种设置项的 CupertinoPicker 弹窗 -/// 上次更新: 新增通知时间选择器 +/// 上次更新: 移除DailyNotifyService;使用setNotifyTime统一调度 /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -12,7 +12,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../../../core/services/audio/sfx_service.dart'; import '../../../../../../core/services/device/haptic_service.dart'; -import '../../../../../../core/services/notification/daily_notify_service.dart'; import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; import '../../../../../../core/theme/app_radius.dart'; @@ -815,17 +814,10 @@ mixin GeneralSettingsPickers on ConsumerState { ), onSelectedItemChanged: (index) { final selected = _notifyTimeOptions[index]; - final notifier = ref.read(generalSettingsProvider.notifier); - notifier.setNotifyTimeHour(selected.$1); - notifier.setNotifyTimeMinute(selected.$2); - if (settings.dailyNotification) { - DailyNotifyService.instance.scheduleDailyNotification( - hour: selected.$1, - minute: selected.$2, - title: '拾光为你选了一句', - body: '打开看看今天的句子吧 ✨', - ); - } + ref.read(generalSettingsProvider.notifier).setNotifyTime( + selected.$1, + selected.$2, + ); HapticService.impact(); }, children: _notifyTimeOptions.map((opt) { diff --git a/lib/features/mine/settings/presentation/language_settings_page.dart b/lib/features/mine/settings/presentation/language_settings_page.dart index f004a77b..7f963734 100644 --- a/lib/features/mine/settings/presentation/language_settings_page.dart +++ b/lib/features/mine/settings/presentation/language_settings_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 语言选择页面 /// 创建时间: 2026-05-19 /// 更新时间: 2026-05-19 @@ -21,6 +21,7 @@ import '../../../../../l10n/translations.dart'; import '../../../../../l10n/translation_io_service.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../providers/general_settings_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class LanguageSettingsPage extends ConsumerWidget { const LanguageSettingsPage({super.key}); @@ -35,6 +36,7 @@ class LanguageSettingsPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/settings/presentation/more_settings_page.dart b/lib/features/mine/settings/presentation/more_settings_page.dart index 76a189e0..51c10a10 100644 --- a/lib/features/mine/settings/presentation/more_settings_page.dart +++ b/lib/features/mine/settings/presentation/more_settings_page.dart @@ -13,13 +13,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/services/device/haptic_service.dart'; import '../../../../core/services/catcher2_config_service.dart'; -import '../../../../core/router/app_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/router/app_nav_extension.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 '../providers/general_settings_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class MoreSettingsPage extends ConsumerStatefulWidget { const MoreSettingsPage({super.key}); @@ -37,6 +38,7 @@ class _MoreSettingsPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/settings/presentation/notification_settings_page.dart b/lib/features/mine/settings/presentation/notification_settings_page.dart index 0dad0071..f6d24aeb 100644 --- a/lib/features/mine/settings/presentation/notification_settings_page.dart +++ b/lib/features/mine/settings/presentation/notification_settings_page.dart @@ -1,19 +1,17 @@ /// ============================================================ /// 闲言APP — 通知设置页面 /// 创建时间: 2026-05-10 -/// 更新时间: 2026-05-13 +/// 更新时间: 2026-05-22 /// 作用: 管理推送通知开关与时间设置(每日推荐/签到/学习进度/稍后读/运势) -/// 上次更新: 增加每日运势推送设置 +/// 上次更新: 统一NotificationCenter调度;同步generalSettingsProvider;实现学习进度通知;修复cancelAll /// ============================================================ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart' show Divider, TimeOfDay; +import 'package:flutter/material.dart' show Divider; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import '../../../../core/services/device/haptic_service.dart'; -import '../../../../core/services/notification/notification_scheduler.dart'; -import '../../../../core/services/notification/notification_service.dart'; +import '../../../../core/services/notification/notification_center.dart'; import '../../../../core/services/notification/readlater_reminder_service.dart'; import '../../../../core/theme/app_radius.dart'; import '../../../../core/theme/app_spacing.dart'; @@ -21,6 +19,8 @@ import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/glass_container.dart'; +import '../providers/general_settings_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; // ============================================================ // Provider — 通知设置状态 @@ -85,8 +85,7 @@ class NotificationSettingsState { } /// 通知设置 Notifier -class NotificationSettingsNotifier - extends Notifier { +class NotificationSettingsNotifier extends Notifier { @override NotificationSettingsState build() { Future.microtask(loadFromPrefs); @@ -95,143 +94,119 @@ class NotificationSettingsNotifier NotificationSettingsNotifier(); - static const _keyStudyProgress = 'notif_study_progress'; - static const _keyChargingReadLater = 'notif_charging_readlater'; - - /// 从持久化加载状态 Future loadFromPrefs() async { - final dailyEnabled = await NotificationService.isDailyRecommendEnabled(); - final dailyTime = await NotificationService.getDailyRecommendTime(); - final signinEnabled = await NotificationService.isSigninReminderEnabled(); - final signinTime = await NotificationService.getSigninReminderTime(); + final dailyEnabled = NotificationCenter.isDailyRecommendEnabled; + final dailyHour = NotificationCenter.dailyRecommendHour; + final dailyMinute = NotificationCenter.dailyRecommendMinute; - final prefs = await SharedPreferences.getInstance(); - final studyProgress = prefs.getBool(_keyStudyProgress) ?? false; - final chargingReadLater = prefs.getBool(_keyChargingReadLater) ?? false; + final signinEnabled = NotificationCenter.isSigninReminderEnabled; + final signinHour = NotificationCenter.signinReminderHour; + final signinMinute = NotificationCenter.signinReminderMinute; - final fortuneEnabled = NotificationScheduler.isFortuneEnabled; - final fortuneHour = NotificationScheduler.fortuneHour; - final fortuneMinute = NotificationScheduler.fortuneMinute; + final studyProgress = NotificationCenter.isStudyProgressEnabled; + final chargingReadLater = ReadlaterReminderService.isEnabled(); + + final fortuneEnabled = NotificationCenter.isFortuneEnabled; + final fortuneHour = NotificationCenter.fortuneHour; + final fortuneMinute = NotificationCenter.fortuneMinute; state = NotificationSettingsState( dailyRecommend: dailyEnabled, - dailyRecommendHour: dailyTime?.hour ?? 8, - dailyRecommendMinute: dailyTime?.minute ?? 0, + dailyRecommendHour: dailyHour, + dailyRecommendMinute: dailyMinute, signinReminder: signinEnabled, - signinReminderHour: signinTime?.hour ?? 9, - signinReminderMinute: signinTime?.minute ?? 0, + signinReminderHour: signinHour, + signinReminderMinute: signinMinute, studyProgress: studyProgress, chargingReadLater: chargingReadLater, fortuneReminder: fortuneEnabled, fortuneReminderHour: fortuneHour, fortuneReminderMinute: fortuneMinute, ); + + if (chargingReadLater) { + await ReadlaterReminderService.startMonitoring(); + } } - /// 切换每日推荐 Future setDailyRecommend(bool value) async { state = state.copyWith(dailyRecommend: value); if (value) { - final time = TimeOfDay( - hour: state.dailyRecommendHour, - minute: state.dailyRecommendMinute, - ); - await NotificationService.scheduleDailyRecommend(time); - await NotificationScheduler.setDailySentenceEnabled(true); - await NotificationScheduler.setDailySentenceTime( + await NotificationCenter.setDailyRecommendEnabled(true); + await NotificationCenter.setDailyRecommendTime( state.dailyRecommendHour, state.dailyRecommendMinute, ); } else { - await NotificationService.cancelAll(); - await NotificationScheduler.setDailySentenceEnabled(false); + await NotificationCenter.setDailyRecommendEnabled(false); } + ref.read(generalSettingsProvider.notifier).setDailyNotification(value); await _syncMainSwitch(); } - /// 设置每日推荐时间 Future setDailyRecommendTime(int hour, int minute) async { state = state.copyWith( dailyRecommendHour: hour, dailyRecommendMinute: minute, ); if (state.dailyRecommend) { - final time = TimeOfDay(hour: hour, minute: minute); - await NotificationService.scheduleDailyRecommend(time); - await NotificationScheduler.setDailySentenceTime(hour, minute); + await NotificationCenter.setDailyRecommendTime(hour, minute); } + ref.read(generalSettingsProvider.notifier).setNotifyTime(hour, minute); } - /// 切换签到提醒 Future setSigninReminder(bool value) async { state = state.copyWith(signinReminder: value); if (value) { - final time = TimeOfDay( - hour: state.signinReminderHour, - minute: state.signinReminderMinute, - ); - await NotificationService.scheduleSigninReminder(time); - await NotificationScheduler.setSigninReminderEnabled(true); - await NotificationScheduler.setSigninReminderTime( + await NotificationCenter.setSigninReminderEnabled(true); + await NotificationCenter.setSigninReminderTime( state.signinReminderHour, state.signinReminderMinute, ); } else { - await NotificationService.cancelAll(); - await NotificationScheduler.setSigninReminderEnabled(false); + await NotificationCenter.setSigninReminderEnabled(false); } await _syncMainSwitch(); } - /// 设置签到提醒时间 Future setSigninReminderTime(int hour, int minute) async { state = state.copyWith( signinReminderHour: hour, signinReminderMinute: minute, ); if (state.signinReminder) { - final time = TimeOfDay(hour: hour, minute: minute); - await NotificationService.scheduleSigninReminder(time); - await NotificationScheduler.setSigninReminderTime(hour, minute); + await NotificationCenter.setSigninReminderTime(hour, minute); } } - /// 切换学习进度提醒 Future setStudyProgress(bool value) async { state = state.copyWith(studyProgress: value); - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_keyStudyProgress, value); + await NotificationCenter.setStudyProgressEnabled(value); await _syncMainSwitch(); } - /// 切换充电时稍后读提醒 Future setChargingReadLater(bool value) async { state = state.copyWith(chargingReadLater: value); - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_keyChargingReadLater, value); await ReadlaterReminderService.setEnabled(value); await _syncMainSwitch(); } - /// 切换每日运势推送 Future setFortuneReminder(bool value) async { state = state.copyWith(fortuneReminder: value); - await NotificationScheduler.setFortuneEnabled(value); + await NotificationCenter.setFortuneEnabled(value); await _syncMainSwitch(); } - /// 设置运势推送时间 Future setFortuneReminderTime(int hour, int minute) async { state = state.copyWith( fortuneReminderHour: hour, fortuneReminderMinute: minute, ); if (state.fortuneReminder) { - await NotificationScheduler.setFortuneTime(hour, minute); + await NotificationCenter.setFortuneTime(hour, minute); } } - /// 同步主通知开关到 NotificationScheduler Future _syncMainSwitch() async { final anyEnabled = state.dailyRecommend || @@ -239,16 +214,15 @@ class NotificationSettingsNotifier state.studyProgress || state.chargingReadLater || state.fortuneReminder; - await NotificationScheduler.setNotificationsEnabled(anyEnabled); + await NotificationCenter.setNotificationsEnabled(anyEnabled); } } /// 通知设置 Provider final notificationSettingsProvider = - NotifierProvider< - NotificationSettingsNotifier, - NotificationSettingsState - >(NotificationSettingsNotifier.new); + NotifierProvider( + NotificationSettingsNotifier.new, + ); // ============================================================ // 通知设置页面 @@ -280,6 +254,7 @@ class _NotificationSettingsPageState return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/settings/presentation/privacy/crash_log_page.dart b/lib/features/mine/settings/presentation/privacy/crash_log_page.dart index 1bd0faa4..eb68cb48 100644 --- a/lib/features/mine/settings/presentation/privacy/crash_log_page.dart +++ b/lib/features/mine/settings/presentation/privacy/crash_log_page.dart @@ -20,6 +20,7 @@ import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; import '../../../../../../core/theme/app_radius.dart'; import '../../../../../../core/utils/logger.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class CrashLogPage extends StatefulWidget { const CrashLogPage({super.key}); @@ -189,7 +190,7 @@ class _CrashLogPageState extends State { }, child: Text('取消', style: TextStyle(color: ext.accent)), ) - : null, + : const AdaptiveBackButton(), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -373,17 +374,17 @@ class _CrashLogDetailPage extends StatelessWidget { final confirmed = await showCupertinoDialog( context: context, builder: (ctx) => CupertinoAlertDialog( - title: Text('删除此日志'), + title: const Text('删除此日志'), content: Text('确定删除 ${entry.errorId} 吗?'), actions: [ CupertinoDialogAction( onPressed: () => Navigator.of(ctx).pop(false), - child: Text('取消'), + child: const Text('取消'), ), CupertinoDialogAction( isDestructiveAction: true, onPressed: () => Navigator.of(ctx).pop(true), - child: Text('删除'), + child: const Text('删除'), ), ], ), @@ -400,13 +401,13 @@ class _CrashLogDetailPage extends StatelessWidget { barrierDismissible: true, builder: (ctx) => CupertinoAlertDialog( content: Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: Text(msg, style: TextStyle(fontSize: 14)), + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text(msg, style: const TextStyle(fontSize: 14)), ), actions: [ CupertinoDialogAction( onPressed: () => Navigator.of(ctx).pop(), - child: Text('好的'), + child: const Text('好的'), ), ], ), @@ -420,6 +421,7 @@ class _CrashLogDetailPage extends StatelessWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( entry.errorId, style: AppTypography.title3.copyWith( @@ -444,7 +446,7 @@ class _CrashLogDetailPage extends StatelessWidget { CupertinoButton( padding: EdgeInsets.zero, onPressed: () => _delete(context), - child: Icon( + child: const Icon( CupertinoIcons.trash, color: Color(0xFFFF3B30), size: 22, @@ -455,14 +457,14 @@ class _CrashLogDetailPage extends StatelessWidget { ), child: SafeArea( child: SingleChildScrollView( - padding: EdgeInsets.all(AppSpacing.md), + padding: const EdgeInsets.all(AppSpacing.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildMetaSection(ext), - SizedBox(height: AppSpacing.md), + const SizedBox(height: AppSpacing.md), _buildErrorSection(ext), - SizedBox(height: AppSpacing.md), + const SizedBox(height: AppSpacing.md), _buildStackSection(ext), ], ), @@ -473,7 +475,7 @@ class _CrashLogDetailPage extends StatelessWidget { Widget _buildMetaSection(AppThemeExtension ext) { return Container( - padding: EdgeInsets.all(AppSpacing.sm), + padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: ext.bgSecondary, borderRadius: AppRadius.mdBorder, @@ -508,12 +510,12 @@ class _CrashLogDetailPage extends StatelessWidget { String value, ) { return Padding( - padding: EdgeInsets.only(bottom: AppSpacing.xs), + padding: const EdgeInsets.only(bottom: AppSpacing.xs), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 14, color: ext.textHint), - SizedBox(width: AppSpacing.sm), + const SizedBox(width: AppSpacing.sm), Text( '$label: ', style: AppTypography.caption1.copyWith( @@ -534,33 +536,33 @@ class _CrashLogDetailPage extends StatelessWidget { Widget _buildErrorSection(AppThemeExtension ext) { return Container( - padding: EdgeInsets.all(AppSpacing.sm), + padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( - color: Color(0xFFFF3B30).withValues(alpha: 0.08), + color: const Color(0xFFFF3B30).withValues(alpha: 0.08), borderRadius: AppRadius.mdBorder, - border: Border.all(color: Color(0xFFFF3B30).withValues(alpha: 0.2)), + border: Border.all(color: const Color(0xFFFF3B30).withValues(alpha: 0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon( + const Icon( CupertinoIcons.exclamationmark_triangle, size: 14, color: Color(0xFFFF3B30), ), - SizedBox(width: AppSpacing.sm), + const SizedBox(width: AppSpacing.sm), Text( 'Error', style: AppTypography.caption1.copyWith( - color: Color(0xFFFF3B30), + color: const Color(0xFFFF3B30), fontWeight: FontWeight.w600, ), ), ], ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), SelectableText( entry.error, style: AppTypography.caption1.copyWith( @@ -575,7 +577,7 @@ class _CrashLogDetailPage extends StatelessWidget { Widget _buildStackSection(AppThemeExtension ext) { return Container( - padding: EdgeInsets.all(AppSpacing.sm), + padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: ext.bgSecondary, borderRadius: AppRadius.mdBorder, @@ -586,7 +588,7 @@ class _CrashLogDetailPage extends StatelessWidget { Row( children: [ Icon(CupertinoIcons.list_bullet, size: 14, color: ext.textHint), - SizedBox(width: AppSpacing.sm), + const SizedBox(width: AppSpacing.sm), Text( 'Stack Trace', style: AppTypography.caption1.copyWith( @@ -596,7 +598,7 @@ class _CrashLogDetailPage extends StatelessWidget { ), ], ), - SizedBox(height: AppSpacing.xs), + const SizedBox(height: AppSpacing.xs), SelectableText( entry.stackTrace, style: AppTypography.caption1.copyWith( diff --git a/lib/features/mine/settings/presentation/privacy/log_viewer_page.dart b/lib/features/mine/settings/presentation/privacy/log_viewer_page.dart index 755b298c..2d325819 100644 --- a/lib/features/mine/settings/presentation/privacy/log_viewer_page.dart +++ b/lib/features/mine/settings/presentation/privacy/log_viewer_page.dart @@ -14,6 +14,7 @@ import '../../../../../../core/theme/app_typography.dart'; import '../../../../../../core/theme/app_radius.dart'; import '../../../../../../core/utils/logger.dart'; import '../../../../../../shared/widgets/glass_container.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class LogViewerPage extends StatefulWidget { const LogViewerPage({super.key}); @@ -45,6 +46,7 @@ class _LogViewerPageState extends State { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '日志查看器', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/mine/settings/presentation/privacy/permission_management_page.dart b/lib/features/mine/settings/presentation/privacy/permission_management_page.dart index 427abfa3..c24e20e6 100644 --- a/lib/features/mine/settings/presentation/privacy/permission_management_page.dart +++ b/lib/features/mine/settings/presentation/privacy/permission_management_page.dart @@ -1,9 +1,9 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 权限管理页面 /// 创建时间: 2026-05-07 -/// 更新时间: 2026-05-21 -/// 作用: 展示所有权限状态,支持请求/跳转系统设置 -/// 上次更新: 重写权限卡片,增加使用场景展示和必要/可选标签,移除受限权限 +/// 更新时间: 2026-05-22 +/// 作用: 展示所有权限状态,支持请求/跳转系统设置,区分虚拟权限和真实权限 +/// 上次更新: 移除3个高敏感权限(悬浮窗/通讯录/忽略电池优化)后的适配,页面自动遍历AppPermission.values无需手动修改 /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -16,6 +16,7 @@ import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; import '../../../../../../core/theme/app_radius.dart'; import '../../../../../../shared/widgets/glass_container.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; /// 权限状态 Provider final permissionStatusProvider = @@ -49,6 +50,7 @@ class _PermissionManagementPageState return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -84,18 +86,13 @@ class _PermissionManagementPageState children: [ _buildHeader(ext), const SizedBox(height: AppSpacing.md), - ...AppPermission.values.map( - (perm) => Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: _PermissionCard( - permission: perm, - status: - statuses[perm] ?? AppPermissionStatus.notDetermined, - onRequest: () => _requestPermission(perm), - onOpenSettings: () => PermissionService.openSettings(), - ), - ), - ), + _buildSectionTitle(ext, '📱 应用权限', '需要您授权才能使用的功能'), + const SizedBox(height: AppSpacing.sm), + ..._buildRealPermissions(statuses), + const SizedBox(height: AppSpacing.md), + _buildSectionTitle(ext, '⚙️ 系统级能力', '由操作系统管理,无需手动授权'), + const SizedBox(height: AppSpacing.sm), + ..._buildVirtualPermissions(statuses), const SizedBox(height: AppSpacing.md), _buildDisclaimer(ext), const SizedBox(height: AppSpacing.xl), @@ -140,6 +137,73 @@ class _PermissionManagementPageState ); } + /// 分区标题 + Widget _buildSectionTitle( + AppThemeExtension ext, + String title, + String subtitle, + ) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), + child: Row( + children: [ + Text( + title, + style: AppTypography.subhead.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: AppSpacing.xs), + Text( + subtitle, + style: AppTypography.caption1.copyWith(color: ext.textHint), + ), + ], + ), + ); + } + + /// 真实权限列表 + List _buildRealPermissions( + Map statuses, + ) { + return AppPermission.values + .where((p) => !p.isVirtual && p.isPlatformRelevant) + .map( + (perm) => Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.sm), + child: _PermissionCard( + permission: perm, + status: statuses[perm] ?? AppPermissionStatus.notDetermined, + onRequest: () => _requestPermission(perm), + onOpenSettings: () => PermissionService.openSettings(), + ), + ), + ) + .toList(); + } + + /// 虚拟权限列表 + List _buildVirtualPermissions( + Map statuses, + ) { + return AppPermission.values + .where((p) => p.isVirtual) + .map( + (perm) => Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.sm), + child: _PermissionCard( + permission: perm, + status: statuses[perm] ?? AppPermissionStatus.granted, + onRequest: () {}, + onOpenSettings: () {}, + ), + ), + ) + .toList(); + } + /// 底部免责声明 Widget _buildDisclaimer(AppThemeExtension ext) { return Padding( @@ -166,10 +230,10 @@ class _PermissionManagementPageState /// 权限卡片组件 — 增强版 /// -/// 顶部: 图标(40x40圆角容器) + 名称 + isRequired标签(必要/可选) +/// 顶部: 图标(40x40圆角容器) + 名称 + 标签(必要/可选/系统级) /// 中间: 详细说明文字 /// 底部: 使用场景列表(每行一个小圆点 + 场景文字) -/// 右侧: 操作按钮(请求/去设置/已授权箭头) +/// 右侧: 操作按钮(请求/去设置/已授权箭头/系统级信息图标) class _PermissionCard extends StatelessWidget { const _PermissionCard({ required this.permission, @@ -216,7 +280,7 @@ class _PermissionCard extends StatelessWidget { ); } - /// 顶部行: 图标 + 名称 + 必要/可选标签 + /// 顶部行: 图标 + 名称 + 标签 + 状态 Widget _buildTopRow(AppThemeExtension ext) { return Row( children: [ @@ -232,7 +296,7 @@ class _PermissionCard extends StatelessWidget { ), ), const SizedBox(width: AppSpacing.xs), - _buildRequiredBadge(ext), + _buildCategoryBadge(ext), const SizedBox(width: AppSpacing.xs), _buildStatusBadge(ext), ], @@ -254,8 +318,37 @@ class _PermissionCard extends StatelessWidget { ); } - /// 必要/可选标签 - Widget _buildRequiredBadge(AppThemeExtension ext) { + /// 分类标签: 必要/可选/系统级 + Widget _buildCategoryBadge(AppThemeExtension ext) { + if (permission.isVirtual) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF5856D6).withValues(alpha: 0.12), + borderRadius: AppRadius.smBorder, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + CupertinoIcons.gear_solid, + size: 8, + color: Color(0xFF5856D6), + ), + const SizedBox(width: 2), + Text( + '系统级', + style: AppTypography.caption1.copyWith( + color: const Color(0xFF5856D6), + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + final isReq = permission.isRequired; final color = isReq ? const Color(0xFFFF3B30) : const Color(0xFF8E8E93); final text = isReq ? '必要' : '可选'; @@ -380,6 +473,15 @@ class _PermissionCard extends StatelessWidget { /// 右侧操作按钮 Widget _buildActionButton(AppThemeExtension ext) { + if (permission.isVirtual) { + return CupertinoButton( + padding: EdgeInsets.zero, + minimumSize: const Size(32, 32), + onPressed: null, + child: Icon(CupertinoIcons.info_circle, size: 18, color: ext.textHint), + ); + } + if (status.isGranted) { return CupertinoButton( padding: EdgeInsets.zero, diff --git a/lib/features/mine/settings/presentation/privacy/privacy_policy_page.dart b/lib/features/mine/settings/presentation/privacy/privacy_policy_page.dart index c160e4ae..58b2d76c 100644 --- a/lib/features/mine/settings/presentation/privacy/privacy_policy_page.dart +++ b/lib/features/mine/settings/presentation/privacy/privacy_policy_page.dart @@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart'; import '../../../../../../core/theme/app_theme.dart'; import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class PrivacyPolicyPage extends StatelessWidget { const PrivacyPolicyPage({super.key}); @@ -22,6 +23,7 @@ class PrivacyPolicyPage extends StatelessWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '隐私政策', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/mine/settings/presentation/smart_mode_settings_page.dart b/lib/features/mine/settings/presentation/smart_mode_settings_page.dart index eb7a4c3e..5e95888f 100644 --- a/lib/features/mine/settings/presentation/smart_mode_settings_page.dart +++ b/lib/features/mine/settings/presentation/smart_mode_settings_page.dart @@ -18,6 +18,7 @@ import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../shared/widgets/glass_container.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class SmartModeSettingsPage extends ConsumerStatefulWidget { const SmartModeSettingsPage({super.key}); @@ -39,6 +40,7 @@ class _SmartModeSettingsPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/settings/presentation/theme/theme_settings_page.dart b/lib/features/mine/settings/presentation/theme/theme_settings_page.dart index 452df3b6..2a891519 100644 --- a/lib/features/mine/settings/presentation/theme/theme_settings_page.dart +++ b/lib/features/mine/settings/presentation/theme/theme_settings_page.dart @@ -16,6 +16,7 @@ import '../../providers/theme_settings_provider.dart'; import 'theme_sections_basic.dart'; import 'theme_sections_style.dart'; import 'theme_sections_preview.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class ThemeSettingsPage extends ConsumerWidget { const ThemeSettingsPage({super.key}); @@ -28,6 +29,7 @@ class ThemeSettingsPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/settings/providers/general_settings_provider.dart b/lib/features/mine/settings/providers/general_settings_provider.dart index a9cc05e4..f4cbd9eb 100644 --- a/lib/features/mine/settings/providers/general_settings_provider.dart +++ b/lib/features/mine/settings/providers/general_settings_provider.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 通用设置状态管理 /// 创建时间: 2026-04-28 -/// 更新时间: 2026-05-20 +/// 更新时间: 2026-05-22 /// 作用: 管理声音/震动/通知/显示/性能/隐私/高级等通用设置 -/// 上次更新: 新增 dailyNotification/notifyTimeHour/notifyTimeMinute/shaderBackground getter/setter +/// 上次更新: 修复通知状态响应式;统一NotificationScheduler调度;修复clearCache/resetAll /// ============================================================ import 'dart:io'; @@ -14,12 +14,13 @@ import 'package:path_provider/path_provider.dart'; import '../../../../core/services/audio/sfx_service.dart'; import '../../../../core/services/device/haptic_service.dart'; import '../../../../core/services/device/screen_wake_service.dart'; +import '../../../../core/services/ui/status_bar_service.dart'; import '../../../../core/services/sound_service.dart' as svc; import '../../../../core/services/device/app_lock_service.dart'; import '../../../../core/services/device/battery_optimization_service.dart'; import '../../../../core/services/network/network_proxy_service.dart'; -import '../../../../core/services/notification/daily_notify_service.dart'; -import '../../../../core/services/notification/notification_scheduler.dart'; +import '../../../../core/services/notification/notification_center.dart'; +import '../../../../core/services/notification/readlater_reminder_service.dart'; import '../../../../core/storage/app_kv_store.dart'; import '../../../../core/storage/kv_storage.dart'; import '../../../../core/utils/logger.dart'; @@ -522,6 +523,7 @@ class GeneralSettingsNotifier extends Notifier { display: state.display.copyWith(immersiveStatusBar: v), ); KvStorage.setBool('${_keyPrefix}immersive_status', v); + StatusBarService.setImmersive(v); } void setReduceAnimations(bool v) { @@ -713,27 +715,19 @@ class GeneralSettingsNotifier extends Notifier { ); KvStorage.setBool('${_keyPrefix}daily_notification', v); if (v) { - DailyNotifyService.instance.scheduleDailyNotification( - hour: state.notifyTimeHour, - minute: state.notifyTimeMinute, - title: '闲言每日一句', - body: '今天的句子已准备好,来看看吧 ✨', + NotificationCenter.setDailyRecommendTime( + state.notifyTimeHour, + state.notifyTimeMinute, ); - } else { - DailyNotifyService.instance.cancelAll(); } + NotificationCenter.setDailyRecommendEnabled(v); } void setNotifyTimeHour(int v) { state = state.copyWith(general: state.general.copyWith(notifyTimeHour: v)); KvStorage.setInt('${_keyPrefix}notify_time_hour', v); if (state.dailyNotification) { - DailyNotifyService.instance.scheduleDailyNotification( - hour: v, - minute: state.notifyTimeMinute, - title: '闲言每日一句', - body: '今天的句子已准备好,来看看吧 ✨', - ); + NotificationCenter.setDailyRecommendTime(v, state.notifyTimeMinute); } } @@ -743,12 +737,21 @@ class GeneralSettingsNotifier extends Notifier { ); KvStorage.setInt('${_keyPrefix}notify_time_minute', v); if (state.dailyNotification) { - DailyNotifyService.instance.scheduleDailyNotification( - hour: state.notifyTimeHour, - minute: v, - title: '闲言每日一句', - body: '今天的句子已准备好,来看看吧 ✨', - ); + NotificationCenter.setDailyRecommendTime(state.notifyTimeHour, v); + } + } + + void setNotifyTime(int hour, int minute) { + state = state.copyWith( + general: state.general.copyWith( + notifyTimeHour: hour, + notifyTimeMinute: minute, + ), + ); + KvStorage.setInt('${_keyPrefix}notify_time_hour', hour); + KvStorage.setInt('${_keyPrefix}notify_time_minute', minute); + if (state.dailyNotification) { + NotificationCenter.setDailyRecommendTime(hour, minute); } } @@ -846,20 +849,36 @@ class GeneralSettingsNotifier extends Notifier { AppLockService.setEnabled(false); BatteryOptimizationService.setEnabled(false); NetworkProxyService.reset(); + NotificationCenter.cancelAllManaged(); + NotificationCenter.setNotificationsEnabled(false); + ReadlaterReminderService.stopMonitoring(); } Future clearCache() async { try { - final tempDir = await getTemporaryDirectory(); - if (await tempDir.exists()) { - await for (final entity in tempDir.list()) { - try { - if (entity is File) { - await entity.delete(); - } else if (entity is Directory) { - await entity.delete(recursive: true); - } - } catch (_) {} + final dirs = []; + try { + dirs.add((await getTemporaryDirectory()).path); + } catch (_) {} + try { + dirs.add((await getApplicationDocumentsDirectory()).path); + } catch (_) {} + try { + dirs.add((await getApplicationSupportDirectory()).path); + } catch (_) {} + + for (final path in dirs) { + final dir = Directory(path); + if (await dir.exists()) { + await for (final entity in dir.list()) { + try { + if (entity is File) { + await entity.delete(); + } else if (entity is Directory) { + await entity.delete(recursive: true); + } + } catch (_) {} + } } } state = state.copyWith(general: state.general.copyWith(cacheSize: 0)); @@ -876,5 +895,9 @@ final generalSettingsProvider = ); final notificationEnabledProvider = Provider((ref) { - return NotificationScheduler.isNotificationsEnabled; + final dailyNotification = ref.watch( + generalSettingsProvider.select((s) => s.dailyNotification), + ); + if (dailyNotification) return true; + return NotificationCenter.isNotificationsEnabled; }); diff --git a/lib/features/mine/settings/providers/sub/display_settings_provider.dart b/lib/features/mine/settings/providers/sub/display_settings_provider.dart index 1bf90d6e..50c9b2e7 100644 --- a/lib/features/mine/settings/providers/sub/display_settings_provider.dart +++ b/lib/features/mine/settings/providers/sub/display_settings_provider.dart @@ -1,7 +1,8 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../../../core/storage/kv_storage.dart'; import '../../../../../../core/services/device/screen_wake_service.dart'; +import '../../../../../../core/services/ui/status_bar_service.dart'; import '../general_settings_provider.dart'; class DisplaySettingsState { @@ -118,6 +119,7 @@ class DisplaySettingsNotifier extends Notifier { void setImmersiveStatusBar(bool v) { state = state.copyWith(immersiveStatusBar: v); KvStorage.setBool('general_immersive_status', v); + StatusBarService.setImmersive(v); } void setReduceAnimations(bool v) { diff --git a/lib/features/mine/settings/services/font_download_service.dart b/lib/features/mine/settings/services/font_download_service.dart new file mode 100644 index 00000000..445039e4 --- /dev/null +++ b/lib/features/mine/settings/services/font_download_service.dart @@ -0,0 +1,398 @@ +/// ============================================================ +/// 闲言APP — 字体下载服务 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 独立字体下载逻辑,含重试/降级/验证/进度回调/本地导入 +/// 上次更新: 从FontManagementNotifier提取下载逻辑,Notifier仅管理状态 +/// ============================================================ + +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../../../core/utils/logger.dart'; +import '../../../../core/utils/platform_utils.dart' as pu; + +typedef ProgressCallback = void Function(double progress); + +class FontDownloadResult { + const FontDownloadResult({ + required this.success, + this.savePath, + this.fileSize, + this.errorMsg, + this.fontFamily, + this.displayName, + }); + + final bool success; + final String? savePath; + final int? fileSize; + final String? errorMsg; + final String? fontFamily; + final String? displayName; +} + +class FontDownloadService { + FontDownloadService._(); + + static final _dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(minutes: 5), + sendTimeout: const Duration(seconds: 30), + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': '*/*', + }, + ), + ); + + static bool isValidFontFile(Uint8List bytes) { + if (bytes.length < 4) return false; + final h = bytes; + final isTtf = (h[0] == 0x00 && h[1] == 0x01 && h[2] == 0x00 && h[3] == 0x00) || + (h[0] == 0x74 && h[1] == 0x72 && h[2] == 0x75 && h[3] == 0x65); + final isOtf = h[0] == 0x4F && h[1] == 0x54 && h[2] == 0x54 && h[3] == 0x4F; + final isTtc = h[0] == 0x74 && h[1] == 0x74 && h[2] == 0x63 && h[3] == 0x66; + return isTtf || isOtf || isTtc; + } + + static Future getFontDirectory() async { + if (pu.isWeb) throw UnsupportedError('Web端不支持字体管理'); + final dir = await getApplicationDocumentsDirectory(); + final fontDir = Directory('${dir.path}/fonts'); + if (!await fontDir.exists()) { + await fontDir.create(recursive: true); + } + return fontDir; + } + + static Future downloadFont({ + required String fontFamily, + required String displayName, + required String primaryUrl, + List fallbackUrls = const [], + ProgressCallback? onProgress, + }) async { + if (pu.isWeb) { + return const FontDownloadResult( + success: false, + errorMsg: 'Web端暂不支持字体下载', + ); + } + + try { + final fontDir = await getFontDirectory(); + final urlExt = primaryUrl.contains('.') + ? primaryUrl.split('.').last + : 'ttf'; + final fileName = '$fontFamily.$urlExt'; + final savePath = '${fontDir.path}${Platform.pathSeparator}$fileName'; + + final allUrls = [primaryUrl, ...fallbackUrls]; + bool downloadSuccess = false; + String? lastError; + + for (int urlIdx = 0; + urlIdx < allUrls.length && !downloadSuccess; + urlIdx++) { + final currentUrl = allUrls[urlIdx]; + if (currentUrl.isEmpty) continue; + + for (int attempt = 0; attempt < 3 && !downloadSuccess; attempt++) { + try { + if (attempt > 0) { + final delay = Duration(seconds: 1 << (attempt - 1)); + await Future.delayed(delay); + Log.d( + '字体下载重试 [$displayName] URL#${urlIdx + 1} 第${attempt + 1}次', + ); + } + + await _dio.download( + currentUrl, + savePath, + onReceiveProgress: (received, total) { + if (total > 0) { + onProgress?.call(received / total); + } + }, + ); + + final savedFile = File(savePath); + if (await savedFile.exists()) { + final bytes = await savedFile.readAsBytes(); + if (!isValidFontFile(bytes)) { + Log.e( + '字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式', + ); + await savedFile.delete(); + lastError = '文件格式无效'; + continue; + } + downloadSuccess = true; + } else { + lastError = '文件保存失败'; + } + } on DioException catch (e) { + lastError = e.message ?? '网络错误'; + Log.w( + '字体下载失败 [$displayName] URL#${urlIdx + 1} 第${attempt + 1}次: $lastError', + ); + } catch (e) { + lastError = e.toString(); + Log.w( + '字体下载异常 [$displayName] URL#${urlIdx + 1} 第${attempt + 1}次: $lastError', + ); + } + } + } + + if (!downloadSuccess) { + return FontDownloadResult( + success: false, + errorMsg: '${displayName}下载失败: ${lastError ?? "所有URL均不可用"}', + ); + } + + final fileSize = await File(savePath).length(); + return FontDownloadResult( + success: true, + savePath: savePath, + fileSize: fileSize, + fontFamily: fontFamily, + displayName: displayName, + ); + } catch (e) { + Log.e('字体下载异常: $displayName', e); + return FontDownloadResult( + success: false, + errorMsg: '下载异常: $e', + ); + } + } + + static Future downloadFontFromUrl({ + required String url, + String? name, + ProgressCallback? onProgress, + }) async { + if (pu.isWeb) { + return const FontDownloadResult( + success: false, + errorMsg: 'Web端暂不支持字体下载', + ); + } + + try { + final fontDir = await getFontDirectory(); + + final uri = Uri.parse(url); + final urlFileName = uri.pathSegments.isNotEmpty + ? uri.pathSegments.last + : 'custom_font.ttf'; + final ext = urlFileName.contains('.') + ? urlFileName.split('.').last + : 'ttf'; + + String fontFamily = name?.isNotEmpty == true + ? name! + : urlFileName + .replaceAll('.$ext', '') + .replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); + if (fontFamily.isEmpty) { + fontFamily = 'CustomFont_${DateTime.now().millisecondsSinceEpoch}'; + } + + final displayName = + name?.isNotEmpty == true ? name! : fontFamily; + + final fileName = '$fontFamily.$ext'; + final savePath = '${fontDir.path}${Platform.pathSeparator}$fileName'; + + bool downloadSuccess = false; + String? lastError; + + for (int attempt = 0; attempt < 3 && !downloadSuccess; attempt++) { + try { + if (attempt > 0) { + final delay = Duration(seconds: 1 << (attempt - 1)); + await Future.delayed(delay); + Log.d('URL字体下载重试 [$displayName] 第${attempt + 1}次'); + } + + await _dio.download( + url, + savePath, + onReceiveProgress: (received, total) { + if (total > 0) { + onProgress?.call(received / total); + } + }, + ); + + final savedFile = File(savePath); + if (await savedFile.exists()) { + final bytes = await savedFile.readAsBytes(); + if (!isValidFontFile(bytes)) { + Log.e( + 'URL字体文件头验证失败 [$displayName]: 非 TTF/OTF/TTC 格式', + ); + await savedFile.delete(); + lastError = '文件格式无效'; + continue; + } + downloadSuccess = true; + } else { + lastError = '文件保存失败'; + } + } on DioException catch (e) { + lastError = e.message ?? '网络错误'; + Log.w( + 'URL字体下载失败 [$displayName] 第${attempt + 1}次: $lastError', + ); + } catch (e) { + lastError = e.toString(); + Log.w( + 'URL字体下载异常 [$displayName] 第${attempt + 1}次: $lastError', + ); + } + } + + if (!downloadSuccess) { + return FontDownloadResult( + success: false, + errorMsg: '下载失败: ${lastError ?? "网络错误"}', + fontFamily: fontFamily, + displayName: displayName, + ); + } + + final fileSize = await File(savePath).length(); + return FontDownloadResult( + success: true, + savePath: savePath, + fileSize: fileSize, + fontFamily: fontFamily, + displayName: displayName, + ); + } catch (e) { + Log.e('URL字体下载异常', e); + return FontDownloadResult( + success: false, + errorMsg: '下载失败: $e', + ); + } + } + + static Future> importLocalFonts() async { + if (pu.isWeb) { + return [ + const FontDownloadResult( + success: false, + errorMsg: 'Web端暂不支持字体导入', + ), + ]; + } + + try { + final result = await FilePicker.pickFiles( + type: FileType.custom, + allowedExtensions: ['ttf', 'otf'], + allowMultiple: true, + ); + + if (result == null || result.files.isEmpty) return []; + + final fontDir = await getFontDirectory(); + final List results = []; + + for (final platformFile in result.files) { + final filePath = platformFile.path; + if (filePath == null) continue; + + final sourceFile = File(filePath); + if (!await sourceFile.exists()) continue; + + final fileName = platformFile.name; + final name = fileName.replaceAll(RegExp(r'\.(ttf|otf)$'), ''); + final fontFamily = name.replaceAll(' ', ''); + final destPath = + '${fontDir.path}${Platform.pathSeparator}$fileName'; + + if (await File(destPath).exists()) { + await File(destPath).delete(); + } + await sourceFile.copy(destPath); + + final bytes = await File(destPath).readAsBytes(); + if (!isValidFontFile(bytes)) { + await File(destPath).delete(); + results.add(FontDownloadResult( + success: false, + errorMsg: '$name 格式无效', + fontFamily: fontFamily, + displayName: name, + )); + continue; + } + + final fileSize = await File(destPath).length(); + results.add(FontDownloadResult( + success: true, + savePath: destPath, + fileSize: fileSize, + fontFamily: fontFamily, + displayName: name, + )); + } + + return results; + } catch (e) { + Log.e('字体导入失败', e); + return [ + FontDownloadResult(success: false, errorMsg: '导入失败: $e'), + ]; + } + } + + static Future loadFontIntoEngine( + String fontFamily, + String path, + ) async { + try { + final file = File(path); + if (!await file.exists()) return false; + + final bytes = await file.readAsBytes(); + if (!isValidFontFile(bytes)) { + Log.e('字体格式无效 [$fontFamily]: 文件头非 TTF/OTF 格式'); + return false; + } + final loader = FontLoader(fontFamily); + loader.addFont( + Future.value(ByteData.sublistView(Uint8List.fromList(bytes))), + ); + await loader.load(); + Log.i('字体加载成功: $fontFamily ($path)'); + return true; + } on FileSystemException catch (e) { + Log.e('字体文件系统错误 [$fontFamily]: ${e.message}', e); + return false; + } on OutOfMemoryError catch (e) { + Log.e('字体内存不足 [$fontFamily]: 文件可能过大', e); + return false; + } on FormatException catch (e) { + Log.e('字体格式无效 [$fontFamily]: ${e.message}', e); + return false; + } catch (e) { + Log.e('字体加载未知异常 [$fontFamily]', e); + return false; + } + } +} diff --git a/lib/features/mine/settings/theme_audit.dart b/lib/features/mine/settings/theme_audit.dart index 1ae522c5..842c810d 100644 --- a/lib/features/mine/settings/theme_audit.dart +++ b/lib/features/mine/settings/theme_audit.dart @@ -1,12 +1,654 @@ /// ============================================================ -/// 闲言APP — 动态主题审计 & 接入指南 +/// 闲言APP — 主题令牌审计工具 /// 创建时间: 2026-04-28 -/// 更新时间: 2026-04-28 -/// 作用: 审计所有页面是否正确接入动态主题,指导后续接入 -/// 上次更新: 初始创建 — 全量审计 +/// 更新时间: 2026-05-22 +/// 作用: 自动检测 Dart 文件中的硬编码颜色、间距、圆角、字号 +/// 上次更新: 重构为可编程审计引擎,支持 CLI 和 Flutter 内调用 /// ============================================================ +import 'dart:io'; + +/// 审计违规类别 +enum AuditCategory { + color('颜色', '🎨'), + spacing('间距', '📏'), + radius('圆角', '🔘'), + fontSize('字号', '🔤'); + + const AuditCategory(this.label, this.emoji); + final String label; + final String emoji; +} + +/// 单条审计违规记录 +class AuditViolation { + const AuditViolation({ + required this.filePath, + required this.lineNumber, + required this.category, + required this.currentCode, + required this.suggestion, + }); + + final String filePath; + final int lineNumber; + final AuditCategory category; + final String currentCode; + final String suggestion; + + @override + String toString() { + final rel = _relativePath(filePath); + return '${category.emoji} ${category.label} | $rel:$lineNumber\n' + ' 当前: $currentCode\n' + ' 建议: $suggestion'; + } + + static String _relativePath(String path) { + const markers = ['lib\\', 'lib/', 'scripts\\', 'scripts/']; + for (final m in markers) { + final idx = path.indexOf(m); + if (idx >= 0) return path.substring(idx); + } + return path; + } +} + +/// 审计结果汇总 +class AuditSummary { + const AuditSummary({required this.violations}); + + final List violations; + + int get total => violations.length; + + int countByCategory(AuditCategory cat) => + violations.where((v) => v.category == cat).length; + + Map get byCategory => { + for (final cat in AuditCategory.values) cat: countByCategory(cat), + }; + + int get fileCount => + violations.map((v) => v.filePath).toSet().length; + + @override + String toString() { + final buf = StringBuffer(); + buf.writeln('═══════════════════════════════════════════'); + buf.writeln(' 主题令牌审计报告'); + buf.writeln('═══════════════════════════════════════════'); + buf.writeln(); + + for (final cat in AuditCategory.values) { + final n = countByCategory(cat); + buf.writeln(' ${cat.emoji} ${cat.label}: $n 处违规'); + } + + buf.writeln(); + buf.writeln(' 📄 涉及文件: $fileCount 个'); + buf.writeln(' 📊 违规总数: $total 处'); + buf.writeln('═══════════════════════════════════════════'); + return buf.toString(); + } +} + +/// 主题令牌审计引擎 +/// +/// 扫描 lib/ 下所有 .dart 文件,检测硬编码的设计令牌值, +/// 输出违规记录和修复建议。 +class ThemeAuditEngine { + ThemeAuditEngine({ + String? projectRoot, + List? whitelistDirs, + }) : _projectRoot = projectRoot ?? _findProjectRoot(), + _whitelistDirs = whitelistDirs ?? _defaultWhitelist; + + final String _projectRoot; + final List _whitelistDirs; + + static final List _defaultWhitelist = [ + 'lib${Platform.pathSeparator}core${Platform.pathSeparator}theme', + 'lib${Platform.pathSeparator}l10n', + ]; + + static const List _generatedSuffixes = [ + '.g.dart', + '.freezed.dart', + ]; + + static const List _themeFileNames = [ + 'app_colors.dart', + 'app_radius.dart', + 'app_spacing.dart', + 'app_typography.dart', + 'app_shadow.dart', + 'glass_tokens.dart', + 'app_theme.dart', + 'theme.dart', + 'color_tokens.dart', + 'theme_audit.dart', + ]; + + static String _findProjectRoot() { + var dir = Directory.current; + for (var i = 0; i < 10; i++) { + if (File('${dir.path}${Platform.pathSeparator}pubspec.yaml') + .existsSync()) { + return dir.path; + } + final parent = dir.parent; + if (parent.path == dir.path) break; + dir = parent; + } + return Directory.current.path; + } + + /// 执行全量审计 + AuditSummary runAudit() { + final violations = []; + final libDir = Directory( + '$_projectRoot${Platform.pathSeparator}lib', + ); + + if (!libDir.existsSync()) { + stderr.writeln('❌ 未找到 lib/ 目录: ${libDir.path}'); + return AuditSummary(violations: violations); + } + + final files = _collectDartFiles(libDir); + for (final file in files) { + violations.addAll(_auditFile(file)); + } + + violations.sort((a, b) { + final cmp = a.filePath.compareTo(b.filePath); + return cmp != 0 ? cmp : a.lineNumber.compareTo(b.lineNumber); + }); + + return AuditSummary(violations: violations); + } + + List _collectDartFiles(Directory dir) { + final result = []; + for (final entity in dir.listSync()) { + if (entity is File) { + if (_shouldAuditFile(entity)) { + result.add(entity); + } + } else if (entity is Directory) { + if (!_isWhitelistedDir(entity)) { + result.addAll(_collectDartFiles(entity)); + } + } + } + return result; + } + + bool _shouldAuditFile(File file) { + final path = file.path; + if (!path.endsWith('.dart')) return false; + for (final suffix in _generatedSuffixes) { + if (path.endsWith(suffix)) return false; + } + final name = path.split(Platform.pathSeparator).last; + if (_themeFileNames.contains(name)) return false; + return true; + } + + bool _isWhitelistedDir(Directory dir) { + final path = dir.path; + for (final wl in _whitelistDirs) { + if (path.contains(wl)) return true; + } + return false; + } + + List _auditFile(File file) { + final violations = []; + final lines = file.readAsLinesSync(); + + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + final lineNum = i + 1; + + if (_isCommentLine(line)) continue; + + violations.addAll(_detectColors(file.path, lineNum, line)); + violations.addAll(_detectSpacing(file.path, lineNum, line)); + violations.addAll(_detectRadius(file.path, lineNum, line)); + violations.addAll(_detectFontSize(file.path, lineNum, line)); + } + + return violations; + } + + bool _isCommentLine(String line) { + final trimmed = line.trimLeft(); + return trimmed.startsWith('///') || + trimmed.startsWith('//') || + trimmed.startsWith('*'); + } + + // ============================================================ + // 颜色检测 + // ============================================================ + + static final _colorHexPattern = RegExp(r'Color\(0x[0-9A-Fa-f]{8}\)'); + static final _cupertinoColorsPattern = + RegExp(r'CupertinoColors\.\w+'); + static final _materialColorsPattern = RegExp(r'Colors\.\w+'); + static final _hexStringPattern = RegExp(r"'#[0-9A-Fa-f]{6,8}'"); + + List _detectColors( + String path, + int lineNum, + String line, + ) { + final violations = []; + + for (final match in _colorHexPattern.allMatches(line)) { + final code = match.group(0)!; + if (_isInThemeDefinition(line)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: code, + suggestion: '使用 AppTheme.ext(context).xxx 或 AppColors 令牌替代', + )); + } + + for (final match in _cupertinoColorsPattern.allMatches(line)) { + final code = match.group(0)!; + if (_isAllowedCupertinoUsage(line)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: code, + suggestion: '使用 AppTheme.ext(context).xxx 替代 CupertinoColors', + )); + } + + for (final match in _materialColorsPattern.allMatches(line)) { + final code = match.group(0)!; + if (_isAllowedMaterialUsage(line)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: code, + suggestion: '使用 AppTheme.ext(context).xxx 替代 Colors', + )); + } + + for (final match in _hexStringPattern.allMatches(line)) { + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: match.group(0)!, + suggestion: '使用 AppColors 令牌替代硬编码十六进制颜色字符串', + )); + } + + return violations; + } + + bool _isInThemeDefinition(String line) { + return line.contains('AppThemeExtension') || + line.contains('AppColors') || + line.contains('LightColors') || + line.contains('DarkColors') || + line.contains('AmoledColors') || + line.contains('GlassTokens') || + line.contains('ColorScheme.') || + line.contains('CupertinoThemeData') || + line.contains('ThemeData(') || + line.contains('_lightExtension') || + line.contains('_darkExtension') || + line.contains('_amoledExtension'); + } + + bool _isAllowedCupertinoUsage(String line) { + return line.contains('AppThemeExtension') || + line.contains('AppTheme.') || + line.contains('CupertinoThemeData') || + line.contains('ThemeData(') || + line.contains('cupertinoOverrideTheme') || + line.contains('CupertinoTextThemeData') || + line.contains('CupertinoPageTransitionsBuilder') || + line.contains('CupertinoColors.system') || + line.contains('CupertinoColors.activeBlue') || + line.contains('CupertinoColors.activeOrange') || + line.contains('CupertinoColors.separator'); + } + + bool _isAllowedMaterialUsage(String line) { + return line.contains('AppThemeExtension') || + line.contains('AppTheme.') || + line.contains('ThemeData(') || + line.contains('ColorScheme.') || + line.contains('Colors.transparent') || + line.contains('Colors.white') || + line.contains('Colors.black') || + line.contains('WidgetStateProperty'); + } + + // ============================================================ + // 间距检测 + // ============================================================ + + static final _sizedBoxHeightPattern = + RegExp(r'SizedBox\(\s*height:\s*(\d+\.?\d*)\s*\)'); + static final _sizedBoxWidthPattern = + RegExp(r'SizedBox\(\s*width:\s*(\d+\.?\d*)\s*\)'); + static final _edgeInsetsAllPattern = + RegExp(r'EdgeInsets\.all\(\s*(\d+\.?\d*)\s*\)'); + static final _edgeInsetsSymmetricPattern = RegExp( + r'EdgeInsets\.symmetric\(' + r'(?:\s*horizontal:\s*(\d+\.?\d*)\s*)?' + r'(?:,\s*)?' + r'(?:\s*vertical:\s*(\d+\.?\d*)\s*)?' + r'\)', + ); + static final _edgeInsetsOnlyPattern = RegExp( + r'EdgeInsets\.fromLTRB\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)\s*\)', + ); + static final _paddingPattern = + RegExp(r'padding:\s*EdgeInsets\.all\(\s*(\d+\.?\d*)\s*\)'); + + static final _appSpacingValues = {4.0, 8.0, 16.0, 24.0, 32.0, 48.0}; + + List _detectSpacing( + String path, + int lineNum, + String line, + ) { + final violations = []; + + if (line.contains('AppSpacing')) return violations; + + for (final match in _sizedBoxHeightPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'SizedBox(height: ${match.group(1)})', + suggestion: _spacingSuggestion(val), + )); + } + + for (final match in _sizedBoxWidthPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'SizedBox(width: ${match.group(1)})', + suggestion: _spacingSuggestion(val), + )); + } + + for (final match in _edgeInsetsAllPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'EdgeInsets.all(${match.group(1)})', + suggestion: 'EdgeInsets.all(AppSpacing.${_spacingName(val)})', + )); + } + + for (final match in _paddingPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'padding: EdgeInsets.all(${match.group(1)})', + suggestion: 'padding: const EdgeInsets.all(AppSpacing.${_spacingName(val)})', + )); + } + + for (final match in _edgeInsetsSymmetricPattern.allMatches(line)) { + final hStr = match.group(1); + final vStr = match.group(2); + if (hStr != null) { + final val = double.tryParse(hStr); + if (val != null && !_isSpacingToken(val)) { + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'horizontal: $hStr', + suggestion: 'horizontal: AppSpacing.${_spacingName(val)}', + )); + } + } + if (vStr != null) { + final val = double.tryParse(vStr); + if (val != null && !_isSpacingToken(val)) { + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'vertical: $vStr', + suggestion: 'vertical: AppSpacing.${_spacingName(val)}', + )); + } + } + } + + return violations; + } + + bool _isSpacingToken(double val) => _appSpacingValues.contains(val); + + String _spacingName(double val) => _findNearestSpacing(val); + + String _spacingSuggestion(double val) { + final nearest = _findNearestSpacing(val); + return 'SizedBox(height: AppSpacing.$nearest) // $val → ${_spacingValueMap[nearest]}'; + } + + String _findNearestSpacing(double val) { + var bestName = 'md'; + var bestDiff = double.infinity; + for (final entry in _spacingValueMap.entries) { + final diff = (entry.value - val).abs(); + if (diff < bestDiff) { + bestDiff = diff; + bestName = entry.key; + } + } + return bestName; + } + + static const _spacingValueMap = { + 'xs': 4.0, + 'sm': 8.0, + 'md': 16.0, + 'lg': 24.0, + 'xl': 32.0, + 'xxl': 48.0, + }; + + // ============================================================ + // 圆角检测 + // ============================================================ + + static final _borderRadiusCircularPattern = + RegExp(r'BorderRadius\.circular\(\s*(\d+\.?\d*)\s*\)'); + static final _radiusCircularPattern = + RegExp(r'Radius\.circular\(\s*(\d+\.?\d*)\s*\)'); + + static final _appRadiusValues = {2.0, 4.0, 8.0, 12.0, 16.0, 999.0}; + + List _detectRadius( + String path, + int lineNum, + String line, + ) { + final violations = []; + + if (line.contains('AppRadius')) return violations; + + for (final match in _borderRadiusCircularPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _appRadiusValues.contains(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.radius, + currentCode: 'BorderRadius.circular(${match.group(1)})', + suggestion: _radiusSuggestion(val, isBorderRadius: true), + )); + } + + for (final match in _radiusCircularPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _appRadiusValues.contains(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.radius, + currentCode: 'Radius.circular(${match.group(1)})', + suggestion: _radiusSuggestion(val, isBorderRadius: false), + )); + } + + return violations; + } + + String _radiusSuggestion(double val, {required bool isBorderRadius}) { + final nearest = _findNearestRadius(val); + if (isBorderRadius) { + return 'BorderRadius.circular(AppRadius.$nearest) // $val → ${_radiusValueMap[nearest]}'; + } + return 'Radius.circular(AppRadius.$nearest) // $val → ${_radiusValueMap[nearest]}'; + } + + String _findNearestRadius(double val) { + var bestName = 'md'; + var bestDiff = double.infinity; + for (final entry in _radiusValueMap.entries) { + final diff = (entry.value - val).abs(); + if (diff < bestDiff) { + bestDiff = diff; + bestName = entry.key; + } + } + return bestName; + } + + static const _radiusValueMap = { + 'xs': 2.0, + 'sm': 4.0, + 'md': 8.0, + 'lg': 12.0, + 'xl': 16.0, + 'full': 999.0, + }; + + // ============================================================ + // 字号检测 + // ============================================================ + + static final _fontSizePattern = + RegExp(r'fontSize:\s*(\d+\.?\d*)\s*[,)]'); + + static final _appTypographyValues = { + 34.0, + 28.0, + 22.0, + 20.0, + 18.0, + 16.0, + 14.0, + 13.0, + 12.0, + 11.0, + }; + + List _detectFontSize( + String path, + int lineNum, + String line, + ) { + final violations = []; + + if (line.contains('AppTypography')) return violations; + if (line.contains('fontScale')) return violations; + if (line.contains('fontDisplay') || + line.contains('fontTitle') || + line.contains('fontHeadline') || + line.contains('fontBody') || + line.contains('fontCallout') || + line.contains('fontSubhead') || + line.contains('fontFootnote') || + line.contains('fontCaption')) { + return violations; + } + + for (final match in _fontSizePattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _appTypographyValues.contains(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.fontSize, + currentCode: 'fontSize: ${match.group(1)}', + suggestion: _fontSizeSuggestion(val), + )); + } + + return violations; + } + + String _fontSizeSuggestion(double val) { + final nearest = _findNearestFontSize(val); + return 'fontSize: AppTypography.$nearest // $val → ${_typographyValueMap[nearest]}'; + } + + String _findNearestFontSize(double val) { + var bestName = 'fontBody'; + var bestDiff = double.infinity; + for (final entry in _typographyValueMap.entries) { + final diff = (entry.value - val).abs(); + if (diff < bestDiff) { + bestDiff = diff; + bestName = entry.key; + } + } + return bestName; + } + + static const _typographyValueMap = { + 'fontDisplay': 34.0, + 'fontTitle1': 28.0, + 'fontTitle2': 22.0, + 'fontTitle3': 20.0, + 'fontHeadline': 18.0, + 'fontBody': 16.0, + 'fontCallout': 16.0, + 'fontSubhead': 14.0, + 'fontFootnote': 13.0, + 'fontCaption1': 12.0, + 'fontCaption2': 11.0, + }; +} + +/// ============================================================ /// 动态主题接入规范 +/// ============================================================ /// /// ✅ 已接入: 使用 AppTheme.ext(context) 获取主题 /// ⚠️ 部分接入: 使用了 ext 但仍有硬编码颜色 diff --git a/lib/features/mine/signin/presentation/signin_page.dart b/lib/features/mine/signin/presentation/signin_page.dart index 4d9fd3ba..e7216bf0 100644 --- a/lib/features/mine/signin/presentation/signin_page.dart +++ b/lib/features/mine/signin/presentation/signin_page.dart @@ -16,7 +16,7 @@ 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_router.dart'; +import '../../../../core/router/app_routes.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; import '../../../../shared/widgets/app_icon.dart'; import '../../../../shared/widgets/glass_container.dart'; @@ -25,6 +25,7 @@ import '../../../../shared/widgets/responsive_layout.dart'; import '../../../../core/utils/level_utils.dart'; import '../../../auth/providers/auth_provider.dart'; import '../providers/signin_provider.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class SigninPage extends ConsumerStatefulWidget { const SigninPage({super.key}); @@ -93,6 +94,7 @@ class _SigninPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/user_center/presentation/coin_log_page.dart b/lib/features/mine/user_center/presentation/coin_log_page.dart index 3bf2ea07..347827de 100644 --- a/lib/features/mine/user_center/presentation/coin_log_page.dart +++ b/lib/features/mine/user_center/presentation/coin_log_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 金币记录页面 // 创建时间: 2026-04-29 // 更新时间: 2026-05-04 @@ -19,6 +19,7 @@ import '../../../../core/theme/app_typography.dart'; import '../../../../core/theme/app_radius.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../providers/coin_provider.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class CoinLogPage extends ConsumerStatefulWidget { const CoinLogPage({super.key}); @@ -56,6 +57,7 @@ class _CoinLogPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('💰 金币记录'), previousPageTitle: '返回', ), diff --git a/lib/features/mine/user_center/presentation/devices/device_card.dart b/lib/features/mine/user_center/presentation/devices/device_card.dart new file mode 100644 index 00000000..65da94d1 --- /dev/null +++ b/lib/features/mine/user_center/presentation/devices/device_card.dart @@ -0,0 +1,225 @@ +/// ============================================================ +/// 闲言APP — 设备卡片组件 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 设备列表中的单个设备卡片 + 分区标题头 +/// 上次更新: 从 my_devices_page.dart 拆分提取 +/// ============================================================ + +import 'package:flutter/cupertino.dart'; + +import '../../../../../core/theme/app_radius.dart'; +import '../../../../../core/theme/app_spacing.dart'; +import '../../../../../core/theme/app_typography.dart'; +import '../../../../../core/theme/app_theme.dart'; +import '../../../../../shared/widgets/glass_container.dart'; +import '../../../../auth/models/user_model.dart'; +import 'device_utils.dart'; + +class DeviceSectionHeader extends StatelessWidget { + const DeviceSectionHeader({ + super.key, + required this.title, + required this.count, + required this.isOnline, + }); + + final String title; + final int count; + final bool isOnline; + + @override + Widget build(BuildContext context) { + final ext = AppTheme.ext(context); + final dotColor = + isOnline ? CupertinoColors.systemGreen : CupertinoColors.systemGrey; + + return Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.xs), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text( + title, + style: AppTypography.subhead.copyWith( + color: ext.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: ext.bgSecondary, + borderRadius: AppRadius.smBorder, + ), + child: Text( + '$count', + style: AppTypography.caption2.copyWith( + color: ext.textHint, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + +class DeviceCard extends StatelessWidget { + const DeviceCard({ + super.key, + required this.device, + required this.isOnline, + required this.onTap, + required this.onRename, + }); + + final UserDevice device; + final bool isOnline; + final VoidCallback onTap; + final VoidCallback onRename; + + @override + Widget build(BuildContext context) { + final ext = AppTheme.ext(context); + final platformIcon = getPlatformIcon(device.platform); + final platformColor = getPlatformColor(device.platform); + final displayName = getDisplayDeviceName(device); + final platformEmoji = getPlatformEmoji(device.platform); + + return Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.sm), + child: Dismissible( + key: ValueKey('device_${device.id}'), + direction: DismissDirection.endToStart, + confirmDismiss: (_) async { + onRename(); + return false; + }, + background: Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: AppSpacing.lg), + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.1), + borderRadius: AppRadius.lgBorder, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(CupertinoIcons.pencil, size: 18, color: ext.accent), + const SizedBox(width: 4), + Text( + '重命名', + style: AppTypography.caption1.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + child: GestureDetector( + onTap: onTap, + child: GlassContainer( + depth: isOnline ? GlassDepth.elevated : GlassDepth.base, + padding: const EdgeInsets.all(AppSpacing.md), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: platformColor.withValues(alpha: 0.12), + borderRadius: AppRadius.lgBorder, + ), + child: Center( + child: Stack( + alignment: Alignment.center, + children: [ + Icon(platformIcon, size: 20, color: platformColor), + if (platformEmoji.isNotEmpty) + Positioned( + right: 0, + bottom: 0, + child: Text( + platformEmoji.trim(), + style: const TextStyle(fontSize: 8), + ), + ), + ], + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + displayName, + style: AppTypography.body.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (isOnline) + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(left: 6), + decoration: BoxDecoration( + color: CupertinoColors.systemGreen, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: CupertinoColors.systemGreen + .withValues(alpha: 0.4), + blurRadius: 4, + spreadRadius: 1, + ), + ], + ), + ), + ], + ), + if (device.ip.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + device.ip, + style: AppTypography.caption1.copyWith( + color: ext.textSecondary, + fontFamily: 'SF Mono', + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + Icon( + CupertinoIcons.chevron_right, + size: 16, + color: ext.textHint, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/mine/user_center/presentation/devices/device_detail_sheet.dart b/lib/features/mine/user_center/presentation/devices/device_detail_sheet.dart new file mode 100644 index 00000000..cfc32cd5 --- /dev/null +++ b/lib/features/mine/user_center/presentation/devices/device_detail_sheet.dart @@ -0,0 +1,347 @@ +/// ============================================================ +/// 闲言APP — 设备详情底部弹窗 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 展示设备完整信息+操作按钮(下线/移除/重命名) +/// 上次更新: 从 my_devices_page.dart 拆分提取 +/// ============================================================ + +import 'package:flutter/cupertino.dart'; + +import '../../../../../core/theme/app_radius.dart'; +import '../../../../../core/theme/app_spacing.dart'; +import '../../../../../core/theme/app_typography.dart'; +import '../../../../../core/theme/app_theme.dart'; +import '../../../../auth/models/user_model.dart'; +import 'device_utils.dart'; + +class DeviceInfoRow extends StatelessWidget { + const DeviceInfoRow({ + super.key, + required this.icon, + required this.label, + required this.value, + this.trailing, + }); + + final IconData icon; + final String label; + final String value; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + final ext = AppTheme.ext(context); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + child: Row( + children: [ + Icon(icon, size: 14, color: ext.textHint), + const SizedBox(width: 8), + Text( + label, + style: AppTypography.caption1.copyWith(color: ext.textHint), + ), + const Spacer(), + Flexible( + child: Text( + value, + style: AppTypography.caption1.copyWith(color: ext.textPrimary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (trailing != null) ...[const SizedBox(width: 6), trailing!], + ], + ), + ); + } +} + +Future showDeviceDetailSheet({ + required BuildContext context, + required UserDevice device, + required bool isOnline, + required bool isCurrent, + required VoidCallback onRename, + required VoidCallback onOffline, + required VoidCallback onRemove, +}) async { + final ext = AppTheme.ext(context); + final platformIcon = getPlatformIcon(device.platform); + final platformColor = getPlatformColor(device.platform); + final displayName = getDisplayDeviceName(device); + + await showCupertinoModalPopup( + context: context, + builder: (ctx) => Container( + decoration: BoxDecoration( + color: ext.bgCard, + borderRadius: const BorderRadius.vertical(top: Radius.circular(14)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 6, bottom: 4), + child: Container( + width: 36, + height: 5, + decoration: BoxDecoration( + color: ext.textHint.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(3), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + AppSpacing.sm, + AppSpacing.md, + AppSpacing.md, + ), + child: Column( + children: [ + Row( + children: [ + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + color: platformColor.withValues(alpha: 0.12), + borderRadius: AppRadius.lgBorder, + ), + child: Center( + child: Icon( + platformIcon, + size: 24, + color: platformColor, + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + displayName, + style: AppTypography.title3.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (isCurrent) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.12), + borderRadius: AppRadius.smBorder, + ), + child: Text( + '当前', + style: AppTypography.caption2.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 2), + Row( + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: isOnline + ? CupertinoColors.systemGreen + : CupertinoColors.systemGrey, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + isOnline ? '在线' : '离线', + style: AppTypography.caption1.copyWith( + color: isOnline + ? CupertinoColors.systemGreen + : ext.textHint, + ), + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: AppSpacing.md), + Container( + decoration: BoxDecoration( + color: ext.bgSecondary.withValues(alpha: 0.5), + borderRadius: AppRadius.lgBorder, + ), + child: Column( + children: [ + if (device.deviceModel.isNotEmpty) + DeviceInfoRow( + icon: CupertinoIcons.device_phone_portrait, + label: '设备型号', + value: getFriendlyModel(device.deviceModel), + ), + DeviceInfoRow( + icon: CupertinoIcons.desktopcomputer, + label: '平台', + value: platformLabel(device.platform), + ), + if (device.appName.isNotEmpty) + DeviceInfoRow( + icon: CupertinoIcons.app_badge, + label: '应用', + value: device.appName, + ), + if (device.ip.isNotEmpty) + DeviceInfoRow( + icon: CupertinoIcons.globe, + label: 'IP地址', + value: device.ip, + trailing: GestureDetector( + onTap: () => copyToClipboard(device.ip), + child: Icon( + CupertinoIcons.doc_on_doc, + size: 14, + color: ext.textHint, + ), + ), + ), + if (device.hasIpCity) + DeviceInfoRow( + icon: CupertinoIcons.location, + label: 'IP归属地', + value: device.ipCity, + ), + if (device.hasIpRange) + DeviceInfoRow( + icon: CupertinoIcons.map, + label: 'IP段', + value: device.ipRange, + ), + if (device.lastActiveText.isNotEmpty) + DeviceInfoRow( + icon: CupertinoIcons.clock, + label: '最后活跃', + value: device.lastActiveText, + ), + if (device.createtimeText.isNotEmpty) + DeviceInfoRow( + icon: CupertinoIcons.calendar, + label: '首次登录', + value: device.createtimeText, + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + 0, + AppSpacing.md, + AppSpacing.sm, + ), + child: Column( + children: [ + SizedBox( + width: double.infinity, + child: CupertinoButton( + color: ext.accent.withValues(alpha: 0.1), + borderRadius: AppRadius.lgBorder, + onPressed: () { + Navigator.pop(ctx); + onRename(); + }, + child: Text( + '修改名称', + style: AppTypography.subhead.copyWith( + color: ext.accent, + ), + ), + ), + ), + if (isOnline) + SizedBox( + width: double.infinity, + child: CupertinoButton( + color: CupertinoColors.systemRed.withValues(alpha: 0.1), + borderRadius: AppRadius.lgBorder, + onPressed: () { + Navigator.pop(ctx); + onOffline(); + }, + child: Text( + '下线此设备', + style: AppTypography.subhead.copyWith( + color: CupertinoColors.systemRed, + ), + ), + ), + ), + SizedBox( + width: double.infinity, + child: CupertinoButton( + color: CupertinoColors.systemRed.withValues(alpha: 0.1), + borderRadius: AppRadius.lgBorder, + onPressed: () { + Navigator.pop(ctx); + onRemove(); + }, + child: Text( + '移除设备', + style: AppTypography.subhead.copyWith( + color: CupertinoColors.systemRed, + ), + ), + ), + ), + SizedBox( + width: double.infinity, + child: CupertinoButton( + borderRadius: AppRadius.lgBorder, + onPressed: () => Navigator.pop(ctx), + child: Text( + '取消', + style: AppTypography.subhead.copyWith( + color: ext.textSecondary, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); +} diff --git a/lib/features/mine/user_center/presentation/devices/device_overview_card.dart b/lib/features/mine/user_center/presentation/devices/device_overview_card.dart new file mode 100644 index 00000000..1da59e8b --- /dev/null +++ b/lib/features/mine/user_center/presentation/devices/device_overview_card.dart @@ -0,0 +1,198 @@ +/// ============================================================ +/// 闲言APP — 设备概览卡片组件 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 我的设备页面顶部概览卡片 + 文件传输入口卡片 +/// 上次更新: 从 my_devices_page.dart 拆分提取 +/// ============================================================ + +import 'package:flutter/cupertino.dart'; + +import '../../../../../core/theme/app_radius.dart'; +import '../../../../../core/theme/app_spacing.dart'; +import '../../../../../core/theme/app_typography.dart'; +import '../../../../../core/theme/app_theme.dart'; +import '../../../../../shared/widgets/glass_container.dart'; +import '../../providers/device_provider.dart'; + +class DeviceOverviewCard extends StatelessWidget { + const DeviceOverviewCard({super.key, required this.state}); + + final DeviceState state; + + @override + Widget build(BuildContext context) { + final ext = AppTheme.ext(context); + final currentDevice = state.devices.where((d) => d.getIsOnline).firstOrNull; + + return Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + AppSpacing.md, + AppSpacing.md, + AppSpacing.sm, + ), + child: GlassContainer( + padding: const EdgeInsets.all(AppSpacing.md), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.12), + borderRadius: AppRadius.lgBorder, + ), + child: Icon( + CupertinoIcons.device_laptop, + size: 22, + color: ext.accent, + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '设备概览', + style: AppTypography.subhead.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + '${state.onlineCount} 台在线 · 共 ${state.devices.length} 台设备', + style: AppTypography.caption1.copyWith( + color: ext.textSecondary, + ), + ), + if (currentDevice != null && currentDevice.hasIpCity) ...[ + const SizedBox(height: 2), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.location, + size: 12, + color: ext.accent, + ), + const SizedBox(width: 3), + Text( + currentDevice.ipCity, + style: AppTypography.caption2.copyWith( + color: ext.accent, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ], + ), + ), + if (state.onlineCount > 0) + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: 2, + ), + decoration: BoxDecoration( + color: CupertinoColors.systemGreen.withValues(alpha: 0.12), + borderRadius: AppRadius.smBorder, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: CupertinoColors.systemGreen, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + '在线', + style: AppTypography.caption2.copyWith( + color: CupertinoColors.systemGreen, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class FileTransferEntry extends StatelessWidget { + const FileTransferEntry({super.key, required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final ext = AppTheme.ext(context); + + return Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + AppSpacing.sm, + AppSpacing.md, + AppSpacing.sm, + ), + child: GestureDetector( + onTap: onTap, + child: GlassContainer( + padding: const EdgeInsets.all(AppSpacing.md), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.12), + borderRadius: AppRadius.lgBorder, + ), + child: Icon( + CupertinoIcons.arrow_up_arrow_down_circle_fill, + size: 22, + color: ext.accent, + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '文件传输助手', + style: AppTypography.body.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + '在设备间快速传输文件', + style: AppTypography.caption1.copyWith( + color: ext.textSecondary, + ), + ), + ], + ), + ), + Icon(CupertinoIcons.chevron_right, size: 16, color: ext.textHint), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/mine/user_center/presentation/devices/device_utils.dart b/lib/features/mine/user_center/presentation/devices/device_utils.dart new file mode 100644 index 00000000..75087a0a --- /dev/null +++ b/lib/features/mine/user_center/presentation/devices/device_utils.dart @@ -0,0 +1,154 @@ +/// ============================================================ +/// 闲言APP — 设备工具函数集 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 设备相关通用工具函数(平台图标/颜色/名称/认证等) +/// 上次更新: 从 my_devices_page.dart 拆分提取 +/// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth/local_auth.dart'; + +import '../../../../../core/services/device/device_info_service.dart'; +import '../../../../../shared/widgets/app_toast.dart'; +import '../../../../auth/models/user_model.dart'; + +IconData getPlatformIcon(String platform) { + switch (platform.toLowerCase()) { + case 'ios': + return CupertinoIcons.device_phone_portrait; + case 'android': + return CupertinoIcons.device_phone_portrait; + case 'ohos': + return CupertinoIcons.device_phone_portrait; + case 'web': + return CupertinoIcons.globe; + case 'windows': + return CupertinoIcons.desktopcomputer; + case 'mac': + return CupertinoIcons.desktopcomputer; + case 'linux': + return CupertinoIcons.desktopcomputer; + default: + return CupertinoIcons.device_laptop; + } +} + +String getPlatformEmoji(String platform) { + switch (platform.toLowerCase()) { + case 'ohos': + return ' 鸿蒙'; + case 'android': + return ' 🤖'; + case 'ios': + return ''; + default: + return ''; + } +} + +Color getPlatformColor(String platform) { + switch (platform.toLowerCase()) { + case 'ios': + case 'mac': + return CupertinoColors.systemBlue; + case 'android': + return CupertinoColors.systemGreen; + case 'ohos': + return CupertinoColors.systemRed; + case 'web': + return CupertinoColors.systemOrange; + case 'windows': + return CupertinoColors.systemTeal; + case 'linux': + return CupertinoColors.systemPurple; + default: + return CupertinoColors.systemGrey; + } +} + +String platformLabel(String platform) { + switch (platform.toLowerCase()) { + case 'ios': + return 'iOS'; + case 'android': + return 'Android'; + case 'ohos': + return 'HarmonyOS 🪶'; + case 'web': + return 'Web'; + case 'windows': + return 'Windows'; + case 'mac': + return 'macOS'; + case 'linux': + return 'Linux'; + default: + return platform; + } +} + +String getFriendlyModel(String model) { + final friendly = DeviceInfoService.getModelFriendlyName(model); + if (friendly != null) return friendly; + return model; +} + +String getDisplayDeviceName(UserDevice device) { + final model = device.deviceModel; + final friendly = DeviceInfoService.getModelFriendlyName(model); + if (friendly != null) return friendly; + if (device.deviceName.isNotEmpty && + device.deviceName != 'Unknown' && + device.deviceName != 'unknown' && + device.deviceName != 'HarmonyOS Device') { + return device.deviceName; + } + return model.isNotEmpty ? model : device.deviceName; +} + +Future authenticate(LocalAuthentication localAuth, String reason) async { + try { + final canAuth = + await localAuth.canCheckBiometrics || + await localAuth.isDeviceSupported(); + if (!canAuth) return true; + return await localAuth.authenticate( + localizedReason: reason, + persistAcrossBackgrounding: true, + ); + } catch (e) { + return true; + } +} + +void copyToClipboard(String text) { + Clipboard.setData(ClipboardData(text: text)); + AppToast.showSuccess('已复制: $text'); +} + +Future showConfirmDialog( + BuildContext context, + String title, + String message, +) { + return showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: Text(title), + content: Text(message), + actions: [ + CupertinoDialogAction( + child: const Text('取消'), + onPressed: () => Navigator.pop(ctx, false), + ), + CupertinoDialogAction( + isDestructiveAction: true, + child: const Text('确认'), + onPressed: () => Navigator.pop(ctx, true), + ), + ], + ), + ); +} diff --git a/lib/features/mine/user_center/presentation/learning_center_page.dart b/lib/features/mine/user_center/presentation/learning_center_page.dart index b58a57b6..2ffde003 100644 --- a/lib/features/mine/user_center/presentation/learning_center_page.dart +++ b/lib/features/mine/user_center/presentation/learning_center_page.dart @@ -21,7 +21,7 @@ import '../../../../core/theme/app_radius.dart'; import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; -import '../../../../core/router/app_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/utils/interaction_animations.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../../../../shared/widgets/offline_banner.dart'; @@ -31,6 +31,7 @@ import '../providers/dashboard_provider.dart'; import '../providers/learning_progress_provider.dart'; import 'widgets/learning_heatmap.dart'; import 'widgets/learning_charts.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class LearningCenterPage extends ConsumerStatefulWidget { const LearningCenterPage({super.key}); @@ -66,6 +67,7 @@ class _LearningCenterPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/mine/user_center/presentation/my_devices_page.dart b/lib/features/mine/user_center/presentation/my_devices_page.dart index c8e55026..3c0a492f 100644 --- a/lib/features/mine/user_center/presentation/my_devices_page.dart +++ b/lib/features/mine/user_center/presentation/my_devices_page.dart @@ -1,31 +1,33 @@ /// ============================================================ /// 闲言APP — 我的设备页面 /// 创建时间: 2026-05-10 -/// 更新时间: 2026-05-16 +/// 更新时间: 2026-05-22 /// 作用: 查看和管理用户登录设备,支持在线状态/下线/移除/IP归属地显示 -/// 上次更新: v10.2.0 修复4项问题: 过滤未知设备/简化卡片+详情Sheet/路由修复/下线退出登录 +/// 上次更新: 拆分子组件到 devices/ 目录,主页面精简 /// ============================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show RefreshIndicator; -import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:xianyan/core/router/app_nav_extension.dart'; import 'package:local_auth/local_auth.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../../../core/router/app_router.dart'; +import '../../../../core/router/app_nav_extension.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/services/device/device_info_service.dart'; -import '../../../../core/theme/app_radius.dart'; import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../features/auth/models/user_model.dart'; import '../../../../features/auth/providers/auth_provider.dart'; -import '../../../../shared/widgets/glass_container.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; import '../../../../shared/widgets/app_toast.dart'; import '../providers/device_provider.dart'; +import 'devices/device_card.dart'; +import 'devices/device_detail_sheet.dart'; +import 'devices/device_overview_card.dart'; +import 'devices/device_utils.dart'; class MyDevicesPage extends ConsumerStatefulWidget { const MyDevicesPage({super.key}); @@ -40,21 +42,6 @@ class _MyDevicesPageState extends ConsumerState { static const _prefKeyLastAccount = 'last_login_account'; - Future _authenticate(String reason) async { - try { - final canAuth = - await _localAuth.canCheckBiometrics || - await _localAuth.isDeviceSupported(); - if (!canAuth) return true; - return await _localAuth.authenticate( - localizedReason: reason, - persistAcrossBackgrounding: true, - ); - } catch (e) { - return true; - } - } - bool _isCurrentDevice(UserDevice device) { if (!device.getIsOnline) return false; if (_currentPlatform.isEmpty) return false; @@ -88,6 +75,7 @@ class _MyDevicesPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -130,22 +118,20 @@ class _MyDevicesPageState extends ConsumerState { ) else ...[ SliverToBoxAdapter( - child: _buildOnlineSummary( - ext, - deviceState, - ).animate().fadeIn(duration: 300.ms), + child: DeviceOverviewCard(state: deviceState) + .animate() + .fadeIn(duration: 300.ms), ), SliverToBoxAdapter( - child: _buildFileTransferEntry( - ext, + child: FileTransferEntry( + onTap: () => context.appPush(AppRoutes.fileTransfer), ).animate().fadeIn(duration: 300.ms, delay: 50.ms), ), if (deviceState.hasDevices) SliverToBoxAdapter( - child: _buildDeviceList( - ext, - deviceState.devices, - ).animate().fadeIn(duration: 300.ms, delay: 100.ms), + child: _buildDeviceList(ext, deviceState.devices) + .animate() + .fadeIn(duration: 300.ms, delay: 100.ms), ) else SliverFillRemaining(child: _buildEmptyView(ext)), @@ -160,182 +146,6 @@ class _MyDevicesPageState extends ConsumerState { ); } - // ============================================================ - // 设备概览 - // ============================================================ - - Widget _buildOnlineSummary(AppThemeExtension ext, DeviceState state) { - final currentDevice = state.devices.where((d) => d.getIsOnline).firstOrNull; - return Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.md, - AppSpacing.md, - AppSpacing.md, - AppSpacing.sm, - ), - child: GlassContainer( - padding: const EdgeInsets.all(AppSpacing.md), - child: Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: ext.accent.withValues(alpha: 0.12), - borderRadius: AppRadius.lgBorder, - ), - child: Icon( - CupertinoIcons.device_laptop, - size: 22, - color: ext.accent, - ), - ), - const SizedBox(width: AppSpacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '设备概览', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 2), - Text( - '${state.onlineCount} 台在线 · 共 ${state.devices.length} 台设备', - style: AppTypography.caption1.copyWith( - color: ext.textSecondary, - ), - ), - if (currentDevice != null && currentDevice.hasIpCity) ...[ - const SizedBox(height: 2), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - CupertinoIcons.location, - size: 12, - color: ext.accent, - ), - const SizedBox(width: 3), - Text( - currentDevice.ipCity, - style: AppTypography.caption2.copyWith( - color: ext.accent, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - ], - ), - ), - if (state.onlineCount > 0) - Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.sm, - vertical: 2, - ), - decoration: BoxDecoration( - color: CupertinoColors.systemGreen.withValues(alpha: 0.12), - borderRadius: AppRadius.smBorder, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 6, - height: 6, - decoration: const BoxDecoration( - color: CupertinoColors.systemGreen, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 4), - Text( - '在线', - style: AppTypography.caption2.copyWith( - color: CupertinoColors.systemGreen, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - // ============================================================ - // 文件传输入口 - // ============================================================ - - Widget _buildFileTransferEntry(AppThemeExtension ext) { - return Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.md, - AppSpacing.sm, - AppSpacing.md, - AppSpacing.sm, - ), - child: GestureDetector( - onTap: () => context.appPush(AppRoutes.fileTransfer), - child: GlassContainer( - padding: const EdgeInsets.all(AppSpacing.md), - child: Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: ext.accent.withValues(alpha: 0.12), - borderRadius: AppRadius.lgBorder, - ), - child: Icon( - CupertinoIcons.arrow_up_arrow_down_circle_fill, - size: 22, - color: ext.accent, - ), - ), - const SizedBox(width: AppSpacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '文件传输助手', - style: AppTypography.body.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 2), - Text( - '在设备间快速传输文件', - style: AppTypography.caption1.copyWith( - color: ext.textSecondary, - ), - ), - ], - ), - ), - Icon(CupertinoIcons.chevron_right, size: 16, color: ext.textHint), - ], - ), - ), - ), - ); - } - - // ============================================================ - // 设备列表 - // ============================================================ - Widget _buildDeviceList(AppThemeExtension ext, List devices) { final onlineDevices = devices.where((d) => d.getIsOnline).toList(); final offlineDevices = devices.where((d) => !d.getIsOnline).toList(); @@ -346,605 +156,55 @@ class _MyDevicesPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (onlineDevices.isNotEmpty) ...[ - _buildSectionHeader( - ext, - '当前在线', - onlineDevices.length, + DeviceSectionHeader( + title: '当前在线', + count: onlineDevices.length, isOnline: true, ), const SizedBox(height: AppSpacing.sm), - ...onlineDevices.map((d) => _buildDeviceCard(ext, d, true)), + ...onlineDevices.map( + (d) => DeviceCard( + device: d, + isOnline: true, + onTap: () => _onDeviceTap(d, true), + onRename: () => _showRenameDialog(ext, d), + ), + ), const SizedBox(height: AppSpacing.md), ], if (offlineDevices.isNotEmpty) ...[ - _buildSectionHeader( - ext, - '离线设备', - offlineDevices.length, + DeviceSectionHeader( + title: '离线设备', + count: offlineDevices.length, isOnline: false, ), const SizedBox(height: AppSpacing.sm), - ...offlineDevices.map((d) => _buildDeviceCard(ext, d, false)), + ...offlineDevices.map( + (d) => DeviceCard( + device: d, + isOnline: false, + onTap: () => _onDeviceTap(d, false), + onRename: () => _showRenameDialog(ext, d), + ), + ), ], ], ), ); } - Widget _buildSectionHeader( - AppThemeExtension ext, - String title, - int count, { - required bool isOnline, - }) { - final dotColor = isOnline - ? CupertinoColors.systemGreen - : CupertinoColors.systemGrey; - return Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.xs), - child: Row( - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), - ), - const SizedBox(width: 6), - Text( - title, - style: AppTypography.subhead.copyWith( - color: ext.textSecondary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 6), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), - decoration: BoxDecoration( - color: ext.bgSecondary, - borderRadius: AppRadius.smBorder, - ), - child: Text( - '$count', - style: AppTypography.caption2.copyWith( - color: ext.textHint, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ); - } - - // ============================================================ - // 设备卡片 — 简化版,只显示设备名+IP,左滑重命名 - // ============================================================ - - Widget _buildDeviceCard( - AppThemeExtension ext, - UserDevice device, - bool isOnline, - ) { - final platformIcon = _getPlatformIcon(device.platform); - final platformColor = _getPlatformColor(device.platform); - - return Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: Dismissible( - key: ValueKey('device_${device.id}'), - direction: DismissDirection.endToStart, - confirmDismiss: (_) async { - _showRenameDialog(ext, device); - return false; - }, - background: Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: AppSpacing.lg), - decoration: BoxDecoration( - color: ext.accent.withValues(alpha: 0.1), - borderRadius: AppRadius.lgBorder, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(CupertinoIcons.pencil, size: 18, color: ext.accent), - const SizedBox(width: 4), - Text( - '重命名', - style: AppTypography.caption1.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - child: GestureDetector( - onTap: () => _showDeviceDetailSheet(context, ext, device, isOnline), - child: GlassContainer( - depth: isOnline ? GlassDepth.elevated : GlassDepth.base, - padding: const EdgeInsets.all(AppSpacing.md), - child: Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: platformColor.withValues(alpha: 0.12), - borderRadius: AppRadius.lgBorder, - ), - child: Center( - child: Icon(platformIcon, size: 20, color: platformColor), - ), - ), - const SizedBox(width: AppSpacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - device.deviceName, - style: AppTypography.body.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (isOnline) - Container( - width: 8, - height: 8, - margin: const EdgeInsets.only(left: 6), - decoration: BoxDecoration( - color: CupertinoColors.systemGreen, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: CupertinoColors.systemGreen - .withValues(alpha: 0.4), - blurRadius: 4, - spreadRadius: 1, - ), - ], - ), - ), - ], - ), - if (device.ip.isNotEmpty) ...[ - const SizedBox(height: 2), - Text( - device.ip, - style: AppTypography.caption1.copyWith( - color: ext.textSecondary, - fontFamily: 'SF Mono', - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ), - Icon( - CupertinoIcons.chevron_right, - size: 16, - color: ext.textHint, - ), - ], - ), - ), - ), - ), - ); - } - - // ============================================================ - // 设备详情 Sheet — 显示全部信息+操作按钮 - // ============================================================ - - Future _showDeviceDetailSheet( - BuildContext context, - AppThemeExtension ext, - UserDevice device, - bool isOnline, - ) async { - final platformIcon = _getPlatformIcon(device.platform); - final platformColor = _getPlatformColor(device.platform); - final isCurrent = _isCurrentDevice(device); - - await showCupertinoModalPopup( + void _onDeviceTap(UserDevice device, bool isOnline) { + showDeviceDetailSheet( context: context, - builder: (ctx) => Container( - decoration: BoxDecoration( - color: ext.bgCard, - borderRadius: const BorderRadius.vertical(top: Radius.circular(14)), - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(top: 6, bottom: 4), - child: Container( - width: 36, - height: 5, - decoration: BoxDecoration( - color: ext.textHint.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(3), - ), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.md, - AppSpacing.sm, - AppSpacing.md, - AppSpacing.md, - ), - child: Column( - children: [ - Row( - children: [ - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - color: platformColor.withValues(alpha: 0.12), - borderRadius: AppRadius.lgBorder, - ), - child: Center( - child: Icon( - platformIcon, - size: 24, - color: platformColor, - ), - ), - ), - const SizedBox(width: AppSpacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Flexible( - child: Text( - device.deviceName, - style: AppTypography.title3.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (isCurrent) ...[ - const SizedBox(width: 6), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: ext.accent.withValues( - alpha: 0.12, - ), - borderRadius: AppRadius.smBorder, - ), - child: Text( - '当前', - style: AppTypography.caption2.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ], - ), - const SizedBox(height: 2), - Row( - children: [ - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: isOnline - ? CupertinoColors.systemGreen - : CupertinoColors.systemGrey, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 4), - Text( - isOnline ? '在线' : '离线', - style: AppTypography.caption1.copyWith( - color: isOnline - ? CupertinoColors.systemGreen - : ext.textHint, - ), - ), - ], - ), - ], - ), - ), - ], - ), - const SizedBox(height: AppSpacing.md), - Container( - decoration: BoxDecoration( - color: ext.bgSecondary.withValues(alpha: 0.5), - borderRadius: AppRadius.lgBorder, - ), - child: Column( - children: [ - if (device.deviceModel.isNotEmpty) - _infoRow( - ext, - CupertinoIcons.device_phone_portrait, - '设备型号', - device.deviceModel, - ), - _infoRow( - ext, - CupertinoIcons.desktopcomputer, - '平台', - _platformLabel(device.platform), - ), - if (device.appName.isNotEmpty) - _infoRow( - ext, - CupertinoIcons.app_badge, - '应用', - device.appName, - ), - if (device.ip.isNotEmpty) - _infoRow( - ext, - CupertinoIcons.globe, - 'IP地址', - device.ip, - trailing: GestureDetector( - onTap: () => _copyToClipboard(device.ip), - child: Icon( - CupertinoIcons.doc_on_doc, - size: 14, - color: ext.textHint, - ), - ), - ), - if (device.hasIpCity) - _infoRow( - ext, - CupertinoIcons.location, - 'IP归属地', - device.ipCity, - ), - if (device.hasIpRange) - _infoRow( - ext, - CupertinoIcons.map, - 'IP段', - device.ipRange, - ), - if (device.lastActiveText.isNotEmpty) - _infoRow( - ext, - CupertinoIcons.clock, - '最后活跃', - device.lastActiveText, - ), - if (device.createtimeText.isNotEmpty) - _infoRow( - ext, - CupertinoIcons.calendar, - '首次登录', - device.createtimeText, - ), - ], - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.md, - 0, - AppSpacing.md, - AppSpacing.sm, - ), - child: Column( - children: [ - SizedBox( - width: double.infinity, - child: CupertinoButton( - color: ext.accent.withValues(alpha: 0.1), - borderRadius: AppRadius.lgBorder, - onPressed: () { - Navigator.pop(ctx); - _showRenameDialog(ext, device); - }, - child: Text( - '修改名称', - style: AppTypography.subhead.copyWith( - color: ext.accent, - ), - ), - ), - ), - if (isOnline) - SizedBox( - width: double.infinity, - child: CupertinoButton( - color: CupertinoColors.systemRed.withValues( - alpha: 0.1, - ), - borderRadius: AppRadius.lgBorder, - onPressed: () { - Navigator.pop(ctx); - _offlineDevice(device); - }, - child: Text( - '下线此设备', - style: AppTypography.subhead.copyWith( - color: CupertinoColors.systemRed, - ), - ), - ), - ), - SizedBox( - width: double.infinity, - child: CupertinoButton( - color: CupertinoColors.systemRed.withValues(alpha: 0.1), - borderRadius: AppRadius.lgBorder, - onPressed: () { - Navigator.pop(ctx); - _removeDevice(device); - }, - child: Text( - '移除设备', - style: AppTypography.subhead.copyWith( - color: CupertinoColors.systemRed, - ), - ), - ), - ), - SizedBox( - width: double.infinity, - child: CupertinoButton( - borderRadius: AppRadius.lgBorder, - onPressed: () => Navigator.pop(ctx), - child: Text( - '取消', - style: AppTypography.subhead.copyWith( - color: ext.textSecondary, - ), - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), + device: device, + isOnline: isOnline, + isCurrent: _isCurrentDevice(device), + onRename: () => _showRenameDialog(AppTheme.ext(context), device), + onOffline: () => _offlineDevice(device), + onRemove: () => _removeDevice(device), ); } - Widget _infoRow( - AppThemeExtension ext, - IconData icon, - String label, - String value, { - Widget? trailing, - }) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - child: Row( - children: [ - Icon(icon, size: 14, color: ext.textHint), - const SizedBox(width: 8), - Text( - label, - style: AppTypography.caption1.copyWith(color: ext.textHint), - ), - const Spacer(), - Flexible( - child: Text( - value, - style: AppTypography.caption1.copyWith(color: ext.textPrimary), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (trailing != null) ...[const SizedBox(width: 6), trailing], - ], - ), - ); - } - - String _platformLabel(String platform) { - switch (platform.toLowerCase()) { - case 'ios': - return 'iOS'; - case 'android': - return 'Android'; - case 'ohos': - return 'HarmonyOS'; - case 'web': - return 'Web'; - case 'windows': - return 'Windows'; - case 'mac': - return 'macOS'; - case 'linux': - return 'Linux'; - default: - return platform; - } - } - - // ============================================================ - // 平台图标/颜色 - // ============================================================ - - IconData _getPlatformIcon(String platform) { - switch (platform.toLowerCase()) { - case 'ios': - return CupertinoIcons.device_phone_portrait; - case 'android': - return CupertinoIcons.device_phone_portrait; - case 'ohos': - return CupertinoIcons.device_phone_portrait; - case 'web': - return CupertinoIcons.globe; - case 'windows': - return CupertinoIcons.desktopcomputer; - case 'mac': - return CupertinoIcons.desktopcomputer; - case 'linux': - return CupertinoIcons.keyboard; - default: - return CupertinoIcons.device_laptop; - } - } - - Color _getPlatformColor(String platform) { - switch (platform.toLowerCase()) { - case 'ios': - case 'mac': - return CupertinoColors.systemBlue; - case 'android': - return CupertinoColors.systemGreen; - case 'ohos': - return CupertinoColors.systemRed; - case 'web': - return CupertinoColors.systemOrange; - case 'windows': - return CupertinoColors.systemTeal; - case 'linux': - return CupertinoColors.systemPurple; - default: - return CupertinoColors.systemGrey; - } - } - - // ============================================================ - // 设备重命名 - // ============================================================ - void _showRenameDialog(AppThemeExtension ext, UserDevice device) { final controller = TextEditingController(text: device.deviceName); showCupertinoDialog( @@ -960,7 +220,7 @@ class _MyDevicesPageState extends ConsumerState { padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: ext.bgSecondary, - borderRadius: AppRadius.mdBorder, + borderRadius: BorderRadius.circular(8), ), maxLength: 30, ), @@ -994,15 +254,12 @@ class _MyDevicesPageState extends ConsumerState { } } - // ============================================================ - // 设备操作 - // ============================================================ - void _offlineDevice(UserDevice device) async { final isCurrent = _isCurrentDevice(device); if (isCurrent) { - final confirmed = await _showConfirmDialog( + final confirmed = await showConfirmDialog( + context, '下线当前设备', '下线当前设备将退出登录,您需要重新登录才能继续使用。确定要下线吗?', ); @@ -1021,13 +278,14 @@ class _MyDevicesPageState extends ConsumerState { return; } - final authed = await _authenticate('请验证身份以下线设备'); + final authed = await authenticate(_localAuth, '请验证身份以下线设备'); if (!authed) { if (mounted) AppToast.showWarning('验证未通过'); return; } - final confirmed = await _showConfirmDialog( + final confirmed = await showConfirmDialog( + context, '下线设备', '确定要下线「${device.deviceName}」吗?下线后该设备需要重新登录。', ); @@ -1042,12 +300,13 @@ class _MyDevicesPageState extends ConsumerState { } void _removeDevice(UserDevice device) async { - final authed = await _authenticate('请验证身份以移除设备'); + final authed = await authenticate(_localAuth, '请验证身份以移除设备'); if (!authed) { if (mounted) AppToast.showWarning('验证未通过'); return; } - final confirmed = await _showConfirmDialog( + final confirmed = await showConfirmDialog( + context, '移除设备', '确定要移除「${device.deviceName}」吗?移除后该设备需重新登录。', ); @@ -1065,12 +324,13 @@ class _MyDevicesPageState extends ConsumerState { BuildContext context, AppThemeExtension ext, ) async { - final authed = await _authenticate('请验证身份以下线所有设备'); + final authed = await authenticate(_localAuth, '请验证身份以下线所有设备'); if (!authed) { if (mounted) AppToast.showWarning('验证未通过'); return; } - final confirmed = await _showConfirmDialog( + final confirmed = await showConfirmDialog( + context, '下线所有设备', '确定要将所有设备下线吗?下线后其他设备需要重新登录,当前设备也将退出登录。', ); @@ -1088,36 +348,6 @@ class _MyDevicesPageState extends ConsumerState { } } - // ============================================================ - // 工具方法 - // ============================================================ - - void _copyToClipboard(String text) { - Clipboard.setData(ClipboardData(text: text)); - AppToast.showSuccess('已复制: $text'); - } - - Future _showConfirmDialog(String title, String message) { - return showCupertinoDialog( - context: context, - builder: (ctx) => CupertinoAlertDialog( - title: Text(title), - content: Text(message), - actions: [ - CupertinoDialogAction( - child: const Text('取消'), - onPressed: () => Navigator.pop(ctx, false), - ), - CupertinoDialogAction( - isDestructiveAction: true, - child: const Text('确认'), - onPressed: () => Navigator.pop(ctx, true), - ), - ], - ), - ); - } - Widget _buildEmptyView(AppThemeExtension ext) { return Center( child: Column( diff --git a/lib/features/mine/user_center/presentation/public_profile_page.dart b/lib/features/mine/user_center/presentation/public_profile_page.dart index db363e41..7d5c977f 100644 --- a/lib/features/mine/user_center/presentation/public_profile_page.dart +++ b/lib/features/mine/user_center/presentation/public_profile_page.dart @@ -18,6 +18,7 @@ import '../../../../core/theme/app_typography.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../services/user_center_service.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class PublicProfilePage extends ConsumerStatefulWidget { const PublicProfilePage({super.key, required this.uid}); @@ -73,6 +74,7 @@ class _PublicProfilePageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text(_profile?['display_name'] as String? ?? '用户主页'), previousPageTitle: '返回', backgroundColor: ext.bgElevated.withValues(alpha: 0.85), diff --git a/lib/features/mine/user_center/presentation/user_center_page.dart b/lib/features/mine/user_center/presentation/user_center_page.dart index 7fbe391a..2816a3e7 100644 --- a/lib/features/mine/user_center/presentation/user_center_page.dart +++ b/lib/features/mine/user_center/presentation/user_center_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 个人中心页面 /// 创建时间: 2026-05-01 /// 更新时间: 2026-05-14 @@ -16,7 +16,7 @@ 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_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../../../../shared/widgets/offline_banner.dart'; import '../../../../shared/widgets/app_toast.dart'; diff --git a/lib/features/mine/user_center/presentation/widgets/account_section.dart b/lib/features/mine/user_center/presentation/widgets/account_section.dart index 71d7d37f..cfd2b81d 100644 --- a/lib/features/mine/user_center/presentation/widgets/account_section.dart +++ b/lib/features/mine/user_center/presentation/widgets/account_section.dart @@ -15,7 +15,7 @@ import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; import '../../../../../../core/theme/app_radius.dart'; import '../../../../../../core/constants/app_constants.dart'; -import '../../../../../../core/router/app_router.dart'; +import '../../../../../../core/router/app_routes.dart'; import '../../../../../../shared/widgets/glass_container.dart'; // ============================================================ diff --git a/lib/features/mine/user_center/presentation/widgets/quick_action_grid.dart b/lib/features/mine/user_center/presentation/widgets/quick_action_grid.dart index 18e5165d..2283ab14 100644 --- a/lib/features/mine/user_center/presentation/widgets/quick_action_grid.dart +++ b/lib/features/mine/user_center/presentation/widgets/quick_action_grid.dart @@ -13,7 +13,7 @@ import 'package:xianyan/core/router/app_nav_extension.dart'; import '../../../../../../core/theme/app_theme.dart'; import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; -import '../../../../../../core/router/app_router.dart'; +import '../../../../../../core/router/app_routes.dart'; import '../../../../../../shared/widgets/glass_container.dart'; import '../../../../auth/providers/auth_provider.dart'; diff --git a/lib/features/mine/user_center/presentation/widgets/user_stats_bar.dart b/lib/features/mine/user_center/presentation/widgets/user_stats_bar.dart index df03b1a0..9ad1870a 100644 --- a/lib/features/mine/user_center/presentation/widgets/user_stats_bar.dart +++ b/lib/features/mine/user_center/presentation/widgets/user_stats_bar.dart @@ -12,7 +12,7 @@ import 'package:xianyan/core/router/app_nav_extension.dart'; import '../../../../../../core/theme/app_theme.dart'; import '../../../../../../core/theme/app_spacing.dart'; import '../../../../../../core/theme/app_typography.dart'; -import '../../../../../../core/router/app_router.dart'; +import '../../../../../../core/router/app_routes.dart'; import '../../../../../../shared/widgets/glass_container.dart'; import '../../../../auth/models/user_model.dart'; diff --git a/lib/features/note/presentation/note_edit_page.dart b/lib/features/note/presentation/note_edit_page.dart index 137e7c41..34363463 100644 --- a/lib/features/note/presentation/note_edit_page.dart +++ b/lib/features/note/presentation/note_edit_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 笔记编辑页面 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-01 @@ -21,6 +21,7 @@ import '../../../shared/widgets/app_markdown.dart'; import '../../../shared/widgets/app_toast.dart'; import '../models/note_model.dart'; import '../providers/note_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class NoteEditPage extends ConsumerStatefulWidget { const NoteEditPage({super.key, this.noteId}); @@ -191,6 +192,7 @@ class _NoteEditPageState extends ConsumerState }, child: CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text(_isEdit ? '✏️ 编辑笔记' : '📝 新建笔记'), trailing: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/features/note/presentation/note_list_page.dart b/lib/features/note/presentation/note_list_page.dart index 650a02d9..6697de5b 100644 --- a/lib/features/note/presentation/note_list_page.dart +++ b/lib/features/note/presentation/note_list_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 笔记列表页面 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-01 @@ -19,13 +19,14 @@ 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_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../shared/widgets/glass_container.dart'; import '../../../shared/widgets/app_slidable.dart'; import '../../../shared/widgets/app_toast.dart'; import '../../auth/providers/auth_provider.dart'; import '../models/note_model.dart'; import '../providers/note_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; enum NoteLayout { list, grid, timeline } @@ -125,6 +126,7 @@ class _NoteListPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('📝 我的笔记'), trailing: authState.isLoggedIn ? Row( diff --git a/lib/features/onboarding/data/onboarding_constants.dart b/lib/features/onboarding/data/onboarding_constants.dart index ea5a9307..4ae518d2 100644 --- a/lib/features/onboarding/data/onboarding_constants.dart +++ b/lib/features/onboarding/data/onboarding_constants.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — 引导页常量定义 // 创建时间: 2026-05-21 -// 更新时间: 2026-05-21 +// 更新时间: 2026-05-22 // 作用: 引导页流程中使用的常量、文案、功能列表 -// 上次更新: 初始创建 +// 上次更新: 新增扫一扫功能卡片 // ============================================================ class OnboardingConstants { @@ -15,6 +15,7 @@ class OnboardingConstants { {'icon': 'sparkles', 'name': '每日拾句', 'desc': '每天精选一句,开启美好阅读'}, {'icon': 'photo', 'name': '壁纸制作', 'desc': '将句子变成精美壁纸,分享给朋友'}, {'icon': 'upload', 'name': '文件传输', 'desc': '局域网/蓝牙跨设备极速传输'}, + {'icon': 'chat_bubble', 'name': '扫一扫', 'desc': '扫码配对传输、扫描二维码'}, ]; static const List agreementTabs = ['隐私政策', '用户协议', '权限说明']; diff --git a/lib/features/onboarding/presentation/onboarding_page.dart b/lib/features/onboarding/presentation/onboarding_page.dart index 4ee5bb49..f8793a65 100644 --- a/lib/features/onboarding/presentation/onboarding_page.dart +++ b/lib/features/onboarding/presentation/onboarding_page.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — 引导页主容器 // 创建时间: 2026-05-21 -// 更新时间: 2026-05-21 +// 更新时间: 2026-05-22 // 作用: PageView包裹3页引导流程,底部Step Dots指示器 -// 上次更新: 添加ref.listen同步PageView与provider状态 +// 上次更新: PC端宽屏响应式布局;宽屏左右分栏;内容区域maxWidth约束 // ============================================================ import 'package:flutter/cupertino.dart'; @@ -14,6 +14,7 @@ import '../../../core/services/device/haptic_service.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; +import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/appbar_character_sprite.dart'; import '../data/onboarding_constants.dart'; import '../providers/onboarding_provider.dart'; @@ -119,31 +120,16 @@ class _OnboardingPageState extends ConsumerState { children: [ MeshGradientBackground(isDark: ext.isDark), SafeArea( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: AppSpacing.sm), - child: AppBarCharacterSprite( - characterId: 'cat', - expression: _expressionForPage(state.currentPage), - size: 56.0, - ), - ), - Expanded( - child: PageView( - controller: _pageController, - physics: const ClampingScrollPhysics(), - onPageChanged: _onPageChanged, - children: const [ - WelcomePage(), - AgreementPage(), - PersonalizationPage(), - ], - ), - ), - _buildStepDots(ext, state), - const SizedBox(height: AppSpacing.lg), - ], + child: LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth > 900; + + if (isWide) { + return _buildWideLayout(ext, state, size); + } + + return _buildNarrowLayout(ext, state, size); + }, ), ), if (_showLottie) @@ -169,6 +155,95 @@ class _OnboardingPageState extends ConsumerState { ); } + Widget _buildNarrowLayout( + AppThemeExtension ext, + OnboardingState state, + Size size, + ) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: AppBarCharacterSprite( + characterId: 'cat', + expression: _expressionForPage(state.currentPage), + size: 56.0, + ), + ), + Expanded( + child: PageView( + controller: _pageController, + physics: const ClampingScrollPhysics(), + onPageChanged: _onPageChanged, + children: const [ + WelcomePage(), + AgreementPage(), + PersonalizationPage(), + ], + ), + ), + _buildStepDots(ext, state), + const SizedBox(height: AppSpacing.lg), + ], + ); + } + + Widget _buildWideLayout( + AppThemeExtension ext, + OnboardingState state, + Size size, + ) { + return Row( + children: [ + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppBarCharacterSprite( + characterId: 'cat', + expression: _expressionForPage(state.currentPage), + size: 80.0, + ), + const SizedBox(height: AppSpacing.lg), + Text( + '闲言', + style: AppTypography.title1.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + '文字阅读更纯粹', + style: AppTypography.subhead.copyWith( + color: ext.textSecondary, + ), + ), + const SizedBox(height: AppSpacing.xl), + _buildStepDots(ext, state), + ], + ), + ), + ), + SizedBox( + width: 500, + child: PageView( + controller: _pageController, + physics: const ClampingScrollPhysics(), + onPageChanged: _onPageChanged, + children: const [ + WelcomePage(), + AgreementPage(), + PersonalizationPage(), + ], + ), + ), + const SizedBox(width: AppSpacing.xl), + ], + ); + } + Widget _buildStepDots(AppThemeExtension ext, OnboardingState state) { return Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/features/onboarding/presentation/pages/agreement_page.dart b/lib/features/onboarding/presentation/pages/agreement_page.dart index c077d943..c20c4d89 100644 --- a/lib/features/onboarding/presentation/pages/agreement_page.dart +++ b/lib/features/onboarding/presentation/pages/agreement_page.dart @@ -1,9 +1,9 @@ -// ============================================================ +// ============================================================ // 闲言APP — 软件协议页(引导页第2页) // 创建时间: 2026-05-21 -// 更新时间: 2026-05-21 +// 更新时间: 2026-05-22 // 作用: 隐私政策/用户协议/权限说明,勾选同意后继续 -// 上次更新: 添加跳过引导按钮(从欢迎页迁移) +// 上次更新: 权限说明Tab展示权限列表;响应式宽屏布局;移除重复SafeArea;跳过按钮保存完成状态 // ============================================================ import 'package:flutter/cupertino.dart'; @@ -11,7 +11,8 @@ import 'package:flutter/material.dart' show Colors; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/router/app_nav_extension.dart'; -import '../../../../core/router/app_router.dart'; +import '../../../../core/router/app_routes.dart'; +import '../../../../core/services/auth/permission_service.dart'; import '../../../../core/services/device/haptic_service.dart'; import '../../../../core/theme/app_radius.dart'; import '../../../../core/theme/app_spacing.dart'; @@ -33,34 +34,37 @@ class AgreementPage extends ConsumerWidget { final state = ref.watch(onboardingProvider); final notifier = ref.read(onboardingProvider.notifier); - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - child: Column( - children: [ - const SizedBox(height: AppSpacing.lg), - PageNavHeader( - icon: CupertinoIcons.doc_text_fill, - previousLabel: '欢迎与指引', - onPrevious: () { - HapticService.light(); - notifier.previousPage(); - }, - ), - const SizedBox(height: AppSpacing.xl), - _buildTitle(ext), - const SizedBox(height: AppSpacing.lg), - _buildTabBar(ext, state, notifier), - const SizedBox(height: AppSpacing.md), - Expanded(child: _buildContent(ext, state)), - const SizedBox(height: AppSpacing.md), - _buildCheckboxes(ext, state, notifier), - const SizedBox(height: AppSpacing.md), - _buildContinueButton(ext, state, notifier), - const SizedBox(height: AppSpacing.sm), - _buildSkipButton(ext, state, notifier, context), - const SizedBox(height: AppSpacing.xl), - ], + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + child: Column( + children: [ + const SizedBox(height: AppSpacing.lg), + PageNavHeader( + icon: CupertinoIcons.doc_text_fill, + previousLabel: '欢迎与指引', + onPrevious: () { + HapticService.light(); + notifier.previousPage(); + }, + ), + const SizedBox(height: AppSpacing.xl), + _buildTitle(ext), + const SizedBox(height: AppSpacing.lg), + _buildTabBar(ext, state, notifier), + const SizedBox(height: AppSpacing.md), + Expanded(child: _buildContent(ext, state)), + const SizedBox(height: AppSpacing.md), + _buildCheckboxes(ext, state, notifier), + const SizedBox(height: AppSpacing.md), + _buildContinueButton(ext, state, notifier), + const SizedBox(height: AppSpacing.sm), + _buildSkipButton(ext, state, notifier, context), + const SizedBox(height: AppSpacing.xl), + ], + ), ), ), ); @@ -133,6 +137,10 @@ class AgreementPage extends ConsumerWidget { } Widget _buildContent(AppThemeExtension ext, OnboardingState state) { + if (state.agreementTabIndex == 2) { + return _buildPermissionList(ext); + } + final agreementType = _agreementTypeForIndex(state.agreementTabIndex); final content = AgreementData.getContent(agreementType); final updateDate = AgreementData.getUpdateDate(agreementType); @@ -176,6 +184,137 @@ class AgreementPage extends ConsumerWidget { ); } + Widget _buildPermissionList(AppThemeExtension ext) { + final permissions = AppPermission.values + .where((p) => p.isPlatformRelevant) + .toList(); + + return GlassContainer( + depth: GlassDepth.elevated, + borderRadius: AppRadius.lgBorder, + padding: const EdgeInsets.all(AppSpacing.md), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + CupertinoIcons.lock_shield_fill, + size: 18, + color: ext.accent, + ), + const SizedBox(width: AppSpacing.sm), + Text( + '软件权限使用说明', + style: AppTypography.headline.copyWith( + color: ext.textPrimary, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.xs), + Text( + '以下权限仅在您使用相关功能时请求', + style: AppTypography.caption1.copyWith(color: ext.textHint), + ), + const SizedBox(height: AppSpacing.md), + ...permissions.map((perm) => _buildPermissionItem(ext, perm)), + ], + ), + ), + ); + } + + Widget _buildPermissionItem(AppThemeExtension ext, AppPermission perm) { + return Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.md), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: perm.color.withValues(alpha: 0.12), + borderRadius: AppRadius.mdBorder, + ), + child: Icon(perm.icon, size: 16, color: perm.color), + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + perm.label, + style: AppTypography.subhead.copyWith( + color: ext.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + if (perm.isRequired) ...[ + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), + decoration: BoxDecoration( + color: const Color(0xFFFF3B30).withValues(alpha: 0.12), + borderRadius: AppRadius.xsBorder, + ), + child: Text( + '必要', + style: AppTypography.caption2.copyWith( + color: const Color(0xFFFF3B30), + fontWeight: FontWeight.w600, + ), + ), + ), + ], + if (perm.isVirtual) ...[ + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.12), + borderRadius: AppRadius.xsBorder, + ), + child: Text( + '系统级', + style: AppTypography.caption2.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 2), + Text( + perm.description, + style: AppTypography.caption1.copyWith( + color: ext.textSecondary, + height: 1.4, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } + Widget _buildCheckboxes( AppThemeExtension ext, OnboardingState state, @@ -282,10 +421,12 @@ class AgreementPage extends ConsumerWidget { final enabled = state.canProceedAgreement; return CupertinoButton( onPressed: enabled - ? () { + ? () async { HapticService.light(); - notifier.completeOnboarding(); - context.appGo(AppRoutes.home); + await notifier.completeOnboarding(); + if (context.mounted) { + context.appGo(AppRoutes.home); + } } : null, child: Text( diff --git a/lib/features/onboarding/presentation/pages/personalization_page.dart b/lib/features/onboarding/presentation/pages/personalization_page.dart index 0875acb2..d0a67fe1 100644 --- a/lib/features/onboarding/presentation/pages/personalization_page.dart +++ b/lib/features/onboarding/presentation/pages/personalization_page.dart @@ -1,22 +1,23 @@ // ============================================================ // 闲言APP — 个性化设置页(引导页第3页) // 创建时间: 2026-05-21 -// 更新时间: 2026-05-21 +// 更新时间: 2026-05-22 // 作用: 主题/强调色/字体/效果/功能开关设置,实时预览 -// 上次更新: 添加撒花庆祝效果 +// 上次更新: 功能开关同步generalSettingsProvider;新增特效背景开关;字体大小同步fontScale;响应式宽屏布局;移除重复SafeArea // ============================================================ import 'package:confetti/confetti.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/router/app_nav_extension.dart'; -import '../../../../core/router/app_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/services/device/haptic_service.dart'; import '../../../../core/theme/app_radius.dart'; import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../shared/widgets/glass_container.dart'; +import '../../../mine/settings/providers/general_settings_provider.dart'; import '../../providers/onboarding_provider.dart'; import '../../../mine/settings/providers/theme_settings_provider.dart'; import '../widgets/page_nav_header.dart'; @@ -64,42 +65,45 @@ class _PersonalizationPageState extends ConsumerState { return Stack( children: [ - SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - child: Column( - children: [ - const SizedBox(height: AppSpacing.lg), - PageNavHeader( - icon: CupertinoIcons.paintbrush_fill, - previousLabel: '软件协议', - onPrevious: () { - HapticService.light(); - ref.read(onboardingProvider.notifier).previousPage(); - }, - ), - const SizedBox(height: AppSpacing.xl), - _buildTitle(ext), - const SizedBox(height: AppSpacing.lg), - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - _buildPreviewCard(ext, theme), - const SizedBox(height: AppSpacing.lg), - _buildAppearanceSection(ext, theme), - const SizedBox(height: AppSpacing.lg), - _buildEffectsSection(ext), - const SizedBox(height: AppSpacing.lg), - _buildFeaturesSection(ext, onboardingState), - const SizedBox(height: AppSpacing.xl), - ], + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + child: Column( + children: [ + const SizedBox(height: AppSpacing.lg), + PageNavHeader( + icon: CupertinoIcons.paintbrush_fill, + previousLabel: '软件协议', + onPrevious: () { + HapticService.light(); + ref.read(onboardingProvider.notifier).previousPage(); + }, + ), + const SizedBox(height: AppSpacing.xl), + _buildTitle(ext), + const SizedBox(height: AppSpacing.lg), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + _buildPreviewCard(ext, theme), + const SizedBox(height: AppSpacing.lg), + _buildAppearanceSection(ext, theme), + const SizedBox(height: AppSpacing.lg), + _buildEffectsSection(ext), + const SizedBox(height: AppSpacing.lg), + _buildFeaturesSection(ext, onboardingState), + const SizedBox(height: AppSpacing.xl), + ], + ), ), ), - ), - _buildCompleteButton(ext), - const SizedBox(height: AppSpacing.xl), - ], + _buildCompleteButton(ext), + const SizedBox(height: AppSpacing.xl), + ], + ), ), ), ), @@ -147,62 +151,67 @@ class _PersonalizationPageState extends ConsumerState { } Widget _buildPreviewCard(AppThemeExtension ext, ThemeSettingsState theme) { - return GlassContainer( - depth: GlassDepth.prominent, - borderRadius: AppRadius.lgBorder, - padding: const EdgeInsets.all(AppSpacing.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: GlassContainer( + depth: GlassDepth.prominent, + borderRadius: AppRadius.lgBorder, + padding: const EdgeInsets.all(AppSpacing.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: ext.accent.withValues(alpha: 0.15), - borderRadius: AppRadius.mdBorder, - ), - child: Icon( - CupertinoIcons.sparkles, - size: 16, - color: ext.accent, - ), + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.15), + borderRadius: AppRadius.mdBorder, + ), + child: Icon( + CupertinoIcons.sparkles, + size: 16, + color: ext.accent, + ), + ), + const SizedBox(width: AppSpacing.sm), + Text( + '实时预览', + style: AppTypography.subhead.copyWith(color: ext.textSecondary), + ), + ], ), - const SizedBox(width: AppSpacing.sm), + const SizedBox(height: AppSpacing.md), Text( - '实时预览', - style: AppTypography.subhead.copyWith(color: ext.textSecondary), + '山有木兮木有枝,心悦君兮君不知', + style: AppTypography.title3.copyWith( + color: ext.textPrimary, + fontSize: AppTypography.fontTitle3 * _fontSizeScale, + ), + ), + const SizedBox(height: AppSpacing.sm), + Text( + '—— 越人歌', + style: AppTypography.subhead.copyWith( + color: ext.textSecondary, + fontSize: AppTypography.fontSubhead * _fontSizeScale, + ), + ), + const SizedBox(height: AppSpacing.md), + Row( + children: [ + _buildPreviewChip(ext, '👍 12'), + const SizedBox(width: AppSpacing.sm), + _buildPreviewChip(ext, '⭐ 收藏'), + const SizedBox(width: AppSpacing.sm), + _buildPreviewChip(ext, '📤 分享'), + ], ), ], ), - const SizedBox(height: AppSpacing.md), - Text( - '山有木兮木有枝,心悦君兮君不知', - style: AppTypography.title3.copyWith( - color: ext.textPrimary, - fontSize: AppTypography.fontTitle3 * _fontSizeScale, - ), - ), - const SizedBox(height: AppSpacing.sm), - Text( - '—— 越人歌', - style: AppTypography.subhead.copyWith( - color: ext.textSecondary, - fontSize: AppTypography.fontSubhead * _fontSizeScale, - ), - ), - const SizedBox(height: AppSpacing.md), - Row( - children: [ - _buildPreviewChip(ext, '👍 12'), - const SizedBox(width: AppSpacing.sm), - _buildPreviewChip(ext, '⭐ 收藏'), - const SizedBox(width: AppSpacing.sm), - _buildPreviewChip(ext, '📤 分享'), - ], - ), - ], + ), ), ); } @@ -245,9 +254,9 @@ class _PersonalizationPageState extends ConsumerState { icon: '🔤', onChanged: (v) { setState(() => _fontSizeScale = v); - ref - .read(themeSettingsProvider.notifier) - .setFontSize(_fontSizeIdForScale(v)); + final fontId = _fontSizeIdForScale(v); + ref.read(themeSettingsProvider.notifier).setFontSize(fontId); + ref.read(generalSettingsProvider.notifier).setFontScale(_fontScaleIdForFontSizeId(fontId)); }, ), ], @@ -309,7 +318,22 @@ class _PersonalizationPageState extends ConsumerState { value: onboardingState.shakeEnabled, onChanged: () { HapticService.toggleSwitch(); + final newVal = !onboardingState.shakeEnabled; ref.read(onboardingProvider.notifier).toggleShake(); + ref.read(generalSettingsProvider.notifier).setShakeToSwitch(newVal); + }, + ), + const SizedBox(height: AppSpacing.md), + _buildSwitchRow( + ext, + label: '特效背景', + icon: '🌈', + value: onboardingState.shaderBackground, + onChanged: () { + HapticService.toggleSwitch(); + final newVal = !onboardingState.shaderBackground; + ref.read(onboardingProvider.notifier).toggleShaderBackground(); + ref.read(generalSettingsProvider.notifier).setShaderBackground(newVal); }, ), const SizedBox(height: AppSpacing.md), @@ -320,7 +344,9 @@ class _PersonalizationPageState extends ConsumerState { value: onboardingState.soundEnabled, onChanged: () { HapticService.toggleSwitch(); + final newVal = !onboardingState.soundEnabled; ref.read(onboardingProvider.notifier).toggleSound(); + ref.read(generalSettingsProvider.notifier).setSfxEnabled(newVal); }, ), const SizedBox(height: AppSpacing.md), @@ -625,8 +651,6 @@ class _PersonalizationPageState extends ConsumerState { ); } - // ── ID ↔ 数值映射辅助方法 ── - String _fontSizeIdForScale(double scale) { if (scale <= 0.8) return 'xs'; if (scale <= 0.9) return 'small'; @@ -635,6 +659,17 @@ class _PersonalizationPageState extends ConsumerState { return 'xlarge'; } + String _fontScaleIdForFontSizeId(String fontSizeId) { + return switch (fontSizeId) { + 'xs' => 'xs', + 'small' => 'sm', + 'normal' => 'md', + 'large' => 'lg', + 'xlarge' => 'xl', + _ => 'md', + }; + } + String _glassIntensityIdForValue(double v) { if (v <= 0.0) return 'off'; if (v <= 0.5) return 'light'; diff --git a/lib/features/onboarding/presentation/pages/welcome_page.dart b/lib/features/onboarding/presentation/pages/welcome_page.dart index 9317b00d..d75b9f7a 100644 --- a/lib/features/onboarding/presentation/pages/welcome_page.dart +++ b/lib/features/onboarding/presentation/pages/welcome_page.dart @@ -1,9 +1,9 @@ -// ============================================================ +// ============================================================ // 闲言APP — 欢迎与指引页(引导页第1页) // 创建时间: 2026-05-21 -// 更新时间: 2026-05-21 +// 更新时间: 2026-05-22 // 作用: 展示核心功能、语言选择、权限入口 -// 上次更新: 移除跳过引导按钮(移至协议页)、重设计权限按钮、标题添加图标 +// 上次更新: 添加扫一扫点击提示;响应式宽屏布局;移除重复SafeArea // ============================================================ import 'package:flutter/cupertino.dart'; @@ -11,12 +11,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../../../core/router/app_nav_extension.dart'; -import '../../../../core/router/app_router.dart'; +import '../../../../core/router/app_routes.dart'; import '../../../../core/services/device/haptic_service.dart'; import '../../../../core/theme/app_radius.dart'; import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_typography.dart'; +import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../../data/onboarding_constants.dart'; import '../../providers/onboarding_provider.dart'; @@ -30,27 +31,36 @@ class WelcomePage extends ConsumerWidget { final ext = AppTheme.ext(context); final state = ref.watch(onboardingProvider); - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - child: Column( - children: [ - const SizedBox(height: AppSpacing.lg), - const PageNavHeader(icon: CupertinoIcons.sparkles), - const SizedBox(height: AppSpacing.xl), - _buildTitle(ext), - const SizedBox(height: AppSpacing.lg), - _buildFeatureList(ext), - const SizedBox(height: AppSpacing.md), - _buildPermissionEntry(ext, context), - const SizedBox(height: AppSpacing.md), - _buildLocaleChips(ext, ref, state), - const Spacer(), - _buildBottomActions(ext, ref, context), - const SizedBox(height: AppSpacing.xl), - ], - ), - ), + return LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth > 600; + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + child: Column( + children: [ + const SizedBox(height: AppSpacing.lg), + const PageNavHeader(icon: CupertinoIcons.sparkles), + const SizedBox(height: AppSpacing.xl), + _buildTitle(ext), + const SizedBox(height: AppSpacing.lg), + _buildFeatureList(ext, context, isWide), + const SizedBox(height: AppSpacing.md), + _buildPermissionEntry(ext, context), + const SizedBox(height: AppSpacing.md), + _buildLocaleChips(ext, ref, state), + const Spacer(), + _buildBottomActions(ext, ref, context), + const SizedBox(height: AppSpacing.xl), + ], + ), + ), + ), + ); + }, ); } @@ -87,65 +97,110 @@ class WelcomePage extends ConsumerWidget { ); } - Widget _buildFeatureList(AppThemeExtension ext) { + Widget _buildFeatureList( + AppThemeExtension ext, + BuildContext context, + bool isWide, + ) { + const features = OnboardingConstants.coreFeatures; + + if (isWide) { + const spacing = AppSpacing.sm; + const padding = AppSpacing.md; + return Wrap( + spacing: spacing, + runSpacing: spacing, + children: features.map((feature) { + final isScan = feature['name'] == '扫一扫'; + return SizedBox( + width: (MediaQuery.sizeOf(context).width.clamp(0, 600) - + padding * 2 - + spacing) / + 2, + child: _buildFeatureCard(ext, context, feature, isScan), + ); + }).toList(), + ); + } + return Column( - children: OnboardingConstants.coreFeatures.map((feature) { + children: features.map((feature) { + final isScan = feature['name'] == '扫一扫'; return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: GlassContainer( - depth: GlassDepth.elevated, - borderRadius: AppRadius.lgBorder, - padding: const EdgeInsets.all(AppSpacing.md), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: ext.accent.withValues(alpha: 0.12), - borderRadius: AppRadius.mdBorder, - ), - child: Center( - child: SvgPicture.asset( - OnboardingConstants.featureSvgPath(feature['icon']!), - width: 20, - height: 20, - colorFilter: ColorFilter.mode( - ext.accent, - BlendMode.srcIn, - ), - ), - ), - ), - const SizedBox(width: AppSpacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - feature['name']!, - style: AppTypography.headline.copyWith( - color: ext.textPrimary, - ), - ), - const SizedBox(height: 2), - Text( - feature['desc']!, - style: AppTypography.caption1.copyWith( - color: ext.textSecondary, - ), - ), - ], - ), - ), - ], - ), - ), + child: _buildFeatureCard(ext, context, feature, isScan), ); }).toList(), ); } + Widget _buildFeatureCard( + AppThemeExtension ext, + BuildContext context, + Map feature, + bool isScan, + ) { + return GestureDetector( + onTap: () { + HapticService.selection(); + if (isScan) { + AppToast.showInfo('引导完成后可在首页工具中心使用扫一扫 📱'); + } else { + AppToast.showInfo('引导完成后即可体验${feature['name']} ✨'); + } + }, + child: GlassContainer( + depth: GlassDepth.elevated, + borderRadius: AppRadius.lgBorder, + padding: const EdgeInsets.all(AppSpacing.md), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.12), + borderRadius: AppRadius.mdBorder, + ), + child: Center( + child: SvgPicture.asset( + OnboardingConstants.featureSvgPath(feature['icon']!), + width: 20, + height: 20, + colorFilter: ColorFilter.mode( + ext.accent, + BlendMode.srcIn, + ), + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + feature['name']!, + style: AppTypography.headline.copyWith( + color: ext.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + feature['desc']!, + style: AppTypography.caption1.copyWith( + color: ext.textSecondary, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + Widget _buildPermissionEntry(AppThemeExtension ext, BuildContext context) { return GestureDetector( onTap: () => context.appPush(AppRoutes.permissionManagement), diff --git a/lib/features/onboarding/providers/onboarding_provider.dart b/lib/features/onboarding/providers/onboarding_provider.dart index ee92f901..0c0fcb2b 100644 --- a/lib/features/onboarding/providers/onboarding_provider.dart +++ b/lib/features/onboarding/providers/onboarding_provider.dart @@ -1,15 +1,16 @@ // ============================================================ // 闲言APP — 引导页状态管理 // 创建时间: 2026-05-21 -// 更新时间: 2026-05-21 +// 更新时间: 2026-05-22 // 作用: 管理引导页流程状态(页面切换、协议勾选、个性化设置) -// 上次更新: 改用Riverpod Notifier模式替代StateNotifier +// 上次更新: 新增shaderBackground字段;completeOnboarding同步设置到generalSettingsProvider // ============================================================ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/storage/kv_storage.dart'; import '../../../core/utils/logger.dart'; +import '../../mine/settings/providers/general_settings_provider.dart'; import '../data/onboarding_constants.dart'; class OnboardingState { @@ -24,6 +25,7 @@ class OnboardingState { this.agreementTabIndex = 0, this.shakeEnabled = true, this.soundEnabled = true, + this.shaderBackground = false, }); final int currentPage; @@ -36,8 +38,10 @@ class OnboardingState { final int agreementTabIndex; final bool shakeEnabled; final bool soundEnabled; + final bool shaderBackground; - bool get canProceedAgreement => privacyAgreed && termsAgreed && permissionRead; + bool get canProceedAgreement => + privacyAgreed && termsAgreed && permissionRead; bool get isLastPage => currentPage >= OnboardingConstants.totalPages - 1; @@ -52,6 +56,7 @@ class OnboardingState { int? agreementTabIndex, bool? shakeEnabled, bool? soundEnabled, + bool? shaderBackground, }) { return OnboardingState( currentPage: currentPage ?? this.currentPage, @@ -64,6 +69,7 @@ class OnboardingState { agreementTabIndex: agreementTabIndex ?? this.agreementTabIndex, shakeEnabled: shakeEnabled ?? this.shakeEnabled, soundEnabled: soundEnabled ?? this.soundEnabled, + shaderBackground: shaderBackground ?? this.shaderBackground, ); } } @@ -117,6 +123,10 @@ class OnboardingNotifier extends Notifier { state = state.copyWith(soundEnabled: !state.soundEnabled); } + void toggleShaderBackground() { + state = state.copyWith(shaderBackground: !state.shaderBackground); + } + void setLocale(String locale) { state = state.copyWith(selectedLocale: locale); Log.i('Onboarding: setLocale → $locale'); @@ -131,10 +141,22 @@ class OnboardingNotifier extends Notifier { try { await KvStorage.markLaunched(); await KvStorage.setBool('onboarding_completed', true); - await KvStorage.setBool('show_on_next_launch', state.showOnNextLaunch); + await KvStorage.setShowOnboarding(state.showOnNextLaunch); await KvStorage.setString('locale', state.selectedLocale); await KvStorage.setBool('shake_enabled', state.shakeEnabled); await KvStorage.setBool('sound_enabled', state.soundEnabled); + await KvStorage.setBool('shader_background', state.shaderBackground); + + ref + .read(generalSettingsProvider.notifier) + .setShakeToSwitch(state.shakeEnabled); + ref + .read(generalSettingsProvider.notifier) + .setSfxEnabled(state.soundEnabled); + ref + .read(generalSettingsProvider.notifier) + .setShaderBackground(state.shaderBackground); + Log.i('Onboarding: completeOnboarding ✓'); } catch (e) { Log.i('Onboarding: completeOnboarding error → $e'); @@ -146,5 +168,5 @@ class OnboardingNotifier extends Notifier { final onboardingProvider = NotifierProvider( - OnboardingNotifier.new, -); + OnboardingNotifier.new, + ); diff --git a/lib/features/poetry/presentation/poetry_page.dart b/lib/features/poetry/presentation/poetry_page.dart index 8ae0a1c7..ffcd086f 100644 --- a/lib/features/poetry/presentation/poetry_page.dart +++ b/lib/features/poetry/presentation/poetry_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 今日诗词页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-21 @@ -12,7 +12,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; -import '../../../core/router/app_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; @@ -21,6 +21,7 @@ import '../../../core/utils/extensions.dart'; import '../../../shared/widgets/skeleton.dart'; import '../models/jinrishici_models.dart'; import '../providers/poetry_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class _ChatMessage { const _ChatMessage({ @@ -205,6 +206,7 @@ class _PoetryPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '📜 今日诗词', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/poetry/presentation/poetry_settings_page.dart b/lib/features/poetry/presentation/poetry_settings_page.dart index b6d636f5..fbfe12f1 100644 --- a/lib/features/poetry/presentation/poetry_settings_page.dart +++ b/lib/features/poetry/presentation/poetry_settings_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 今日诗词设置页面 /// 创建时间: 2026-05-19 /// 更新时间: 2026-05-19 @@ -15,6 +15,7 @@ import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/glass_container.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class PoetrySettingsNotifier extends Notifier> { @override @@ -50,6 +51,7 @@ class PoetrySettingsPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/pomodoro/presentation/pomodoro_page.dart b/lib/features/pomodoro/presentation/pomodoro_page.dart index 2a4f06f0..91c5e9dc 100644 --- a/lib/features/pomodoro/presentation/pomodoro_page.dart +++ b/lib/features/pomodoro/presentation/pomodoro_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 番茄钟页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -18,6 +18,7 @@ import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_typography.dart'; import '../models/pomodoro_models.dart'; import '../providers/pomodoro_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class PomodoroPage extends ConsumerStatefulWidget { const PomodoroPage({super.key}); @@ -37,6 +38,7 @@ class _PomodoroPageState extends ConsumerState return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '🍅 番茄钟', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/progress/presentation/progress_page.dart b/lib/features/progress/presentation/progress_page.dart index 3bf93765..9d2a1d31 100644 --- a/lib/features/progress/presentation/progress_page.dart +++ b/lib/features/progress/presentation/progress_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 进度会话页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -20,6 +20,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/keyboard_safe_sheet.dart'; import '../models/progress_models.dart'; import '../providers/progress_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class ProgressPage extends ConsumerStatefulWidget { const ProgressPage({super.key}); @@ -67,6 +68,7 @@ class _ProgressPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '📊 进度', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/rank/presentation/rank_page.dart b/lib/features/rank/presentation/rank_page.dart index 08403a7f..4a38459a 100644 --- a/lib/features/rank/presentation/rank_page.dart +++ b/lib/features/rank/presentation/rank_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/rank_provider.dart'; import '../../../shared/widgets/rank_item_card.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class RankPage extends ConsumerStatefulWidget { const RankPage({super.key}); @@ -40,6 +41,7 @@ class _RankPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('🏆 排行榜'), ), child: SafeArea( diff --git a/lib/features/reading_report/presentation/reading_report_page.dart b/lib/features/reading_report/presentation/reading_report_page.dart index ad47ed4c..bdbb0984 100644 --- a/lib/features/reading_report/presentation/reading_report_page.dart +++ b/lib/features/reading_report/presentation/reading_report_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 阅读报告主页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -19,6 +19,7 @@ import 'widgets/achievement_badges.dart'; import 'widgets/reading_heatmap.dart'; import 'widgets/summary_cards.dart'; import 'widgets/trend_chart.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class ReadingReportPage extends ConsumerStatefulWidget { const ReadingReportPage({super.key}); @@ -37,6 +38,7 @@ class _ReadingReportPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('📊 阅读报告'), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, diff --git a/lib/features/share/presentation/share_history_page.dart b/lib/features/share/presentation/share_history_page.dart index 763cb3c0..1285e191 100644 --- a/lib/features/share/presentation/share_history_page.dart +++ b/lib/features/share/presentation/share_history_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 分享历史页面 /// 创建时间: 2026-04-30 /// 更新时间: 2026-04-30 @@ -16,6 +16,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_radius.dart'; import '../../../shared/widgets/share_sheet.dart'; import '../../../shared/widgets/app_toast.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; final shareHistoryProvider = FutureProvider>((ref) { return AppDatabase.instance.getShareHistories(); @@ -38,6 +39,7 @@ class _ShareHistoryPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '📤 分享历史', style: AppTypography.title3.copyWith( diff --git a/lib/features/share/presentation/share_target_edit_page.dart b/lib/features/share/presentation/share_target_edit_page.dart index b6f59928..762292c1 100644 --- a/lib/features/share/presentation/share_target_edit_page.dart +++ b/lib/features/share/presentation/share_target_edit_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 分享目标编辑页面 /// 创建时间: 2026-04-30 /// 更新时间: 2026-04-30 @@ -15,6 +15,7 @@ import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_radius.dart'; import '../../../shared/widgets/app_toast.dart'; import '../models/share_target.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class ShareTargetEditPage extends StatefulWidget { const ShareTargetEditPage({super.key}); @@ -98,6 +99,7 @@ class _ShareTargetEditPageState extends State { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '⚙️ 分享目标管理', style: AppTypography.title3.copyWith( diff --git a/lib/features/solar_term/presentation/solar_term_page.dart b/lib/features/solar_term/presentation/solar_term_page.dart index 98ba415c..56aaa110 100644 --- a/lib/features/solar_term/presentation/solar_term_page.dart +++ b/lib/features/solar_term/presentation/solar_term_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 节气日历页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -17,6 +17,7 @@ import '../../../core/theme/app_radius.dart'; import '../models/solar_term_models.dart'; import '../providers/solar_term_provider.dart'; import '../services/solar_term_service.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class SolarTermPage extends ConsumerWidget { const SolarTermPage({super.key}); @@ -29,6 +30,7 @@ class SolarTermPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '🌿 节气日历', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/source/presentation/source_page.dart b/lib/features/source/presentation/source_page.dart index f724b365..8bd0356e 100644 --- a/lib/features/source/presentation/source_page.dart +++ b/lib/features/source/presentation/source_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 句子来源页面 /// 创建时间: 2026-04-20 /// 更新时间: 2026-05-13 @@ -13,7 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; -import '../../../core/router/app_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_typography.dart'; diff --git a/lib/features/statistics/presentation/statistics_page.dart b/lib/features/statistics/presentation/statistics_page.dart index 25553de8..a36303e7 100644 --- a/lib/features/statistics/presentation/statistics_page.dart +++ b/lib/features/statistics/presentation/statistics_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 统计页面 (增强版) // 创建时间: 2026-04-28 // 更新时间: 2026-05-04 @@ -25,6 +25,7 @@ import '../../../shared/widgets/glass_container.dart'; import '../../../shared/widgets/offline_banner.dart'; import '../../../shared/widgets/responsive_layout.dart'; import '../providers/statistics_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class StatisticsPage extends ConsumerStatefulWidget { const StatisticsPage({super.key}); @@ -50,6 +51,7 @@ class _StatisticsPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/statistics/presentation/user_stats_page.dart b/lib/features/statistics/presentation/user_stats_page.dart index 8b00567b..83f0b33e 100644 --- a/lib/features/statistics/presentation/user_stats_page.dart +++ b/lib/features/statistics/presentation/user_stats_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 用户数据统计页面 /// 创建时间: 2026-05-14 /// 更新时间: 2026-05-14 @@ -19,6 +19,7 @@ import 'widgets/coin_stats_tab.dart'; import 'widgets/favorite_stats_tab.dart'; import 'widgets/learning_stats_tab.dart'; import 'widgets/stats_shared.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class UserStatsPage extends ConsumerStatefulWidget { const UserStatsPage({super.key}); @@ -45,6 +46,7 @@ class _UserStatsPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/study_plan/presentation/study_plan_page.dart b/lib/features/study_plan/presentation/study_plan_page.dart index 1260a32c..48758c99 100644 --- a/lib/features/study_plan/presentation/study_plan_page.dart +++ b/lib/features/study_plan/presentation/study_plan_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 学习计划页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -20,6 +20,7 @@ import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_typography.dart'; import '../models/study_plan_models.dart'; import '../providers/study_plan_provider.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class StudyPlanPage extends ConsumerStatefulWidget { const StudyPlanPage({super.key}); @@ -43,6 +44,7 @@ class _StudyPlanPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '📚 学习计划', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/task/presentation/daily_task_page.dart b/lib/features/task/presentation/daily_task_page.dart index f24438e5..2a8a0981 100644 --- a/lib/features/task/presentation/daily_task_page.dart +++ b/lib/features/task/presentation/daily_task_page.dart @@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/task_provider.dart'; import '../../../shared/widgets/task_card.dart'; import '../../../core/theme/app_theme.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class DailyTaskPage extends ConsumerStatefulWidget { const DailyTaskPage({super.key}); @@ -32,7 +33,10 @@ class _DailyTaskPageState extends ConsumerState { final ext = AppTheme.ext(context); return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(middle: Text('📋 每日任务')), + navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), + middle: Text('📋 每日任务'), + ), child: SafeArea( child: taskState.isLoading ? const Center(child: CupertinoActivityIndicator()) diff --git a/lib/features/template/presentation/template_gallery_page.dart b/lib/features/template/presentation/template_gallery_page.dart index cf31be37..93b04bb4 100644 --- a/lib/features/template/presentation/template_gallery_page.dart +++ b/lib/features/template/presentation/template_gallery_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 壁纸模板广场主页面 /// 创建时间: 2026-05-01 /// 更新时间: 2026-05-04 @@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart'; import '../../../core/theme/app_theme.dart'; import '../../../shared/widgets/wallpaper_gallery/wallpaper_gallery.dart'; import 'widgets/wallpaper_preview_sheet.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class TemplateGalleryPage extends StatefulWidget { const TemplateGalleryPage({super.key}); @@ -26,6 +27,7 @@ class _TemplateGalleryPageState extends State { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('🎨 壁纸模板'), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, diff --git a/lib/features/tool_center/game/presentation/game_center_page.dart b/lib/features/tool_center/game/presentation/game_center_page.dart index d7316f01..71087554 100644 --- a/lib/features/tool_center/game/presentation/game_center_page.dart +++ b/lib/features/tool_center/game/presentation/game_center_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 游戏中心主页 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -18,6 +18,7 @@ import '../services/game_service.dart'; import 'poetry_fill_page.dart'; import 'idiom_chain_page.dart'; import 'quiz_page.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class GameCenterPage extends StatelessWidget { const GameCenterPage({super.key}); @@ -28,6 +29,7 @@ class GameCenterPage extends StatelessWidget { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), middle: Text('🎮 游戏中心'), previousPageTitle: '返回', ), diff --git a/lib/features/tool_center/game/presentation/idiom_chain_page.dart b/lib/features/tool_center/game/presentation/idiom_chain_page.dart index 5cb56d27..1a757c84 100644 --- a/lib/features/tool_center/game/presentation/idiom_chain_page.dart +++ b/lib/features/tool_center/game/presentation/idiom_chain_page.dart @@ -16,6 +16,7 @@ import '../../../../shared/widgets/glass_container.dart'; import '../../../../shared/widgets/app_toast.dart'; import '../services/game_service.dart'; import '../models/game_models.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class IdiomChainPage extends StatefulWidget { const IdiomChainPage({super.key}); @@ -95,6 +96,7 @@ class _IdiomChainPageState extends State { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('🔗 成语接龙'), previousPageTitle: '游戏', trailing: Text( diff --git a/lib/features/tool_center/game/presentation/poetry_fill_page.dart b/lib/features/tool_center/game/presentation/poetry_fill_page.dart index a2fc440a..53087f94 100644 --- a/lib/features/tool_center/game/presentation/poetry_fill_page.dart +++ b/lib/features/tool_center/game/presentation/poetry_fill_page.dart @@ -16,6 +16,7 @@ import '../../../../shared/widgets/glass_container.dart'; import '../../../../shared/widgets/app_toast.dart'; import '../services/game_service.dart'; import '../models/game_models.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class PoetryFillPage extends StatefulWidget { const PoetryFillPage({super.key}); @@ -85,6 +86,7 @@ class _PoetryFillPageState extends State { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: const Text('📝 诗词填空'), previousPageTitle: '游戏', trailing: Text( diff --git a/lib/features/tool_center/game/presentation/quiz_page.dart b/lib/features/tool_center/game/presentation/quiz_page.dart index bcf9c463..18c18723 100644 --- a/lib/features/tool_center/game/presentation/quiz_page.dart +++ b/lib/features/tool_center/game/presentation/quiz_page.dart @@ -16,6 +16,7 @@ import '../../../../core/theme/app_typography.dart'; import '../../../../shared/widgets/glass_container.dart'; import '../../../home/models/feed_model.dart'; import '../services/game_service.dart'; +import '../../../../shared/widgets/adaptive_back_button.dart'; class QuizPage extends ConsumerStatefulWidget { const QuizPage({super.key, required this.channel, required this.title}); @@ -55,6 +56,7 @@ class _QuizPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text(widget.title), previousPageTitle: '游戏', ), diff --git a/lib/features/tool_center/inspiration/presentation/pages/chat/chat_flow_page.dart b/lib/features/tool_center/inspiration/presentation/pages/chat/chat_flow_page.dart index 88ab1293..76505a4a 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/chat/chat_flow_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/chat/chat_flow_page.dart @@ -15,7 +15,7 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_typography.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/features/tool_center/inspiration/models/chat_message.dart'; import 'package:xianyan/features/tool_center/inspiration/models/chat_session.dart'; import 'package:xianyan/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_top_bar.dart'; @@ -29,6 +29,7 @@ import 'package:xianyan/features/tool_center/inspiration/providers/chat_attachme import 'package:xianyan/features/tool_center/inspiration/providers/chat_conversation_provider.dart'; import 'package:xianyan/features/tool_center/inspiration/services/chat_conversation_service.dart'; import 'package:xianyan/shared/widgets/keyboard_safe_sheet.dart'; +import '../../../../../../shared/widgets/adaptive_back_button.dart'; class ChatFlowPage extends ConsumerStatefulWidget { const ChatFlowPage({ @@ -268,6 +269,7 @@ class _ChatFlowPageState extends ConsumerState return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( widget.isReadlater ? '📖 稍后读' : '💬 会话流', style: AppTypography.headline.copyWith(color: ext.textPrimary), diff --git a/lib/features/tool_center/inspiration/presentation/pages/chat/chat_settings_page.dart b/lib/features/tool_center/inspiration/presentation/pages/chat/chat_settings_page.dart index 9d920e4b..f8bee21e 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/chat/chat_settings_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/chat/chat_settings_page.dart @@ -24,6 +24,7 @@ import 'package:xianyan/features/tool_center/inspiration/services/chat_file_serv import 'package:xianyan/features/tool_center/inspiration/services/chat_conversation_service.dart'; import 'package:xianyan/core/storage/database/app_database.dart'; import 'package:xianyan/shared/widgets/keyboard_safe_sheet.dart'; +import '../../../../../../shared/widgets/adaptive_back_button.dart'; class ChatSettingsPage extends ConsumerStatefulWidget { const ChatSettingsPage({super.key, this.conversationId = 'default'}); @@ -51,6 +52,7 @@ class _ChatSettingsPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '⚙️ 聊天设置', style: AppTypography.headline.copyWith(color: ext.textPrimary), diff --git a/lib/features/tool_center/inspiration/presentation/pages/chat/hidden_sessions_page.dart b/lib/features/tool_center/inspiration/presentation/pages/chat/hidden_sessions_page.dart index 6e8f68f4..9cf4a499 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/chat/hidden_sessions_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/chat/hidden_sessions_page.dart @@ -17,6 +17,7 @@ import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; import 'package:xianyan/features/tool_center/inspiration/models/chat_session.dart'; import 'package:xianyan/features/tool_center/inspiration/providers/chat_session_provider.dart'; +import '../../../../../../../shared/widgets/adaptive_back_button.dart'; class HiddenSessionsPage extends ConsumerWidget { const HiddenSessionsPage({super.key}); @@ -30,6 +31,7 @@ class HiddenSessionsPage extends ConsumerWidget { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '👁️‍🗨️ 隐藏的会话', style: AppTypography.headline.copyWith(color: ext.textPrimary), diff --git a/lib/features/tool_center/inspiration/presentation/pages/document_preview_page.dart b/lib/features/tool_center/inspiration/presentation/pages/document_preview_page.dart index 116b1bf0..e81ab295 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/document_preview_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/document_preview_page.dart @@ -21,6 +21,7 @@ import 'package:xianyan/shared/widgets/glass_container.dart'; import 'package:xianyan/shared/widgets/share_sheet.dart'; import 'package:xianyan/features/tool_center/inspiration/models/chat_message.dart'; import 'package:xianyan/features/tool_center/inspiration/services/chat_file_service.dart'; +import '../../../../../shared/widgets/adaptive_back_button.dart'; class DocumentPreviewPage extends StatefulWidget { const DocumentPreviewPage({ @@ -143,6 +144,7 @@ class _DocumentPreviewPageState extends State { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '文档预览', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/tool_center/inspiration/presentation/pages/home/inspiration_page.dart b/lib/features/tool_center/inspiration/presentation/pages/home/inspiration_page.dart index db7eb15b..046dd86d 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/home/inspiration_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/home/inspiration_page.dart @@ -20,7 +20,7 @@ import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/features/auth/providers/auth_provider.dart'; import 'package:xianyan/features/tool_center/inspiration/models/chat_session.dart'; import 'package:xianyan/features/tool_center/inspiration/providers/chat_session_provider.dart'; diff --git a/lib/features/tool_center/inspiration/presentation/pages/readlater_stats_page.dart b/lib/features/tool_center/inspiration/presentation/pages/readlater_stats_page.dart index bbcf04b2..be0e4719 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/readlater_stats_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/readlater_stats_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 稍后读统计页面 // 创建时间: 2026-05-15 // 更新时间: 2026-05-15 @@ -14,6 +14,7 @@ import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/shared/widgets/glass_container.dart'; import 'package:xianyan/features/tool_center/inspiration/models/chat_message.dart'; +import '../../../../../../shared/widgets/adaptive_back_button.dart'; class ReadlaterStatsPage extends StatelessWidget { const ReadlaterStatsPage({super.key, required this.messages}); @@ -26,6 +27,7 @@ class ReadlaterStatsPage extends StatelessWidget { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '📊 阅读统计', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/tool_center/inspiration/presentation/pages/tool/ocr_tool_page.dart b/lib/features/tool_center/inspiration/presentation/pages/tool/ocr_tool_page.dart index 4385ff99..b06bdcfc 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/tool/ocr_tool_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/tool/ocr_tool_page.dart @@ -23,6 +23,7 @@ import '../../../../../../../core/utils/logger.dart'; import '../../../../../../../shared/widgets/app_slidable.dart'; import '../../../../../../../shared/widgets/app_toast.dart'; import '../../../services/tool_api_service.dart'; +import '../../../../../../shared/widgets/adaptive_back_button.dart'; class OcrToolPage extends ConsumerStatefulWidget { const OcrToolPage({super.key}); @@ -43,6 +44,7 @@ class _OcrToolPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text('📷 OCR识别', style: AppTypography.title3), backgroundColor: ext.bgPrimary.withValues(alpha: 0.9), border: null, diff --git a/lib/features/tool_center/inspiration/presentation/pages/translate/translate_page.dart b/lib/features/tool_center/inspiration/presentation/pages/translate/translate_page.dart index 7db9fb81..7351e705 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/translate/translate_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/translate/translate_page.dart @@ -16,12 +16,13 @@ import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_radius.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/features/tool_center/inspiration/models/translate_message.dart'; import 'package:xianyan/features/tool_center/inspiration/models/translate_language.dart'; import 'package:xianyan/features/tool_center/inspiration/providers/translate_provider.dart'; import 'package:xianyan/shared/widgets/glass_container.dart'; import 'package:xianyan/shared/widgets/app_toast.dart'; +import '../../../../../../shared/widgets/adaptive_back_button.dart'; class TranslatePage extends ConsumerStatefulWidget { const TranslatePage({super.key}); @@ -89,6 +90,7 @@ class _TranslatePageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), previousPageTitle: '工作流', middle: const Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/features/tool_center/inspiration/presentation/pages/translate/translate_settings_page.dart b/lib/features/tool_center/inspiration/presentation/pages/translate/translate_settings_page.dart index 2888157f..66ed7a3d 100644 --- a/lib/features/tool_center/inspiration/presentation/pages/translate/translate_settings_page.dart +++ b/lib/features/tool_center/inspiration/presentation/pages/translate/translate_settings_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 翻译设置页面 // 创建时间: 2026-05-19 // 更新时间: 2026-05-19 @@ -21,6 +21,7 @@ import 'package:xianyan/features/tool_center/inspiration/providers/custom_api_pr import 'package:xianyan/features/tool_center/inspiration/services/translate_api_service.dart'; import 'package:xianyan/shared/widgets/glass_container.dart'; import 'package:xianyan/shared/widgets/app_toast.dart'; +import '../../../../../../shared/widgets/adaptive_back_button.dart'; class TranslateSettingsPage extends ConsumerStatefulWidget { const TranslateSettingsPage({super.key}); @@ -43,6 +44,7 @@ class _TranslateSettingsPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( + leading: AdaptiveBackButton(), previousPageTitle: '翻译助手', middle: Text('⚙️ 翻译设置'), ), diff --git a/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_conversation_mixin.dart b/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_conversation_mixin.dart index eda5e5d5..33440410 100644 --- a/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_conversation_mixin.dart +++ b/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_conversation_mixin.dart @@ -12,7 +12,7 @@ import 'package:xianyan/core/router/app_nav_extension.dart'; import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/features/tool_center/inspiration/services/chat_conversation_service.dart'; class ChatFlowConversationHelper { diff --git a/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart b/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart index 797c4fcc..4cec90bb 100644 --- a/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart +++ b/lib/features/tool_center/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart @@ -18,7 +18,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_typography.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/core/services/readlater/readlater_sync_service.dart'; import 'package:xianyan/core/services/readlater/readlater_ai_service.dart'; import 'package:xianyan/core/services/readlater/readlater_collab_service.dart'; diff --git a/lib/features/tool_center/inspiration/presentation/widgets/session/session_search_bar.dart b/lib/features/tool_center/inspiration/presentation/widgets/session/session_search_bar.dart index e01efcc6..4e8d7f34 100644 --- a/lib/features/tool_center/inspiration/presentation/widgets/session/session_search_bar.dart +++ b/lib/features/tool_center/inspiration/presentation/widgets/session/session_search_bar.dart @@ -18,7 +18,7 @@ import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/core/theme/app_spacing.dart'; import 'package:xianyan/core/theme/app_typography.dart'; import 'package:xianyan/core/theme/app_radius.dart'; -import 'package:xianyan/core/router/app_router.dart'; +import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/features/tool_center/inspiration/models/chat_session.dart'; import 'package:xianyan/features/tool_center/inspiration/providers/chat_session_provider.dart'; import 'package:xianyan/features/tool_center/inspiration/providers/chat_provider.dart'; diff --git a/lib/features/weather/presentation/weather_page.dart b/lib/features/weather/presentation/weather_page.dart index 4c9ef3ea..627ff74c 100644 --- a/lib/features/weather/presentation/weather_page.dart +++ b/lib/features/weather/presentation/weather_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 天气诗词页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-21 @@ -10,7 +10,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; -import '../../../core/router/app_router.dart'; +import '../../../core/router/app_routes.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; @@ -19,6 +19,7 @@ import '../../../core/utils/extensions.dart'; import '../../../shared/widgets/skeleton.dart'; import '../models/weather_models.dart'; import '../providers/weather_provider.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class WeatherPage extends ConsumerStatefulWidget { const WeatherPage({super.key}); @@ -57,6 +58,7 @@ class _WeatherPageState extends ConsumerState { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Text( '🌤️ 天气诗词', style: AppTypography.title3.copyWith(color: ext.textPrimary), diff --git a/lib/features/weather/presentation/weather_settings_page.dart b/lib/features/weather/presentation/weather_settings_page.dart index bbf8060b..e82527ce 100644 --- a/lib/features/weather/presentation/weather_settings_page.dart +++ b/lib/features/weather/presentation/weather_settings_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 天气诗词设置页面 /// 创建时间: 2026-05-19 /// 更新时间: 2026-05-19 @@ -15,6 +15,7 @@ import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/glass_container.dart'; +import '../../../shared/widgets/adaptive_back_button.dart'; class WeatherSettingsNotifier extends Notifier> { @override @@ -75,6 +76,7 @@ class _WeatherSettingsPageState extends ConsumerState { return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, navigationBar: CupertinoNavigationBar( + leading: const AdaptiveBackButton(), middle: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/features/weather/providers/weather_provider.dart b/lib/features/weather/providers/weather_provider.dart index 6ff0a4f8..286a8432 100644 --- a/lib/features/weather/providers/weather_provider.dart +++ b/lib/features/weather/providers/weather_provider.dart @@ -132,9 +132,7 @@ class WeatherNotifier extends Notifier { .toList() ?? []; - final safeMoodIndex = moodIndex != null - ? moodIndex.clamp(0, WeatherPoetryMood.values.length - 1) - : null; + final safeMoodIndex = moodIndex?.clamp(0, WeatherPoetryMood.values.length - 1); return WeatherState( weather: weatherJson != null ? WeatherData.fromTianqi(weatherJson) : null, mood: safeMoodIndex != null ? WeatherPoetryMood.values[safeMoodIndex] : null, diff --git a/lib/features/widget/providers/widget_provider.dart b/lib/features/widget/providers/widget_provider.dart index af89d79f..cd23e180 100644 --- a/lib/features/widget/providers/widget_provider.dart +++ b/lib/features/widget/providers/widget_provider.dart @@ -100,7 +100,7 @@ class WidgetNotifier extends Notifier { // 官方SDK下不支持,使用 dynamic 调用绕过编译检查 if (pu.isOhos) { try { - final dynamic requestPin = HomeWidget.requestPinWidget; + const dynamic requestPin = HomeWidget.requestPinWidget; await requestPin( qualifiedAndroidName: type.qualifiedAndroidName, androidName: type.androidProviderName, diff --git a/lib/main.dart b/lib/main.dart index f562b113..10a2c25a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -71,16 +71,6 @@ void main() async { if (!pu.isWeb) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.dark, - statusBarBrightness: Brightness.light, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: Brightness.dark, - systemNavigationBarDividerColor: Colors.transparent, - ), - ); } if (pu.isOhos) Log.i('🟢 [OHOS] SystemChrome 配置完成'); diff --git a/lib/shared/widgets/adaptive_back_button.dart b/lib/shared/widgets/adaptive_back_button.dart new file mode 100644 index 00000000..4b77bc2a --- /dev/null +++ b/lib/shared/widgets/adaptive_back_button.dart @@ -0,0 +1,36 @@ +/// ============================================================ +/// 闲言APP — 自适应返回按钮 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 桌面端(Windows/macOS/Linux)显示Cupertino风格返回按钮 +/// 移动端(Android/iOS/鸿蒙)不显示,依赖系统手势返回 +/// 上次更新: 初始创建 +/// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; +import 'package:xianyan/core/theme/app_theme.dart'; + +class AdaptiveBackButton extends StatelessWidget { + const AdaptiveBackButton({super.key}); + + @override + Widget build(BuildContext context) { + if (!pu.isDesktop) return const SizedBox.shrink(); + + final canPop = Navigator.of(context).canPop(); + if (!canPop) return const SizedBox.shrink(); + + final ext = AppTheme.ext(context); + + return CupertinoButton( + padding: EdgeInsets.zero, + onPressed: () => Navigator.of(context).maybePop(), + child: Icon( + CupertinoIcons.chevron_left, + color: ext.accent, + size: 24, + ), + ); + } +} diff --git a/lib/shared/widgets/app_page_transitions.dart b/lib/shared/widgets/app_page_transitions.dart new file mode 100644 index 00000000..1d2c97b2 --- /dev/null +++ b/lib/shared/widgets/app_page_transitions.dart @@ -0,0 +1,126 @@ +/// ============================================================ +/// 闲言APP — 自定义页面转场动画 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 提供iOS风格自定义PageRouteBuilder转场,支持减少动画模式 +/// 上次更新: 初始创建,AppSlideTransition + AppFadeScaleTransition +/// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../features/mine/settings/providers/general_settings_provider.dart'; + +typedef CustomTransition = PageRouteBuilder Function({ + required WidgetBuilder builder, + required RouteSettings settings, + bool fullscreenDialog, +}); + +PageRouteBuilder appSlideTransition({ + required WidgetBuilder builder, + required RouteSettings settings, + bool fullscreenDialog = false, +}) { + return PageRouteBuilder( + settings: settings, + fullscreenDialog: fullscreenDialog, + pageBuilder: (context, animation, secondaryAnimation) => + builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final tween = Tween( + begin: const Offset(1.0, 0.0), + end: Offset.zero, + ).chain(CurveTween(curve: Curves.easeOutCubic)); + + final fadeTween = Tween(begin: 0.0, end: 1.0).chain( + CurveTween(curve: Curves.easeOutCubic), + ); + + return SlideTransition( + position: animation.drive(tween), + child: FadeTransition( + opacity: animation.drive(fadeTween), + child: child, + ), + ); + }, + transitionDuration: const Duration(milliseconds: 350), + ); +} + +PageRouteBuilder appFadeScaleTransition({ + required WidgetBuilder builder, + required RouteSettings settings, + bool fullscreenDialog = false, +}) { + return PageRouteBuilder( + settings: settings, + fullscreenDialog: fullscreenDialog, + pageBuilder: (context, animation, secondaryAnimation) => + builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final scaleTween = Tween(begin: 0.95, end: 1.0).chain( + CurveTween(curve: Curves.easeOut), + ); + final fadeTween = Tween(begin: 0.0, end: 1.0).chain( + CurveTween(curve: Curves.easeOut), + ); + + return FadeTransition( + opacity: animation.drive(fadeTween), + child: ScaleTransition( + scale: animation.drive(scaleTween), + child: child, + ), + ); + }, + ); +} + +PageRouteBuilder instantTransition({ + required WidgetBuilder builder, + required RouteSettings settings, + bool fullscreenDialog = false, +}) { + return PageRouteBuilder( + settings: settings, + fullscreenDialog: fullscreenDialog, + pageBuilder: (context, animation, secondaryAnimation) => + builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + child, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ); +} + +CustomTransition resolveTransition(WidgetRef ref) { + final reduceAnimations = ref.read( + generalSettingsProvider.select((s) => s.reduceAnimations), + ); + if (reduceAnimations) return instantTransition; + + final mode = ref.read( + generalSettingsProvider.select((s) => s.pageTransitionMode), + ); + return switch (mode) { + PageTransitionMode.navigate => appSlideTransition, + PageTransitionMode.sheet => appFadeScaleTransition, + }; +} + +CustomTransition resolveTransitionFromContext(BuildContext context) { + final reduceAnimations = ProviderScope.containerOf(context).read( + generalSettingsProvider.select((s) => s.reduceAnimations), + ); + if (reduceAnimations) return instantTransition; + + final mode = ProviderScope.containerOf(context).read( + generalSettingsProvider.select((s) => s.pageTransitionMode), + ); + return switch (mode) { + PageTransitionMode.navigate => appSlideTransition, + PageTransitionMode.sheet => appFadeScaleTransition, + }; +} diff --git a/lib/shared/widgets/bottom_sheet.dart b/lib/shared/widgets/bottom_sheet.dart index 5e2129b0..b80d50c7 100644 --- a/lib/shared/widgets/bottom_sheet.dart +++ b/lib/shared/widgets/bottom_sheet.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 底部弹窗组件 /// 创建时间: 2026-04-20 -/// 更新时间: 2026-04-20 +/// 更新时间: 2026-05-22 /// 作用: iOS 26 液态玻璃底部面板 (GlassActionSheet + GlassSheet) -/// 上次更新: 集成 stupid_simple_sheet + liquid_glass_widgets +/// 上次更新: 添加 ScrollAwareSheetContent 解决 ScrollView 嵌套手势冲突 /// ============================================================ import 'package:flutter/cupertino.dart'; @@ -28,6 +28,67 @@ class BottomSheetOption { final VoidCallback? onTap; } +/// ScrollView 嵌套在 BottomSheet 中的手势冲突修复组件 +/// +/// 当 ListView/ScrollView 嵌套在底部面板内时: +/// - 内部滚动视图在非顶部位置时,拖拽手势用于滚动内容,不会误触关闭面板 +/// - 内部滚动视图在顶部位置时,继续向下拖拽可以关闭面板 +/// - 使用 OverscrollIndicatorNotification 检测内部列表到达顶部 +/// +/// 配合 stupid_simple_sheet 的 ScrollDragDetector 使用, +/// 提供额外的手势冲突保护和 overscroll 指示器控制。 +class ScrollAwareSheetContent extends StatefulWidget { + const ScrollAwareSheetContent({ + super.key, + required this.child, + }); + + final Widget child; + + @override + State createState() => + _ScrollAwareSheetContentState(); +} + +class _ScrollAwareSheetContentState extends State { + bool _isScrolledToTop = true; + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: _handleScrollNotification, + child: NotificationListener( + onNotification: _handleOverscrollIndicator, + child: widget.child, + ), + ); + } + + bool _handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification) { + final atTop = notification.metrics.pixels <= 0; + if (atTop != _isScrolledToTop) { + setState(() => _isScrolledToTop = atTop); + } + } + + if (notification is OverscrollNotification) { + if (!_isScrolledToTop && notification.overscroll < 0) { + return true; + } + } + + return false; + } + + bool _handleOverscrollIndicator(OverscrollIndicatorNotification notification) { + if (!_isScrolledToTop) { + notification.disallowIndicator(); + } + return false; + } +} + /// iOS 26 液态玻璃底部面板 /// /// 两种模式: @@ -73,6 +134,8 @@ class AppBottomSheet { /// - 拖拽关闭 /// - 背景模糊 /// - 多级吸附 (SheetSnappingConfig) + /// - [scrollAware] 为 true 时,自动包裹 ScrollAwareSheetContent, + /// 解决内部 ScrollView 与面板拖拽的手势冲突 static Future showCustom({ required BuildContext context, required WidgetBuilder builder, @@ -81,10 +144,13 @@ class AppBottomSheet { Color barrierColor = Colors.black54, Color backgroundColor = CupertinoColors.systemBackground, ShapeBorder? shape, + bool scrollAware = false, }) { return showGlassSheet( context: context, - builder: builder, + builder: scrollAware + ? (ctx) => ScrollAwareSheetContent(child: builder(ctx)) + : builder, snappingConfig: snappingConfig, blurBehindBarrier: blurBehindBarrier, barrierColor: barrierColor, @@ -96,13 +162,18 @@ class AppBottomSheet { /// 显示半屏面板 (detent 风格) /// /// iOS 26 风格半屏 Sheet,可拖拽到不同高度。 + /// - [scrollAware] 为 true 时,自动包裹 ScrollAwareSheetContent, + /// 解决内部 ScrollView 与面板拖拽的手势冲突 static Future showHalf({ required BuildContext context, required WidgetBuilder builder, + bool scrollAware = false, }) { return showGlassSheet( context: context, - builder: builder, + builder: scrollAware + ? (ctx) => ScrollAwareSheetContent(child: builder(ctx)) + : builder, snappingConfig: const SheetSnappingConfig([ 0.4, 0.7, diff --git a/lib/shared/widgets/keyboard_back_handler.dart b/lib/shared/widgets/keyboard_back_handler.dart new file mode 100644 index 00000000..8fcff110 --- /dev/null +++ b/lib/shared/widgets/keyboard_back_handler.dart @@ -0,0 +1,55 @@ +/// ============================================================ +/// 闲言APP — 键盘返回快捷键处理器 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 桌面端注册 Alt+Left(Windows/Linux) / Cmd+Left(macOS) +/// 快捷键执行 Navigator.maybePop() 返回上一页 +/// 通过rootNavigatorKey访问Navigator,无需在Navigator子树内 +/// 上次更新: 初始创建 +/// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:xianyan/core/router/app_router.dart' show rootNavigatorKey; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; + +class KeyboardBackHandler extends StatelessWidget { + const KeyboardBackHandler({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + if (!pu.isDesktop) return child; + + return Shortcuts( + shortcuts: { + LogicalKeySet( + pu.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.alt, + LogicalKeyboardKey.arrowLeft, + ): const _BackIntent(), + }, + child: Actions( + actions: >{ + _BackIntent: _BackAction(), + }, + child: child, + ), + ); + } +} + +class _BackIntent extends Intent { + const _BackIntent(); +} + +class _BackAction extends Action<_BackIntent> { + @override + Object? invoke(_BackIntent intent) { + final navigator = rootNavigatorKey.currentState; + if (navigator != null && navigator.canPop()) { + navigator.maybePop(); + } + return null; + } +} diff --git a/lib/shared/widgets/shader_card_background.dart b/lib/shared/widgets/shader_card_background.dart index be5747ef..3b68f099 100644 --- a/lib/shared/widgets/shader_card_background.dart +++ b/lib/shared/widgets/shader_card_background.dart @@ -3,7 +3,7 @@ // 创建时间: 2026-05-20 // 更新时间: 2026-05-22 // 作用: Fragment Shader流体渐变背景 -// 上次更新: 集成PerformanceOrchestrator帧率节流+前后台暂停Ticker +// 上次更新: 移除冗余WidgetsBindingObserver,统一使用PerformanceOrchestrator前后台回调 // ============================================================ import 'dart:ui' as ui; @@ -31,7 +31,7 @@ class ShaderCardBackground extends StatefulWidget { } class _ShaderCardBackgroundState extends State - with SingleTickerProviderStateMixin, WidgetsBindingObserver { + with SingleTickerProviderStateMixin { late Ticker _ticker; final ValueNotifier _timeNotifier = ValueNotifier(0); ui.FragmentProgram? _program; @@ -42,7 +42,6 @@ class _ShaderCardBackgroundState extends State @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); _shouldRenderFrame = PerformanceOrchestrator.instance.createFrameThrottle(); _ticker = createTicker(_onTick); _ticker.start(); @@ -74,19 +73,8 @@ class _ShaderCardBackgroundState extends State if (!_ticker.isActive && _isVisible) _ticker.start(); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - if (state == AppLifecycleState.paused) { - _pauseTicker(); - } else if (state == AppLifecycleState.resumed && _isVisible) { - _resumeTicker(); - } - } - @override void dispose() { - WidgetsBinding.instance.removeObserver(this); PerformanceOrchestrator.instance.removeCallbacks(_resumeTicker); PerformanceOrchestrator.instance.removeCallbacks(_pauseTicker); _ticker.dispose(); diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 84eb4aec..ce90e5eb 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 共享组件导出 /// 创建时间: 2026-04-20 -/// 更新时间: 2026-05-04 +/// 更新时间: 2026-05-22 /// 作用: 统一导出所有共享组件 -/// 上次更新: 新增 responsive_layout 导出 +/// 上次更新: 新增 adaptive_back_button / keyboard_back_handler 导出 /// ============================================================ export 'glass_container.dart'; @@ -18,3 +18,5 @@ export 'app_toast.dart'; export 'category_icon.dart'; export 'animated_widgets.dart'; export 'responsive_layout.dart'; +export 'adaptive_back_button.dart'; +export 'keyboard_back_handler.dart'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c6f3e02a..e1e99aa4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,7 +16,7 @@ import flutter_blue_plus_darwin import flutter_image_compress_macos import flutter_inappwebview_macos import flutter_local_notifications -import flutter_secure_storage_darwin +import flutter_secure_storage_macos import flutter_tts import flutter_webrtc import gal @@ -49,7 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 218cef77..5ada362b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -711,7 +711,7 @@ packages: path: "packages/file_picker" relative: true source: path - version: "8.3.7-ohos.1" + version: "11.0.0-ohos.1" file_selector_linux: dependency: transitive description: diff --git a/scripts/theme_audit.dart b/scripts/theme_audit.dart new file mode 100644 index 00000000..1c5e369c --- /dev/null +++ b/scripts/theme_audit.dart @@ -0,0 +1,836 @@ +/// ============================================================ +/// 闲言APP — 主题令牌审计 CLI 脚本 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 命令行运行主题令牌审计,检测硬编码颜色/间距/圆角/字号 +/// 上次更新: 初始创建 — 自包含脚本,不依赖 Flutter 项目包 +/// 运行方式: dart run scripts/theme_audit.dart [--category color|spacing|radius|fontSize] [--fix-hints] [--json] +/// ============================================================ + +import 'dart:convert'; +import 'dart:io'; + +// ============================================================ +// 数据模型 (与 lib/features/mine/settings/theme_audit.dart 保持同步) +// ============================================================ + +enum AuditCategory { + color('颜色', '🎨'), + spacing('间距', '📏'), + radius('圆角', '🔘'), + fontSize('字号', '🔤'); + + const AuditCategory(this.label, this.emoji); + final String label; + final String emoji; +} + +class AuditViolation { + const AuditViolation({ + required this.filePath, + required this.lineNumber, + required this.category, + required this.currentCode, + required this.suggestion, + }); + + final String filePath; + final int lineNumber; + final AuditCategory category; + final String currentCode; + final String suggestion; + + @override + String toString() { + final rel = relativePath(filePath); + return '${category.emoji} ${category.label} | $rel:$lineNumber\n' + ' 当前: $currentCode\n' + ' 建议: $suggestion'; + } + + static String relativePath(String path) { + const markers = ['lib\\', 'lib/', 'scripts\\', 'scripts/']; + for (final m in markers) { + final idx = path.indexOf(m); + if (idx >= 0) return path.substring(idx); + } + return path; + } +} + +class AuditSummary { + const AuditSummary({required this.violations}); + + final List violations; + + int get total => violations.length; + + int countByCategory(AuditCategory cat) => + violations.where((v) => v.category == cat).length; + + Map get byCategory => { + for (final cat in AuditCategory.values) cat: countByCategory(cat), + }; + + int get fileCount => + violations.map((v) => v.filePath).toSet().length; + + @override + String toString() { + final buf = StringBuffer(); + buf.writeln('═══════════════════════════════════════════'); + buf.writeln(' 主题令牌审计报告'); + buf.writeln('═══════════════════════════════════════════'); + buf.writeln(); + + for (final cat in AuditCategory.values) { + final n = countByCategory(cat); + buf.writeln(' ${cat.emoji} ${cat.label}: $n 处违规'); + } + + buf.writeln(); + buf.writeln(' 📄 涉及文件: $fileCount 个'); + buf.writeln(' 📊 违规总数: $total 处'); + buf.writeln('═══════════════════════════════════════════'); + return buf.toString(); + } +} + +// ============================================================ +// 审计引擎 +// ============================================================ + +class ThemeAuditEngine { + ThemeAuditEngine({ + required this.projectRoot, + List? whitelistDirs, + }) : _whitelistDirs = whitelistDirs ?? _defaultWhitelist; + + final String projectRoot; + final List _whitelistDirs; + + static final List _defaultWhitelist = [ + 'lib${Platform.pathSeparator}core${Platform.pathSeparator}theme', + 'lib${Platform.pathSeparator}l10n', + ]; + + static const List _generatedSuffixes = [ + '.g.dart', + '.freezed.dart', + ]; + + static const List _themeFileNames = [ + 'app_colors.dart', + 'app_radius.dart', + 'app_spacing.dart', + 'app_typography.dart', + 'app_shadow.dart', + 'glass_tokens.dart', + 'app_theme.dart', + 'theme.dart', + 'color_tokens.dart', + 'theme_audit.dart', + ]; + + AuditSummary runAudit() { + final violations = []; + final libDir = Directory( + '$projectRoot${Platform.pathSeparator}lib', + ); + + if (!libDir.existsSync()) { + stderr.writeln('❌ 未找到 lib/ 目录: ${libDir.path}'); + return AuditSummary(violations: violations); + } + + final files = _collectDartFiles(libDir); + for (final file in files) { + violations.addAll(_auditFile(file)); + } + + violations.sort((a, b) { + final cmp = a.filePath.compareTo(b.filePath); + return cmp != 0 ? cmp : a.lineNumber.compareTo(b.lineNumber); + }); + + return AuditSummary(violations: violations); + } + + List _collectDartFiles(Directory dir) { + final result = []; + for (final entity in dir.listSync(recursive: false)) { + if (entity is File) { + if (_shouldAuditFile(entity)) { + result.add(entity); + } + } else if (entity is Directory) { + if (!_isWhitelistedDir(entity)) { + result.addAll(_collectDartFiles(entity)); + } + } + } + return result; + } + + bool _shouldAuditFile(File file) { + final path = file.path; + if (!path.endsWith('.dart')) return false; + for (final suffix in _generatedSuffixes) { + if (path.endsWith(suffix)) return false; + } + final name = path.split(Platform.pathSeparator).last; + if (_themeFileNames.contains(name)) return false; + return true; + } + + bool _isWhitelistedDir(Directory dir) { + final path = dir.path; + for (final wl in _whitelistDirs) { + if (path.contains(wl)) return true; + } + return false; + } + + List _auditFile(File file) { + final violations = []; + final lines = file.readAsLinesSync(); + + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + final lineNum = i + 1; + + if (_isCommentLine(line)) continue; + + violations.addAll(_detectColors(file.path, lineNum, line)); + violations.addAll(_detectSpacing(file.path, lineNum, line)); + violations.addAll(_detectRadius(file.path, lineNum, line)); + violations.addAll(_detectFontSize(file.path, lineNum, line)); + } + + return violations; + } + + bool _isCommentLine(String line) { + final trimmed = line.trimLeft(); + return trimmed.startsWith('///') || + trimmed.startsWith('//') || + trimmed.startsWith('*'); + } + + // ============================================================ + // 颜色检测 + // ============================================================ + + static final _colorHexPattern = RegExp(r"Color\(0x[0-9A-Fa-f]{8}\)"); + static final _cupertinoColorsPattern = RegExp(r'CupertinoColors\.\w+'); + static final _materialColorsPattern = RegExp(r'Colors\.\w+'); + static final _hexStringPattern = RegExp(r"'#[0-9A-Fa-f]{6,8}'"); + + List _detectColors( + String path, + int lineNum, + String line, + ) { + final violations = []; + + for (final match in _colorHexPattern.allMatches(line)) { + final code = match.group(0)!; + if (_isInThemeDefinition(line)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: code, + suggestion: '使用 AppTheme.ext(context).xxx 或 AppColors 令牌替代', + )); + } + + for (final match in _cupertinoColorsPattern.allMatches(line)) { + final code = match.group(0)!; + if (_isAllowedCupertinoUsage(line)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: code, + suggestion: '使用 AppTheme.ext(context).xxx 替代 CupertinoColors', + )); + } + + for (final match in _materialColorsPattern.allMatches(line)) { + final code = match.group(0)!; + if (_isAllowedMaterialUsage(line)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: code, + suggestion: '使用 AppTheme.ext(context).xxx 替代 Colors', + )); + } + + for (final match in _hexStringPattern.allMatches(line)) { + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.color, + currentCode: match.group(0)!, + suggestion: '使用 AppColors 令牌替代硬编码十六进制颜色字符串', + )); + } + + return violations; + } + + bool _isInThemeDefinition(String line) { + return line.contains('AppThemeExtension') || + line.contains('AppColors') || + line.contains('LightColors') || + line.contains('DarkColors') || + line.contains('AmoledColors') || + line.contains('GlassTokens') || + line.contains('ColorScheme.') || + line.contains('CupertinoThemeData') || + line.contains('ThemeData(') || + line.contains('_lightExtension') || + line.contains('_darkExtension') || + line.contains('_amoledExtension'); + } + + bool _isAllowedCupertinoUsage(String line) { + return line.contains('AppThemeExtension') || + line.contains('AppTheme.') || + line.contains('CupertinoThemeData') || + line.contains('ThemeData(') || + line.contains('cupertinoOverrideTheme') || + line.contains('CupertinoTextThemeData') || + line.contains('CupertinoPageTransitionsBuilder') || + line.contains('CupertinoColors.system') || + line.contains('CupertinoColors.activeBlue') || + line.contains('CupertinoColors.activeOrange') || + line.contains('CupertinoColors.separator'); + } + + bool _isAllowedMaterialUsage(String line) { + return line.contains('AppThemeExtension') || + line.contains('AppTheme.') || + line.contains('ThemeData(') || + line.contains('ColorScheme.') || + line.contains('Colors.transparent') || + line.contains('Colors.white') || + line.contains('Colors.black') || + line.contains('WidgetStateProperty'); + } + + // ============================================================ + // 间距检测 + // ============================================================ + + static final _sizedBoxHeightPattern = + RegExp(r'SizedBox\(\s*height:\s*(\d+\.?\d*)\s*\)'); + static final _sizedBoxWidthPattern = + RegExp(r'SizedBox\(\s*width:\s*(\d+\.?\d*)\s*\)'); + static final _edgeInsetsAllPattern = + RegExp(r'EdgeInsets\.all\(\s*(\d+\.?\d*)\s*\)'); + static final _edgeInsetsSymmetricPattern = RegExp( + r'EdgeInsets\.symmetric\(' + r'(?:\s*horizontal:\s*(\d+\.?\d*)\s*)?' + r'(?:,\s*)?' + r'(?:\s*vertical:\s*(\d+\.?\d*)\s*)?' + r'\)', + ); + static final _paddingPattern = + RegExp(r'padding:\s*EdgeInsets\.all\(\s*(\d+\.?\d*)\s*\)'); + + static final _appSpacingValues = {4.0, 8.0, 16.0, 24.0, 32.0, 48.0}; + + List _detectSpacing( + String path, + int lineNum, + String line, + ) { + final violations = []; + + if (line.contains('AppSpacing')) return violations; + + for (final match in _sizedBoxHeightPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'SizedBox(height: ${match.group(1)})', + suggestion: _spacingSuggestion(val), + )); + } + + for (final match in _sizedBoxWidthPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'SizedBox(width: ${match.group(1)})', + suggestion: _spacingSuggestion(val), + )); + } + + for (final match in _edgeInsetsAllPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'EdgeInsets.all(${match.group(1)})', + suggestion: 'EdgeInsets.all(AppSpacing.${_spacingName(val)})', + )); + } + + for (final match in _paddingPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _isSpacingToken(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'padding: EdgeInsets.all(${match.group(1)})', + suggestion: + 'padding: const EdgeInsets.all(AppSpacing.${_spacingName(val)})', + )); + } + + for (final match in _edgeInsetsSymmetricPattern.allMatches(line)) { + final hStr = match.group(1); + final vStr = match.group(2); + if (hStr != null) { + final val = double.tryParse(hStr); + if (val != null && !_isSpacingToken(val)) { + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'horizontal: $hStr', + suggestion: 'horizontal: AppSpacing.${_spacingName(val)}', + )); + } + } + if (vStr != null) { + final val = double.tryParse(vStr); + if (val != null && !_isSpacingToken(val)) { + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.spacing, + currentCode: 'vertical: $vStr', + suggestion: 'vertical: AppSpacing.${_spacingName(val)}', + )); + } + } + } + + return violations; + } + + bool _isSpacingToken(double val) => _appSpacingValues.contains(val); + + String _spacingName(double val) => _findNearestSpacing(val); + + String _spacingSuggestion(double val) { + final nearest = _findNearestSpacing(val); + return 'SizedBox(height: AppSpacing.$nearest) // $val → ${_spacingValueMap[nearest]}'; + } + + String _findNearestSpacing(double val) { + var bestName = 'md'; + var bestDiff = double.infinity; + for (final entry in _spacingValueMap.entries) { + final diff = (entry.value - val).abs(); + if (diff < bestDiff) { + bestDiff = diff; + bestName = entry.key; + } + } + return bestName; + } + + static const _spacingValueMap = { + 'xs': 4.0, + 'sm': 8.0, + 'md': 16.0, + 'lg': 24.0, + 'xl': 32.0, + 'xxl': 48.0, + }; + + // ============================================================ + // 圆角检测 + // ============================================================ + + static final _borderRadiusCircularPattern = + RegExp(r'BorderRadius\.circular\(\s*(\d+\.?\d*)\s*\)'); + static final _radiusCircularPattern = + RegExp(r'Radius\.circular\(\s*(\d+\.?\d*)\s*\)'); + + static final _appRadiusValues = {2.0, 4.0, 8.0, 12.0, 16.0, 999.0}; + + List _detectRadius( + String path, + int lineNum, + String line, + ) { + final violations = []; + + if (line.contains('AppRadius')) return violations; + + for (final match in _borderRadiusCircularPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _appRadiusValues.contains(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.radius, + currentCode: 'BorderRadius.circular(${match.group(1)})', + suggestion: _radiusSuggestion(val, isBorderRadius: true), + )); + } + + for (final match in _radiusCircularPattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _appRadiusValues.contains(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.radius, + currentCode: 'Radius.circular(${match.group(1)})', + suggestion: _radiusSuggestion(val, isBorderRadius: false), + )); + } + + return violations; + } + + String _radiusSuggestion(double val, {required bool isBorderRadius}) { + final nearest = _findNearestRadius(val); + if (isBorderRadius) { + return 'BorderRadius.circular(AppRadius.$nearest) // $val → ${_radiusValueMap[nearest]}'; + } + return 'Radius.circular(AppRadius.$nearest) // $val → ${_radiusValueMap[nearest]}'; + } + + String _findNearestRadius(double val) { + var bestName = 'md'; + var bestDiff = double.infinity; + for (final entry in _radiusValueMap.entries) { + final diff = (entry.value - val).abs(); + if (diff < bestDiff) { + bestDiff = diff; + bestName = entry.key; + } + } + return bestName; + } + + static const _radiusValueMap = { + 'xs': 2.0, + 'sm': 4.0, + 'md': 8.0, + 'lg': 12.0, + 'xl': 16.0, + 'full': 999.0, + }; + + // ============================================================ + // 字号检测 + // ============================================================ + + static final _fontSizePattern = RegExp(r'fontSize:\s*(\d+\.?\d*)\s*[,)]'); + + static final _appTypographyValues = { + 34.0, + 28.0, + 22.0, + 20.0, + 18.0, + 16.0, + 14.0, + 13.0, + 12.0, + 11.0, + }; + + List _detectFontSize( + String path, + int lineNum, + String line, + ) { + final violations = []; + + if (line.contains('AppTypography')) return violations; + if (line.contains('fontScale')) return violations; + if (line.contains('fontDisplay') || + line.contains('fontTitle') || + line.contains('fontHeadline') || + line.contains('fontBody') || + line.contains('fontCallout') || + line.contains('fontSubhead') || + line.contains('fontFootnote') || + line.contains('fontCaption')) { + return violations; + } + + for (final match in _fontSizePattern.allMatches(line)) { + final val = double.tryParse(match.group(1) ?? ''); + if (val == null || _appTypographyValues.contains(val)) continue; + violations.add(AuditViolation( + filePath: path, + lineNumber: lineNum, + category: AuditCategory.fontSize, + currentCode: 'fontSize: ${match.group(1)}', + suggestion: _fontSizeSuggestion(val), + )); + } + + return violations; + } + + String _fontSizeSuggestion(double val) { + final nearest = _findNearestFontSize(val); + return 'fontSize: AppTypography.$nearest // $val → ${_typographyValueMap[nearest]}'; + } + + String _findNearestFontSize(double val) { + var bestName = 'fontBody'; + var bestDiff = double.infinity; + for (final entry in _typographyValueMap.entries) { + final diff = (entry.value - val).abs(); + if (diff < bestDiff) { + bestDiff = diff; + bestName = entry.key; + } + } + return bestName; + } + + static const _typographyValueMap = { + 'fontDisplay': 34.0, + 'fontTitle1': 28.0, + 'fontTitle2': 22.0, + 'fontTitle3': 20.0, + 'fontHeadline': 18.0, + 'fontBody': 16.0, + 'fontCallout': 16.0, + 'fontSubhead': 14.0, + 'fontFootnote': 13.0, + 'fontCaption1': 12.0, + 'fontCaption2': 11.0, + }; +} + +// ============================================================ +// CLI 入口 +// ============================================================ + +void main(List args) { + final sw = Stopwatch()..start(); + + final config = _parseArgs(args); + + final projectRoot = _findProjectRoot(); + if (projectRoot == null) { + stderr.writeln('❌ 未找到项目根目录 (pubspec.yaml)'); + exit(1); + } + + stdout.writeln(); + stdout.writeln('🔍 闲言APP — 主题令牌审计'); + stdout.writeln(' 项目: $projectRoot'); + stdout.writeln(' 类别: ${config.category ?? "全部"}'); + stdout.writeln(); + + final engine = ThemeAuditEngine(projectRoot: projectRoot); + final summary = engine.runAudit(); + + sw.stop(); + + var filteredViolations = summary.violations; + if (config.category != null) { + final cat = _categoryFromString(config.category!); + if (cat != null) { + filteredViolations = + filteredViolations.where((v) => v.category == cat).toList(); + } + } + + final filteredSummary = AuditSummary(violations: filteredViolations); + + if (config.jsonOutput) { + _outputJson(filteredSummary, sw.elapsedMilliseconds); + } else { + _outputHuman(filteredSummary, config, sw.elapsedMilliseconds); + } + + if (filteredSummary.total > 0) { + exit(1); + } else { + exit(0); + } +} + +void _outputHuman( + AuditSummary summary, + _CliConfig config, + int elapsedMs, +) { + if (summary.violations.isEmpty) { + stdout.writeln('✅ 未发现主题令牌违规,所有设计值均使用令牌定义!'); + stdout.writeln(' 耗时: ${elapsedMs}ms'); + return; + } + + stdout.writeln(summary.toString()); + stdout.writeln(); + + String? lastFile; + for (final v in summary.violations) { + final relPath = AuditViolation.relativePath(v.filePath); + if (lastFile != relPath) { + if (lastFile != null) stdout.writeln(); + stdout.writeln('📄 $relPath'); + lastFile = relPath; + } + + stdout.writeln( + ' L${v.lineNumber.toString().padLeft(4)} │ ${v.category.emoji} ${v.currentCode}'); + + if (config.fixHints) { + stdout.writeln(' │ 💡 ${v.suggestion}'); + } + } + + stdout.writeln(); + stdout.writeln('───────────────────────────────────────────'); + stdout.writeln( + ' 总计: ${summary.total} 处违规 | ${summary.fileCount} 个文件 | ${elapsedMs}ms'); + + if (!config.fixHints) { + stdout.writeln(' 💡 使用 --fix-hints 查看修复建议'); + } + + stdout.writeln('───────────────────────────────────────────'); + stdout.writeln(); + stdout.writeln('⚠️ 发现 ${summary.total} 处主题令牌违规,请修复后重新审计'); +} + +void _outputJson(AuditSummary summary, int elapsedMs) { + final data = { + 'total': summary.total, + 'fileCount': summary.fileCount, + 'elapsedMs': elapsedMs, + 'byCategory': { + for (final cat in AuditCategory.values) + cat.name: summary.countByCategory(cat), + }, + 'violations': summary.violations + .map((v) => { + 'file': AuditViolation.relativePath(v.filePath), + 'line': v.lineNumber, + 'category': v.category.name, + 'currentCode': v.currentCode, + 'suggestion': v.suggestion, + }) + .toList(), + }; + + stdout.writeln(const JsonEncoder.withIndent(' ').convert(data)); +} + +_CliConfig _parseArgs(List args) { + String? category; + var fixHints = false; + var jsonOutput = false; + + for (var i = 0; i < args.length; i++) { + final arg = args[i]; + if (arg == '--fix-hints') { + fixHints = true; + } else if (arg == '--json') { + jsonOutput = true; + } else if (arg.startsWith('--category=')) { + category = arg.substring('--category='.length); + } else if (arg == '--category' && i + 1 < args.length) { + category = args[++i]; + } else if (arg == '--help' || arg == '-h') { + _printHelp(); + exit(0); + } + } + + return _CliConfig( + category: category, + fixHints: fixHints, + jsonOutput: jsonOutput, + ); +} + +void _printHelp() { + stdout.writeln(''' +🔍 闲言APP — 主题令牌审计工具 + +用法: + dart run scripts/theme_audit.dart [选项] + +选项: + --category= 仅审计指定类别 (color|spacing|radius|fontSize) + --fix-hints 显示修复建议 + --json JSON 格式输出 (适合 CI 集成) + --help, -h 显示帮助信息 + +退出码: + 0 — 无违规 + 1 — 发现违规 (CI 可据此判断) + +示例: + dart run scripts/theme_audit.dart + dart run scripts/theme_audit.dart --category=color --fix-hints + dart run scripts/theme_audit.dart --json > audit_report.json +'''); +} + +AuditCategory? _categoryFromString(String s) { + return switch (s.toLowerCase()) { + 'color' => AuditCategory.color, + 'spacing' => AuditCategory.spacing, + 'radius' => AuditCategory.radius, + 'fontsize' || 'fontsize' || 'font_size' => AuditCategory.fontSize, + _ => null, + }; +} + +String? _findProjectRoot() { + var dir = Directory.current; + for (var i = 0; i < 10; i++) { + if (File('${dir.path}${Platform.pathSeparator}pubspec.yaml') + .existsSync()) { + return dir.path; + } + final parent = dir.parent; + if (parent.path == dir.path) break; + dir = parent; + } + return null; +} + +class _CliConfig { + const _CliConfig({ + this.category, + required this.fixHints, + required this.jsonOutput, + }); + + final String? category; + final bool fixHints; + final bool jsonOutput; +} diff --git a/scripts/verify_fonts.dart b/scripts/verify_fonts.dart new file mode 100644 index 00000000..2760e558 --- /dev/null +++ b/scripts/verify_fonts.dart @@ -0,0 +1,298 @@ +/// ============================================================ +/// 闲言APP — 字体下载URL验证脚本 +/// 创建时间: 2026-05-22 +/// 更新时间: 2026-05-22 +/// 作用: 验证所有在线字体URL的可访问性和文件格式有效性 +/// 上次更新: 初始创建,验证8个字体+备用URL的下载和文件头校验 +/// 运行方式: dart run Scripts/verify_fonts.dart +/// ============================================================ + +import 'dart:io'; +import 'dart:typed_data'; + +/// 字体数据定义(与 font_models.dart 保持同步) +const onlineFontData = [ + ( + '霞鹜文楷', + 'LXGWWenKai', + 'https://raw.githubusercontent.com/lxgw/LxgwWenKai/main/fonts/LXGWWenKai-Regular.ttf', + '🖋️', + ), + ( + '阿里巴巴普惠体', + 'AlibabaPuHuiTi', + 'https://puhuiti.oss-cn-hangzhou.aliyuncs.com/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular/AlibabaPuHuiTi-3-55-Regular.ttf', + '💼', + ), + ( + '站酷快乐体', + 'ZCOOLKuaiLe', + 'https://raw.githubusercontent.com/googlefonts/zcool-kuaile/main/fonts/ttf/ZCOOLKuaiLe-Regular.ttf', + '🎉', + ), + ( + '站酷小薇', + 'ZCOOLXiaoWei', + 'https://raw.githubusercontent.com/googlefonts/zcool-xiaowei/main/fonts/ZCOOLXiaoWei-Regular.ttf', + '🌸', + ), + ( + '思源黑体', + 'NotoSansSC', + 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf', + '📐', + ), + ( + '思源宋体', + 'NotoSerifSC', + 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Serif/OTF/SimplifiedChinese/NotoSerifCJKsc-Regular.otf', + '📜', + ), + ( + '更纱黑体', + 'SarasaGothicSC', + 'https://raw.githubusercontent.com/be5invis/Sarasa-Gothic/main/fonts/SarasaGothicSC-Regular.ttf', + '🎯', + ), + ( + '文泉驿微米黑', + 'WenQuanYiMicroHei', + 'https://raw.githubusercontent.com/niclas/wqy-microhei-font/master/wqy-microhei.ttc', + '✒️', + ), +]; + +const fontFallbackUrls = >{ + 'LXGWWenKai': [ + 'https://cdn.jsdelivr.net/gh/lxgw/LxgwWenKai@v1.501/fonts/LXGWWenKai-Regular.ttf', + 'https://github.com/lxgw/LxgwWenKai/releases/download/v1.501/LXGWWenKai-Regular.ttf', + ], + 'AlibabaPuHuiTi': [ + 'https://fonts.alicdn.com/font/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular.ttf', + ], + 'ZCOOLKuaiLe': [ + 'https://cdn.jsdelivr.net/gh/googlefonts/zcool-kuaile@main/fonts/ttf/ZCOOLKuaiLe-Regular.ttf', + ], + 'ZCOOLXiaoWei': [ + 'https://cdn.jsdelivr.net/gh/googlefonts/zcool-xiaowei@main/fonts/ZCOOLXiaoWei-Regular.ttf', + ], + 'NotoSansSC': [ + 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf', + ], + 'NotoSerifSC': [ + 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Serif/OTF/SimplifiedChinese/NotoSerifCJKsc-Regular.otf', + ], + 'SarasaGothicSC': [ + 'https://github.com/be5invis/Sarasa-Gothic/releases/download/v1.0.19/SarasaGothicSC-Regular.ttf', + ], + 'WenQuanYiMicroHei': [ + 'https://cdn.jsdelivr.net/gh/niclas/wqy-microhei-font@master/wqy-microhei.ttc', + ], +}; + +/// 校验字体文件头是否为有效 TTF/OTF/TTC +bool isValidFontFile(Uint8List bytes) { + if (bytes.length < 4) return false; + final h = bytes; + final isTtf = (h[0] == 0x00 && h[1] == 0x01 && h[2] == 0x00 && h[3] == 0x00) || + (h[0] == 0x74 && h[1] == 0x72 && h[2] == 0x75 && h[3] == 0x65); + final isOtf = h[0] == 0x4F && h[1] == 0x54 && h[2] == 0x54 && h[3] == 0x4F; + final isTtc = h[0] == 0x74 && h[1] == 0x74 && h[2] == 0x63 && h[3] == 0x66; + return isTtf || isOtf || isTtc; +} + +/// 检测字体格式类型 +String detectFontType(Uint8List bytes) { + if (bytes.length < 4) return '未知(数据不足)'; + final h = bytes; + if (h[0] == 0x00 && h[1] == 0x01 && h[2] == 0x00 && h[3] == 0x00) return 'TTF'; + if (h[0] == 0x74 && h[1] == 0x72 && h[2] == 0x75 && h[3] == 0x65) return 'TTF(true)'; + if (h[0] == 0x4F && h[1] == 0x54 && h[2] == 0x54 && h[3] == 0x4F) return 'OTF'; + if (h[0] == 0x74 && h[1] == 0x74 && h[2] == 0x63 && h[3] == 0x66) return 'TTC'; + return '未知(0x${h[0].toRadixString(16)}${h[1].toRadixString(16)}${h[2].toRadixString(16)}${h[3].toRadixString(16)})'; +} + +/// 下载字体并验证 +Future<_FontVerifyResult> verifyFontUrl( + String name, + String fontFamily, + String url, + HttpClient client, +) async { + final result = _FontVerifyResult(name: name, fontFamily: fontFamily, url: url); + + try { + final request = await client.getUrl(Uri.parse(url)); + final response = await request.close(); + + result.statusCode = response.statusCode; + + if (response.statusCode != 200) { + result.success = false; + result.error = 'HTTP ${response.statusCode}'; + return result; + } + + final bytes = await consolidateBytes(response); + result.fileSize = bytes.length; + result.fontType = detectFontType(bytes); + result.validFormat = isValidFontFile(bytes); + result.success = result.validFormat; + + if (!result.validFormat) { + result.error = '文件头非 TTF/OTF/TTC 格式,检测为: ${result.fontType}'; + } + } catch (e) { + result.success = false; + result.error = e.toString(); + } + + return result; +} + +/// 合并 HttpClientResponse 的字节流 +Future consolidateBytes(HttpClientResponse response) async { + final builder = BytesBuilder(); + await for (final chunk in response) { + builder.add(chunk); + } + return builder.toBytes(); +} + +/// 验证结果 +class _FontVerifyResult { + _FontVerifyResult({ + required this.name, + required this.fontFamily, + required this.url, + }); + + final String name; + final String fontFamily; + final String url; + bool success = false; + int? statusCode; + int? fileSize; + String? fontType; + bool validFormat = false; + String? error; + + String get fileSizeStr { + if (fileSize == null) return 'N/A'; + if (fileSize! < 1024) return '$fileSize B'; + if (fileSize! < 1024 * 1024) return '${(fileSize! / 1024).toStringAsFixed(1)} KB'; + return '${(fileSize! / (1024 * 1024)).toStringAsFixed(1)} MB'; + } +} + +Future main() async { + print('═══════════════════════════════════════════════════════════'); + print(' 闲言APP — 字体下载URL验证工具'); + print(' 运行时间: ${DateTime.now().toIso8601String()}'); + print('═══════════════════════════════════════════════════════════'); + print(''); + + final client = HttpClient(); + client.connectionTimeout = const Duration(seconds: 30); + client.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'; + + final results = <_FontVerifyResult>[]; + + print('━━━ 阶段1: 主URL验证 ━━━'); + print(''); + + for (final font in onlineFontData) { + final name = font.$1; + final fontFamily = font.$2; + final url = font.$3; + + stdout.write(' $name ($fontFamily)... '); + final result = await verifyFontUrl(name, fontFamily, url, client); + results.add(result); + + if (result.success) { + print('✅ 通过 [${result.fontType}] ${result.fileSizeStr}'); + } else { + print('❌ 失败 [${result.error}]'); + } + } + + print(''); + print('━━━ 阶段2: 备用URL验证 ━━━'); + print(''); + + for (final entry in fontFallbackUrls.entries) { + final fontFamily = entry.key; + final fallbacks = entry.value; + final fontName = onlineFontData + .where((f) => f.$2 == fontFamily) + .map((f) => f.$1) + .firstOrNull ?? + fontFamily; + + for (int i = 0; i < fallbacks.length; i++) { + final url = fallbacks[i]; + stdout.write(' $fontName 备用#${i + 1}... '); + final result = await verifyFontUrl(fontName, fontFamily, url, client); + results.add(result); + + if (result.success) { + print('✅ 通过 [${result.fontType}] ${result.fileSizeStr}'); + } else { + print('❌ 失败 [${result.error}]'); + } + } + } + + client.close(); + + print(''); + print('═══════════════════════════════════════════════════════════'); + print(' 验证结果汇总'); + print('═══════════════════════════════════════════════════════════'); + print(''); + + final primaryResults = results.sublist(0, onlineFontData.length); + final fallbackResults = results.sublist(onlineFontData.length); + + print(' 主URL结果:'); + for (final r in primaryResults) { + final icon = r.success ? '✅' : '❌'; + print(' $icon ${r.name} — ${r.success ? '${r.fontType} ${r.fileSizeStr}' : r.error}'); + } + + print(''); + print(' 备用URL结果:'); + for (final r in fallbackResults) { + final icon = r.success ? '✅' : '❌'; + print(' $icon ${r.name} — ${r.success ? '${r.fontType} ${r.fileSizeStr}' : r.error}'); + } + + final primaryPass = primaryResults.where((r) => r.success).length; + final primaryTotal = primaryResults.length; + final allPass = results.where((r) => r.success).length; + final allTotal = results.length; + + print(''); + print(' 主URL: $primaryPass/$primaryTotal 通过'); + print(' 全部: $allPass/$allTotal 通过'); + print(''); + + final failedPrimary = primaryResults.where((r) => !r.success).toList(); + if (failedPrimary.isNotEmpty) { + print(' ⚠️ 以下字体主URL不可用,需要更新:'); + for (final r in failedPrimary) { + final hasFallback = fallbackResults.any( + (f) => f.fontFamily == r.fontFamily && f.success, + ); + print(' - ${r.name}: ${r.error} ${hasFallback ? '(有可用备用URL)' : '(⚠️ 无可用备用URL!)'}'); + } + } else { + print(' 🎉 所有字体主URL验证通过!'); + } + + print(''); + print('═══════════════════════════════════════════════════════════'); + + exit(failedPrimary.isNotEmpty ? 1 : 0); +}