diff --git a/.gitignore b/.gitignore index 3820a95..4b3f8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,21 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Kotlin temporary files +android/.kotlin/ + +# Packages - exclude unnecessary files +packages/*/example/ +packages/*/test/ +packages/*/integration_test/ +packages/*/.* +packages/*/*.md +packages/*/analysis_options.yaml +packages/audioplayers_ohos/** +**/*.mp3 +**/*.wav +**/*.bin +**/*.m3u8 +**/*.ts +**/.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2ead0..35c9b65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,241 @@ All notable changes to this project will be documented in this file. --- +## [1.3.31] - 2026-04-02 + +### 优化 +- 🎨 **统一气泡提示样式为 Get.snackbar** + - 将 `home_components.dart` 中的所有 `PoetryStateManager.showSnackBar` 改为 `Get.snackbar` + - 使用透明毛玻璃样式,与调试信息气泡保持一致 + - 包括分享、复制等操作的提示 + - 涉及文件: + - `lib/views/home/home_components.dart` - 统一气泡样式 + +--- + +## [1.3.30] - 2026-04-02 + +### 新增 +- ⭐ **添加全局 Tips 开关管理器** + - 创建 `GlobalTipsManager` 单例类,支持 ValueNotifier 状态监听 + - 修改设置页面,全局 Tips 开关使用 GlobalTipsManager 管理 + - 修改发现页面,使用 ValueListenableBuilder 监听状态变化 + - 开关关闭后,发现页面的 Tips 提示会立即隐藏,下方布局自动向上占位 + - 涉及文件: + - `lib/views/home/home-load.dart` - 添加 GlobalTipsManager + - `lib/views/profile/settings/app_fun.dart` - 使用 GlobalTipsManager + - `lib/views/discover_page.dart` - 使用 ValueListenableBuilder 监听状态 + +### 优化 +- 🎨 **给诗词原文添加轻微背景色** + - 给诗词原文区域添加主题色 20% 透明度的背景 + - 添加"诗词原文"标题,使区域更清晰 + - 涉及文件: + - `lib/views/home/home_part.dart` - 添加背景色和标题 + +--- + +## [1.3.29] - 2026-04-02 + +### 修复 +- 🐛 **修复调试信息开关关闭后仍显示调试信息的问题** + - 修改 `home_controller.dart`,将所有 `Get.snackbar` 调用改为通过 `DebugInfoManager` 显示 + - 调试信息现在完全受设置页面中的"调试信息"开关控制 + - 开关关闭后,所有调试提示(刷新、下一条、上一条、点赞、复制等)都不会显示 + - 涉及文件: + - `lib/services/get/home_controller.dart` - 统一通过 DebugInfoManager 显示调试信息 + +--- + +## [1.3.28] - 2026-04-02 + +### 优化 +- 🚀 **优化设置页面,内外实时生效** + - 修改 `DebugInfoManager`,添加 `ValueNotifier` 支持实时状态监听 + - 修改 `AutoRefreshManager`,添加 `ValueNotifier` 支持实时状态监听,init 时自动启动定时器 + - 修改 `home_page.dart`,从 `StatelessWidget` 改为 `StatefulWidget`,添加 `ValueListenableBuilder` 监听次要按钮状态 + - 次要按钮(上一条、分享)的显示/隐藏现在可以实时响应设置变化 + - 涉及文件: + - `lib/views/home/home-load.dart` - 添加 ValueNotifier 支持 + - `lib/views/home/home_page.dart` - 添加实时监听次要按钮状态 + - `lib/views/profile/settings/app_fun.dart` - 已优化设置逻辑 + +--- + +## [1.3.27] - 2026-04-02 + +### 优化 +- 🎨 **优化调试信息气泡样式** + - 改用透明毛玻璃效果,使用 `barBlur` 实现模糊效果 + - 移除纯色背景,使用半透明黑色背景 (alpha: 0.15) + - 保持 iOS 风格的简洁设计 + - 涉及文件: + - `lib/views/home/home-load.dart` - 修改 showMessage 方法样式 + +--- + +## [1.3.26] - 2026-04-02 + +### 优化 +- 🎨 **优化主页悬浮按钮位置,紧贴底部导航栏** + - 修改悬浮按钮 bottom 定位值,从使用 liquidGlassTotalHeight 改为使用 liquidGlassHeight + liquidGlassBottomMargin + - 调整按钮间距,使布局更紧凑美观 + - 确保按钮固定位置,不随诗词卡片上下滚动 + - 涉及文件: + - `lib/views/home/home_page.dart` - 修改悬浮按钮定位 + +--- + +## [1.3.25] - 2026-04-02 + +### 新增 +- ✨ **新增液态玻璃导航栏透明度级别控制** + - 在 TapLiquidGlassController 中添加 TransparencyLevel 枚举 + - 支持三个透明度级别:弱、中、强 + - 弱:当前效果(白色 0.35/0.2,灰色 0.35/0.2) + - 中:中等透明度(白色 0.22/0.12,灰色 0.22/0.12) + - 强:几乎透明(白色 0.1/0.05,灰色 0.1/0.05) + - 在功能设置页面添加 3 级开关,开启 Tap 沉浸光感后显示 + - 透明度设置持久化到 SharedPreferences + - 涉及文件: + - `lib/services/get/tap_liquid_glass_controller.dart` - 新增透明度级别控制 + - `lib/views/profile/settings/app_fun.dart` - 添加透明度级别开关 + - `lib/widgets/tap-liquid-glass.dart` - 应用透明度级别 + +--- + +## [1.3.24] - 2026-04-02 + +### 修复 +- 🔧 **修复发现页 Tips 显示和关闭功能** + - 在 DiscoverController 中添加全局 tips 开关设置加载逻辑 + - 修改 discover_page.dart 显示条件,根据全局开关和本地关闭状态控制显示 + - 全局Tips关闭时,发现页不显示提示 + - 全局Tips开启时,点击X可关闭提示(仅当前会话,不写入SharedPreferences) + - 涉及文件: + - `lib/services/get/discover_controller.dart` - 添加全局tips开关加载 + - `lib/views/discover_page.dart` - 修改tips显示逻辑 + +--- + +## [1.3.23] - 2026-04-02 + +### 优化 +- 🎨 **优化 Tap 沉浸光感液态玻璃导航栏视觉效果** + - 增强透明效果,降低背景透明度约 30-40% + - 移除选中项胶囊形背景,消除两层包裹效果 + - 移除点击水波纹和高亮效果,保持纯净玻璃质感 + - 添加灰度渐变底层,提升玻璃逼真度 + - 优化图标缩放效果(从 1.12 调整为 1.1) + - 增加模糊强度(从 20 提升至 35) + - 增强阴影效果,提升悬浮感 + - 涉及文件: + - `lib/widgets/tap-liquid-glass.dart` - 优化视觉效果 + - `lib/config/app_config.dart` - 增加模糊强度 + +--- + +## [1.3.22] - 2026-04-02 + +### 重新设计 +- 🎨 **重新设计 Tap 沉浸光感液态玻璃导航栏** + - 实现 iOS 26 风格的液态玻璃效果 + - 使用 BackdropFilter 实现真正的毛玻璃模糊 + - 下层页面滑动时,能透过底栏模糊看到底部大概轮廓 + - 添加双层阴影效果,增强悬浮感 + - 使用渐变背景替代纯色,提升玻璃质感 + - 选中项添加胶囊形背景指示器 + - 优化图标动画效果,选中时放大 1.12 倍 + - 支持动画开关控制,响应主题控制器设置 + - 添加详细的使用示例和注意事项注释 + - 涉及文件: + - `lib/widgets/tap-liquid-glass.dart` - 重新设计液态玻璃导航栏 + +### 修复 +- 🔧 **修复液态玻璃导航栏布局遮挡问题** + - 使用 Stack 布局替代 Scaffold.bottomNavigationBar + - 导航栏悬浮在页面内容上方,不再遮挡下方布局 + - 页面内容延伸到屏幕底部,透过玻璃可见 + - 添加 `liquidGlassTotalHeight` 配置计算导航栏总高度 + - 修改所有子页面 ListView/ScrollView 底部内边距 + - 涉及文件: + - `lib/widgets/main_navigation.dart` - 使用 Stack 布局实现悬浮效果 + - `lib/config/app_config.dart` - 添加导航栏高度计算 + - `lib/views/home/home_page.dart` - 添加底部内边距 + - `lib/views/discover_page.dart` - 添加底部内边距 + - `lib/views/active/popular_page.dart` - 添加底部内边距 + - `lib/views/footprint/all_list.dart` - 添加底部内边距 + - `lib/views/footprint/liked_poetry_manager.dart` - 添加底部内边距 + - `lib/views/footprint/local_jilu.dart` - 添加底部内边距 + - `lib/views/profile/profile_page.dart` - 添加底部内边距 + +--- + +## [1.3.21] - 2026-04-02 + +### 优化 +- 🎨 **优化Tap沉浸光感液态玻璃导航栏** + - 恢复毛玻璃效果,去掉外层纯色背景遮挡 + - 滑动时能模糊看到下层视图的大概轮廓 + - 保持椭圆形悬浮设计和毛玻璃模糊效果 + - 优化点击区域,使用 InkWell 替代 GestureDetector + - 点击区域扩展到整个 Expanded 区域,包括空白区域 + - 添加水波纹效果,提升点击反馈 + - 涉及文件: + - `lib/widgets/tap-liquid-glass.dart` - 优化液态玻璃导航栏 + +--- + +## [1.3.20] - 2026-04-01 + +### 新增 +- ✨ **深色模式支持全面上线** + - 创建 ThemeController 管理深色模式状态 + - 使用 SharedPreferences 持久化深色模式设置 + - 全局深色模式适配,涉及 20+ 页面 + - iOS 风格的深色配色方案 + - 统一的暗色背景、卡片、文字颜色 + - 涉及文件: + - `lib/models/night-mode/theme_model.dart` - 主题数据模型 + - `lib/services/get/theme_controller.dart` - 主题控制器 + - `lib/views/profile/theme/app-diy.dart` - 深色模式开关 + - `lib/views/home/home_page.dart` - 首页深色模式 + - `lib/views/home/home_part.dart` - 首页组件 + - `lib/views/home/home-load.dart` - 首页加载 + - `lib/views/home/home_components.dart` - 首页组件 + - `lib/views/discover_page.dart` - 发现页深色模式 + - `lib/views/active/rate.dart` - 评价页深色模式 + - `lib/views/active/category_page.dart` - 分类页深色模式 + - `lib/views/active/active_search_page.dart` - 搜索页深色模式 + - `lib/views/active/tags/corr_page.dart` - 标签页深色模式 + - `lib/views/favorites_page.dart` - 收藏页深色模式 + - `lib/views/footprint/all_list.dart` - 全部列表深色模式 + - `lib/views/footprint/collect_notes.dart` - 笔记页深色模式 + - `lib/views/footprint/footprint_page.dart` - 足迹页深色模式 + - `lib/views/footprint/local_jilu.dart` - 本地记录深色模式 + - `lib/views/profile/profile_page.dart` - 个人中心深色模式 + - `lib/views/profile/history_page.dart` - 历史页深色模式 + - `lib/views/profile/app-info.dart` - 应用信息深色模式 + - `lib/views/profile/level/distinguish.dart` - 辨别页深色模式 + - `lib/views/profile/level/flow-anim.dart` - 动画页深色模式 + - `lib/views/profile/level/level-jilu.dart` - 级别记录深色模式 + - `lib/views/profile/level/poetry.dart` - 诗词页深色模式 + - `lib/views/profile/guide/beginner_page.dart` - 教程页深色模式 + - `lib/views/profile/guide/sp-guide.dart` - 引导页深色模式 + - `lib/views/profile/guide/tongji.dart` - 统计页深色模式 + - `lib/utils/app_initializer.dart` - 应用初始化 + - `lib/utils/app_theme.dart` - 应用主题 + +### 修复 +- 🔧 **修复诗词卡片加载状态问题** + - 使用 Obx 包装 PoetryCard 组件 + - 确保 sectionLoadingStates 更新时 UI 响应式刷新 + - 解决"xx加载中..."一直显示的问题 + - 涉及文件: + - `lib/views/home/home_page.dart` - 修复加载状态 + +--- + ## [1.3.19] - 2026-04-01 ### 新增 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 807368d..0000000 --- a/LICENSE +++ /dev/null @@ -1,18 +0,0 @@ -MIT License - -Copyright (c) 2026 520kiss - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a1ef82d..499030d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -42,3 +42,40 @@ android { flutter { source = "../.." } + +// 为所有依赖的模块设置统一的 JVM 目标版本 +configurations.all { + resolutionStrategy { + eachDependency { + // 这里可以添加依赖解析策略 + } + } +} + +// 为所有任务设置统一的 JVM 目标版本 +allprojects { + tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + + tasks.withType { + kotlinOptions { + jvmTarget = "17" + } + } +} + +// 强制所有模块使用相同的 JVM 版本 +subprojects { + tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + + tasks.withType { + kotlinOptions { + jvmTarget = "17" + } + } +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 66d244b..2a6c9b0 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,3 +1,7 @@ +import org.gradle.api.JavaVersion +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.AppExtension + allprojects { repositories { google() @@ -25,6 +29,7 @@ allprojects { } } +// 统一设置 JVM 版本 allprojects { tasks.withType { sourceCompatibility = "17" @@ -38,19 +43,53 @@ allprojects { } } -// 注释掉 audioplayers_android 项目配置,因为我们使用的是 Git 仓库版本 -// project(":audioplayers_android") { -// tasks.withType { -// sourceCompatibility = "1.8" -// targetCompatibility = "1.8" -// } -// -// tasks.withType { -// kotlinOptions { -// jvmTarget = "1.8" -// } -// } -// } +// 为所有子项目(包括依赖的模块)设置统一的 JVM 目标版本 +subprojects { + tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + + tasks.withType { + kotlinOptions { + jvmTarget = "17" + } + } +} + +// 为所有模块设置统一的 JVM 目标版本 +subprojects { + afterEvaluate { + // 为 Java 编译任务设置 JVM 版本 + tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + + // 为 Kotlin 编译任务设置 JVM 版本 + tasks.withType { + kotlinOptions { + jvmTarget = "17" + } + } + + // 为 Android 模块设置编译选项 + if (plugins.hasPlugin("com.android.library") || plugins.hasPlugin("com.android.application")) { + extensions.findByType()?.apply { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } + extensions.findByType()?.apply { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } + } + } +} val newBuildDir: Directory = rootProject.layout.buildDirectory diff --git a/android/gradle.properties b/android/gradle.properties index 32bf4d7..fe4f0a4 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -3,8 +3,24 @@ android.useAndroidX=true android.enableJetifier=true # 统一 JVM 版本配置 -# org.gradle.java.home=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot +org.gradle.java.home=C:/Program Files/Eclipse Adoptium/jdk-21.0.10.7-hotspot # 为所有 Kotlin 编译任务设置 JVM 目标版本 kotlin.compiler.execution.strategy=in-process kotlin.incremental=false + +# 全局 JVM 目标版本配置 +org.gradle.java.version=17 +kotlin.jvm.target=17 + +# 确保所有 Java 编译任务使用相同的版本 +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# 为所有 Android 模块设置 JVM 版本 +android.compileOptions.sourceCompatibility=17 +android.compileOptions.targetCompatibility=17 + +# 为所有 Kotlin 模块设置 JVM 版本 +kotlin.compiler.execution.strategy=in-process +kotlin.incremental=false +kotlin.compiler.options.jvmTarget=17 diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index cb800bf..eb1b394 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,6 +1,38 @@ - ## 软件特性功能 +## 版本 1.3.2 -### 已开发完成 +### 功能变更 +- 🌙 **深色模式功能实现** - 完整实现应用深色模式切换功能: + - 新增 `ThemeController` GetX 控制器管理主题状态 + - 新增 `ThemeModel` 数据模型定义主题配置结构 + - 深色模式状态持久化到 SharedPreferences + - 支持实时切换主题,无需重启应用 + - 个性化设置页面深色模式开关已连接控制器 + - 主题切换时显示友好的提示消息 + - 优先级:5 + +## 版本 1.0.5 + +### 功能变更 +- 📱 **GetX状态管理集成** - 为主要页面接入GetX状态管理库,包括: + - 主页(HomePage):创建HomeController管理状态和业务逻辑 + - 发现页(DiscoverPage):创建DiscoverController管理分类、标签和用户交互 + - 收藏页(FavoritesPage):创建FavoritesController管理收藏数据和筛选 + - 个人页(ProfilePage):创建ProfileController管理用户数据和设置 + - 主导航(MainNavigation):创建MainNavigationController管理页面切换 + - 分类页(CategoryPage):创建CategoryController管理分类数据和导航 +- 🛠️ **路由系统优化** - 完善AppRoutes路由配置,添加corrPage路由支持分类详情页导航 +- 🎨 **代码架构优化** - 将状态管理和业务逻辑从UI中分离,使用GetBuilder连接控制器和UI +- 🐛 **bug修复** - 修复多个页面的错误: + - 修复discover_page.dart中的OverlayEntry变量声明顺序问题 + - 修复category_page.dart中的类型转换问题 + - 修复main_navigation.dart中的ProfileController方法调用问题 + - 修复profile_controller.dart中的导入路径问题 + - 修复DefaultTabController.of(context)空值错误 + - 修复deprecated方法警告 + +## 版本 1.0.4 + +### 功能变更 - 🎛️ **隐藏次要按钮功能** - 在功能设置页面添加开关、开启后隐藏主页的"上一条"和"分享"悬浮按钮、默认关闭、状态保存到SharedPreferences、使用SecondaryButtonsManager单例管理、实时响应开关状态变化无需重启 - 优先级:3 - 📸 **诗词卡片截图分享功能** - 主页点赞按钮上方添加悬浮分享按钮、点击生成诗词卡片高清图片并分享、使用RepaintBoundary和GlobalKey实现Widget截图、集成share_plus库实现跨平台分享、包含生成中/成功/失败提示 @@ -11,20 +43,36 @@ - 优先级:4 - 🌐 **网络状态自动检测** - 个人卡片加载时自动检测网络状态、无网络时自动调整为离线状态、避免网络异常导致的错误 - 优先级:3 + +## 版本 1.0.3 + +### 功能变更 - 📊 **服务器信息显示** - 在离线数据页面添加服务器信息卡片、显示API地址/版本/频率限制等信息 - 优先级:2 - 📱 **离线数据下载功能增强** - 新增下载类型选择(诗句和答题)、诗句数量选项(20/30/60/100条)、答题数量选项(20/50/80/100条)、100条下载需加入用户体验计划、实现下载一条写入一条、取消下载时保存已下载数据、实时更新缓存状态、返回上一页继续后台下载、清空缓存时弹窗选择清空内容、缓存状态同时显示诗句和答题数量 - 优先级:5 - <20><> **已知bug列表功能** - 从下到上弹出页面显示已知bug、解决方法和解决时间、支持下拉刷新和滚动查看、显示bug优先级和状态、提供详细解决方案描述、显示影响用户范围和时间信息 - 📜 **投稿记录功能** - 投稿记录页面显示历史投稿列表、按时间倒序排列、支持展开查看详细信息、提供清空记录功能、投稿成功后自动保存到SharedPreferences、最多保存50条记录 + +## 版本 1.0.2 + +### 功能变更 - 🗳️ **投票功能完整实现** - 用户登录/注册、获取投票列表、投票详情、提交投票、投票结果展示、API服务基础URL修改、登录注册逻辑简化(只需用户名、默认密码123456、自动注册登录、设备标识)、投票页面调试功能、user_identifier增加Flutter后缀、setState调用安全修复(添加mounted检查)、投票登录状态持久化修复(添加Cookie管理器支持PHP Session认证) - 🎨 **个人卡片标签栏布局优化** - 将标签栏区域一分为二,左侧区域可以点击展开/收起个人卡片 - 🎲 **题目随机化功能** - 进入答题页面时调用 fetch 接口获取新题,使用 Fisher-Yates 算法打乱题目 ID 顺序 - 💬 **答对答错反馈信息修复** - 当 API 返回的提示信息为空时,自行添加提示内容 + +## 版本 1.0.1 + +### 功能变更 - 📚 **App 自行管理题目 ID** - 实现题目 ID 管理逻辑,不再随机生成 - 🔧 **API 接口路径和参数最终修复** - 确认 API 路径,调整随机题目 ID 范围 - 🔧 **API 接口路径和参数修复** - 修复 API 路径和参数,使用正确的新 API 接口 - 🐛 **HttpResponse 处理修复** - 修复 HttpResponse 对象处理方式,使用正确的属性访问 + +## 软件特性功能 + +### 已开发完成 - 🔧 **API 请求参数更新** - 根据新的 API 文档更新所有请求参数 - 📊 **个人页面统计数字动态化** - 从SharedPreferences读取真实的答题统计数据 - 📝 **记录页显示问题修复** - 修复答题记录页面显示未知题目和标签的问题 @@ -42,6 +90,9 @@ ### 开发进度 - 🏗️ **HarmonyOS桌面小组件** - 开发中,包含2x2布局、天气显示、诗句展示等功能 - 优先级:3 -getx 加入 -二维码能力 -HarmonyOS HongMeng Kernel \ No newline at end of file +- 🔄 **GetX状态管理完善** - 开发中,继续为更多页面接入GetX状态管理 + - 优先级:4 +- 📱 **二维码能力** - 开发中,添加二维码扫描和生成功能 + - 优先级:3 +- 🏗️ **HarmonyOS HongMeng Kernel** - 开发中,适配HarmonyOS内核 + - 优先级:2 diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 2dd50a6..323d563 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -6,9 +6,9 @@ class AppConfig { // 应用基础配置 static const String appName = '情景诗词'; - static const String appVersion = '1.3.1'; + static const String appVersion = '1.3.31'; static const String appDescription = 'A new Flutter project'; - static const int appVersionCode = 26033101; + static const int appVersionCode = 26040206; // 响应式布局断点 static const double mobileBreakpoint = 768.0; @@ -138,4 +138,20 @@ class AppConfig { static const String keyShowGuideOnStartup = 'show_guide_on_startup'; static const String keyUserPlanJoined = 'user_plan_joined'; static const String keyAppVersion = 'app_version'; + + // 液态玻璃导航栏配置 + static const String keyTapLiquidGlass = 'tap_liquid_glass_enabled'; + static const double liquidGlassHeight = 72.0; + static const double liquidGlassBlur = 35.0; + static const double liquidGlassCornerRadius = 32.0; + static const double liquidGlassHorizontalMargin = 16.0; + static const double liquidGlassBottomMargin = 16.0; + + /// 液态玻璃导航栏总高度(包含边距) + /// 用于计算页面底部内边距,确保内容不被导航栏遮挡 + static double get liquidGlassTotalHeight => + liquidGlassHeight + + liquidGlassBottomMargin + + 8 + + 16; // height + bottomMargin + topPadding + SafeArea } diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index d57e1df..7a03165 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; class AppConstants { // 应用信息 static const String appName = '情景诗词'; - static const String appVersion = '1.3.1'; + static const String appVersion = '1.3.25'; // 响应式断点 static const double mobileBreakpoint = 768.0; @@ -119,6 +119,7 @@ class AppConstants { static const String themeKey = 'theme_mode'; static const String languageKey = 'language_code'; static const String firstLaunchKey = 'first_launch'; + static const String tapLiquidGlassKey = 'tap_liquid_glass_enabled'; // 响应式存储键名 static const String layoutModeKey = 'layout_mode'; diff --git a/lib/main.dart b/lib/main.dart index 69d187c..e27c412 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'utils/app_theme.dart'; import 'utils/app_initializer.dart'; import 'utils/force_guide_checker.dart'; import 'routes/app_routes.dart'; import 'constants/app_constants.dart'; import 'controllers/shared_preferences_storage_controller.dart'; +import 'services/get/theme_controller.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await SharedPreferencesStorageController.init(); + // 初始化 ThemeController(在 AppInitializer 之前,确保主题最先加载) + Get.put(ThemeController(), permanent: true); + final result = await AppInitializer.initialize(); runApp( @@ -29,14 +34,18 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: AppConstants.appName, - debugShowCheckedModeBanner: false, - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.system, - initialRoute: initialRoute, - onGenerateRoute: AppRoutes.generateRoute, - ); + // 使用 Obx 监听主题变化 + return Obx(() { + final themeController = Get.find(); + return GetMaterialApp( + title: AppConstants.appName, + debugShowCheckedModeBanner: false, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: themeController.currentThemeMode, + initialRoute: initialRoute, + onGenerateRoute: AppRoutes.generateRoute, + ); + }); } } diff --git a/lib/models/night-mode/theme_model.dart b/lib/models/night-mode/theme_model.dart new file mode 100644 index 0000000..0af6d0c --- /dev/null +++ b/lib/models/night-mode/theme_model.dart @@ -0,0 +1,135 @@ +// 时间: 2026-04-02 +// 功能: 深色模式主题模型 +// 介绍: 定义主题模式枚举和相关数据结构 + +import 'package:flutter/material.dart'; + +/// 应用主题模式枚举 +enum AppThemeMode { + /// 跟随系统 + system, + /// 浅色模式 + light, + /// 深色模式 + dark, +} + +/// 主题模式扩展方法 +extension AppThemeModeExtension on AppThemeMode { + /// 获取主题模式对应的 ThemeMode + ThemeMode get themeMode { + switch (this) { + case AppThemeMode.system: + return ThemeMode.system; + case AppThemeMode.light: + return ThemeMode.light; + case AppThemeMode.dark: + return ThemeMode.dark; + } + } + + /// 获取主题模式显示名称 + String get displayName { + switch (this) { + case AppThemeMode.system: + return '跟随系统'; + case AppThemeMode.light: + return '浅色模式'; + case AppThemeMode.dark: + return '深色模式'; + } + } + + /// 获取主题模式图标 + IconData get icon { + switch (this) { + case AppThemeMode.system: + return Icons.brightness_auto; + case AppThemeMode.light: + return Icons.brightness_high; + case AppThemeMode.dark: + return Icons.brightness_2; + } + } +} + +/// 主题配置数据类 +class ThemeConfig { + /// 是否启用深色模式(简化开关,true=深色, false=跟随系统或浅色) + final bool isDarkMode; + + /// 主题模式 + final AppThemeMode themeMode; + + /// 主题色索引 + final int themeColorIndex; + + /// 强调色索引 + final int accentColorIndex; + + /// 字体大小索引 (0: 小, 1: 中, 2: 大) + final int fontSizeIndex; + + /// 是否启用动画 + final bool enableAnimation; + + /// 是否启用模糊效果 + final bool enableBlurEffect; + + const ThemeConfig({ + this.isDarkMode = false, + this.themeMode = AppThemeMode.system, + this.themeColorIndex = 0, + this.accentColorIndex = 0, + this.fontSizeIndex = 1, + this.enableAnimation = true, + this.enableBlurEffect = true, + }); + + /// 从 JSON 创建 + factory ThemeConfig.fromJson(Map json) { + return ThemeConfig( + isDarkMode: json['isDarkMode'] ?? false, + themeMode: AppThemeMode.values[json['themeMode'] ?? 0], + themeColorIndex: json['themeColorIndex'] ?? 0, + accentColorIndex: json['accentColorIndex'] ?? 0, + fontSizeIndex: json['fontSizeIndex'] ?? 1, + enableAnimation: json['enableAnimation'] ?? true, + enableBlurEffect: json['enableBlurEffect'] ?? true, + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'isDarkMode': isDarkMode, + 'themeMode': themeMode.index, + 'themeColorIndex': themeColorIndex, + 'accentColorIndex': accentColorIndex, + 'fontSizeIndex': fontSizeIndex, + 'enableAnimation': enableAnimation, + 'enableBlurEffect': enableBlurEffect, + }; + } + + /// 复制并修改 + ThemeConfig copyWith({ + bool? isDarkMode, + AppThemeMode? themeMode, + int? themeColorIndex, + int? accentColorIndex, + int? fontSizeIndex, + bool? enableAnimation, + bool? enableBlurEffect, + }) { + return ThemeConfig( + isDarkMode: isDarkMode ?? this.isDarkMode, + themeMode: themeMode ?? this.themeMode, + themeColorIndex: themeColorIndex ?? this.themeColorIndex, + accentColorIndex: accentColorIndex ?? this.accentColorIndex, + fontSizeIndex: fontSizeIndex ?? this.fontSizeIndex, + enableAnimation: enableAnimation ?? this.enableAnimation, + enableBlurEffect: enableBlurEffect ?? this.enableBlurEffect, + ); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 0699f14..85bd39f 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,111 +1,56 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../constants/app_constants.dart'; -import '../config/app_config.dart'; -import '../views/main_navigation.dart'; +import '../widgets/main_navigation.dart'; +import '../views/profile/guide/beginner_page.dart'; +import '../views/profile/guide/permission.dart'; +import '../views/profile/guide/app-data.dart'; import '../views/profile/guide/sp-guide.dart'; -import '../views/profile/profile_page.dart'; -import '../views/profile/settings/widgets.dart'; -import '../views/footprint/collect_notes.dart'; - -/// 时间: 2026-03-27 -/// 功能: 应用路由配置 -/// 介绍: 定义应用的路由规则和页面导航 -/// 最新变化: 简化路由逻辑,移除黑屏检测机制 +import '../views/active/tags/corr_page.dart'; class AppRoutes { - static const String initial = AppConstants.routeHome; - - static Future getInitialRoute(SharedPreferences prefs) async { - try { - debugPrint('========================================'); - debugPrint('=== AppRoutes.getInitialRoute ==='); - - // 获取首次启动状态 - final bool firstLaunch = prefs.getBool(AppConfig.keyFirstLaunch) ?? true; - final bool agreementAccepted = - prefs.getBool(AppConfig.keyAgreementAccepted) ?? false; - - debugPrint('firstLaunch: $firstLaunch'); - debugPrint('agreementAccepted: $agreementAccepted'); - - // 简化逻辑,避免复杂条件判断 - if (firstLaunch) { - debugPrint('✅ 首次启动,返回: /guide'); - await prefs.setBool(AppConfig.keyFirstLaunch, false); - return '/guide'; - } - - if (!agreementAccepted) { - debugPrint('✅ 未同意协议,返回: /guide'); - return '/guide'; - } - - debugPrint('✅ 正常启动,返回: ${AppConstants.routeHome}'); - return AppConstants.routeHome; - } catch (e) { - debugPrint('❌ 异常: $e,返回主页'); - return AppConstants.routeHome; - } - } + static const String mainNavigation = '/main'; + static const String beginnerGuide = '/beginner-guide'; + static const String permissionGuide = '/permission-guide'; + static const String appDataGuide = '/app-data-guide'; + static const String spGuide = '/sp-guide'; + static const String corrPage = '/corrPage'; static Route generateRoute(RouteSettings settings) { - debugPrint('🎯 generateRoute 被调用: ${settings.name}'); switch (settings.name) { - case AppConstants.routeHome: - debugPrint('✅ 返回主页'); + case mainNavigation: + return MaterialPageRoute(builder: (_) => const MainNavigation()); + case beginnerGuide: + return MaterialPageRoute(builder: (_) => const BeginnerPage()); + case permissionGuide: + return MaterialPageRoute(builder: (_) => const PermissionPage()); + case appDataGuide: + return MaterialPageRoute(builder: (_) => const AppDataPage()); + case spGuide: + return MaterialPageRoute(builder: (_) => const SpGuidePage()); + case corrPage: + final args = settings.arguments as Map; return MaterialPageRoute( - builder: (_) => const MainNavigation(), - settings: settings, - ); - case '/guide': - debugPrint('✅ 返回引导页'); - return MaterialPageRoute( - builder: (_) => const SpGuidePage(), - settings: settings, - ); - case AppConstants.routeSettings: - debugPrint('✅ 返回设置页'); - return MaterialPageRoute( - builder: (_) => const SettingsPage(), - settings: settings, - ); - case AppConstants.routeProfile: - debugPrint('✅ 返回个人页'); - return MaterialPageRoute( - builder: (_) => const ProfilePage(), - settings: settings, - ); - case AppConstants.routeWidgetsPage: - debugPrint('✅ 返回卡片设置页'); - return MaterialPageRoute( - builder: (_) => const WidgetsPage(), - settings: settings, - ); - case AppConstants.routeCollectNotes: - debugPrint('✅ 返回笔记页面'); - return MaterialPageRoute( - builder: (_) => const CollectNotesPage(), - settings: settings, + builder: (_) => + CorrPage(label: args['label']!, searchType: args['searchType']!), ); default: - debugPrint('⚠️ 未知路由: ${settings.name},直接返回主页'); - return MaterialPageRoute( - builder: (_) => const MainNavigation(), - settings: settings, - ); + return MaterialPageRoute(builder: (_) => const MainNavigation()); } } -} -class SettingsPage extends StatelessWidget { - const SettingsPage({super.key}); + static Future getInitialRoute(SharedPreferences prefs) async { + final bool? firstLaunch = prefs.getBool('firstLaunch'); + final bool? agreementAccepted = prefs.getBool('agreementAccepted'); + final bool? showGuideOnStartup = prefs.getBool('showGuideOnStartup'); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('设置')), - body: const Center(child: Text('设置页面')), - ); + if (firstLaunch == true || showGuideOnStartup == true) { + return beginnerGuide; + } + + if (agreementAccepted == false) { + return permissionGuide; + } + + return mainNavigation; } } diff --git a/lib/services/document/getx.md b/lib/services/document/getx.md new file mode 100644 index 0000000..1422866 --- /dev/null +++ b/lib/services/document/getx.md @@ -0,0 +1,211 @@ +# GetX 框架文档 + +## 1. 核心功能 + +### 1.1 状态管理 +- **响应式状态管理**:使用 Rx 变量实现数据响应式更新 +- **简单状态管理**:通过 GetBuilder 实现局部状态更新 +- **依赖注入**:使用 Get.put() 和 Get.find() 管理依赖 +- **服务定位**:全局访问控制器和服务 + +### 1.2 路由管理 +- **导航系统**:Get.to()、Get.back()、Get.off()、Get.offAll() +- **命名路由**:支持命名路由配置 +- **参数传递**:方便的页面间参数传递 +- **过渡动画**:内置多种过渡动画效果 +- **嵌套导航**:支持嵌套路由结构 + +### 1.3 依赖注入 +- **全局依赖**:单例模式管理服务 +- **懒加载**:按需初始化服务 +- **生命周期管理**:自动处理依赖的生命周期 + +### 1.4 国际化 +- **多语言支持**:配置多语言资源 +- **动态语言切换**:运行时切换语言 +- **语言资源管理**:集中管理语言文件 + +### 1.5 主题管理 +- **亮/暗主题**:支持主题模式切换 +- **自定义主题**:灵活配置主题样式 +- **主题资源**:统一管理主题相关资源 + +## 2. 组件 + +### 2.1 消息弹窗 +- **Snackbar**: + ```dart + Get.snackbar('标题', '内容'); + ``` + - 支持自定义标题、内容、图标、颜色 + - 可设置持续时间、位置、行为 + +### 2.2 对话框 +- **默认对话框**: + ```dart + Get.defaultDialog( + title: '标题', + content: Text('内容'), + confirm: ElevatedButton(onPressed: () => Get.back(), child: Text('确定')), + ); + ``` +- **自定义对话框**: + ```dart + Get.dialog(AlertDialog(...)); + ``` + +### 2.3 底部弹窗 +- **BottomSheet**: + ```dart + Get.bottomSheet( + Container( + child: Column(children: [...]), + ), + ); + ``` + +### 2.4 状态管理组件 +- **GetBuilder**: + ```dart + GetBuilder( + builder: (controller) => Text(controller.count.toString()), + ); + ``` +- **Obx**: + ```dart + Obx(() => Text(controller.count.value.toString())); + ``` +- **GetX**: + ```dart + GetX( + init: Controller(), + builder: (controller) => Text(controller.count.value.toString()), + ); + ``` + +## 3. 工具类 + +### 3.1 设备信息 +- `Get.width`/`Get.height`:屏幕尺寸 +- `Get.context`:当前上下文 +- `Get.isDarkMode`:深色模式状态 +- `Get.mediaQuery`:媒体查询 + +### 3.2 存储 +- `GetStorage`:轻量级本地存储 + ```dart + final box = GetStorage(); + box.write('key', 'value'); + box.read('key'); + ``` + +### 3.3 网络请求 +- `GetConnect`:网络请求工具 + ```dart + final api = GetConnect(); + var response = await api.get('https://api.example.com'); + ``` + +### 3.4 其他工具 +- `Get.focusScope`:焦点管理 +- `Get.keyboard`:键盘管理 +- `Get.platform`:平台信息 +- `Get.isIOS`/`Get.isAndroid`:平台判断 + +## 4. 性能优势 + +### 4.1 轻量级 +- 核心库体积小 +- 内存占用低 +- 无代码生成需求 + +### 4.2 高效响应式 +- 基于 Streams 的响应式系统 +- 智能依赖跟踪 +- 避免不必要的重建 + +### 4.3 启动速度 +- 初始化快速 +- 路由切换流畅 +- 热重载响应快 + +### 4.4 开发效率 +- 简洁的 API +- 减少样板代码 +- 易于学习和使用 + +## 5. 最佳实践 + +### 5.1 控制器设计 +- 按功能模块划分控制器 +- 使用 `onInit()` 初始化数据 +- 使用 `onClose()` 清理资源 + +### 5.2 状态管理 +- 简单状态使用 GetBuilder +- 复杂状态使用 Obx +- 全局状态使用单例控制器 + +### 5.3 路由管理 +- 使用命名路由提高代码可读性 +- 合理使用过渡动画 +- 注意路由栈管理 + +### 5.4 依赖注入 +- 服务类使用懒加载 +- 控制器使用 Get.put() +- 避免过度依赖注入 + +## 6. 版本信息 + +- **当前版本**:get: ^4.7.3 +- **更新日志**:参考 pub.dev 官方文档 + +## 7. 示例代码 + +### 7.1 基本用法 +```dart +// 1. 创建控制器 +class HomeController extends GetxController { + var count = 0.obs; + + void increment() { + count.value++; + } +} + +// 2. 注入控制器 +final controller = Get.put(HomeController()); + +// 3. 使用状态 +Obx(() => Text('Count: ${controller.count.value}')); + +// 4. 导航 +Get.to(SecondPage()); + +// 5. 显示消息 +Get.snackbar('提示', '操作成功'); +``` + +### 7.2 高级用法 +```dart +// 命名路由 +GetMaterialApp( + routes: { + '/': (context) => HomePage(), + '/second': (context) => SecondPage(), + }, +); + +// 参数传递 +Get.toNamed('/second', arguments: {'id': 123}); + +// 接收参数 +final arguments = Get.arguments; +``` + +## 8. 总结 + +GetX 是一个功能强大、性能优秀的 Flutter 框架,通过提供简洁的 API 和丰富的功能,大大简化了 Flutter 应用的开发流程。它不仅提供了状态管理、路由管理等核心功能,还提供了许多实用的工具和组件,帮助开发者快速构建高质量的 Flutter 应用。 + +GetX 的设计理念是"简洁而强大",通过减少模板代码和提供直观的 API,让开发者能够更专注于业务逻辑的实现。同时,它的性能优势也使得应用运行更加流畅,用户体验更好。 \ No newline at end of file diff --git a/lib/services/get/category_controller.dart b/lib/services/get/category_controller.dart new file mode 100644 index 0000000..20b92ed --- /dev/null +++ b/lib/services/get/category_controller.dart @@ -0,0 +1,110 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; + +/// 时间: 2026-04-01 +/// 功能: 分类页面控制器,管理分类数据和状态 +/// 介绍: 负责管理分类页面的标签切换、分类数据和导航逻辑 +class CategoryController extends GetxController + with GetSingleTickerProviderStateMixin { + // Tab控制器 + late TabController tabController; + + // 标签分类 + final tabCategories = [ + {'label': '场景分类', 'icon': Icons.category}, + {'label': '朝代分类', 'icon': Icons.history}, + ]; + + // 场景分类数据 + final sceneData = { + "节日": ["七夕节", "中秋节", "元宵节", "寒食节", "清明节", "端午节", "重阳节", "春节", "节日"], + "季节": ["三月", "二月", "冬天", "夏天", "春天", "春季", "秋天"], + "古籍": [ + "三国志", + "三国演义", + "三字经", + "中庸", + "列子", + "史记", + "后汉书", + "吕氏春秋", + "商君书", + "围炉夜话", + "增广贤文", + "墨子", + "孙子兵法", + "孟子", + "小窗幽记", + "尚书", + "左传", + "幼学琼林", + "庄子", + "战国策", + "文心雕龙", + "易传", + "晋书", + "汉书", + "淮南子", + "礼记", + "管子", + "红楼梦", + "老子", + "荀子", + "菜根谭", + "警世通言", + "论语", + "资治通鉴", + "韩非子", + "鬼谷子", + "古籍", + "格言联璧", + ], + "情感": ["伤感", "励志", "友情", "思乡", "思念", "感恩", "爱国", "爱情", "离别"], + "景物": ["庐山", "泰山", "西湖", "长江", "黄河", "边塞", "田园", "山水", "夜景"], + "天文气象": ["写云", "写雨", "写雪", "写风", "星星", "月亮", "流星"], + "动植物": ["写鸟", "柳树", "桃花", "梅花", "竹子", "荷花", "菊花"], + "语言文学": ["对联", "谚语", "一言", "读书", "哲理"], + "其他": ["母亲", "老师", "户外", "礼物", "酒"], + }; + + // 朝代分类数据 + final dynastyData = { + "主要朝代": ["唐代", "宋代", "元代", "明代", "清代"], + "古代朝代": ["南北朝", "五代", "隋代"], + "近现代": ["近现代", "用户投稿", "管理员测试"], + "其他": ["暂无朝代"], + }; + + @override + void onInit() { + super.onInit(); + // 初始化Tab控制器 + tabController = TabController(length: tabCategories.length, vsync: this); + } + + @override + void onClose() { + // 释放Tab控制器 + tabController.dispose(); + super.onClose(); + } + + // 获取当前分类类型 + String getCurrentCategoryType() { + return tabCategories[tabController.index]['label'] as String; + } + + // 获取当前分类数据 + Map> getCurrentCategoryData() { + return tabController.index == 0 ? sceneData : dynastyData; + } + + // 导航到分类详情页 + void navigateToCategoryDetail(String label, String categoryType) { + final searchType = categoryType == '朝代分类' ? 'alias' : 'keywords'; + Get.toNamed( + '/corrPage', + arguments: {'label': label, 'searchType': searchType}, + ); + } +} diff --git a/lib/services/get/discover_controller.dart b/lib/services/get/discover_controller.dart new file mode 100644 index 0000000..9f1fb99 --- /dev/null +++ b/lib/services/get/discover_controller.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/shared_preferences_storage_controller.dart'; +import '../../constants/app_constants.dart'; + +class DiscoverController extends GetxController { + var categories = [].obs; + var showTips = true.obs; + var globalTipsEnabled = true.obs; + var isDeveloperMode = false.obs; + var isInitialized = false.obs; + var currentTabIndex = 0.obs; + + @override + void onInit() { + super.onInit(); + // 立即初始化分类,不等待异步操作 + updateCategories(); + // 异步加载开发者模式设置 + loadDeveloperMode(); + // 异步加载全局Tips开关设置 + loadGlobalTipsSetting(); + } + + Future loadGlobalTipsSetting() async { + try { + final isEnabled = await SharedPreferencesStorageController.getBool( + 'global_tips_enabled', + defaultValue: true, + ); + globalTipsEnabled.value = isEnabled; + // 如果全局开关关闭,本地showTips也设置为false + if (!isEnabled) { + showTips.value = false; + } + } catch (e) { + // 忽略错误,使用默认值 + globalTipsEnabled.value = true; + } + } + + Future loadDeveloperMode() async { + try { + final isEnabled = await SharedPreferencesStorageController.getBool( + 'developer_mode_enabled', + defaultValue: false, + ); + isDeveloperMode.value = isEnabled; + // 更新分类(如果开发者模式状态改变) + updateCategories(); + } catch (e) { + // 忽略错误,使用默认值 + isDeveloperMode.value = false; + } + } + + void updateCategories() { + categories.value = ['分类', '热门', '搜索']; + if (isDeveloperMode.value) { + categories.value.add('活跃'); + } + isInitialized.value = true; + } + + Future refreshContent() async { + await Future.delayed(const Duration(seconds: 1)); + Get.snackbar('提示', '内容已刷新'); + } + + void toggleTips() { + showTips.value = false; + } + + void likeContent(int index) { + Get.snackbar('提示', '点赞了内容${index + 1}'); + } + + void commentContent(int index) { + Get.snackbar('提示', '评论了内容${index + 1}'); + } + + void shareContent(int index) { + Get.snackbar('提示', '分享了内容${index + 1}'); + } + + void bookmarkContent(int index) { + Get.snackbar('提示', '收藏了内容${index + 1}'); + } + + void showContentDetails(BuildContext context, int index, String category) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('$category - 内容${index + 1}'), + content: Text('这是$category分类下内容${index + 1}的详细信息。'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('关闭'), + ), + ], + ), + ); + } + + void showMoreOptions(BuildContext context, int index) { + showModalBottomSheet( + context: context, + builder: (context) => Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.report), + title: const Text('举报'), + onTap: () { + Navigator.pop(context); + Get.snackbar('提示', '举报内容'); + }, + ), + ListTile( + leading: const Icon(Icons.block), + title: const Text('屏蔽用户'), + onTap: () { + Navigator.pop(context); + Get.snackbar('提示', '屏蔽用户'); + }, + ), + ListTile( + leading: const Icon(Icons.link), + title: const Text('复制链接'), + onTap: () { + Navigator.pop(context); + Get.snackbar('提示', '链接已复制'); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/services/get/favorites_controller.dart b/lib/services/get/favorites_controller.dart new file mode 100644 index 0000000..9e7451d --- /dev/null +++ b/lib/services/get/favorites_controller.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../constants/app_constants.dart'; + +class FavoritesController extends GetxController { + var categories = ['全部', '点赞', '笔记', '推送', '每日一句'].obs; + var isGridView = true.obs; + var currentTabIndex = 0.obs; + var searchQuery = ''.obs; + + void toggleViewMode() { + isGridView.value = !isGridView.value; + } + + void setCurrentTabIndex(int index) { + currentTabIndex.value = index; + } + + void setSearchQuery(String query) { + searchQuery.value = query; + } + + Future refreshContent() async { + await Future.delayed(const Duration(milliseconds: 500)); + Get.snackbar('提示', '内容已刷新'); + } + + void showFilterOptions(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '筛选选项', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + ListTile( + leading: const Icon(Icons.date_range), + title: const Text('按时间排序'), + onTap: () { + Navigator.pop(context); + Get.snackbar('提示', '按时间排序'); + }, + ), + ListTile( + leading: const Icon(Icons.title), + title: const Text('按标题排序'), + onTap: () { + Navigator.pop(context); + Get.snackbar('提示', '按标题排序'); + }, + ), + ], + ), + ), + ); + } + + void navigateToSearch() { + Get.toNamed('/search', arguments: searchQuery.value.isEmpty ? null : searchQuery.value); + } + + void navigateToCollectNotes() { + Get.toNamed('/collect-notes'); + } +} \ No newline at end of file diff --git a/lib/services/get/history_controller.dart b/lib/services/get/history_controller.dart new file mode 100644 index 0000000..1d428c7 --- /dev/null +++ b/lib/services/get/history_controller.dart @@ -0,0 +1,344 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; +import '../../controllers/history_controller.dart' as history_controller; + +class HistoryController extends GetxController { + var historyList = >[].obs; + var filteredHistoryList = >[].obs; + var isLoading = true.obs; + var searchKeyword = ''.obs; + var selectedSortType = 0.obs; // 0: 时间倒序, 1: 时间正序, 2: 按名称排序 + var currentPage = 1.obs; + var totalPages = 1.obs; + var pageSize = 20; + var totalCount = 0.obs; + + final List sortTypes = ['时间倒序', '时间正序', '按名称排序']; + + @override + void onInit() { + super.onInit(); + loadHistory(); + } + + // === 加载历史记录 === + Future loadHistory() async { + isLoading.value = true; + + try { + final history = await history_controller.HistoryController.getHistory(); + historyList.value = history; + totalCount.value = history.length; + totalPages.value = (totalCount.value / pageSize).ceil(); + filterAndSortHistory(); + } catch (e) { + print('加载历史记录失败: $e'); + } finally { + isLoading.value = false; + } + } + + // === 搜索历史记录 === + void searchHistory(String keyword) { + searchKeyword.value = keyword; + currentPage.value = 1; + filterAndSortHistory(); + } + + // === 排序历史记录 === + void sortHistory(int sortType) { + selectedSortType.value = sortType; + currentPage.value = 1; + filterAndSortHistory(); + } + + // === 过滤和排序历史记录 === + void filterAndSortHistory() { + var filtered = historyList; + + // 搜索过滤 + if (searchKeyword.value.isNotEmpty) { + filtered = historyList + .where((item) { + final name = item['name']?.toString().toLowerCase() ?? ''; + final alias = item['alias']?.toString().toLowerCase() ?? ''; + final introduce = item['introduce']?.toString().toLowerCase() ?? ''; + final keyword = searchKeyword.value.toLowerCase(); + return name.contains(keyword) || + alias.contains(keyword) || + introduce.contains(keyword); + }) + .toList() + .obs; + } + + // 排序 + final sortedList = List>.from(filtered); + + switch (selectedSortType.value) { + case 0: // 时间倒序 + sortedList.sort( + (a, b) => (b['timestamp'] ?? 0).compareTo(a['timestamp'] ?? 0), + ); + break; + case 1: // 时间正序 + sortedList.sort( + (a, b) => (a['timestamp'] ?? 0).compareTo(b['timestamp'] ?? 0), + ); + break; + case 2: // 按名称排序 + sortedList.sort((a, b) => (a['name'] ?? '').compareTo(b['name'] ?? '')); + break; + } + + totalCount.value = sortedList.length; + totalPages.value = (totalCount.value / pageSize).ceil(); + filteredHistoryList.value = sortedList; + } + + // === 获取当前页的数据 === + List> getCurrentPageData() { + final startIndex = (currentPage.value - 1) * pageSize; + final endIndex = startIndex + pageSize; + if (startIndex >= filteredHistoryList.length) { + return []; + } + return filteredHistoryList.sublist( + startIndex, + endIndex > filteredHistoryList.length + ? filteredHistoryList.length + : endIndex, + ); + } + + // === 切换到下一页 === + void nextPage() { + if (currentPage.value < totalPages.value) { + currentPage.value++; + } + } + + // === 切换到上一页 === + void previousPage() { + if (currentPage.value > 1) { + currentPage.value--; + } + } + + // === 跳转到指定页 === + void goToPage(int page) { + if (page >= 1 && page <= totalPages.value) { + currentPage.value = page; + } + } + + // === 清空历史记录 === + Future clearHistory() async { + try { + final success = await history_controller.HistoryController.clearHistory(); + + if (success) { + historyList.clear(); + filteredHistoryList.clear(); + totalCount.value = 0; + totalPages.value = 1; + currentPage.value = 1; + Get.snackbar('提示', '历史记录已清空'); + } else { + Get.snackbar('提示', '清空失败'); + } + } catch (e) { + print('清空历史记录失败: $e'); + Get.snackbar('提示', '清空失败'); + } + } + + // === 删除单条记录 === + Future deleteHistoryItem(int index, Map item) async { + try { + final poetryId = item['id'] as int; + final success = await history_controller + .HistoryController.removeFromHistory(poetryId); + + if (success) { + await loadHistory(); + Get.snackbar('提示', '删除成功'); + } else { + Get.snackbar('提示', '删除失败'); + } + } catch (e) { + Get.snackbar('提示', '删除失败'); + } + } + + // === 导出历史记录 === + Future exportHistory() async { + try { + if (filteredHistoryList.isEmpty) { + Get.snackbar('提示', '无数据可导出'); + return; + } + + // 显示格式选择对话框 + final selectedFormat = await Get.dialog( + CupertinoAlertDialog( + title: const Text('选择导出格式'), + content: const Text('请选择要导出的文件格式'), + actions: [ + CupertinoDialogAction( + child: const Text('JSON'), + onPressed: () => Get.back(result: 'json'), + ), + CupertinoDialogAction( + child: const Text('TXT'), + onPressed: () => Get.back(result: 'txt'), + ), + CupertinoDialogAction( + isDestructiveAction: true, + child: const Text('取消'), + onPressed: () => Get.back(result: null), + ), + ], + ), + ); + + if (selectedFormat == null) { + return; // 用户取消 + } + + // 生成导出内容 + String content; + String fileName; + + if (selectedFormat == 'json') { + content = _generateJsonContent(); + fileName = + 'history_export_${DateTime.now().millisecondsSinceEpoch}.json'; + } else { + content = _generateTxtContent(); + fileName = + 'history_export_${DateTime.now().millisecondsSinceEpoch}.txt'; + } + + // 保存到临时文件 + final tempDir = await getTemporaryDirectory(); + final file = File('${tempDir.path}/$fileName'); + await file.writeAsString(content); + + // 使用 share_plus 分享文件 + final result = await Share.shareXFiles( + [XFile(file.path)], + subject: '历史记录导出', + text: '诗词历史记录导出文件', + ); + + if (result.status == ShareResultStatus.success) { + Get.snackbar('成功', '历史记录已导出并分享'); + } else if (result.status == ShareResultStatus.dismissed) { + Get.snackbar('提示', '分享已取消'); + } + + // 清理临时文件 + if (await file.exists()) { + await file.delete(); + } + } catch (e) { + print('导出历史记录失败: $e'); + Get.snackbar('错误', '导出失败: $e'); + } + } + + // === 生成 JSON 格式内容 === + String _generateJsonContent() { + final exportData = { + 'exportTime': DateTime.now().toIso8601String(), + 'totalCount': filteredHistoryList.length, + 'records': filteredHistoryList + .map( + (item) => { + 'id': item['id'], + 'name': item['name'], + 'alias': item['alias'], + 'date': item['date'], + 'introduce': item['introduce'], + 'timestamp': item['timestamp'], + }, + ) + .toList(), + }; + return const JsonEncoder.withIndent(' ').convert(exportData); + } + + // === 生成 TXT 格式内容 === + String _generateTxtContent() { + final buffer = StringBuffer(); + buffer.writeln('诗词历史记录导出'); + buffer.writeln('导出时间: ${DateTime.now()}'); + buffer.writeln('记录总数: ${filteredHistoryList.length}'); + buffer.writeln('=' * 50); + buffer.writeln(); + + for (int i = 0; i < filteredHistoryList.length; i++) { + final item = filteredHistoryList[i]; + buffer.writeln('【${i + 1}】${item['name'] ?? '未知诗词'}'); + buffer.writeln('朝代: ${item['alias'] ?? '未知朝代'}'); + buffer.writeln('日期: ${item['date'] ?? ''}'); + if (item['introduce']?.toString().isNotEmpty == true) { + buffer.writeln('简介: ${item['introduce']}'); + } + buffer.writeln('-' * 50); + buffer.writeln(); + } + + return buffer.toString(); + } + + // === 获取历史记录统计 === + Future showStatsDialog() async { + try { + final stats = + await history_controller.HistoryController.getHistoryStats(); + + if (stats.isEmpty) { + Get.snackbar('提示', '暂无统计数据'); + return; + } + + Get.dialog( + AlertDialog( + title: const Text('历史记录统计'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('总数: ${stats['totalCount']}'), + Text('今日: ${stats['todayCount']}'), + Text('本周: ${stats['thisWeekCount']}'), + Text('本月: ${stats['thisMonthCount']}'), + const SizedBox(height: 16), + const Text('热门朝代:'), + if (stats['topDynasties'] != null && + stats['topDynasties'] is Map) ...[ + ...(stats['topDynasties'] as Map).entries + .map((entry) => Text('${entry.key}: ${entry.value}')) + .toList(), + ], + ], + ), + ), + actions: [ + TextButton(onPressed: () => Get.back(), child: const Text('关闭')), + ], + ), + ); + } catch (e) { + Get.snackbar('提示', '获取统计失败'); + } + } +} diff --git a/lib/services/get/home_controller.dart b/lib/services/get/home_controller.dart new file mode 100644 index 0000000..00ccba5 --- /dev/null +++ b/lib/services/get/home_controller.dart @@ -0,0 +1,637 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controllers/history_controller.dart'; +import '../../../utils/http/poetry_api.dart'; +import '../../../services/network_listener_service.dart'; +import '../../../utils/audio_manager.dart'; +import '../../../constants/app_constants.dart'; +import '../../views/home/set/home_components.dart'; +import '../../views/home/set/home-load.dart'; +import '../../../views/profile/guide/tongji.dart'; + +class HomeController extends GetxController with NetworkListenerMixin { + var poetryData = Rxn(); + var keywordList = [].obs; + var loading = false.obs; + var isLiked = false.obs; + var isLoadingLike = false.obs; + var errorMessage = ''.obs; + var starDisplay = ''.obs; + var historyList = >[].obs; + var currentHistoryIndex = (-1).obs; + + // 动态加载状态 + var isLoadingNext = false.obs; + var sectionLoadingStates = { + 'title': false, + 'content': false, + 'name': false, + 'keywords': false, + 'introduction': false, + }.obs; + + @override + void onInit() { + super.onInit(); + // 异步初始化所有服务,然后加载诗词 + _initializeAndLoadPoetry(); + } + + Future _initializeAndLoadPoetry() async { + try { + // 并行初始化所有服务 + await Future.wait([ + _initAudioManager(), + _initSecondaryButtonsManager(), + _initAutoRefresh(), + _initDebugInfo(), + _initOfflineDataManager(), + ]); + // 所有初始化完成后,加载诗词 + await loadPoetry(); + } catch (e) { + // 初始化失败时,仍然尝试加载诗词 + await loadPoetry(); + } + } + + Future _initSecondaryButtonsManager() async { + await SecondaryButtonsManager().init(); + } + + Future _initAudioManager() async { + await AudioManager().init(); + } + + Future _initOfflineDataManager() async { + final offlineDataManager = OfflineDataManager(); + await offlineDataManager.init(); + } + + Future _initAutoRefresh() async { + final autoRefreshManager = AutoRefreshManager(); + await autoRefreshManager.init(); + autoRefreshManager.setOnRefresh(() { + loadNextPoetry(); + }); + if (autoRefreshManager.isEnabled) { + autoRefreshManager.setEnabled(true); + } + } + + Future _initDebugInfo() async { + final debugInfoManager = DebugInfoManager(); + await debugInfoManager.init(); + } + + @override + void onClose() { + // 只停止定时器,不释放单例资源 + AutoRefreshManager().stopTimer(); + super.onClose(); + } + + Future loadPoetry() async { + if (loading.value) return; + + loading.value = true; + PoetryStateManager.triggerHapticFeedback(); + + try { + final offlineDataManager = OfflineDataManager(); + final isOnline = await offlineDataManager.isOnline(); + final hasCachedData = await offlineDataManager.hasCachedData(); + + if (isOnline) { + // 在线状态:从网络加载 + final response = await PoetryApi.getRandomPoetry(); + + if (response.data != null) { + // 记录浏览统计 + try { + await StatisticsManager().recordView(); + await StatisticsManager().recordFirstUse(); + await StatisticsManager().recordTotalView(); + } catch (e) { + // 忽略错误 + } + + poetryData.value = response.data; + keywordList.value = PoetryDataUtils.extractKeywords(response.data); + starDisplay.value = PoetryDataUtils.getStarDisplay(response.data); + isLiked.value = false; + loading.value = false; + errorMessage.value = ''; + + checkIfLiked(); + await saveToHistory(response.data!); + DebugInfoManager().showRefreshSuccess(); + update(); // 通知UI更新 + } else { + // 数据为空时,显示默认内容 + poetryData.value = createDefaultPoetryData(); + keywordList.value = []; + starDisplay.value = '⭐⭐⭐⭐⭐'; + isLiked.value = false; + loading.value = false; + errorMessage.value = ''; + + DebugInfoManager().showRefreshFailed(); + update(); // 通知UI更新 + } + } else { + // 离线状态:从本地缓存加载 + if (hasCachedData) { + final poetryData = await offlineDataManager.getNextPoetry(); + + if (poetryData != null) { + // 记录浏览统计 + try { + await StatisticsManager().recordView(); + await StatisticsManager().recordFirstUse(); + await StatisticsManager().recordTotalView(); + } catch (e) { + // 忽略错误 + } + + this.poetryData.value = poetryData; + keywordList.value = PoetryDataUtils.extractKeywords(poetryData); + starDisplay.value = PoetryDataUtils.getStarDisplay(poetryData); + isLiked.value = false; + loading.value = false; + errorMessage.value = ''; + + checkIfLiked(); + DebugInfoManager().showRefreshSuccess(); + update(); // 通知UI更新 + } else { + // 缓存为空时,显示错误信息 + loading.value = false; + errorMessage.value = '离线模式下无缓存数据,请先在线下载'; + DebugInfoManager().showRefreshFailed(); + update(); // 通知UI更新 + } + } else { + // 离线且无缓存时,显示错误信息 + loading.value = false; + errorMessage.value = '离线模式下无缓存数据,请先在线下载'; + DebugInfoManager().showRefreshFailed(); + update(); // 通知UI更新 + } + } + } catch (e) { + loading.value = false; + errorMessage.value = '加载失败,请检查网络连接'; + DebugInfoManager().showRefreshFailed(); + update(); // 通知UI更新 + } + } + + // 创建默认诗词数据,确保页面始终有内容显示 + PoetryData createDefaultPoetryData() { + final now = DateTime.now(); + final dateStr = now.toString().substring(0, 10); + final monthStr = dateStr.substring(0, 7); + final timeStr = now.toString().substring(11, 19); + + return PoetryData( + id: 1, + name: '静夜思', + alias: '李白', + keywords: '思乡,月亮,静夜', + introduce: '床前明月光,疑是地上霜。举头望明月,低头思故乡。', + drtime: '唐·李白《静夜思》', + like: 0, + url: '李白-静夜思', + tui: 1, + star: 5, + hitsTotal: 10000, + hitsMonth: 1000, + hitsDay: 100, + date: dateStr, + datem: monthStr, + time: timeStr, + createTime: timeStr, + updateTime: timeStr, + ); + } + + Future loadPoetryById(int poetryId) async { + if (loading.value) return; + + loading.value = true; + + try { + final response = await PoetryApi.getPoetryById(poetryId); + + if (response.data != null) { + // 记录浏览统计 + try { + await StatisticsManager().recordView(); + await StatisticsManager().recordFirstUse(); + await StatisticsManager().recordTotalView(); + } catch (e) { + // 忽略错误 + } + + poetryData.value = response.data; + keywordList.value = PoetryDataUtils.extractKeywords(response.data); + starDisplay.value = PoetryDataUtils.getStarDisplay(response.data); + loading.value = false; + errorMessage.value = ''; + update(); // 通知UI更新 + + checkIfLiked(); + updateCurrentHistoryIndex(); + } else { + // 数据为空时,显示默认内容 + poetryData.value = createDefaultPoetryData(); + keywordList.value = []; + starDisplay.value = '⭐⭐⭐⭐⭐'; + isLiked.value = false; + loading.value = false; + errorMessage.value = ''; + update(); // 通知UI更新 + } + } catch (e) { + poetryData.value = createDefaultPoetryData(); + keywordList.value = []; + starDisplay.value = '⭐⭐⭐⭐⭐'; + isLiked.value = false; + loading.value = false; + errorMessage.value = ''; + update(); // 通知UI更新 + } + } + + Future toggleLike() async { + if (poetryData.value == null || isLoadingLike.value) return; + + // 播放点赞音效(不等待完成) + AudioManager().playLikeSound(); + + // 立即切换按钮状态和显示加载 + isLoadingLike.value = true; + + // 发送网络加载开始事件 + startNetworkLoading('toggle_like'); + + try { + final response = await PoetryApi.toggleLike(poetryData.value!.id); + + // 根据API响应消息直接判断状态 + isLiked.value = response.message.contains('点赞成功'); + + // 更新诗词数据 + if (response.data != null) { + poetryData.value = PoetryData( + id: poetryData.value!.id, + name: poetryData.value!.name, + alias: poetryData.value!.alias, + keywords: poetryData.value!.keywords, + introduce: poetryData.value!.introduce, + drtime: poetryData.value!.drtime, + like: response.data!.like, + url: poetryData.value!.url, + tui: poetryData.value!.tui, + star: poetryData.value!.star, + hitsTotal: poetryData.value!.hitsTotal, + hitsMonth: poetryData.value!.hitsMonth, + hitsDay: poetryData.value!.hitsDay, + date: poetryData.value!.date, + datem: poetryData.value!.datem, + time: poetryData.value!.time, + createTime: poetryData.value!.createTime, + updateTime: poetryData.value!.updateTime, + ); + update(); // 通知UI更新 + } + + // 通知UI更新点赞状态 + update(); + + // 管理点赞存储 + if (isLiked.value) { + // 添加到点赞列表 + await HistoryController.addToLiked(poetryData.value!.toJson()); + // 记录今日点赞 + await StatisticsManager().recordTodayLike(); + // 记录累计点赞 + await StatisticsManager().recordTotalLike(); + } else { + // 从点赞列表移除 + await HistoryController.removeLikedPoetry( + poetryData.value!.id.toString(), + ); + } + + // 发送点赞事件通知其他页面 + sendLikeEvent(poetryData.value!.id.toString(), isLiked.value); + + if (isLiked.value) { + DebugInfoManager().showLiked(); + } else { + DebugInfoManager().showUnliked(); + } + } catch (e) { + Get.snackbar( + '提示', + '操作失败,请重试', + snackPosition: SnackPosition.TOP, + backgroundColor: AppConstants.errorColor.withValues(alpha: 0.8), + colorText: Colors.white, + duration: const Duration(seconds: 2), + margin: const EdgeInsets.all(16), + borderRadius: 20, + animationDuration: const Duration(milliseconds: 300), + ); + } finally { + isLoadingLike.value = false; + update(); // 通知UI更新加载状态 + // 发送网络加载结束事件 + endNetworkLoading('toggle_like'); + } + } + + Future loadHistory() async { + try { + final historyJson = await HistoryController.getHistory(); + historyList.value = historyJson; + // 重新计算当前诗词在历史记录中的索引 + if (poetryData.value != null && historyList.isNotEmpty) { + currentHistoryIndex.value = historyList.indexWhere( + (item) => item['id'] == poetryData.value!.id, + ); + } else { + currentHistoryIndex.value = -1; + } + } catch (e) { + // 加载失败 + } + } + + Future saveToHistory(PoetryData poetryData) async { + try { + final poetryMap = { + 'id': poetryData.id, + 'name': poetryData.name, // 诗句名称/标题 + 'alias': poetryData.alias, // 诗句朝代/作者 + 'introduce': poetryData.introduce, // 诗句译文/解释 + 'drtime': poetryData.drtime, // 诗句原文/内容 + }; + + await HistoryController.addToHistory(poetryMap); + } catch (e) { + // 保存失败 + } + } + + void updateCurrentHistoryIndex() { + if (poetryData.value?.id != null) { + final index = historyList.indexWhere( + (item) => item['id'] == poetryData.value!.id, + ); + currentHistoryIndex.value = index >= 0 ? index : -1; + } + } + + Future checkIfLiked() async { + // 这里可以实现检查收藏状态的逻辑 + // 暂时使用简单的模拟逻辑 + } + + Future loadNextPoetry() async { + if (isLoadingNext.value) return; + + // 播放下一条音效(不等待完成) + AudioManager().playNextSound(); + + isLoadingNext.value = true; + // 设置所有区域为加载状态 + sectionLoadingStates.value = { + 'title': true, + 'content': true, + 'name': true, + 'keywords': true, + 'introduction': true, + }; + + try { + final offlineDataManager = OfflineDataManager(); + final isOnline = await offlineDataManager.isOnline(); + final hasCachedData = await offlineDataManager.hasCachedData(); + + PoetryData? newPoetryData; + + if (isOnline) { + // 在线状态:从网络加载 + // 确保历史记录已加载 + if (historyList.isEmpty) { + await loadHistory(); + } + + if (currentHistoryIndex.value < 0 || + currentHistoryIndex.value >= historyList.length - 1) { + // 如果没有下一条了,加载新的诗词 + final response = await PoetryApi.getRandomPoetry(); + newPoetryData = response.data; + + if (newPoetryData != null) { + await saveToHistory(newPoetryData); + } + } else { + // 如果有下一条,加载下一条 + final nextPoetry = historyList[currentHistoryIndex.value + 1]; + final response = await PoetryApi.getPoetryById(nextPoetry['id']); + newPoetryData = response.data; + } + } else { + // 离线状态:从本地缓存加载 + if (hasCachedData) { + newPoetryData = await offlineDataManager.getNextPoetry(); + } else { + // 离线且无缓存时,显示错误信息 + Get.snackbar( + '提示', + '离线模式下无缓存数据,请先在线下载', + snackPosition: SnackPosition.TOP, + backgroundColor: AppConstants.errorColor.withValues(alpha: 0.8), + colorText: Colors.white, + duration: const Duration(seconds: 2), + margin: const EdgeInsets.all(16), + borderRadius: 20, + animationDuration: const Duration(milliseconds: 300), + ); + // 重置所有加载状态 + var states = {}; + sectionLoadingStates.forEach((key, value) { + states[key] = false; + }); + sectionLoadingStates.assignAll(states); + return; + } + } + + if (newPoetryData != null) { + // 模拟分步加载 + await simulateSectionLoading(newPoetryData); + DebugInfoManager().showNextSuccess(); + } + } catch (e) { + DebugInfoManager().showNextFailed(); + // 重置所有加载状态 + var states = {}; + sectionLoadingStates.forEach((key, value) { + states[key] = false; + }); + sectionLoadingStates.assignAll(states); + } finally { + isLoadingNext.value = false; + } + } + + // 模拟分步加载过程 + Future simulateSectionLoading(PoetryData newPoetryData) async { + // 记录浏览统计 + try { + await StatisticsManager().recordView(); + await StatisticsManager().recordFirstUse(); + await StatisticsManager().recordTotalView(); + } catch (e) { + // 忽略错误 + } + + // 1. 加载标题区域 + var states = {}; + sectionLoadingStates.forEach((key, value) { + states[key] = value; + }); + states['title'] = false; + sectionLoadingStates.assignAll(states); + poetryData.value = newPoetryData; + update(); // 通知UI更新 + await Future.delayed(const Duration(milliseconds: 200)); + + // 2. 加载诗句区域 + states = {}; + sectionLoadingStates.forEach((key, value) { + states[key] = value; + }); + states['name'] = false; + sectionLoadingStates.assignAll(states); + update(); // 通知UI更新 + await Future.delayed(const Duration(milliseconds: 200)); + + // 3. 加载原文区域 + states = {}; + sectionLoadingStates.forEach((key, value) { + states[key] = value; + }); + states['content'] = false; + sectionLoadingStates.assignAll(states); + update(); // 通知UI更新 + await Future.delayed(const Duration(milliseconds: 200)); + + // 4. 加载关键词区域 + states = {}; + sectionLoadingStates.forEach((key, value) { + states[key] = value; + }); + states['keywords'] = false; + sectionLoadingStates.assignAll(states); + keywordList.value = PoetryDataUtils.extractKeywords(newPoetryData); + update(); // 通知UI更新 + await Future.delayed(const Duration(milliseconds: 200)); + + // 5. 加载译文区域 + states = {}; + sectionLoadingStates.forEach((key, value) { + states[key] = value; + }); + states['introduction'] = false; + sectionLoadingStates.assignAll(states); + starDisplay.value = PoetryDataUtils.getStarDisplay(newPoetryData); + isLiked.value = false; + errorMessage.value = ''; + update(); // 通知UI更新 + + checkIfLiked(); + updateCurrentHistoryIndex(); + } + + Future loadPreviousPoetry() async { + try { + final offlineDataManager = OfflineDataManager(); + final isOnline = await offlineDataManager.isOnline(); + final hasCachedData = await offlineDataManager.hasCachedData(); + + if (isOnline) { + // 在线状态:从历史记录加载 + // 确保历史记录已加载 + if (historyList.isEmpty) { + await loadHistory(); + } + + if (currentHistoryIndex.value <= 0) { + // 如果当前索引无效或已经是第一条,则重新加载历史记录 + await loadHistory(); + + // 如果历史记录为空,显示提示 + if (historyList.isEmpty) { + DebugInfoManager().showPreviousFailed(); + return; + } + + // 如果当前索引无效,设置为最新的一条 + if (currentHistoryIndex.value < 0) { + currentHistoryIndex.value = 0; + } + } + + // 检查是否有上一条 + if (currentHistoryIndex.value > 0) { + final previousPoetry = historyList[currentHistoryIndex.value - 1]; + await loadPoetryById(previousPoetry['id']); + } else { + // 如果没有上一条了,显示提示 + DebugInfoManager().showPreviousFailed(); + } + } else { + // 离线状态:从本地缓存加载 + if (hasCachedData) { + final poetryData = await offlineDataManager.getPreviousPoetry(); + + if (poetryData != null) { + this.poetryData.value = poetryData; + keywordList.value = PoetryDataUtils.extractKeywords(poetryData); + starDisplay.value = PoetryDataUtils.getStarDisplay(poetryData); + isLiked.value = false; + errorMessage.value = ''; + update(); // 通知UI更新 + + checkIfLiked(); + DebugInfoManager().showPreviousSuccess(); + } else { + // 缓存为空时,显示错误信息 + DebugInfoManager().showPreviousFailed(); + } + } else { + // 离线且无缓存时,显示错误信息 + DebugInfoManager().showPreviousFailed(); + } + } + } catch (e) { + DebugInfoManager().showPreviousFailed(); + } + } + + Future refreshPoetry() async { + if (poetryData.value?.id != null) { + // 如果有当前诗词ID,则重新加载当前诗词 + await loadPoetryById(poetryData.value!.id); + } else { + // 如果没有当前诗词ID,则加载新的诗词 + await loadPoetry(); + } + } +} diff --git a/lib/services/get/main_navigation_controller.dart b/lib/services/get/main_navigation_controller.dart new file mode 100644 index 0000000..26faad9 --- /dev/null +++ b/lib/services/get/main_navigation_controller.dart @@ -0,0 +1,21 @@ +import 'package:get/get.dart'; + +/// 时间: 2025-03-21 +/// 功能: 主导航控制器,管理底部导航栏的状态 +/// 介绍: 负责管理当前选中的页面索引,提供页面切换的方法 +class MainNavigationController extends GetxController { + // 当前选中的页面索引 + final currentIndex = 0.obs; + + // 切换页面 + void switchPage(int index) { + currentIndex.value = index; + update(); + } + + // 刷新个人页面数据 + void refreshProfileData() { + // 这里可以通过Get.find()获取ProfileController并调用其刷新方法 + // 但为了避免循环依赖,我们让ProfilePage自己处理刷新逻辑 + } +} diff --git a/lib/services/get/profile_controller.dart b/lib/services/get/profile_controller.dart new file mode 100644 index 0000000..1de1645 --- /dev/null +++ b/lib/services/get/profile_controller.dart @@ -0,0 +1,329 @@ +import 'dart:convert'; +import 'dart:math' show Random; +import 'dart:io' as io; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:get/get.dart'; + +import '../../constants/app_constants.dart'; +import '../../controllers/history_controller.dart'; +import '../../controllers/shared_preferences_storage_controller.dart'; +import '../isweb/wakelock_service.dart'; +import '../../views/profile/guide/tongji.dart'; + +class ProfileController extends GetxController with WidgetsBindingObserver { + // 页面状态 + var currentPage = 1.obs; // 默认显示第2页(设置) + var isCardExpanded = false.obs; // 个人卡片展开状态 + var isStatsHidden = false.obs; // 统计数据隐藏状态 + var isScreenWakeEnabled = false.obs; // 屏幕常亮状态 + var startY = 0.0.obs; + + // 历史记录相关 + var poetryHistory = >[].obs; + + // 答题统计数据 + var correctAnswers = 0.obs; + var weekQuestions = 0.obs; + + // 统计数据 + var todayViews = 0.obs; + var weekViews = 0.obs; + var firstUseTime = '未记录'.obs; + var useDays = 1.obs; + var dataSize = '0 B'.obs; + var noteCount = 0.obs; + var totalQuestions = 0.obs; + var todayQuestions = 0.obs; + var todayLikes = 0.obs; + + // 可爱的emoji列表 + final List avatars = [ + '👤', + '😊', + '🎉', + '🌟', + '🔥', + '💎', + '🌈', + '🦋', + '🌸', + '🐱', + '🐶', + '🐼', + '🐨', + '🐵', + '🦄', + '🐸', + '🐹', + '🐰', + '🦊', + '🐻', + ]; + + // 模拟用户数据 + var userData = { + 'avatar': '👤', // 使用emoji代替网络图片 + 'nickname': '诗词爱好者', + 'signature': '人生如诗,岁月如歌', + 'level': 'Lv.12', + 'vip': true, + 'posts': 156, + 'followers': 1280, + 'following': 89, + 'likes': 2560, + 'favorites': 128, + 'views': 3560, + }.obs; + + @override + void onInit() { + super.onInit(); + WidgetsBinding.instance.addObserver(this); + loadPoetryHistory(); + loadPoetryStatistics(); + loadStatistics(); + } + + @override + void onClose() { + WidgetsBinding.instance.removeObserver(this); + super.onClose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + // 应用恢复时刷新数据 + refreshData(); + } + } + + // 更换头像 + void changeAvatar() { + final random = Random(); + userData['avatar'] = avatars[random.nextInt(avatars.length)]; + } + + // 刷新所有数据 + Future refreshData() async { + await loadPoetryStatistics(); + await loadStatistics(); + } + + // 页面切换 + void onPageChanged(int page) { + currentPage.value = page; + HapticFeedback.lightImpact(); + } + + // 切换卡片展开状态 + void toggleCardExpanded() { + isCardExpanded.value = !isCardExpanded.value; + } + + // 切换统计数据显示状态 + void toggleStatsHidden() { + isStatsHidden.value = !isStatsHidden.value; + HapticFeedback.lightImpact(); + } + + // === 历史记录相关方法 === + Future loadPoetryHistory() async { + try { + final history = await HistoryController.getHistory(); + poetryHistory.value = history; + } catch (e) { + print('加载历史记录失败: $e'); + } + } + + // === 答题统计相关方法 === + Future loadPoetryStatistics() async { + try { + // 加载总体统计 + correctAnswers.value = await SharedPreferencesStorageController.getInt( + 'correctAnswers', + defaultValue: 0, + ); + + // 加载答题记录列表来计算今日和本周答题数 + List records = + await SharedPreferencesStorageController.getStringList( + 'poetryAnswerRecords', + defaultValue: [], + ); + + final now = DateTime.now(); + final todayStart = DateTime(now.year, now.month, now.day); + final weekStart = todayStart.subtract( + Duration(days: todayStart.weekday - 1), + ); + + todayQuestions.value = 0; + weekQuestions.value = 0; + + for (String recordStr in records) { + try { + final record = jsonDecode(recordStr) as Map; + final answerTime = record['answerTime']; + if (answerTime != null) { + final time = DateTime.parse(answerTime); + if (time.isAfter(todayStart)) { + todayQuestions.value++; + } + if (time.isAfter(weekStart)) { + weekQuestions.value++; + } + } + } catch (e) { + continue; + } + } + } catch (e) { + print('加载答题统计失败: $e'); + } + } + + // === 加载统计数据 === + Future loadStatistics() async { + try { + // 加载浏览统计 + todayViews.value = await StatisticsManager().getTodayViews(); + weekViews.value = await StatisticsManager().getWeekViews(); + firstUseTime.value = await StatisticsManager().getFirstUseTime(); + useDays.value = await StatisticsManager().getUseDays(); + dataSize.value = await StatisticsManager().getDataSize(); + todayLikes.value = await StatisticsManager().getTodayLikes(); + todayQuestions.value = await StatisticsManager().getTodayQuestions(); + + // 加载笔记总数 + noteCount.value = await HistoryController.getNotesCount(); + + // 加载累计答题数 + totalQuestions.value = await SharedPreferencesStorageController.getInt( + 'totalQuestions', + defaultValue: 0, + ); + } catch (e) { + print('加载统计数据失败: $e'); + } + } + + // === 屏幕常亮相关方法 === + Future toggleScreenWake(bool enable) async { + // Web 平台不支持 wakelock_plus + if (kIsWeb) { + Get.snackbar('提示', 'Web 平台不支持屏幕常亮功能'); + return; + } + + try { + // 使用 io.Platform 检测平台 + final String osName = io.Platform.operatingSystem; + final String osVersion = io.Platform.operatingSystemVersion; + print('Current platform: $osName, version: $osVersion'); + + if (enable) { + await WakelockService.instance.enable(); + Get.snackbar('提示', '屏幕常亮已开启'); + } else { + await WakelockService.instance.disable(); + Get.snackbar('提示', '屏幕常亮已关闭'); + } + + // 不再更新开关状态,由 UI 层直接控制 + // isScreenWakeEnabled.value = enable; + } catch (e, stackTrace) { + print('WakelockService error: $e'); + print('Stack trace: $stackTrace'); + // 检查错误类型,判断是否是设备不支持 + String errorMessage; + if (e.toString().contains('not supported') || + e.toString().contains('unsupported') || + e.toString().contains('不支持')) { + errorMessage = '该设备不支持屏幕常亮功能'; + } else { + errorMessage = '屏幕常亮功能异常: $e'; + } + + // 不再更新开关状态,由 UI 层直接控制 + // isScreenWakeEnabled.value = enable; + + // 显示错误提示,但不影响开关状态 + Get.dialog( + AlertDialog( + title: const Text('提示'), + content: Text(errorMessage), + actions: [ + TextButton(onPressed: () => Get.back(), child: const Text('确定')), + ], + ), + ); + } + } + + // === 导航方法 === + void navigateToHistoryPage() { + Get.toNamed('/history'); + } + + void navigateToAppFunSettings() { + Get.toNamed('/app-fun-settings'); + } + + void navigateToUserPlanPage() { + Get.toNamed('/user-plan'); + } + + void navigateToOfflineDataPage() { + Get.toNamed('/offline-data'); + } + + void navigateToPermissionPage() { + Get.toNamed('/permission'); + } + + void navigateToAppDataPage() { + Get.toNamed('/app-data'); + } + + void navigateToPrivacyPage() { + Get.toNamed('/privacy'); + } + + void navigateToLearnUsPage() { + Get.toNamed('/learn-us'); + } + + void navigateToAppInfoPage() { + Get.toNamed('/app-info'); + } + + void navigateToAppDiyPage() { + Get.toNamed('/app-diy'); + } + + void navigateToVotePage() { + Get.toNamed('/vote'); + } + + void navigateToPoetryLevelPage() { + Get.toNamed('/poetry-level'); + } + + void navigateToEntirePage() { + Get.toNamed('/entire'); + } + + void navigateToManuscriptPage() { + Get.toNamed('/manuscript'); + } + + // === 显示提示 === + void showSnackBar(String message) { + Get.snackbar('提示', message); + } +} diff --git a/lib/services/get/tap_liquid_glass_controller.dart b/lib/services/get/tap_liquid_glass_controller.dart new file mode 100644 index 0000000..435b0bb --- /dev/null +++ b/lib/services/get/tap_liquid_glass_controller.dart @@ -0,0 +1,89 @@ +// 时间: 2026-04-02 +// 功能: Tap沉浸光感导航栏控制器 +// 介绍: 管理液态玻璃导航栏的开关状态和透明度级别 +// 最新变化: 添加透明度级别控制(弱、中、强) + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../config/app_config.dart'; + +/// 透明度级别枚举 +enum TransparencyLevel { + weak, // 弱 - 当前效果 + medium, // 中 - 中等透明度 + strong, // 强 - 几乎透明 +} + +class TapLiquidGlassController extends GetxController { + SharedPreferences? _prefs; + final _isEnabled = true.obs; + final _transparencyLevel = TransparencyLevel.weak.obs; + + bool get isEnabled => _isEnabled.value; + RxBool get isEnabledRx => _isEnabled; + TransparencyLevel get transparencyLevel => _transparencyLevel.value; + Rx get transparencyLevelRx => _transparencyLevel; + + /// 获取透明度级别的索引(用于视图层) + int get transparencyLevelIndex => _transparencyLevel.value.index; + + /// 获取透明度配置 + /// 返回整体背景透明度 + Map get transparencyValues { + switch (_transparencyLevel.value) { + case TransparencyLevel.weak: + return {'backgroundOpacity': 0.5}; + case TransparencyLevel.medium: + return {'backgroundOpacity': 0.2}; + case TransparencyLevel.strong: + return {'backgroundOpacity': 0.01}; + } + } + + /// 获取透明度级别的中文名称 + String get transparencyLevelLabel { + switch (_transparencyLevel.value) { + case TransparencyLevel.weak: + return '弱'; + case TransparencyLevel.medium: + return '中'; + case TransparencyLevel.strong: + return '强'; + } + } + + /// 通过索引设置透明度级别 + Future setTransparencyLevelByIndex(int index) async { + final level = TransparencyLevel + .values[index.clamp(0, TransparencyLevel.values.length - 1)]; + await setTransparencyLevel(level); + } + + @override + void onInit() { + super.onInit(); + _loadSettings(); + } + + Future _loadSettings() async { + _prefs = await SharedPreferences.getInstance(); + _isEnabled.value = _prefs?.getBool(AppConfig.keyTapLiquidGlass) ?? true; + final levelIndex = _prefs?.getInt('tap_liquid_glass_transparency') ?? 0; + _transparencyLevel.value = TransparencyLevel + .values[levelIndex.clamp(0, TransparencyLevel.values.length - 1)]; + } + + Future toggleEnabled(bool enabled) async { + if (_isEnabled.value == enabled) return; + _isEnabled.value = enabled; + _prefs ??= await SharedPreferences.getInstance(); + await _prefs?.setBool(AppConfig.keyTapLiquidGlass, enabled); + } + + Future setTransparencyLevel(TransparencyLevel level) async { + if (_transparencyLevel.value == level) return; + _transparencyLevel.value = level; + _prefs ??= await SharedPreferences.getInstance(); + await _prefs?.setInt('tap_liquid_glass_transparency', level.index); + } +} diff --git a/lib/services/get/theme_controller.dart b/lib/services/get/theme_controller.dart new file mode 100644 index 0000000..5d1e057 --- /dev/null +++ b/lib/services/get/theme_controller.dart @@ -0,0 +1,240 @@ +// 时间: 2026-04-02 +// 功能: 主题控制器 - 管理深色模式和主题设置 +// 介绍: 使用 GetX 管理主题状态,支持状态持久化到 SharedPreferences + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../models/night-mode/theme_model.dart'; + +/// 主题控制器 +/// 管理应用的主题模式、深色模式状态,并持久化到 SharedPreferences +class ThemeController extends GetxController { + // SharedPreferences 实例 + SharedPreferences? _prefs; + + // 存储键名 + static const String _darkModeKey = 'darkMode'; + static const String _themeModeKey = 'themeMode'; + static const String _themeColorIndexKey = 'themeColorIndex'; + static const String _accentColorIndexKey = 'accentColorIndex'; + static const String _fontSizeIndexKey = 'fontSizeIndex'; + static const String _enableAnimationKey = 'enableAnimation'; + static const String _enableBlurEffectKey = 'enableBlurEffect'; + + // 可观察状态 + final _isDarkMode = false.obs; + final _themeMode = AppThemeMode.system.obs; + final _themeColorIndex = 0.obs; + final _accentColorIndex = 0.obs; + final _fontSizeIndex = 1.obs; + final _enableAnimation = true.obs; + final _enableBlurEffect = true.obs; + + // Getters + bool get isDarkMode => _isDarkMode.value; + AppThemeMode get themeMode => _themeMode.value; + int get themeColorIndex => _themeColorIndex.value; + int get accentColorIndex => _accentColorIndex.value; + int get fontSizeIndex => _fontSizeIndex.value; + bool get enableAnimation => _enableAnimation.value; + bool get enableBlurEffect => _enableBlurEffect.value; + + /// 获取当前 Flutter ThemeMode + ThemeMode get currentThemeMode { + if (_isDarkMode.value) { + return ThemeMode.dark; + } + return _themeMode.value.themeMode; + } + + /// 获取 Rx 状态(供 Obx 使用) + RxBool get isDarkModeRx => _isDarkMode; + Rx get themeModeRx => _themeMode; + RxInt get themeColorIndexRx => _themeColorIndex; + RxInt get accentColorIndexRx => _accentColorIndex; + RxInt get fontSizeIndexRx => _fontSizeIndex; + RxBool get enableAnimationRx => _enableAnimation; + RxBool get enableBlurEffectRx => _enableBlurEffect; + + @override + void onInit() { + super.onInit(); + _loadThemeSettings(); + } + + /// SharedPreferences 实例(公开访问,供其他组件使用) + SharedPreferences? get prefs => _prefs; + + /// 加载主题设置 + Future _loadThemeSettings() async { + _prefs = await SharedPreferences.getInstance(); + + _isDarkMode.value = _prefs?.getBool(_darkModeKey) ?? false; + _themeMode.value = AppThemeMode.values[ + (_prefs?.getInt(_themeModeKey) ?? 0).clamp(0, AppThemeMode.values.length - 1) + ]; + _themeColorIndex.value = _prefs?.getInt(_themeColorIndexKey) ?? 0; + _accentColorIndex.value = _prefs?.getInt(_accentColorIndexKey) ?? 0; + _fontSizeIndex.value = _prefs?.getInt(_fontSizeIndexKey) ?? 1; + _enableAnimation.value = _prefs?.getBool(_enableAnimationKey) ?? true; + _enableBlurEffect.value = _prefs?.getBool(_enableBlurEffectKey) ?? true; + + // 应用主题模式 + _applyThemeMode(); + } + + /// 保存主题设置 + Future _saveThemeSettings() async { + _prefs ??= await SharedPreferences.getInstance(); + + await _prefs?.setBool(_darkModeKey, _isDarkMode.value); + await _prefs?.setInt(_themeModeKey, _themeMode.value.index); + await _prefs?.setInt(_themeColorIndexKey, _themeColorIndex.value); + await _prefs?.setInt(_accentColorIndexKey, _accentColorIndex.value); + await _prefs?.setInt(_fontSizeIndexKey, _fontSizeIndex.value); + await _prefs?.setBool(_enableAnimationKey, _enableAnimation.value); + await _prefs?.setBool(_enableBlurEffectKey, _enableBlurEffect.value); + } + + /// 应用主题模式到 GetX + void _applyThemeMode() { + Get.changeThemeMode(currentThemeMode); + } + + /// 切换深色模式 + /// [enabled] true 开启深色模式, false 关闭深色模式 + Future toggleDarkMode(bool enabled) async { + if (_isDarkMode.value == enabled) return; + + _isDarkMode.value = enabled; + await _saveThemeSettings(); + _applyThemeMode(); + + // 显示提示 + Get.snackbar( + '主题切换', + enabled ? '已切换到深色模式 🌙' : '已切换到浅色模式 ☀️', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 2), + margin: const EdgeInsets.all(16), + borderRadius: 12, + ); + } + + /// 设置主题模式 + Future setThemeMode(AppThemeMode mode) async { + if (_themeMode.value == mode) return; + + _themeMode.value = mode; + await _saveThemeSettings(); + _applyThemeMode(); + } + + /// 切换主题模式(循环切换: system -> light -> dark -> system) + Future cycleThemeMode() async { + final currentIndex = _themeMode.value.index; + final nextIndex = (currentIndex + 1) % AppThemeMode.values.length; + await setThemeMode(AppThemeMode.values[nextIndex]); + } + + /// 设置主题色索引 + Future setThemeColorIndex(int index) async { + if (_themeColorIndex.value == index) return; + + _themeColorIndex.value = index; + await _saveThemeSettings(); + } + + /// 设置强调色索引 + Future setAccentColorIndex(int index) async { + if (_accentColorIndex.value == index) return; + + _accentColorIndex.value = index; + await _saveThemeSettings(); + } + + /// 设置字体大小索引 + Future setFontSizeIndex(int index) async { + if (_fontSizeIndex.value == index) return; + + _fontSizeIndex.value = index; + await _saveThemeSettings(); + } + + /// 切换动画效果 + Future toggleAnimation(bool enabled) async { + if (_enableAnimation.value == enabled) return; + + _enableAnimation.value = enabled; + await _saveThemeSettings(); + } + + /// 切换模糊效果 + Future toggleBlurEffect(bool enabled) async { + if (_enableBlurEffect.value == enabled) return; + + _enableBlurEffect.value = enabled; + await _saveThemeSettings(); + } + + /// 获取主题配置 + ThemeConfig get themeConfig { + return ThemeConfig( + isDarkMode: _isDarkMode.value, + themeMode: _themeMode.value, + themeColorIndex: _themeColorIndex.value, + accentColorIndex: _accentColorIndex.value, + fontSizeIndex: _fontSizeIndex.value, + enableAnimation: _enableAnimation.value, + enableBlurEffect: _enableBlurEffect.value, + ); + } + + /// 应用完整主题配置 + Future applyThemeConfig(ThemeConfig config) async { + _isDarkMode.value = config.isDarkMode; + _themeMode.value = config.themeMode; + _themeColorIndex.value = config.themeColorIndex; + _accentColorIndex.value = config.accentColorIndex; + _fontSizeIndex.value = config.fontSizeIndex; + _enableAnimation.value = config.enableAnimation; + _enableBlurEffect.value = config.enableBlurEffect; + + await _saveThemeSettings(); + _applyThemeMode(); + } + + /// 重置为默认主题设置 + Future resetToDefault() async { + _isDarkMode.value = false; + _themeMode.value = AppThemeMode.system; + _themeColorIndex.value = 0; + _accentColorIndex.value = 0; + _fontSizeIndex.value = 1; + _enableAnimation.value = true; + _enableBlurEffect.value = true; + + await _saveThemeSettings(); + _applyThemeMode(); + + Get.snackbar( + '主题重置', + '已恢复默认主题设置', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 2), + margin: const EdgeInsets.all(16), + borderRadius: 12, + ); + } + + /// 判断当前是否为深色模式(考虑系统设置) + bool isDarkModeEffective(BuildContext context) { + if (_isDarkMode.value) return true; + if (_themeMode.value == AppThemeMode.dark) return true; + if (_themeMode.value == AppThemeMode.system) { + return MediaQuery.platformBrightnessOf(context) == Brightness.dark; + } + return false; + } +} diff --git a/lib/services/wakelock_service.dart b/lib/services/isweb/wakelock_service.dart similarity index 73% rename from lib/services/wakelock_service.dart rename to lib/services/isweb/wakelock_service.dart index 7283aae..88728c4 100644 --- a/lib/services/wakelock_service.dart +++ b/lib/services/isweb/wakelock_service.dart @@ -1,4 +1,5 @@ -import 'wakelock_service_web.dart' if (dart.library.io) 'wakelock_service_io.dart'; +import 'wakelock_service_web.dart' + if (dart.library.io) 'wakelock_service_io.dart'; abstract class WakelockService { static final WakelockService instance = getWakelockService(); diff --git a/lib/services/wakelock_service_io.dart b/lib/services/isweb/wakelock_service_io.dart similarity index 100% rename from lib/services/wakelock_service_io.dart rename to lib/services/isweb/wakelock_service_io.dart diff --git a/lib/services/wakelock_service_web.dart b/lib/services/isweb/wakelock_service_web.dart similarity index 100% rename from lib/services/wakelock_service_web.dart rename to lib/services/isweb/wakelock_service_web.dart diff --git a/lib/utils/app_initializer.dart b/lib/utils/app_initializer.dart index 440f353..b886644 100644 --- a/lib/utils/app_initializer.dart +++ b/lib/utils/app_initializer.dart @@ -4,6 +4,7 @@ import '../config/app_config.dart'; import '../routes/app_routes.dart'; import 'screen_adapter.dart'; import 'force_guide_checker.dart'; +import 'audio_manager.dart'; class AppInitializer { static const bool _isDebugMode = @@ -29,6 +30,9 @@ class AppInitializer { _setupScreenAdapter(); + // 初始化音频管理器 + await AudioManager().init(); + final initialRoute = await _getInitialRoute(prefs); return InitializationResult( diff --git a/lib/utils/audio_manager.dart b/lib/utils/audio_manager.dart index cc602cb..c6b1144 100644 --- a/lib/utils/audio_manager.dart +++ b/lib/utils/audio_manager.dart @@ -4,12 +4,13 @@ import 'package:shared_preferences/shared_preferences.dart'; /// 时间: 2026-03-30 /// 功能: 音频管理类 /// 介绍: 管理应用中的音效播放,包括点击音效、点赞音效等 -/// 最新变化: 使用最简单的音频播放方式 +/// 最新变化: 重写声音播放逻辑,确保声音正常播放,不依赖GlobalAudioScope class AudioManager { static AudioManager? _instance; bool _isInitialized = false; bool _isMuted = false; + AudioPlayer? _player; AudioManager._internal(); @@ -23,9 +24,13 @@ class AudioManager { try { await _loadSoundSetting(); + // 不依赖GlobalAudioScope的初始化,直接创建AudioPlayer + _player = AudioPlayer(); _isInitialized = true; + print('AudioManager初始化成功'); } catch (e) { - // 初始化失败 + print('AudioManager初始化失败: $e'); + _isInitialized = true; } } @@ -33,10 +38,12 @@ class AudioManager { Future _loadSoundSetting() async { try { final prefs = await SharedPreferences.getInstance(); - bool soundEnabled = prefs.getBool('sound_enabled') ?? false; + bool soundEnabled = prefs.getBool('sound_enabled') ?? true; _isMuted = !soundEnabled; + print('声音设置: soundEnabled=$soundEnabled, _isMuted=$_isMuted'); } catch (e) { - // 加载设置失败 + print('加载声音设置失败: $e'); + _isMuted = false; } } @@ -57,27 +64,43 @@ class AudioManager { /// 播放音频 void _playSound(String assetPath) { - if (_isMuted) return; + if (_isMuted) { + print('声音已静音,不播放: $assetPath'); + return; + } + print('播放声音: $assetPath'); _playSoundAsync(assetPath); } /// 异步播放音频 - void _playSoundAsync(String assetPath) async { + Future _playSoundAsync(String assetPath) async { try { - final player = AudioPlayer(); - await player.play(AssetSource(assetPath)); + // 确保player已初始化 + if (_player == null) { + _player = AudioPlayer(); + } - player.onPlayerComplete.listen((_) { - player.dispose(); - }); + // 播放音频 + await _player!.play(AssetSource(assetPath)); + print('声音播放成功: $assetPath'); } catch (e) { - // 播放失败 + print('声音播放失败: $assetPath, 错误: $e'); + // 如果播放失败,尝试重新创建player + try { + _player?.dispose(); + _player = AudioPlayer(); + await _player!.play(AssetSource(assetPath)); + print('声音重播成功: $assetPath'); + } catch (retryError) { + print('声音重播失败: $assetPath, 错误: $retryError'); + } } } /// 设置静音状态 void setMuted(bool muted) { _isMuted = muted; + print('设置静音状态: $_isMuted'); } /// 获取静音状态 diff --git a/lib/views/active/active_search_page.dart b/lib/views/active/active_search_page.dart index 499e536..e24efb8 100644 --- a/lib/views/active/active_search_page.dart +++ b/lib/views/active/active_search_page.dart @@ -1,19 +1,20 @@ // 时间: 2026-03-22 // 功能: 全站诗词搜索页(收藏入口可跳转) // 介绍: 调用 search.php,配合 NetworkListenerService 上报加载与搜索完成事件 -// 最新变化: 初始版本 +// 最新变化: 2026-04-02 支持深色模式 import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; import '../../services/network_listener_service.dart'; +import '../../services/get/theme_controller.dart'; import '../../utils/http/poetry_api.dart'; -import '../main_navigation.dart'; +import '../../widgets/main_navigation.dart'; /// 诗词搜索页(独立路由栈页面) class ActiveSearchPage extends StatefulWidget { const ActiveSearchPage({super.key, this.initialQuery}); - /// 从收藏页带入的预填关键词 final String? initialQuery; @override @@ -26,8 +27,8 @@ class _ActiveSearchPageState extends State final TextEditingController _controller = TextEditingController(); final FocusNode _focusNode = FocusNode(); + final ThemeController _themeController = Get.find(); - /// 空=不限字段;name / keywords / introduce 见 API 文档 String _field = ''; int _page = 1; final int _pageSize = 20; @@ -106,258 +107,336 @@ class _ActiveSearchPageState extends State Widget build(BuildContext context) { final loading = isNetworkLoading(_loadKey); - // 检查是否在 TabBarView 中显示(通过上下文判断) - final bool isInTabBarView = ModalRoute.of(context)?.settings.name == null; + bool isInTabBarView = false; + try { + final ancestor = context.findAncestorWidgetOfExactType(); + isInTabBarView = ancestor != null; + } catch (e) { + isInTabBarView = false; + } - return Scaffold( - backgroundColor: Colors.grey[50], - // 当在 TabBarView 中时显示自定义标题栏,在单独页面中不显示 - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 自定义标题栏 - if (isInTabBarView) - SafeArea( - child: Container( - height: 56, - padding: const EdgeInsets.symmetric(horizontal: 16), - color: Colors.white, - child: Row( + return Obx(() { + final isDark = _themeController.isDarkModeRx.value; + + return Scaffold( + backgroundColor: isDark ? const Color(0xFF121212) : Colors.grey[50], + appBar: !isInTabBarView + ? AppBar( + title: Row( children: [ - // 返回按钮 - //todo - IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black87), - onPressed: () { - // 检查是否可以返回,避免黑屏 - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } else { - // 如果无法返回(如在 TabBarView 中),跳转到主页 - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => const MainNavigation(), - ), - ); - } - }, - tooltip: '返回上一页', + Icon( + Icons.travel_explore, + size: 20, + color: isDark ? Colors.white : Colors.black87, ), - // 标题 - Expanded( - child: Row( - children: [ - const Icon( - Icons.travel_explore, - size: 20, - color: Colors.black87, + const SizedBox(width: 8), + Text( + '诗词搜索', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), + ), + ], + ), + backgroundColor: isDark ? Colors.grey[900] : Colors.white, + elevation: 1, + actions: [ + IconButton( + icon: Icon( + Icons.more_vert, + color: isDark ? Colors.white : Colors.black87, + ), + onPressed: () { + showModalBottomSheet( + context: context, + backgroundColor: isDark + ? Colors.grey[850] + : Colors.white, + builder: (context) => Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon( + Icons.history, + color: isDark + ? Colors.grey[300] + : Colors.black87, + ), + title: Text( + '搜索历史(开发中)', + style: TextStyle( + color: isDark + ? Colors.white + : Colors.black87, + ), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon( + Icons.settings, + color: isDark + ? Colors.grey[300] + : Colors.black87, + ), + title: Text( + '搜索设置(开发中)', + style: TextStyle( + color: isDark + ? Colors.white + : Colors.black87, + ), + ), + onTap: () { + Navigator.pop(context); + }, + ), + ], ), - const SizedBox(width: 8), - const Text( - '诗词搜索', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.black87, + ), + ); + }, + tooltip: '更多', + ), + ], + ) + : null, + body: SafeArea( + top: !isInTabBarView, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: EdgeInsets.fromLTRB( + AppConstants.pageHorizontalPadding, + 8, + AppConstants.pageHorizontalPadding, + 4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Material( + color: isDark ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(12), + child: TextField( + controller: _controller, + focusNode: _focusNode, + textInputAction: TextInputAction.search, + onSubmitted: (_) => _runSearch(reset: true), + style: TextStyle( + color: isDark ? Colors.white : Colors.black87, + ), + decoration: InputDecoration( + hintText: '输入关键词,搜标题 / 标签 / 译文…', + hintStyle: TextStyle( + color: isDark + ? Colors.grey[500] + : Colors.grey, + ), + prefixIcon: Icon( + Icons.search, + color: isDark + ? Colors.grey[400] + : Colors.grey, + ), + suffixIcon: _controller.text.isNotEmpty + ? IconButton( + icon: Icon( + Icons.clear, + color: isDark + ? Colors.grey[400] + : Colors.grey, + ), + onPressed: () { + _controller.clear(); + setState(() { + _items = []; + _total = 0; + _error = null; + }); + }, + ) + : IconButton( + icon: Icon( + Icons.arrow_forward, + color: isDark + ? Colors.grey[400] + : Colors.grey, + ), + tooltip: '搜索', + onPressed: () => + _runSearch(reset: true), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + ), + onChanged: (_) => setState(() {}), ), ), + Wrap( + spacing: 4, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ChoiceChip( + label: Text( + '全部', + style: TextStyle( + color: _field.isEmpty + ? Colors.white + : (isDark + ? Colors.grey[300] + : Colors.black87), + ), + ), + selected: _field.isEmpty, + selectedColor: AppConstants.primaryColor, + backgroundColor: isDark + ? Colors.grey[800] + : Colors.grey[200], + onSelected: (_) { + setState(() => _field = ''); + if (_controller.text.trim().isNotEmpty) { + _runSearch(reset: true); + } + }, + ), + ChoiceChip( + label: Text( + '标题', + style: TextStyle( + color: _field == 'name' + ? Colors.white + : (isDark + ? Colors.grey[300] + : Colors.black87), + ), + ), + selected: _field == 'name', + selectedColor: AppConstants.primaryColor, + backgroundColor: isDark + ? Colors.grey[800] + : Colors.grey[200], + onSelected: (_) { + setState(() => _field = 'name'); + if (_controller.text.trim().isNotEmpty) { + _runSearch(reset: true); + } + }, + ), + ChoiceChip( + label: Text( + '标签', + style: TextStyle( + color: _field == 'keywords' + ? Colors.white + : (isDark + ? Colors.grey[300] + : Colors.black87), + ), + ), + selected: _field == 'keywords', + selectedColor: AppConstants.primaryColor, + backgroundColor: isDark + ? Colors.grey[800] + : Colors.grey[200], + onSelected: (_) { + setState(() => _field = 'keywords'); + if (_controller.text.trim().isNotEmpty) { + _runSearch(reset: true); + } + }, + ), + ChoiceChip( + label: Text( + '译文', + style: TextStyle( + color: _field == 'introduce' + ? Colors.white + : (isDark + ? Colors.grey[300] + : Colors.black87), + ), + ), + selected: _field == 'introduce', + selectedColor: AppConstants.primaryColor, + backgroundColor: isDark + ? Colors.grey[800] + : Colors.grey[200], + onSelected: (_) { + setState(() => _field = 'introduce'); + if (_controller.text.trim().isNotEmpty) { + _runSearch(reset: true); + } + }, + ), + ], + ), + if (_error != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + _error!, + style: const TextStyle( + color: Colors.red, + fontSize: 13, + ), + ), + ), ], ), ), - // 更多按钮 - IconButton( - icon: const Icon(Icons.more_vert, color: Colors.black87), - onPressed: () { - // 更多按钮的点击事件 - showModalBottomSheet( - context: context, - builder: (context) => Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.history), - title: const Text('搜索历史(开发中)'), - onTap: () { - Navigator.pop(context); - // 实现搜索历史功能 - }, - ), - ListTile( - leading: const Icon(Icons.settings), - title: const Text('搜索设置(开发中)'), - onTap: () { - Navigator.pop(context); - // 实现搜索设置功能 - }, - ), - ], - ), - ), - ); - }, - tooltip: '更多', + Expanded( + child: RefreshIndicator( + color: AppConstants.primaryColor, + onRefresh: () async { + await _runSearch(reset: true); + }, + child: _buildListBody(loading, isDark), + ), ), ], ), ), - ), - // 搜索内容区域 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.fromLTRB( - AppConstants.pageHorizontalPadding, - isInTabBarView ? 8 : 4, - AppConstants.pageHorizontalPadding, - 4, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Material( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12), - child: TextField( - controller: _controller, - focusNode: _focusNode, - textInputAction: TextInputAction.search, - onSubmitted: (_) => _runSearch(reset: true), - decoration: InputDecoration( - hintText: '输入关键词,搜标题 / 标签 / 译文…', - prefixIcon: const Icon( - Icons.search, - color: Colors.grey, - ), - suffixIcon: _controller.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _controller.clear(); - setState(() { - _items = []; - _total = 0; - _error = null; - }); - }, - ) - : IconButton( - icon: const Icon(Icons.arrow_forward), - tooltip: '搜索', - onPressed: () => _runSearch(reset: true), - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - ), - onChanged: (_) => setState(() {}), - ), - ), - Wrap( - spacing: 4, - runSpacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - const Text('', style: TextStyle(fontSize: 13)), - // const Text('范围:', style: TextStyle(fontSize: 13)), - ChoiceChip( - label: const Text('全部'), - selected: _field.isEmpty, - onSelected: (_) { - setState(() => _field = ''); - if (_controller.text.trim().isNotEmpty) { - _runSearch(reset: true); - } - }, - ), - ChoiceChip( - label: const Text('标题'), - selected: _field == 'name', - onSelected: (_) { - setState(() => _field = 'name'); - if (_controller.text.trim().isNotEmpty) { - _runSearch(reset: true); - } - }, - ), - ChoiceChip( - label: const Text('标签'), - selected: _field == 'keywords', - onSelected: (_) { - setState(() => _field = 'keywords'); - if (_controller.text.trim().isNotEmpty) { - _runSearch(reset: true); - } - }, - ), - ChoiceChip( - label: const Text('译文'), - selected: _field == 'introduce', - onSelected: (_) { - setState(() => _field = 'introduce'); - if (_controller.text.trim().isNotEmpty) { - _runSearch(reset: true); - } - }, - ), - ], - ), - if (_error != null) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - _error!, - style: const TextStyle( - color: Colors.red, - fontSize: 13, - ), - ), - ), - ], - ), - ), - Expanded( - child: RefreshIndicator( - color: AppConstants.primaryColor, - onRefresh: () async { - await _runSearch(reset: true); - }, - child: _buildListBody(loading), - ), - ), - ], - ), + ], ), - ], - ), - // 始终显示返回按钮 - // todo 二次黑屏处理 标记 - floatingActionButton: FloatingActionButton( - onPressed: () { - // 检查是否可以返回,避免黑屏 - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } else { - // 如果无法返回,跳转到主页 - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const MainNavigation()), - ); - } - }, - backgroundColor: AppConstants.primaryColor, - foregroundColor: Colors.white, - child: const Icon(Icons.arrow_back), - tooltip: '返回上一页', - ), - ); + ), + floatingActionButton: !isInTabBarView + ? FloatingActionButton( + onPressed: () { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } else { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const MainNavigation()), + ); + } + }, + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + tooltip: '返回上一页', + child: const Icon(Icons.arrow_back), + ) + : null, + ); + }); } - Widget _buildListBody(bool loading) { + Widget _buildListBody(bool loading, bool isDark) { if (loading && _items.isEmpty) { return ListView( physics: const AlwaysScrollableScrollPhysics(), @@ -373,12 +452,19 @@ class _ActiveSearchPageState extends State physics: const AlwaysScrollableScrollPhysics(), children: [ SizedBox(height: MediaQuery.of(context).size.height * 0.15), - Icon(Icons.manage_search, size: 64, color: Colors.grey[400]), + Icon( + Icons.manage_search, + size: 64, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), const SizedBox(height: 12), Center( child: Text( _controller.text.trim().isEmpty ? '输入关键词开始搜索' : '暂无结果', - style: TextStyle(color: Colors.grey[600], fontSize: 16), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[600], + fontSize: 16, + ), ), ), ], @@ -402,8 +488,16 @@ class _ActiveSearchPageState extends State ? const CircularProgressIndicator() : TextButton.icon( onPressed: () => _runSearch(reset: false), - icon: const Icon(Icons.expand_more), - label: const Text('加载更多'), + icon: Icon( + Icons.expand_more, + color: isDark ? Colors.grey[400] : Colors.black87, + ), + label: Text( + '加载更多', + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.black87, + ), + ), ), ), ); @@ -412,6 +506,7 @@ class _ActiveSearchPageState extends State return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 1, + color: isDark ? Colors.grey[850] : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -424,7 +519,10 @@ class _ActiveSearchPageState extends State p.name.isNotEmpty ? p.name : p.url, maxLines: 2, overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.w600), + style: TextStyle( + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), ), subtitle: Padding( padding: const EdgeInsets.only(top: 6), @@ -434,19 +532,28 @@ class _ActiveSearchPageState extends State if (p.alias.isNotEmpty) Text( '📜 ${p.alias}', - style: TextStyle(fontSize: 12, color: Colors.grey[700]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[700], + ), ), if (p.introduce.isNotEmpty) Text( p.introduce, maxLines: 3, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 13, color: Colors.grey[800]), + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[300] : Colors.grey[800], + ), ), const SizedBox(height: 4), Text( '👍 ${p.like} · 🔥 ${p.hitsTotal}', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), diff --git a/lib/views/active/category_page.dart b/lib/views/active/category_page.dart index 1360831..4c820eb 100644 --- a/lib/views/active/category_page.dart +++ b/lib/views/active/category_page.dart @@ -1,156 +1,95 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; -import 'tags/corr_page.dart'; +import '../../services/get/category_controller.dart'; +import '../../services/get/theme_controller.dart'; /// 时间: 2026-04-01 /// 功能: 分类页面 /// 介绍: 展示诗词分类,包括场景分类和朝代分类 -/// 最新变化: 重新设计iOS风格布局,减少间距,加大字体,显示分类数量 +/// 最新变化: 2026-04-02 支持深色模式 -class CategoryPage extends StatefulWidget { +class CategoryPage extends StatelessWidget { const CategoryPage({super.key}); - @override - State createState() => _CategoryPageState(); -} - -class _CategoryPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - final List> _tabCategories = [ - {'label': '场景分类', 'icon': Icons.category}, - {'label': '朝代分类', 'icon': Icons.history}, - ]; - - static const sceneData = { - "节日": ["七夕节", "中秋节", "元宵节", "寒食节", "清明节", "端午节", "重阳节", "春节", "节日"], - "季节": ["三月", "二月", "冬天", "夏天", "春天", "春季", "秋天"], - "古籍": [ - "三国志", - "三国演义", - "三字经", - "中庸", - "列子", - "史记", - "后汉书", - "吕氏春秋", - "商君书", - "围炉夜话", - "增广贤文", - "墨子", - "孙子兵法", - "孟子", - "小窗幽记", - "尚书", - "左传", - "幼学琼林", - "庄子", - "战国策", - "文心雕龙", - "易传", - "晋书", - "汉书", - "淮南子", - "礼记", - "管子", - "红楼梦", - "老子", - "荀子", - "菜根谭", - "警世通言", - "论语", - "资治通鉴", - "韩非子", - "鬼谷子", - "古籍", - "格言联璧", - ], - "情感": ["伤感", "励志", "友情", "思乡", "思念", "感恩", "爱国", "爱情", "离别"], - "景物": ["庐山", "泰山", "西湖", "长江", "黄河", "边塞", "田园", "山水", "夜景"], - "天文气象": ["写云", "写雨", "写雪", "写风", "星星", "月亮", "流星"], - "动植物": ["写鸟", "柳树", "桃花", "梅花", "竹子", "荷花", "菊花"], - "语言文学": ["对联", "谚语", "一言", "读书", "哲理"], - "其他": ["母亲", "老师", "户外", "礼物", "酒"], - }; - - static const dynastyData = { - "主要朝代": ["唐代", "宋代", "元代", "明代", "清代"], - "古代朝代": ["南北朝", "五代", "隋代"], - "近现代": ["近现代", "用户投稿", "管理员测试"], - "其他": ["暂无朝代"], - }; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: _tabCategories.length, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return Column( - children: [ - // Tab栏 - Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TabBar( - controller: _tabController, - tabs: _tabCategories - .map( - (category) => Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(category['icon'], size: 18), - const SizedBox(width: 6), - Text(category['label']), - ], + final controller = Get.put(CategoryController()); + final themeController = Get.find(); + + return Obx(() { + final isDark = themeController.isDarkModeRx.value; + + return Column( + children: [ + Container( + color: isDark ? Colors.grey[900] : Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TabBar( + controller: controller.tabController, + tabs: controller.tabCategories + .map( + (category) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(category['icon'] as IconData, size: 18), + const SizedBox(width: 6), + Text(category['label'] as String), + ], + ), ), + ) + .toList(), + labelColor: AppConstants.primaryColor, + unselectedLabelColor: isDark + ? Colors.grey[400] + : Colors.grey[600], + indicatorColor: AppConstants.primaryColor, + indicatorWeight: 3, + labelStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 16, + ), + ), + ), + Container( + height: 0.5, + color: isDark ? Colors.grey[800] : const Color(0xFFE5E5EA), + ), + Expanded( + child: Container( + color: isDark ? const Color(0xFF121212) : const Color(0xFFF2F2F7), + child: TabBarView( + controller: controller.tabController, + children: [ + _buildCategoryList( + controller.sceneData, + controller.tabCategories[0]['label'] as String, + isDark, ), - ) - .toList(), - labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppConstants.primaryColor, - indicatorWeight: 3, - labelStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - ), - unselectedLabelStyle: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 16, + _buildCategoryList( + controller.dynastyData, + controller.tabCategories[1]['label'] as String, + isDark, + ), + ], + ), ), ), - ), - Container(height: 0.5, color: const Color(0xFFE5E5EA)), - // 内容区域 - Expanded( - child: Container( - color: const Color(0xFFF2F2F7), - child: TabBarView( - controller: _tabController, - children: [ - _buildCategoryList(sceneData, _tabCategories[0]['label']), - _buildCategoryList(dynastyData, _tabCategories[1]['label']), - ], - ), - ), - ), - ], - ); + ], + ); + }); } Widget _buildCategoryList( Map> data, String categoryType, + bool isDark, ) { return ListView.separated( padding: const EdgeInsets.symmetric(vertical: 8), @@ -163,11 +102,11 @@ class _CategoryPageState extends State return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withAlpha(isDark ? 0 : 10), blurRadius: 8, offset: const Offset(0, 2), ), @@ -186,10 +125,10 @@ class _CategoryPageState extends State Expanded( child: Text( category, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), ), ), @@ -199,9 +138,7 @@ class _CategoryPageState extends State vertical: 4, ), decoration: BoxDecoration( - color: AppConstants.primaryColor.withValues( - alpha: 0.1, - ), + color: AppConstants.primaryColor.withAlpha(26), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -220,7 +157,7 @@ class _CategoryPageState extends State spacing: 10, runSpacing: 10, children: items.map((item) { - return _buildCategoryChip(item, categoryType); + return _buildCategoryChip(item, categoryType, isDark); }).toList(), ), ], @@ -232,24 +169,20 @@ class _CategoryPageState extends State ); } - Widget _buildCategoryChip(String label, String categoryType) { + Widget _buildCategoryChip(String label, String categoryType, bool isDark) { + final controller = Get.find(); + return GestureDetector( onTap: () { - final searchType = categoryType == '朝代分类' ? 'alias' : 'keywords'; - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => CorrPage(label: label, searchType: searchType), - ), - ); + controller.navigateToCategoryDetail(label, categoryType); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - color: AppConstants.primaryColor.withValues(alpha: 0.1), + color: AppConstants.primaryColor.withAlpha(26), borderRadius: BorderRadius.circular(20), border: Border.all( - color: AppConstants.primaryColor.withValues(alpha: 0.3), + color: AppConstants.primaryColor.withAlpha(77), width: 1, ), ), diff --git a/lib/views/active/popular_page.dart b/lib/views/active/popular_page.dart index 9ace848..2ee3098 100644 --- a/lib/views/active/popular_page.dart +++ b/lib/views/active/popular_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../constants/app_constants.dart'; +import '../../config/app_config.dart'; import '../../utils/http/http_client.dart'; import '../../models/poetry_model.dart'; import '../../controllers/load/locally.dart'; @@ -156,7 +157,13 @@ class _PopularPageState extends State return false; }, child: ListView.builder( - padding: const EdgeInsets.all(16), + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: AppConfig.liquidGlassTotalHeight + 16, + ), itemCount: _rankList.length + (_showBottomIndicator ? 1 : 0), itemBuilder: (context, index) { if (index == _rankList.length) { diff --git a/lib/views/active/rate.dart b/lib/views/active/rate.dart index b5cde2c..82f69ec 100644 --- a/lib/views/active/rate.dart +++ b/lib/views/active/rate.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; +import '../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 活跃页面 /// 介绍: 展示用户活跃度热力图,参考 GitHub 贡献图样式 -/// 最新变化: 添加调试选项,修复布局溢出,调整活跃阈值颜色 +/// 最新变化: 2026-04-02 支持深色模式 class RatePage extends StatefulWidget { const RatePage({super.key}); @@ -17,13 +19,12 @@ class _RatePageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final List _tabs = ['周活跃', '月活跃']; + final ThemeController _themeController = Get.find(); - // 调试参数 int _debugDayCount = 7; int _debugBarCount = 7; bool _showDebugPanel = false; - // 模拟活跃度数据 (实际条数) List _weekData = []; List> _monthData = []; @@ -40,10 +41,8 @@ class _RatePageState extends State super.dispose(); } - // 生成模拟数据 void _generateMockData() { _weekData = List.generate(_debugDayCount, (index) { - // 生成 0-150 的随机活跃值 return [0, 3, 8, 15, 35, 80, 150][index % 7]; }); @@ -56,46 +55,45 @@ class _RatePageState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: Column( - children: [ - // Tab 切换栏 - Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border(bottom: BorderSide(color: Colors.grey[200]!)), + return Obx(() { + final isDark = _themeController.isDarkModeRx.value; + + return Scaffold( + backgroundColor: isDark ? const Color(0xFF121212) : Theme.of(context).scaffoldBackgroundColor, + body: Column( + children: [ + Container( + decoration: BoxDecoration( + color: isDark ? Colors.grey[900] : Colors.white, + border: Border(bottom: BorderSide(color: isDark ? Colors.grey[800]! : Colors.grey[200]!)), + ), + child: TabBar( + controller: _tabController, + tabs: _tabs.map((tab) => Tab(text: tab)).toList(), + labelColor: AppConstants.primaryColor, + unselectedLabelColor: isDark ? Colors.grey[400] : Colors.grey[600], + indicatorColor: AppConstants.primaryColor, + indicatorWeight: 2, + labelStyle: const TextStyle(fontWeight: FontWeight.w600), + ), ), - child: TabBar( - controller: _tabController, - tabs: _tabs.map((tab) => Tab(text: tab)).toList(), - labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppConstants.primaryColor, - indicatorWeight: 2, - labelStyle: const TextStyle(fontWeight: FontWeight.w600), + _buildDebugToggle(isDark), + if (_showDebugPanel) _buildDebugPanel(isDark), + Expanded( + child: TabBarView( + controller: _tabController, + children: [_buildWeekView(isDark), _buildMonthView(isDark)], + ), ), - ), - // 调试开关 - _buildDebugToggle(), - // 调试面板 - if (_showDebugPanel) _buildDebugPanel(), - // 内容区域 - Expanded( - child: TabBarView( - controller: _tabController, - children: [_buildWeekView(), _buildMonthView()], - ), - ), - ], - ), - ); + ], + ), + ); + }); } - // 调试开关 - Widget _buildDebugToggle() { + Widget _buildDebugToggle(bool isDark) { return Container( - color: Colors.orange[50], + color: isDark ? Colors.orange[900]!.withAlpha(50) : Colors.orange[50], padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ @@ -130,21 +128,20 @@ class _RatePageState extends State ); } - // 调试面板 - Widget _buildDebugPanel() { + Widget _buildDebugPanel(bool isDark) { return Container( - color: Colors.orange[50], + color: isDark ? Colors.orange[900]!.withAlpha(50) : Colors.orange[50], padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - // 天数调节 _buildDebugSlider( label: '天数', value: _debugDayCount.toDouble(), min: 3, max: 31, + isDark: isDark, onChanged: (value) { setState(() { _debugDayCount = value.round(); @@ -153,12 +150,12 @@ class _RatePageState extends State }, ), const SizedBox(height: 12), - // 条数调节 _buildDebugSlider( label: '条数', value: _debugBarCount.toDouble(), min: 3, max: 14, + isDark: isDark, onChanged: (value) { setState(() { _debugBarCount = value.round(); @@ -167,11 +164,10 @@ class _RatePageState extends State }, ), const SizedBox(height: 16), - // 活跃阈值说明 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange[200]!), ), @@ -189,19 +185,22 @@ class _RatePageState extends State _buildThresholdItem( '1-5', '浅色', - AppConstants.primaryColor.withValues(alpha: 0.3), + AppConstants.primaryColor.withAlpha(77), + isDark, ), _buildThresholdItem( '6-20', '中浅色', - AppConstants.primaryColor.withValues(alpha: 0.5), + AppConstants.primaryColor.withAlpha(128), + isDark, ), _buildThresholdItem( '21-100', '中深色', - AppConstants.primaryColor.withValues(alpha: 0.7), + AppConstants.primaryColor.withAlpha(179), + isDark, ), - _buildThresholdItem('100+', '最深色', AppConstants.primaryColor), + _buildThresholdItem('100+', '最深色', AppConstants.primaryColor, isDark), ], ), ), @@ -210,12 +209,12 @@ class _RatePageState extends State ); } - // 调试滑块 Widget _buildDebugSlider({ required String label, required double value, required double min, required double max, + required bool isDark, required ValueChanged onChanged, }) { return Row( @@ -253,8 +252,7 @@ class _RatePageState extends State ); } - // 阈值项 - Widget _buildThresholdItem(String range, String label, Color color) { + Widget _buildThresholdItem(String range, String label, Color color, bool isDark) { return Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( @@ -270,15 +268,14 @@ class _RatePageState extends State const SizedBox(width: 8), Text( '$range: $label', - style: TextStyle(fontSize: 12, color: Colors.grey[700]), + style: TextStyle(fontSize: 12, color: isDark ? Colors.grey[300] : Colors.grey[700]), ), ], ), ); } - // 周活跃视图 - Widget _buildWeekView() { + Widget _buildWeekView(bool isDark) { final weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; return SingleChildScrollView( @@ -286,17 +283,16 @@ class _RatePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('本周活跃趋势(生成图片分享)'), + _buildSectionTitle('本周活跃趋势(生成图片分享)', isDark), const SizedBox(height: 16), - // 热力图 - 使用 Wrap 防止溢出 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withAlpha(isDark ? 0 : 13), blurRadius: 10, offset: const Offset(0, 2), ), @@ -304,7 +300,6 @@ class _RatePageState extends State ), child: Column( children: [ - // 星期标签 Wrap( spacing: 8, runSpacing: 8, @@ -317,7 +312,7 @@ class _RatePageState extends State textAlign: TextAlign.center, style: TextStyle( fontSize: 11, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], fontWeight: FontWeight.w500, ), ), @@ -325,44 +320,42 @@ class _RatePageState extends State }), ), const SizedBox(height: 12), - // 活跃度方块 Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: List.generate(_weekData.length, (index) { - return _buildActivityBlock(_weekData[index], size: 40); + return _buildActivityBlock(_weekData[index], size: 40, isDark: isDark); }), ), ], ), ), const SizedBox(height: 16), - _buildStatsSection(), + _buildStatsSection(isDark), const SizedBox(height: 16), - _buildLegend(), + _buildLegend(isDark), ], ), ); } - // 月活跃视图 - Widget _buildMonthView() { + Widget _buildMonthView(bool isDark) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionTitle('本月活跃热力图'), + _buildSectionTitle('本月活跃热力图', isDark), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withAlpha(isDark ? 0 : 13), blurRadius: 10, offset: const Offset(0, 2), ), @@ -370,7 +363,6 @@ class _RatePageState extends State ), child: Column( children: [ - // 星期标签 Wrap( spacing: 4, alignment: WrapAlignment.center, @@ -383,7 +375,7 @@ class _RatePageState extends State textAlign: TextAlign.center, style: TextStyle( fontSize: 10, - color: Colors.grey[500], + color: isDark ? Colors.grey[500] : Colors.grey[500], ), ), ), @@ -391,19 +383,17 @@ class _RatePageState extends State .toList(), ), const SizedBox(height: 8), - // 四周热力图 - 使用 Wrap 防止溢出 ...List.generate(4, (weekIndex) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Wrap( spacing: 4, alignment: WrapAlignment.center, - children: List.generate(_monthData[weekIndex].length, ( - dayIndex, - ) { + children: List.generate(_monthData[weekIndex].length, (dayIndex) { return _buildActivityBlock( _monthData[weekIndex][dayIndex], size: 28, + isDark: isDark, ); }), ), @@ -413,16 +403,15 @@ class _RatePageState extends State ), ), const SizedBox(height: 16), - _buildStatsSection(), + _buildStatsSection(isDark), const SizedBox(height: 16), - _buildLegend(), + _buildLegend(isDark), ], ), ); } - // 活跃度方块 - Widget _buildActivityBlock(int value, {required double size}) { + Widget _buildActivityBlock(int value, {required double size, required bool isDark}) { return Tooltip( message: '活跃度: $value', child: Container( @@ -431,14 +420,14 @@ class _RatePageState extends State decoration: BoxDecoration( color: _getActivityColor(value), borderRadius: BorderRadius.circular(4), - border: value == 0 ? Border.all(color: Colors.grey[200]!) : null, + border: value == 0 ? Border.all(color: isDark ? Colors.grey[700]! : Colors.grey[200]!) : null, ), child: value > 0 ? Center( child: value >= 100 ? Icon( Icons.local_fire_department, - color: Colors.white.withValues(alpha: 0.9), + color: Colors.white.withAlpha(230), size: size * 0.6, ) : null, @@ -448,27 +437,21 @@ class _RatePageState extends State ); } - // 获取活跃度颜色 (根据新的阈值) Color _getActivityColor(int value) { if (value == 0) { return Colors.grey[100]!; } else if (value >= 1 && value <= 5) { - // 1-5: 浅色 - return AppConstants.primaryColor.withValues(alpha: 0.3); + return AppConstants.primaryColor.withAlpha(77); } else if (value >= 6 && value <= 20) { - // 6-20: 中浅色 - return AppConstants.primaryColor.withValues(alpha: 0.5); + return AppConstants.primaryColor.withAlpha(128); } else if (value >= 21 && value <= 100) { - // 21-100: 中深色 - return AppConstants.primaryColor.withValues(alpha: 0.7); + return AppConstants.primaryColor.withAlpha(179); } else { - // 100+: 最深色 return AppConstants.primaryColor; } } - // 标题组件 - Widget _buildSectionTitle(String title) { + Widget _buildSectionTitle(String title, bool isDark) { return Row( children: [ Container( @@ -482,22 +465,21 @@ class _RatePageState extends State const SizedBox(width: 8), Text( title, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), ), ], ); } - // 统计信息 - Widget _buildStatsSection() { + Widget _buildStatsSection(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withAlpha(isDark ? 0 : 13), blurRadius: 10, offset: const Offset(0, 2), ), @@ -506,64 +488,53 @@ class _RatePageState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildStatItem('活跃天数', '12', Icons.calendar_today), - _buildStatItem('连续活跃', '5', Icons.local_fire_department), - _buildStatItem('总活跃度', '89%', Icons.trending_up), + _buildStatItem('活跃天数', '12', Icons.calendar_today, isDark), + _buildStatItem('连续活跃', '5', Icons.local_fire_department, isDark), + _buildStatItem('总活跃度', '89%', Icons.trending_up, isDark), ], ), ); } - // 统计项 - Widget _buildStatItem(String label, String value, IconData icon) { + Widget _buildStatItem(String label, String value, IconData icon, bool isDark) { return Column( children: [ Icon(icon, color: AppConstants.primaryColor, size: 24), const SizedBox(height: 8), Text( value, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black), ), const SizedBox(height: 4), - Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])), + Text(label, style: TextStyle(fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey[600])), ], ); } - // 图例说明 - Widget _buildLegend() { + Widget _buildLegend(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '活跃度说明', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black), ), const SizedBox(height: 12), Wrap( spacing: 12, runSpacing: 8, children: [ - _buildLegendItem('无活跃', Colors.grey[100]!), - _buildLegendItem( - '1-5', - AppConstants.primaryColor.withValues(alpha: 0.3), - ), - _buildLegendItem( - '6-20', - AppConstants.primaryColor.withValues(alpha: 0.5), - ), - _buildLegendItem( - '21-100', - AppConstants.primaryColor.withValues(alpha: 0.7), - ), - _buildLegendItem('100+', AppConstants.primaryColor), + _buildLegendItem('无活跃', Colors.grey[100]!, isDark), + _buildLegendItem('1-5', AppConstants.primaryColor.withAlpha(77), isDark), + _buildLegendItem('6-20', AppConstants.primaryColor.withAlpha(128), isDark), + _buildLegendItem('21-100', AppConstants.primaryColor.withAlpha(179), isDark), + _buildLegendItem('100+', AppConstants.primaryColor, isDark), ], ), ], @@ -571,8 +542,7 @@ class _RatePageState extends State ); } - // 图例项 - Widget _buildLegendItem(String label, Color color) { + Widget _buildLegendItem(String label, Color color, bool isDark) { return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -582,13 +552,11 @@ class _RatePageState extends State decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), - border: label == '无活跃' - ? Border.all(color: Colors.grey[300]!) - : null, + border: label == '无活跃' ? Border.all(color: isDark ? Colors.grey[600]! : Colors.grey[300]!) : null, ), ), const SizedBox(width: 4), - Text(label, style: TextStyle(fontSize: 11, color: Colors.grey[600])), + Text(label, style: TextStyle(fontSize: 11, color: isDark ? Colors.grey[400] : Colors.grey[600])), ], ); } diff --git a/lib/views/active/tags/corr_page.dart b/lib/views/active/tags/corr_page.dart index 3988469..02fca57 100644 --- a/lib/views/active/tags/corr_page.dart +++ b/lib/views/active/tags/corr_page.dart @@ -1,15 +1,17 @@ /// 时间: 2026-04-01 /// 功能: 标签/朝代诗词列表页面 /// 介绍: 展示指定标签或朝代相关的诗词列表,支持搜索和浏览 -/// 最新变化: 新建页面,iOS风格设计,集成搜索API +/// 最新变化: 2026-04-02 支持深色模式 library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/http_client.dart'; import '../../../controllers/history_controller.dart'; import '../../../services/network_listener_service.dart'; +import '../../../services/get/theme_controller.dart'; /// 标签诗词列表页面 /// [label] 标签名称或朝代名称 @@ -42,6 +44,7 @@ class _CorrPageState extends State final ScrollController _scrollController = ScrollController(); late AnimationController _skeletonAnimationController; late Animation _skeletonAnimation; + final ThemeController _themeController = Get.find(); @override void initState() { @@ -207,33 +210,36 @@ class _CorrPageState extends State @override Widget build(BuildContext context) { - return AnnotatedRegion( - value: SystemUiOverlayStyle.dark, - child: Scaffold( - backgroundColor: const Color(0xFFF2F2F7), - appBar: _buildAppBar(), - body: _buildBody(), - ), - ); + return Obx(() { + final isDark = _themeController.isDarkModeRx.value; + + return AnnotatedRegion( + value: isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, + child: Scaffold( + backgroundColor: isDark + ? const Color(0xFF121212) + : const Color(0xFFF2F2F7), + appBar: _buildAppBar(isDark), + body: _buildBody(isDark), + ), + ); + }); } - PreferredSizeWidget _buildAppBar() { + PreferredSizeWidget _buildAppBar(bool isDark) { return AppBar( - backgroundColor: Colors.white, + backgroundColor: isDark ? Colors.grey[900] : Colors.white, elevation: 0, leading: IconButton( - icon: const Icon( - Icons.arrow_back_ios, - color: AppConstants.primaryColor, - ), + icon: Icon(Icons.arrow_back_ios, color: AppConstants.primaryColor), onPressed: () => Navigator.pop(context), ), title: Column( children: [ Text( widget.label, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: isDark ? Colors.white : Colors.black, fontSize: 17, fontWeight: FontWeight.w600, ), @@ -241,7 +247,7 @@ class _CorrPageState extends State Text( widget.searchType == 'alias' ? '朝代' : '标签', style: TextStyle( - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], fontSize: 12, fontWeight: FontWeight.normal, ), @@ -257,7 +263,7 @@ class _CorrPageState extends State child: Text( '$_totalCount 篇 / $_totalPages 页', style: TextStyle( - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], fontSize: 13, fontWeight: FontWeight.w500, ), @@ -267,22 +273,25 @@ class _CorrPageState extends State ], bottom: PreferredSize( preferredSize: const Size.fromHeight(0.5), - child: Container(height: 0.5, color: const Color(0xFFE5E5EA)), + child: Container( + height: 0.5, + color: isDark ? Colors.grey[800] : const Color(0xFFE5E5EA), + ), ), ); } - Widget _buildBody() { + Widget _buildBody(bool isDark) { if (_isLoading) { - return _buildSkeletonView(); + return _buildSkeletonView(isDark); } if (_errorMessage != null) { - return _buildErrorView(); + return _buildErrorView(isDark); } if (_poetryList.isEmpty) { - return _buildEmptyView(); + return _buildEmptyView(isDark); } return AnimatedSwitcher( @@ -297,33 +306,33 @@ class _CorrPageState extends State itemCount: _poetryList.length + (_hasMore ? 1 : 0), itemBuilder: (context, index) { if (index >= _poetryList.length) { - return _buildLoadingMoreIndicator(); + return _buildLoadingMoreIndicator(isDark); } - return _buildPoetryCard(_poetryList[index]); + return _buildPoetryCard(_poetryList[index], isDark); }, ), ), ); } - Widget _buildSkeletonView() { + Widget _buildSkeletonView(bool isDark) { return ListView.builder( padding: const EdgeInsets.all(16), itemCount: 5, itemBuilder: (context, index) { - return _buildSkeletonCard(); + return _buildSkeletonCard(isDark); }, ); } - Widget _buildSkeletonCard() { + Widget _buildSkeletonCard(bool isDark) { return AnimatedBuilder( animation: _skeletonAnimation, builder: (context, child) { return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), ), child: Padding( @@ -331,23 +340,44 @@ class _CorrPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSkeletonLine(180, 20, _skeletonAnimation.value), + _buildSkeletonLine(180, 20, _skeletonAnimation.value, isDark), const SizedBox(height: 8), _buildSkeletonLine( double.infinity, 16, _skeletonAnimation.value * 0.9, + isDark, ), const SizedBox(height: 6), - _buildSkeletonLine(250, 16, _skeletonAnimation.value * 0.8), + _buildSkeletonLine( + 250, + 16, + _skeletonAnimation.value * 0.8, + isDark, + ), const SizedBox(height: 6), - _buildSkeletonLine(200, 16, _skeletonAnimation.value * 0.7), + _buildSkeletonLine( + 200, + 16, + _skeletonAnimation.value * 0.7, + isDark, + ), const SizedBox(height: 12), Row( children: [ - _buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.6), + _buildSkeletonLine( + 60, + 16, + _skeletonAnimation.value * 0.6, + isDark, + ), const SizedBox(width: 16), - _buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.5), + _buildSkeletonLine( + 60, + 16, + _skeletonAnimation.value * 0.5, + isDark, + ), ], ), ], @@ -358,20 +388,26 @@ class _CorrPageState extends State ); } - Widget _buildSkeletonLine(double width, double height, double opacity) { + Widget _buildSkeletonLine( + double width, + double height, + double opacity, + bool isDark, + ) { + final baseColor = isDark + ? Colors.grey[700] ?? Colors.grey + : const Color(0xFFE5E5EA); return Container( width: width, height: height, decoration: BoxDecoration( - color: const Color( - 0xFFE5E5EA, - ).withValues(alpha: opacity.clamp(0.3, 0.8)), + color: baseColor.withAlpha((opacity.clamp(0.3, 0.8) * 255).toInt()), borderRadius: BorderRadius.circular(4), ), ); } - Widget _buildErrorView() { + Widget _buildErrorView(bool isDark) { return Center( child: Padding( padding: const EdgeInsets.all(24), @@ -382,7 +418,10 @@ class _CorrPageState extends State const SizedBox(height: 16), Text( _errorMessage ?? '加载失败', - style: TextStyle(color: Colors.grey[600], fontSize: 14), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[600], + fontSize: 14, + ), textAlign: TextAlign.center, ), const SizedBox(height: 24), @@ -407,28 +446,38 @@ class _CorrPageState extends State ); } - Widget _buildEmptyView() { + Widget _buildEmptyView(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.inbox_outlined, color: Colors.grey[400], size: 64), + Icon( + Icons.inbox_outlined, + color: isDark ? Colors.grey[500] : Colors.grey[400], + size: 64, + ), const SizedBox(height: 16), Text( '暂无相关诗词', - style: TextStyle(color: Colors.grey[600], fontSize: 14), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[600], + fontSize: 14, + ), ), const SizedBox(height: 8), Text( '试试其他标签或朝代', - style: TextStyle(color: Colors.grey[400], fontSize: 12), + style: TextStyle( + color: isDark ? Colors.grey[500] : Colors.grey[400], + fontSize: 12, + ), ), ], ), ); } - Widget _buildLoadingMoreIndicator() { + Widget _buildLoadingMoreIndicator(bool isDark) { return Padding( padding: const EdgeInsets.all(16), child: Center( @@ -444,7 +493,7 @@ class _CorrPageState extends State ); } - Widget _buildPoetryCard(Map poetry) { + Widget _buildPoetryCard(Map poetry, bool isDark) { final name = poetry['name']?.toString() ?? '未知标题'; final url = poetry['url']?.toString() ?? ''; final alias = poetry['alias']?.toString() ?? ''; @@ -455,11 +504,11 @@ class _CorrPageState extends State return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withAlpha(isDark ? 0 : 10), blurRadius: 8, offset: const Offset(0, 2), ), @@ -470,7 +519,7 @@ class _CorrPageState extends State borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), - onTap: () => _showPoetryDetail(poetry), + onTap: () => _showPoetryDetail(poetry, isDark), child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -481,10 +530,10 @@ class _CorrPageState extends State Expanded( child: Text( name, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), ), ), @@ -495,9 +544,7 @@ class _CorrPageState extends State vertical: 4, ), decoration: BoxDecoration( - color: AppConstants.primaryColor.withValues( - alpha: 0.1, - ), + color: AppConstants.primaryColor.withAlpha(26), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -515,7 +562,10 @@ class _CorrPageState extends State const SizedBox(height: 4), Text( url, - style: TextStyle(fontSize: 13, color: Colors.grey[600]), + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -526,7 +576,7 @@ class _CorrPageState extends State introduce, style: TextStyle( fontSize: 14, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], height: 1.5, ), maxLines: 3, @@ -539,23 +589,29 @@ class _CorrPageState extends State Icon( Icons.remove_red_eye_outlined, size: 16, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], ), const SizedBox(width: 4), Text( '$hitsTotal', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), const SizedBox(width: 16), Icon( Icons.favorite_outline, size: 16, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], ), const SizedBox(width: 4), Text( '$like', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), const Spacer(), FutureBuilder( @@ -569,7 +625,11 @@ class _CorrPageState extends State child: Icon( isLiked ? Icons.favorite : Icons.favorite_border, size: 20, - color: isLiked ? Colors.red : Colors.grey[400], + color: isLiked + ? Colors.red + : (isDark + ? Colors.grey[500] + : Colors.grey[400]), ), ); }, @@ -634,7 +694,7 @@ class _CorrPageState extends State } } - void _showPoetryDetail(Map poetry) { + void _showPoetryDetail(Map poetry, bool isDark) { final drtime = poetry['drtime']?.toString() ?? ''; final introduce = poetry['introduce']?.toString() ?? ''; @@ -644,22 +704,22 @@ class _CorrPageState extends State backgroundColor: Colors.transparent, builder: (context) => Container( height: MediaQuery.of(context).size.height * 0.7, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + decoration: BoxDecoration( + color: isDark ? Colors.grey[900] : Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[900] : Colors.white, borderRadius: const BorderRadius.vertical( top: Radius.circular(20), ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withAlpha(isDark ? 0 : 13), blurRadius: 4, offset: const Offset(0, 2), ), @@ -670,14 +730,18 @@ class _CorrPageState extends State Expanded( child: Text( poetry['name']?.toString() ?? '诗词详情', - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black, ), ), ), IconButton( - icon: const Icon(Icons.close), + icon: Icon( + Icons.close, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), onPressed: () => Navigator.pop(context), ), ], @@ -685,91 +749,92 @@ class _CorrPageState extends State ), Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (poetry['url']?.toString().isNotEmpty == true) ...[ + if (drtime.isNotEmpty) ...[ Text( - poetry['url'].toString(), - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + '朝代', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), + ), + const SizedBox(height: 4), + Text( + drtime, + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.white : Colors.black, + ), ), const SizedBox(height: 16), ], - if (drtime.isNotEmpty) ...[ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - drtime, - style: const TextStyle( - fontSize: 16, - height: 1.8, - fontFamily: 'serif', - ), - ), - ), - const SizedBox(height: 20), - ], if (introduce.isNotEmpty) ...[ - const Text( + Text( '译文', style: TextStyle( - fontSize: 16, + fontSize: 14, fontWeight: FontWeight.w600, + color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), - const SizedBox(height: 12), + const SizedBox(height: 4), Text( introduce, style: TextStyle( - fontSize: 15, - color: Colors.grey[700], - height: 1.8, + fontSize: 16, + height: 1.6, + color: isDark ? Colors.grey[200] : Colors.black87, ), ), ], - const SizedBox(height: 20), ], ), ), ), SafeArea( - child: Container( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 4, - offset: const Offset(0, -2), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + Navigator.pop(context); + _createNoteFromPoetry(poetry); + }, + icon: Icon( + Icons.note_add, + color: AppConstants.primaryColor, + ), + label: Text( + '创建笔记', + style: TextStyle(color: AppConstants.primaryColor), + ), + style: OutlinedButton.styleFrom( + side: BorderSide(color: AppConstants.primaryColor), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _toggleLike(poetry), + icon: const Icon(Icons.favorite, color: Colors.white), + label: const Text('点赞'), + style: ElevatedButton.styleFrom( + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), ), ], ), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - Navigator.pop(context); - _createNoteFromPoetry(poetry); - }, - icon: const Icon(Icons.note_add, size: 18), - label: const Text('创建笔记'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange[700], - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - ), - ), - ), ), ), ], diff --git a/lib/views/discover_page.dart b/lib/views/discover_page.dart index 6b92213..d67a097 100644 --- a/lib/views/discover_page.dart +++ b/lib/views/discover_page.dart @@ -1,17 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../constants/app_constants.dart'; +import '../config/app_config.dart'; import '../utils/responsive_layout.dart'; import '../widgets/tabbed_nav_app_bar.dart'; import 'active/active_search_page.dart'; import 'active/category_page.dart'; import 'active/popular_page.dart'; import 'active/rate.dart'; -import '../controllers/shared_preferences_storage_controller.dart'; +import '../services/get/discover_controller.dart'; +import '../services/get/theme_controller.dart'; +import 'home/set/home-load.dart'; /// 时间: 2025-03-21 /// 功能: 发现页面 /// 介绍: 展示发现内容,包括热门话题、推荐内容等 -/// 最新变化: 与收藏页共用 TabbedNavAppBar,压缩标题+Tab 高度,减少主导航子页顶部留白 +/// 最新变化: 2026-04-02 支持深色模式 class DiscoverPage extends StatefulWidget { const DiscoverPage({super.key}); @@ -22,125 +26,113 @@ class DiscoverPage extends StatefulWidget { class _DiscoverPageState extends State with SingleTickerProviderStateMixin { - TabController? _tabController; - List _categories = ['分类', '热门', '搜索']; - bool _showTips = true; - bool _isDeveloperMode = false; - bool _isInitialized = false; - OverlayEntry? _infoOverlayEntry; + late TabController _tabController; + final controller = Get.put(DiscoverController()); + late ThemeController _themeController; @override void initState() { super.initState(); - _loadDeveloperMode(); - } - - Future _loadDeveloperMode() async { - final isEnabled = await SharedPreferencesStorageController.getBool( - 'developer_mode_enabled', - defaultValue: false, + _themeController = Get.find(); + GlobalTipsManager().init(); + _tabController = TabController( + length: controller.categories.length, + vsync: this, ); - if (mounted) { - setState(() { - _isDeveloperMode = isEnabled; - _updateCategories(); - }); - } - } - void _updateCategories() { - setState(() { - _categories = ['分类', '热门', '搜索']; - if (_isDeveloperMode) { - _categories.add('活跃'); - } - _tabController?.dispose(); - _tabController = TabController(length: _categories.length, vsync: this); - _tabController!.addListener(() { - _removeInfoOverlay(); - setState(() {}); - }); - _isInitialized = true; + _tabController.addListener(() { + setState(() {}); }); } @override void dispose() { - _removeInfoOverlay(); - _tabController?.dispose(); + _tabController.dispose(); super.dispose(); } - void _removeInfoOverlay() { - _infoOverlayEntry?.remove(); - _infoOverlayEntry = null; - } - @override Widget build(BuildContext context) { - if (!_isInitialized) { - return const Scaffold(body: Center(child: CircularProgressIndicator())); - } + return Obx(() { + final isDark = _themeController.isDarkModeRx.value; - final isHotTab = _categories[_tabController!.index] == '热门'; + return GetBuilder( + builder: (controller) { + if (!controller.isInitialized.value) { + return Scaffold( + backgroundColor: isDark ? const Color(0xFF121212) : Colors.white, + body: const Center(child: CircularProgressIndicator()), + ); + } - return Scaffold( - appBar: TabbedNavAppBar.build( - title: '发现', - tabController: _tabController!, - tabLabels: _categories, - leading: isHotTab ? _buildInfoButton(context) : null, - actions: [ - IconButton(icon: const Icon(Icons.search), onPressed: _showSearch), - ], - ), - body: Column( - children: [ - // 只有非搜索标签时才显示话题chips - if (_categories[_tabController!.index] != '搜索' && _showTips) - _buildTopicChips(), - Expanded( - child: NotificationListener( - onNotification: (notification) { - if (notification is ScrollStartNotification) { - _removeInfoOverlay(); - } - return false; - }, - child: TabBarView( - controller: _tabController!, - children: _categories.asMap().entries.map((entry) { - final category = entry.value; - // 分类标签显示分类页面 - if (category == '分类') { - return const CategoryPage(); - } - // 热门标签显示排行榜页面 - if (category == '热门') { - return const PopularPage(); - } - // 搜索标签显示 ActiveSearchPage - if (category == '搜索') { - return const ActiveSearchPage(); - } - // 活跃标签显示活跃统计页面 - if (category == '活跃') { - return const RatePage(); - } - // 其他标签显示原有内容 - return _buildContentList(category); - }).toList(), - ), + return Scaffold( + backgroundColor: isDark ? const Color(0xFF121212) : Colors.white, + appBar: TabbedNavAppBar.build( + title: '发现', + tabLabels: controller.categories, + tabController: _tabController, + backgroundColor: isDark ? Colors.grey[900] : Colors.white, + foregroundColor: isDark + ? Colors.white + : AppConstants.primaryColor, + leading: controller.categories[_tabController.index] == '热门' + ? _buildInfoButton(context, isDark) + : null, + actions: [ + IconButton( + icon: Icon( + Icons.search, + color: isDark ? Colors.white : AppConstants.primaryColor, + ), + onPressed: _showSearch, + ), + ], ), - ), - ], - ), - ); + body: Column( + children: [ + ValueListenableBuilder( + valueListenable: GlobalTipsManager().enabledNotifier, + builder: (context, isGlobalTipsEnabled, child) { + if (controller.categories[_tabController.index] != '搜索' && + isGlobalTipsEnabled && + controller.showTips.value) { + return _buildTopicChips(controller, isDark); + } + return const SizedBox.shrink(); + }, + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: controller.categories.map((category) { + if (category == '分类') { + return const CategoryPage(); + } + if (category == '热门') { + return const PopularPage(); + } + if (category == '搜索') { + return const ActiveSearchPage(); + } + if (category == '活跃') { + return const RatePage(); + } + return _buildContentList(category, controller, isDark); + }).toList(), + ), + ), + ], + ), + ); + }, + ); + }); } - Widget _buildTopicChips() { + Widget _buildTopicChips(DiscoverController controller, bool isDark) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: isDark ? const Color(0xFF1E1E1E) : Colors.grey[50], child: Row( children: [ Expanded( @@ -154,19 +146,24 @@ class _DiscoverPageState extends State ), ), GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () { - setState(() { - _showTips = false; - }); + controller.toggleTips(); }, child: Container( width: 30, height: 30, decoration: BoxDecoration( - color: Colors.grey.withValues(alpha: 0.1), + color: isDark + ? Colors.grey[800] + : Colors.grey.withValues(alpha: 0.1), shape: BoxShape.circle, ), - child: Icon(Icons.close, size: 16, color: Colors.grey[600]), + child: Icon( + Icons.close, + size: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ), ], @@ -174,24 +171,41 @@ class _DiscoverPageState extends State ); } - Widget _buildContentList(String category) { - final items = List.generate(10, (index) => index); + Widget _buildContentList( + String category, + DiscoverController controller, + bool isDark, + ) { + final items = List.generate(20, (index) => index); return RefreshIndicator( - onRefresh: _refreshContent, + onRefresh: controller.refreshContent, child: ListView.separated( - padding: const EdgeInsets.all(16), + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: AppConfig.liquidGlassTotalHeight + 16, + ), itemCount: items.length, separatorBuilder: (context, index) => const SizedBox(height: 12), itemBuilder: (context, index) => - _buildContentCard(context, index, category), + _buildContentCard(context, index, category, controller, isDark), ), ); } - Widget _buildContentCard(BuildContext context, int index, String category) { + Widget _buildContentCard( + BuildContext context, + int index, + String category, + DiscoverController controller, + bool isDark, + ) { return ResponsiveCard( - onTap: () => _showContentDetails(context, index, category), + onTap: () => controller.showContentDetails(context, index, category), + backgroundColor: isDark ? const Color(0xFF1E1E1E) : Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -215,38 +229,44 @@ class _DiscoverPageState extends State children: [ Text( '用户${index + 1}', - style: Theme.of(context).textTheme.titleSmall?.copyWith( + style: TextStyle( fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, ), ), Text( '${index + 1}小时前', - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: Colors.grey[600]), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), ), IconButton( - icon: const Icon(Icons.more_horiz), - onPressed: () => _showMoreOptions(context, index), + icon: Icon( + Icons.more_horiz, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + onPressed: () => controller.showMoreOptions(context, index), ), ], ), const SizedBox(height: 12), Text( '这是$category分类下的精彩内容${index + 1}', - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: isDark ? Colors.white : Colors.black87, + ), ), const SizedBox(height: 8), Text( '这里是内容的详细描述,包含了丰富的信息和有趣的内容。这个内容来自于$category分类,展示了最新的趋势和热门话题。', - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: Colors.grey[700]), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[700], + ), maxLines: 3, overflow: TextOverflow.ellipsis, ), @@ -256,13 +276,17 @@ class _DiscoverPageState extends State width: double.infinity, height: 200, decoration: BoxDecoration( - color: AppConstants.secondaryColor.withValues(alpha: 0.1), + color: isDark + ? AppConstants.secondaryColor.withValues(alpha: 0.2) + : AppConstants.secondaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.image, size: 50, - color: AppConstants.secondaryColor, + color: isDark + ? AppConstants.secondaryColor.withValues(alpha: 0.6) + : AppConstants.secondaryColor, ), ), const SizedBox(height: 12), @@ -271,21 +295,29 @@ class _DiscoverPageState extends State _buildActionButton( Icons.favorite_border, '${(index + 1) * 23}', - () => _likeContent(index), + () => controller.likeContent(index), + isDark, ), const SizedBox(width: 16), _buildActionButton( Icons.chat_bubble_outline, '${(index + 1) * 8}', - () => _commentContent(index), + () => controller.commentContent(index), + isDark, ), const SizedBox(width: 16), - _buildActionButton(Icons.share, '分享', () => _shareContent(index)), + _buildActionButton( + Icons.share, + '分享', + () => controller.shareContent(index), + isDark, + ), const Spacer(), _buildActionButton( Icons.bookmark_border, '收藏', - () => _bookmarkContent(index), + () => controller.bookmarkContent(index), + isDark, ), ], ), @@ -298,133 +330,50 @@ class _DiscoverPageState extends State IconData icon, String label, VoidCallback onPressed, + bool isDark, ) { return GestureDetector( onTap: onPressed, child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 18, color: Colors.grey[600]), + Icon( + icon, + size: 18, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 4), - Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])), - ], - ), - ); - } - - Future _refreshContent() async { - await Future.delayed(const Duration(seconds: 1)); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('内容已刷新'))); - } - } - - void _showSearch() { - Navigator.of( - context, - ).push(MaterialPageRoute(builder: (context) => const ActiveSearchPage())); - } - - void _showContentDetails(BuildContext context, int index, String category) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('$category - 内容${index + 1}'), - content: Text('这是$category分类下内容${index + 1}的详细信息。'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('关闭'), + Text( + label, + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), ); } - void _showMoreOptions(BuildContext context, int index) { - showModalBottomSheet( - context: context, - builder: (context) => Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.report), - title: const Text('举报'), - onTap: () { - Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('举报内容'))); - }, - ), - ListTile( - leading: const Icon(Icons.block), - title: const Text('屏蔽用户'), - onTap: () { - Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('屏蔽用户'))); - }, - ), - ListTile( - leading: const Icon(Icons.link), - title: const Text('复制链接'), - onTap: () { - Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('链接已复制'))); - }, - ), - ], - ), - ), - ); + void _showSearch() { + Get.to(const ActiveSearchPage()); } - void _likeContent(int index) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('点赞了内容${index + 1}'))); - } - - void _commentContent(int index) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('评论了内容${index + 1}'))); - } - - void _shareContent(int index) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('分享了内容${index + 1}'))); - } - - void _bookmarkContent(int index) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('收藏了内容${index + 1}'))); - } - - Widget _buildInfoButton(BuildContext context) { + Widget _buildInfoButton(BuildContext context, bool isDark) { return Builder( builder: (buttonContext) { return IconButton( - icon: const Icon(Icons.info_outline), - onPressed: () => _showHotInfoPopup(buttonContext), + icon: Icon( + Icons.info_outline, + color: isDark ? Colors.white : AppConstants.primaryColor, + ), + onPressed: () => _showHotInfoPopup(buttonContext, isDark), ); }, ); } - void _showHotInfoPopup(BuildContext context) { - _removeInfoOverlay(); - + void _showHotInfoPopup(BuildContext context, bool isDark) { final RenderBox button = context.findRenderObject() as RenderBox; final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject() as RenderBox; @@ -433,23 +382,26 @@ class _DiscoverPageState extends State ancestor: overlay, ); - _infoOverlayEntry = OverlayEntry( + late OverlayEntry infoOverlayEntry; + infoOverlayEntry = OverlayEntry( builder: (context) => Positioned( left: buttonPosition.dx, top: buttonPosition.dy + button.size.height + 8, child: Material( color: Colors.transparent, child: GestureDetector( - onTap: _removeInfoOverlay, + onTap: () { + infoOverlayEntry.remove(); + }, child: Container( width: 220, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.15), + color: Colors.black.withValues(alpha: isDark ? 0.4 : 0.15), blurRadius: 12, offset: const Offset(0, 4), ), @@ -466,11 +418,12 @@ class _DiscoverPageState extends State size: 20, ), const SizedBox(width: 8), - const Text( + Text( '热门排行榜', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, + color: isDark ? Colors.white : Colors.black87, ), ), ], @@ -480,7 +433,7 @@ class _DiscoverPageState extends State '展示诗词的浏览量和点赞数排行,包括总榜、日榜、月榜三种类型。帮助您发现最受欢迎的诗词作品。', style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], height: 1.5, ), ), @@ -492,10 +445,10 @@ class _DiscoverPageState extends State ), ); - Overlay.of(context).insert(_infoOverlayEntry!); + Overlay.of(context).insert(infoOverlayEntry); Future.delayed(const Duration(seconds: 3), () { - _removeInfoOverlay(); + infoOverlayEntry.remove(); }); } } diff --git a/lib/views/favorites_page.dart b/lib/views/favorites_page.dart index 87148ed..1640284 100644 --- a/lib/views/favorites_page.dart +++ b/lib/views/favorites_page.dart @@ -1,6 +1,5 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../constants/app_constants.dart'; import '../views/footprint/liked_poetry_manager.dart'; import '../widgets/tabbed_nav_app_bar.dart'; @@ -8,6 +7,8 @@ import 'active/active_search_page.dart'; import 'footprint/all_list.dart'; import 'footprint/collect_notes.dart'; import 'footprint/local_jilu.dart'; +import '../services/get/favorites_controller.dart'; +import '../services/get/theme_controller.dart'; /// 时间: 2026-03-22 /// 功能: 收藏页面 @@ -24,67 +25,78 @@ class FavoritesPage extends StatefulWidget { class _FavoritesPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - final List _categories = ['全部', '点赞', '笔记', '推送', '每日一句']; - final TextEditingController _searchBarController = TextEditingController(); - bool _isGridView = true; + final controller = Get.put(FavoritesController()); + final themeController = Get.find(); @override void initState() { super.initState(); - _tabController = TabController(length: _categories.length, vsync: this); + _tabController = TabController( + length: controller.categories.length, + vsync: this, + ); _tabController.addListener(() { - setState(() {}); + controller.setCurrentTabIndex(_tabController.index); }); } @override void dispose() { _tabController.dispose(); - _searchBarController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: TabbedNavAppBar.build( - title: '足迹', - tabController: _tabController, - tabLabels: _categories, - tabBarScrollable: true, - tabLabelPadding: const EdgeInsets.symmetric(horizontal: 10), - actions: [ - IconButton( - icon: Icon(_isGridView ? Icons.view_list : Icons.grid_view), - onPressed: _toggleViewMode, - ), - IconButton( - icon: const Icon(Icons.filter_list), - onPressed: _showFilterOptions, - ), - ], - ), - body: Column( - children: [ - _buildSearchBar(), - Expanded( - child: TabBarView( - controller: _tabController, - children: _categories - .map((category) => _buildFavoriteList(category)) - .toList(), + return Obx(() { + final isDark = themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark ? const Color(0xFF1A1A1A) : null, + appBar: TabbedNavAppBar.build( + title: '足迹', + tabLabels: controller.categories, + tabController: _tabController, + tabBarScrollable: true, + tabLabelPadding: const EdgeInsets.symmetric(horizontal: 10), + actions: [ + IconButton( + icon: Icon( + controller.isGridView.value ? Icons.view_list : Icons.grid_view, + color: isDark ? Colors.white70 : null, + ), + onPressed: controller.toggleViewMode, ), - ), - ], - ), - floatingActionButton: _buildFloatingButton(), - floatingActionButtonLocation: FloatingActionButtonLocation.miniEndFloat, - ); + IconButton( + icon: Icon( + Icons.filter_list, + color: isDark ? Colors.white70 : null, + ), + onPressed: () => controller.showFilterOptions(context), + ), + ], + ), + body: Column( + children: [ + _buildSearchBar(controller, isDark), + Expanded( + child: TabBarView( + controller: _tabController, + children: controller.categories + .map((category) => _buildFavoriteList(category, controller)) + .toList(), + ), + ), + ], + ), + floatingActionButton: _buildFloatingButton(controller), + floatingActionButtonLocation: FloatingActionButtonLocation.miniEndFloat, + ); + }); } - Widget _buildFloatingButton() { - final currentIndex = _tabController.index; - final currentCategory = _categories[currentIndex]; + Widget _buildFloatingButton(FavoritesController controller) { + final currentCategory = + controller.categories[controller.currentTabIndex.value]; if (currentCategory != '笔记') { return const SizedBox.shrink(); @@ -92,9 +104,7 @@ class _FavoritesPageState extends State return FloatingActionButton( onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const CollectNotesPage()), - ); + Get.to(const CollectNotesPage()); }, child: const Icon(Icons.add), backgroundColor: AppConstants.primaryColor, @@ -103,7 +113,7 @@ class _FavoritesPageState extends State ); } - Widget _buildSearchBar() { + Widget _buildSearchBar(FavoritesController controller, bool isDark) { return Container( margin: EdgeInsets.fromLTRB( AppConstants.pageHorizontalPadding, @@ -112,27 +122,31 @@ class _FavoritesPageState extends State 16, ), decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? Colors.grey[850] : Colors.grey[100], borderRadius: BorderRadius.circular(12), ), child: TextField( - controller: _searchBarController, readOnly: true, onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ActiveSearchPage( - initialQuery: _searchBarController.text.trim().isEmpty - ? null - : _searchBarController.text.trim(), - ), + Get.to( + ActiveSearchPage( + initialQuery: controller.searchQuery.value.isEmpty + ? null + : controller.searchQuery.value, ), ); }, decoration: InputDecoration( hintText: '点按搜索全站诗词…', - prefixIcon: const Icon(Icons.search, color: Colors.grey), - suffixIcon: Icon(Icons.chevron_right, color: Colors.grey[500]), + hintStyle: TextStyle(color: isDark ? Colors.grey[500] : Colors.grey), + prefixIcon: Icon( + Icons.search, + color: isDark ? Colors.grey[500] : Colors.grey, + ), + suffixIcon: Icon( + Icons.chevron_right, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 16, @@ -143,7 +157,7 @@ class _FavoritesPageState extends State ); } - Widget _buildFavoriteList(String category) { + Widget _buildFavoriteList(String category, FavoritesController controller) { // 如果是"全部"标签,显示统一的收藏列表(点赞+笔记) if (category == '全部') { return const AllListPage(); @@ -161,77 +175,40 @@ class _FavoritesPageState extends State // 其他标签显示占位内容,但支持下拉刷新 return RefreshIndicator( - onRefresh: () async { - // 模拟刷新其他分类数据 - await Future.delayed(const Duration(milliseconds: 500)); - }, + onRefresh: controller.refreshContent, child: _buildPlaceholderContent(category), ); } Widget _buildPlaceholderContent(String category) { + final isDark = themeController.isDarkMode; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.category_outlined, size: 64, color: Colors.grey[400]), + Icon( + Icons.category_outlined, + size: 64, + color: isDark ? Colors.grey[600] : Colors.grey[400], + ), const SizedBox(height: 16), Text( '$category功能暂未开放', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), const SizedBox(height: 8), Text( '敬请期待后续更新', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ), ], ), ); } - - void _toggleViewMode() { - setState(() { - _isGridView = !_isGridView; - }); - } - - void _showFilterOptions() { - showModalBottomSheet( - context: context, - builder: (context) => Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - '筛选选项', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - ListTile( - leading: const Icon(Icons.date_range), - title: const Text('按时间排序'), - onTap: () { - Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('按时间排序'))); - }, - ), - ListTile( - leading: const Icon(Icons.title), - title: const Text('按标题排序'), - onTap: () { - Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('按标题排序'))); - }, - ), - ], - ), - ), - ); - } } diff --git a/lib/views/footprint/all_list.dart b/lib/views/footprint/all_list.dart index 5d823f6..2f7bf30 100644 --- a/lib/views/footprint/all_list.dart +++ b/lib/views/footprint/all_list.dart @@ -2,10 +2,13 @@ import 'dart:ui'; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; +import '../../config/app_config.dart'; import '../../controllers/history_controller.dart'; import '../../services/network_listener_service.dart'; import '../../utils/http/poetry_api.dart'; +import '../../services/get/theme_controller.dart'; import 'collect_notes.dart'; import 'liked_poetry_manager.dart'; @@ -37,6 +40,7 @@ class _AllListPageState extends State { List _cards = []; bool _isLoading = false; StreamSubscription? _networkSubscription; + final ThemeController _themeController = Get.find(); @override void initState() { @@ -132,95 +136,129 @@ class _AllListPageState extends State { @override Widget build(BuildContext context) { - if (_isLoading && _cards.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } + return Obx(() { + final isDark = _themeController.isDarkMode; - if (_cards.isEmpty) { - return _buildEmptyState(); - } + if (_isLoading && _cards.isEmpty) { + return Center( + child: CircularProgressIndicator( + color: isDark ? Colors.white : AppConstants.primaryColor, + ), + ); + } - return RefreshIndicator( - onRefresh: _loadAllData, - child: ListView.separated( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 80), - itemCount: _cards.length + 1, - separatorBuilder: (context, index) { - if (index == _cards.length) return const SizedBox.shrink(); - return Container( - height: 1, - color: Colors.black.withOpacity(0.1), - margin: const EdgeInsets.symmetric(vertical: 4), - ); - }, - itemBuilder: (context, index) { - if (index == _cards.length) { - return _buildBottomIndicator(); - } - return _buildCard(_cards[index]); - }, - ), - ); + if (_cards.isEmpty) { + return _buildEmptyState(isDark); + } + + return RefreshIndicator( + onRefresh: _loadAllData, + child: ListView.separated( + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + AppConfig.liquidGlassTotalHeight + 16, + ), + itemCount: _cards.length + 1, + separatorBuilder: (context, index) { + if (index == _cards.length) return const SizedBox.shrink(); + return Container( + height: 1, + color: isDark + ? Colors.white.withValues(alpha: 0.1) + : Colors.black.withValues(alpha: 0.1), + margin: const EdgeInsets.symmetric(vertical: 4), + ); + }, + itemBuilder: (context, index) { + if (index == _cards.length) { + return _buildBottomIndicator(isDark); + } + return _buildCard(_cards[index], isDark); + }, + ), + ); + }); } - Widget _buildBottomIndicator() { + Widget _buildBottomIndicator(bool isDark) { return Container( padding: const EdgeInsets.symmetric(vertical: 24), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '到底了', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ), - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), ], ), ); } - // 构建空状态 - Widget _buildEmptyState() { + Widget _buildEmptyState(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[400]), + Icon( + Icons.inbox_outlined, + size: 64, + color: isDark ? Colors.grey[600] : Colors.grey[400], + ), const SizedBox(height: 16), Text( '暂无收藏内容', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), const SizedBox(height: 8), Text( '点赞诗词或创建笔记后将显示在这里', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ), ], ), ); } - // 根据类型构建卡片 - 使用策略模式 - Widget _buildCard(UnifiedCard card) { + Widget _buildCard(UnifiedCard card, bool isDark) { switch (card.type) { case CardType.like: - return _buildLikeCard(card.data as PoetryData); + return _buildLikeCard(card.data as PoetryData, isDark); case CardType.note: - return _buildNoteCard(card.data as Map); + return _buildNoteCard(card.data as Map, isDark); } } - // 构建点赞卡片 - 简洁紧凑样式 - Widget _buildLikeCard(PoetryData poetry) { + Widget _buildLikeCard(PoetryData poetry, bool isDark) { return Container( margin: const EdgeInsets.only(bottom: 0), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all( color: AppConstants.primaryColor.withValues(alpha: 0.2), @@ -228,7 +266,9 @@ class _AllListPageState extends State { ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.03), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 1), ), @@ -237,23 +277,23 @@ class _AllListPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 顶部行:出处(左) + 标签(右) Padding( padding: const EdgeInsets.fromLTRB(10, 4, 0, 4), child: Row( children: [ - // 出处 - 左上角 if (poetry.url.isNotEmpty) Expanded( child: Text( poetry.url, - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + style: TextStyle( + fontSize: 11, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (poetry.url.isEmpty) const Spacer(), - // 类型标识 - 右上角 Container( padding: const EdgeInsets.symmetric( horizontal: 8, @@ -290,15 +330,14 @@ class _AllListPageState extends State { ), ), - // 诗句内容 Padding( padding: const EdgeInsets.fromLTRB(10, 4, 10, 4), child: Text( poetry.name, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, height: 1.3, ), maxLines: 2, @@ -306,7 +345,6 @@ class _AllListPageState extends State { ), ), - // 操作按钮 Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 6), child: Row( @@ -332,11 +370,14 @@ class _AllListPageState extends State { icon: Icon( Icons.note_add, size: 14, - color: Colors.orange[700], + color: isDark ? Colors.orange[300] : Colors.orange[700], ), label: Text( '创建笔记', - style: TextStyle(fontSize: 12, color: Colors.orange[700]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.orange[300] : Colors.orange[700], + ), ), ), TextButton.icon( @@ -344,11 +385,14 @@ class _AllListPageState extends State { icon: Icon( Icons.favorite_border, size: 14, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], ), label: Text( '取消', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ), ], @@ -359,8 +403,7 @@ class _AllListPageState extends State { ); } - // 构建笔记卡片 - 紧凑阴影样式 - Widget _buildNoteCard(Map note) { + Widget _buildNoteCard(Map note, bool isDark) { final title = note['title'] as String? ?? ''; final content = note['content'] as String? ?? ''; final category = note['category'] as String? ?? ''; @@ -382,11 +425,13 @@ class _AllListPageState extends State { return Container( margin: const EdgeInsets.only(bottom: 0), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.06), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.06), blurRadius: 8, offset: const Offset(0, 2), ), @@ -397,7 +442,6 @@ class _AllListPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 笔记内容 InkWell( onTap: () => _handleNoteTap(note, isLocked), borderRadius: BorderRadius.circular(10), @@ -413,7 +457,7 @@ class _AllListPageState extends State { fontWeight: hasTitle ? FontWeight.w600 : FontWeight.normal, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, height: 1.4, ), maxLines: 2, @@ -426,7 +470,9 @@ class _AllListPageState extends State { '${note['charCount'] ?? displayText.length} 字', style: TextStyle( fontSize: 11, - color: Colors.grey[500], + color: isDark + ? Colors.grey[400] + : Colors.grey[500], ), ), if (hasCategory) ...[ @@ -437,14 +483,18 @@ class _AllListPageState extends State { vertical: 1, ), decoration: BoxDecoration( - color: Colors.grey[200], + color: isDark + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(3), ), child: Text( category, style: TextStyle( fontSize: 9, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], ), ), ), @@ -458,7 +508,6 @@ class _AllListPageState extends State { ], ), - // 类型标识 - 右上角 Positioned( top: 0, right: 0, @@ -477,31 +526,38 @@ class _AllListPageState extends State { Icon( Icons.note_outlined, size: 12, - color: Colors.orange[700], + color: isDark ? Colors.orange[300] : Colors.orange[700], ), const SizedBox(width: 3), Text( '笔记', style: TextStyle( fontSize: 11, - color: Colors.orange[700], + color: isDark ? Colors.orange[300] : Colors.orange[700], fontWeight: FontWeight.w500, ), ), if (isPinned) ...[ const SizedBox(width: 6), - Icon(Icons.push_pin, size: 10, color: Colors.orange[700]), + Icon( + Icons.push_pin, + size: 10, + color: isDark ? Colors.orange[300] : Colors.orange[700], + ), ], if (isLocked) ...[ const SizedBox(width: 6), - Icon(Icons.lock, size: 10, color: Colors.orange[700]), + Icon( + Icons.lock, + size: 10, + color: isDark ? Colors.orange[300] : Colors.orange[700], + ), ], ], ), ), ), - // 锁定遮罩 if (isLocked) Positioned.fill( child: GestureDetector( @@ -511,7 +567,9 @@ class _AllListPageState extends State { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Container( - color: Colors.white.withValues(alpha: 0.4), + color: (isDark ? Colors.black : Colors.white).withValues( + alpha: 0.4, + ), child: Center( child: Row( mainAxisSize: MainAxisSize.min, @@ -519,14 +577,18 @@ class _AllListPageState extends State { Icon( Icons.lock, size: 16, - color: Colors.orange[700], + color: isDark + ? Colors.orange[300] + : Colors.orange[700], ), const SizedBox(width: 6), Text( '已锁定', style: TextStyle( fontSize: 13, - color: Colors.orange[700], + color: isDark + ? Colors.orange[300] + : Colors.orange[700], fontWeight: FontWeight.w500, ), ), diff --git a/lib/views/footprint/collect_notes.dart b/lib/views/footprint/collect_notes.dart index 3adde8f..29030bf 100644 --- a/lib/views/footprint/collect_notes.dart +++ b/lib/views/footprint/collect_notes.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; import '../../controllers/history_controller.dart'; import '../../services/network_listener_service.dart'; +import '../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 笔记编辑页面 @@ -24,6 +26,7 @@ class _CollectNotesPageState extends State { final TextEditingController _contentController = TextEditingController(); final FocusNode _titleFocusNode = FocusNode(); final FocusNode _contentFocusNode = FocusNode(); + final ThemeController _themeController = Get.find(); String? _currentNoteId; DateTime? _lastSavedTime; @@ -279,10 +282,10 @@ class _CollectNotesPageState extends State { horizontal: 16, ), decoration: BoxDecoration( - color: AppConstants.primaryColor.withOpacity(0.1), + color: AppConstants.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( - color: AppConstants.primaryColor.withOpacity(0.3), + color: AppConstants.primaryColor.withValues(alpha: 0.3), ), ), child: Row( @@ -550,113 +553,146 @@ class _CollectNotesPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - _currentNoteId == null ? '新建笔记' : '编辑笔记', - style: const TextStyle( - color: Colors.black87, - fontWeight: FontWeight.w600, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: isDark ? Colors.white70 : AppConstants.primaryColor, + ), + onPressed: () => Navigator.of(context).pop(), ), - ), - actions: [ - // 删除按钮(仅编辑时显示) - if (_currentNoteId != null) + title: Text( + _currentNoteId == null ? '新建笔记' : '编辑笔记', + style: TextStyle( + color: isDark ? Colors.white : Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + actions: [ + if (_currentNoteId != null) + IconButton( + icon: Icon( + Icons.delete_outline, + color: isDark ? Colors.red[300] : Colors.red[400], + ), + onPressed: _showDeleteDialog, + tooltip: '删除笔记', + ), IconButton( - icon: Icon(Icons.delete_outline, color: Colors.red[400]), - onPressed: _showDeleteDialog, - tooltip: '删除笔记', + icon: Icon( + _isLocked ? Icons.lock : Icons.lock_outline, + color: _isLocked + ? AppConstants.primaryColor + : (isDark ? Colors.grey[400] : Colors.grey[600]), + ), + onPressed: _showPasswordDialog, + tooltip: _isLocked ? '修改密码' : '设置密码', ), - // 锁定按钮 - IconButton( - icon: Icon( - _isLocked ? Icons.lock : Icons.lock_outline, - color: _isLocked ? AppConstants.primaryColor : Colors.grey[600], + IconButton( + icon: Icon( + _isPinned ? Icons.push_pin : Icons.push_pin_outlined, + color: _isPinned + ? AppConstants.primaryColor + : (isDark ? Colors.grey[400] : Colors.grey[600]), + ), + onPressed: _togglePin, + tooltip: _isPinned ? '取消置顶' : '置顶', ), - onPressed: _showPasswordDialog, - tooltip: _isLocked ? '修改密码' : '设置密码', - ), - // 置顶按钮 - IconButton( - icon: Icon( - _isPinned ? Icons.push_pin : Icons.push_pin_outlined, - color: _isPinned ? AppConstants.primaryColor : Colors.grey[600], - ), - onPressed: _togglePin, - tooltip: _isPinned ? '取消置顶' : '置顶', - ), - ], - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : GestureDetector( - onTap: () { - // 点击非输入框区域取消焦点 - _titleFocusNode.unfocus(); - _contentFocusNode.unfocus(); - }, - child: Column( - children: [ - _buildStatusBar(), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildTitleInput(), - const SizedBox(height: 16), - _buildContentInput(), - ], + ], + ), + body: _isLoading + ? Center( + child: CircularProgressIndicator( + color: isDark ? Colors.white : AppConstants.primaryColor, + ), + ) + : GestureDetector( + onTap: () { + _titleFocusNode.unfocus(); + _contentFocusNode.unfocus(); + }, + child: Column( + children: [ + _buildStatusBar(isDark), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildTitleInput(isDark), + const SizedBox(height: 16), + _buildContentInput(isDark), + ], + ), ), ), - ), - ], + ], + ), ), - ), - ); + ); + }); } - Widget _buildStatusBar() { + Widget _buildStatusBar(bool isDark) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - color: Colors.grey[50], + color: isDark ? const Color(0xFF2A2A2A) : Colors.grey[50], child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ - // 创建时间 - Icon(Icons.add_circle_outline, size: 14, color: Colors.grey[500]), + Icon( + Icons.add_circle_outline, + size: 14, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), const SizedBox(width: 4), Text( _createTime != null ? '创建 ${_formatDate(_createTime!)}' : '新笔记', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), const SizedBox(width: 16), - // 保存时间 - Icon(Icons.access_time, size: 14, color: Colors.grey[500]), + Icon( + Icons.access_time, + size: 14, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), const SizedBox(width: 4), Text( _lastSavedTime != null ? '保存 ${_formatDateTime(_lastSavedTime!)}' : '未保存', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), const SizedBox(width: 16), - // 字数 - Icon(Icons.text_fields, size: 14, color: Colors.grey[500]), + Icon( + Icons.text_fields, + size: 14, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), const SizedBox(width: 4), Text( '$_charCount 字', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), if (_isPinned) ...[ const SizedBox(width: 16), @@ -740,23 +776,27 @@ class _CollectNotesPageState extends State { ); } - Widget _buildTitleInput() { + Widget _buildTitleInput(bool isDark) { return Row( children: [ Expanded( child: Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: _titleFocusNode.hasFocus ? AppConstants.primaryColor.withValues(alpha: 0.5) - : Colors.grey.withValues(alpha: 0.2), + : (isDark + ? Colors.grey[700]! + : Colors.grey.withValues(alpha: 0.2)), width: 1.5, ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -768,23 +808,30 @@ class _CollectNotesPageState extends State { maxLines: 1, decoration: InputDecoration( hintText: '标题(可选)', - hintStyle: TextStyle(color: Colors.grey[400]), + hintStyle: TextStyle( + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), border: InputBorder.none, contentPadding: const EdgeInsets.all(16), ), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), ), ), ), const SizedBox(width: 12), - // 分类选择 Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -797,7 +844,6 @@ class _CollectNotesPageState extends State { : null, onSelected: (value) async { if (value == _customCategoryKey) { - // 弹出自定义分类输入框 await _showCustomCategoryDialog(); } else { setState(() { @@ -819,13 +865,17 @@ class _CollectNotesPageState extends State { color: AppConstants.primaryColor, ), if (category == _category) const SizedBox(width: 8), - Text(category), + Text( + category, + style: TextStyle( + color: isDark ? Colors.white : Colors.black87, + ), + ), ], ), ); }).toList(); - // 添加自定义分类选项 items.add( PopupMenuItem( value: _customCategoryKey, @@ -843,9 +893,18 @@ class _CollectNotesPageState extends State { _category != null && _category!.isNotEmpty) const SizedBox(width: 8), - Icon(Icons.edit, size: 16, color: Colors.grey[600]), + Icon( + Icons.edit, + size: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 8), - Text('自定义', style: TextStyle(color: Colors.grey[600])), + Text( + '自定义', + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), ], ), ), @@ -874,7 +933,7 @@ class _CollectNotesPageState extends State { Icon( Icons.arrow_drop_down, size: 18, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], ), ], ), @@ -885,21 +944,25 @@ class _CollectNotesPageState extends State { ); } - Widget _buildContentInput() { + Widget _buildContentInput(bool isDark) { return Container( constraints: const BoxConstraints(minHeight: 300), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: _contentFocusNode.hasFocus ? AppConstants.primaryColor.withValues(alpha: 0.5) - : Colors.grey.withValues(alpha: 0.2), + : (isDark + ? Colors.grey[700]! + : Colors.grey.withValues(alpha: 0.2)), width: 1.5, ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -912,11 +975,17 @@ class _CollectNotesPageState extends State { minLines: 12, decoration: InputDecoration( hintText: '开始写笔记...', - hintStyle: TextStyle(color: Colors.grey[400]), + hintStyle: TextStyle( + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), border: InputBorder.none, contentPadding: const EdgeInsets.all(16), ), - style: const TextStyle(fontSize: 16, height: 1.5), + style: TextStyle( + fontSize: 16, + height: 1.5, + color: isDark ? Colors.white : Colors.black87, + ), ), ); } diff --git a/lib/views/footprint/footprint_page.dart b/lib/views/footprint/footprint_page.dart index cc0cd37..8cf8a01 100644 --- a/lib/views/footprint/footprint_page.dart +++ b/lib/views/footprint/footprint_page.dart @@ -3,14 +3,18 @@ /// 介绍: 显示用户点赞的诗词列表,支持删除操作 /// 最新变化: 新增点赞足迹管理功能 +library; + import 'dart:async' show StreamSubscription; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:poes/controllers/history_controller.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/poetry_api.dart'; import '../../../services/network_listener_service.dart'; -import '../home/home_components.dart'; +import '../../../services/get/theme_controller.dart'; +import '../home/set/home_components.dart'; /// 点赞足迹页面 class FootprintPage extends StatefulWidget { @@ -26,6 +30,7 @@ class _FootprintPageState extends State bool _isLoading = false; String _errorMessage = ''; StreamSubscription? _networkSubscription; + final ThemeController _themeController = Get.find(); @override void initState() { @@ -395,23 +400,30 @@ class _FootprintPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[50], - appBar: AppBar( - title: const Text('点赞足迹'), - backgroundColor: AppConstants.primaryColor, - foregroundColor: Colors.white, - elevation: 0, - ), - body: _buildBody(), - ); + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.grey[50], + appBar: AppBar( + title: const Text('点赞足迹'), + backgroundColor: isDark + ? const Color(0xFF2A2A2A) + : AppConstants.primaryColor, + foregroundColor: Colors.white, + elevation: 0, + ), + body: _buildBody(isDark), + ); + }); } - Widget _buildBody() { + Widget _buildBody(bool isDark) { if (_isLoading) { - return const Center( + return Center( child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(AppConstants.primaryColor), + valueColor: AlwaysStoppedAnimation( + isDark ? Colors.white : AppConstants.primaryColor, + ), ), ); } @@ -421,11 +433,18 @@ class _FootprintPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: AppConstants.errorColor), + Icon( + Icons.error_outline, + size: 64, + color: isDark ? Colors.red[300] : AppConstants.errorColor, + ), const SizedBox(height: 16), Text( _errorMessage, - style: TextStyle(fontSize: 16, color: AppConstants.errorColor), + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.red[300] : AppConstants.errorColor, + ), ), const SizedBox(height: 16), ElevatedButton( @@ -446,16 +465,26 @@ class _FootprintPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.favorite_border, size: 64, color: Colors.grey[400]), + Icon( + Icons.favorite_border, + size: 64, + color: isDark ? Colors.grey[600] : Colors.grey[400], + ), const SizedBox(height: 16), Text( '暂无点赞记录', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), const SizedBox(height: 8), Text( '去主页点赞喜欢的诗词吧', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ), ], ), @@ -466,7 +495,6 @@ class _FootprintPageState extends State onRefresh: _loadLikedPoetry, child: Column( children: [ - // 添加不可点击的刷新按钮 if (_likedPoetryList.isNotEmpty) ...[ Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -475,21 +503,26 @@ class _FootprintPageState extends State Icon( Icons.refresh, size: 20, - color: AppConstants.primaryColor, + color: isDark ? Colors.white70 : AppConstants.primaryColor, ), const SizedBox(width: 8), Text( '下拉刷新列表', style: TextStyle( fontSize: 14, - color: AppConstants.primaryColor, + color: isDark + ? Colors.white70 + : AppConstants.primaryColor, fontWeight: FontWeight.w500, ), ), const Spacer(), Text( '共 ${_likedPoetryList.length} 条记录', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -501,7 +534,7 @@ class _FootprintPageState extends State itemCount: _likedPoetryList.length, itemBuilder: (context, index) { final poetry = _likedPoetryList[index]; - return _buildLikedPoetryCard(poetry); + return _buildLikedPoetryCard(poetry, isDark); }, ), ), @@ -510,15 +543,17 @@ class _FootprintPageState extends State ); } - Widget _buildLikedPoetryCard(PoetryData poetry) { + Widget _buildLikedPoetryCard(PoetryData poetry, bool isDark) { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2), ), @@ -527,15 +562,14 @@ class _FootprintPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // URL字段 (出处) Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( "出处: ${poetry.url}", - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.normal, - color: Colors.black, + color: isDark ? Colors.grey[400] : Colors.black, fontStyle: FontStyle.italic, ), overflow: TextOverflow.ellipsis, @@ -543,7 +577,6 @@ class _FootprintPageState extends State ), ), - // Name字段 (精选诗句) - 与主页样式一致 Container( width: double.infinity, padding: const EdgeInsets.all(16), @@ -596,10 +629,10 @@ class _FootprintPageState extends State const SizedBox(height: 8), Text( poetry.name, - style: const TextStyle( + style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, height: 1.4, ), textAlign: TextAlign.center, @@ -608,7 +641,6 @@ class _FootprintPageState extends State ), ), - // drtime字段 (原文) if (poetry.drtime.isNotEmpty) ...[ const SizedBox(height: 12), Padding( @@ -617,15 +649,15 @@ class _FootprintPageState extends State width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), + color: isDark ? Colors.grey[800] : const Color(0xFFF8F8F8), borderRadius: BorderRadius.circular(8), ), child: Text( poetry.drtime, - style: const TextStyle( + style: TextStyle( fontSize: 14, height: 1.6, - color: Colors.black87, + color: isDark ? Colors.grey[300] : Colors.black87, ), maxLines: 3, overflow: TextOverflow.ellipsis, @@ -634,7 +666,6 @@ class _FootprintPageState extends State ), ], - // 操作按钮 Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Row( @@ -645,8 +676,14 @@ class _FootprintPageState extends State icon: const Icon(Icons.favorite_border, size: 18), label: const Text('取消点赞'), style: OutlinedButton.styleFrom( - foregroundColor: AppConstants.errorColor, - side: BorderSide(color: AppConstants.errorColor), + foregroundColor: isDark + ? Colors.red[300] + : AppConstants.errorColor, + side: BorderSide( + color: isDark + ? Colors.red[300]! + : AppConstants.errorColor, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/lib/views/footprint/liked_poetry_manager.dart b/lib/views/footprint/liked_poetry_manager.dart index 4dad2c5..2f30c2d 100644 --- a/lib/views/footprint/liked_poetry_manager.dart +++ b/lib/views/footprint/liked_poetry_manager.dart @@ -1,7 +1,7 @@ /// 时间: 2025-03-22 /// 功能: 点赞诗词管理器 /// 介绍: 管理点赞诗词的加载、显示、删除等操作 -/// 最新变化: 从FavoritesPage分离出来的点赞管理逻辑 +/// 最新变化: 从FavoritesPage分离出来的点赞管理逻辑,添加底部内边距适配液态玻璃导航栏 library liked_poetry_manager; @@ -10,10 +10,11 @@ import 'dart:async' show StreamSubscription; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import '../../../constants/app_constants.dart'; +import '../../../config/app_config.dart'; import '../../../controllers/history_controller.dart'; import '../../../utils/http/poetry_api.dart'; import '../../../services/network_listener_service.dart'; -import '../home/home_components.dart'; +import '../home/set/home_components.dart'; /// 点赞诗词管理器 class LikedPoetryManager extends StatefulWidget { @@ -453,7 +454,13 @@ class _LikedPoetryManagerState extends State return RefreshIndicator( onRefresh: _loadLikedPoetry, child: ListView.builder( - padding: const EdgeInsets.all(16), + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: AppConfig.liquidGlassTotalHeight + 16, + ), itemCount: _likedPoetryList.length + 1, itemBuilder: (context, index) { if (index == _likedPoetryList.length) { diff --git a/lib/views/footprint/local_jilu.dart b/lib/views/footprint/local_jilu.dart index d7cbb62..0375590 100644 --- a/lib/views/footprint/local_jilu.dart +++ b/lib/views/footprint/local_jilu.dart @@ -2,10 +2,13 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; +import '../../config/app_config.dart'; import 'collect_notes.dart'; import '../../controllers/history_controller.dart'; import '../../services/network_listener_service.dart'; +import '../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 本地笔记列表组件 @@ -23,6 +26,7 @@ class _LocalNotesListState extends State { List> _notes = []; bool _isLoadingNotes = false; StreamSubscription? _networkSubscription; + final ThemeController _themeController = Get.find(); @override void initState() { @@ -72,71 +76,107 @@ class _LocalNotesListState extends State { @override Widget build(BuildContext context) { - if (_isLoadingNotes && _notes.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } + return Obx(() { + final isDark = _themeController.isDarkMode; - if (_notes.isEmpty) { - return _buildEmptyNotes(); - } + if (_isLoadingNotes && _notes.isEmpty) { + return Center( + child: CircularProgressIndicator( + color: isDark ? Colors.white : AppConstants.primaryColor, + ), + ); + } - return RefreshIndicator( - onRefresh: _loadNotes, - child: ListView.builder( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), - itemCount: _notes.length + 1, - itemBuilder: (context, index) { - if (index == _notes.length) { - return _buildBottomIndicator(); - } - final note = _notes[index]; - return _buildNoteCard(note); - }, - ), - ); + if (_notes.isEmpty) { + return _buildEmptyNotes(isDark); + } + + return RefreshIndicator( + onRefresh: _loadNotes, + child: ListView.builder( + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.fromLTRB( + 16, + 16, + 16, + AppConfig.liquidGlassTotalHeight + 16, + ), + itemCount: _notes.length + 1, + itemBuilder: (context, index) { + if (index == _notes.length) { + return _buildBottomIndicator(isDark); + } + final note = _notes[index]; + return _buildNoteCard(note, isDark); + }, + ), + ); + }); } - Widget _buildBottomIndicator() { + Widget _buildBottomIndicator(bool isDark) { return Container( padding: const EdgeInsets.symmetric(vertical: 24), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '到底了', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ), - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), ], ), ); } - // 构建空笔记状态 - Widget _buildEmptyNotes() { + Widget _buildEmptyNotes(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.note_add_outlined, size: 64, color: Colors.grey[400]), + Icon( + Icons.note_add_outlined, + size: 64, + color: isDark ? Colors.grey[600] : Colors.grey[400], + ), const SizedBox(height: 16), - Text('暂无笔记', style: TextStyle(fontSize: 16, color: Colors.grey[600])), + Text( + '暂无笔记', + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), const SizedBox(height: 8), Text( '点击右下角按钮创建新笔记', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ), ], ), ); } - // 构建笔记卡片 - Widget _buildNoteCard(Map note) { + Widget _buildNoteCard(Map note, bool isDark) { final title = note['title'] as String? ?? ''; final content = note['content'] as String? ?? ''; final timeStr = note['time'] as String? ?? ''; @@ -145,7 +185,6 @@ class _LocalNotesListState extends State { final isPinned = note['isPinned'] == true; final isLocked = note['isLocked'] == true; - // 显示逻辑:有标题显示标题,无标题有分类显示分类,否则显示内容 String displayText; bool hasTitle = title.isNotEmpty; bool hasCategory = category.isNotEmpty && category != '未分类'; @@ -161,16 +200,20 @@ class _LocalNotesListState extends State { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.08), blurRadius: 12, offset: const Offset(0, 4), ), BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2), ), @@ -180,7 +223,6 @@ class _LocalNotesListState extends State { borderRadius: BorderRadius.circular(12), child: Stack( children: [ - // 原始内容 InkWell( onTap: () => _handleNoteTap(note, isLocked), borderRadius: BorderRadius.circular(12), @@ -189,42 +231,40 @@ class _LocalNotesListState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 顶部:创建时间、保存时间和置顶/锁定按钮 Row( children: [ - // 创建时间 if (createTimeStr.isNotEmpty) ...[ Icon( Icons.add_circle_outline, size: 12, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], ), const SizedBox(width: 2), Text( _formatDate(createTimeStr), style: TextStyle( fontSize: 10, - color: Colors.grey[400], + color: isDark + ? Colors.grey[500] + : Colors.grey[400], ), ), const SizedBox(width: 8), ], - // 保存时间 Icon( Icons.access_time, size: 12, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], ), const SizedBox(width: 2), Text( _formatDateTime(timeStr), style: TextStyle( fontSize: 10, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], ), ), const Spacer(), - // 分类标签 if (hasCategory) Container( padding: const EdgeInsets.symmetric( @@ -246,7 +286,6 @@ class _LocalNotesListState extends State { ), ), if (hasCategory) const SizedBox(width: 8), - // 锁定图标 if (isLocked) Container( padding: const EdgeInsets.all(4), @@ -256,7 +295,6 @@ class _LocalNotesListState extends State { color: AppConstants.primaryColor, ), ), - // 置顶按钮 GestureDetector( onTap: () => _togglePin(note['id'] as String?), child: Container( @@ -268,14 +306,15 @@ class _LocalNotesListState extends State { size: 16, color: isPinned ? AppConstants.primaryColor - : Colors.grey[400], + : (isDark + ? Colors.grey[500] + : Colors.grey[400]), ), ), ), ], ), const SizedBox(height: 12), - // 标题或内容 Text( displayText, style: TextStyle( @@ -283,13 +322,12 @@ class _LocalNotesListState extends State { fontWeight: hasTitle ? FontWeight.w600 : FontWeight.normal, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, height: 1.5, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), - // 底部:字数和删除按钮 const SizedBox(height: 12), Row( children: [ @@ -297,11 +335,10 @@ class _LocalNotesListState extends State { '${note['charCount'] ?? displayText.length} 字', style: TextStyle( fontSize: 12, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], ), ), const Spacer(), - // 删除按钮 GestureDetector( onTap: () => _showDeleteNoteDialog(note['id'] as String?), @@ -310,7 +347,9 @@ class _LocalNotesListState extends State { child: Icon( Icons.delete_outline, size: 18, - color: Colors.grey[400], + color: isDark + ? Colors.grey[500] + : Colors.grey[400], ), ), ), @@ -320,7 +359,6 @@ class _LocalNotesListState extends State { ), ), ), - // 锁定时的毛玻璃遮罩 if (isLocked) Positioned.fill( child: GestureDetector( @@ -330,29 +368,32 @@ class _LocalNotesListState extends State { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), child: Container( - color: Colors.white.withValues(alpha: 0.3), + color: (isDark ? Colors.black : Colors.white) + .withValues(alpha: 0.3), child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ - // 顶部时间信息 Row( children: [ Icon( Icons.access_time, size: 12, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], ), const SizedBox(width: 4), Text( _formatDateTime(timeStr), style: TextStyle( fontSize: 10, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], ), ), const Spacer(), - // 删除按钮 GestureDetector( onTap: () => _showDeleteNoteDialog( note['id'] as String?, @@ -362,14 +403,15 @@ class _LocalNotesListState extends State { child: Icon( Icons.delete_outline, size: 16, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], ), ), ), ], ), const Spacer(), - // 中间锁定图标(左右结构) Row( mainAxisSize: MainAxisSize.min, children: [ @@ -397,7 +439,9 @@ class _LocalNotesListState extends State { '点击输入密码访问', style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], ), ), ], @@ -405,14 +449,15 @@ class _LocalNotesListState extends State { ], ), const Spacer(), - // 底部字数 Row( children: [ Text( '${note['charCount'] ?? displayText.length} 字', style: TextStyle( fontSize: 10, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], ), ), ], diff --git a/lib/views/home/home_page.dart b/lib/views/home/home_page.dart index 06d234d..414c23b 100644 --- a/lib/views/home/home_page.dart +++ b/lib/views/home/home_page.dart @@ -1,20 +1,19 @@ /// 时间: 2025-03-22 /// 功能: 诗词主页(参考微信小程序布局) /// 介绍: 展示诗词内容,支持点赞、收藏、分享等功能,参考wxpm小程序的index页面布局 -/// 最新变化: 2025-03-22 重构代码结构,使用组件化架构简化代码 +/// 最新变化: 2026-04-02 支持深色模式,添加底部内边距适配液态玻璃导航栏 import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; -import '../../../controllers/history_controller.dart'; -import '../../../utils/http/poetry_api.dart'; -import '../../../services/network_listener_service.dart'; -import '../../../utils/audio_manager.dart'; +import '../../config/app_config.dart'; +import '../../../services/get/home_controller.dart'; +import '../../../services/get/theme_controller.dart'; import 'home_part.dart'; -import 'home_components.dart'; -import 'home-load.dart'; -import '../profile/guide/tongji.dart'; +import 'set/home_components.dart'; +import 'set/home-load.dart'; +import 'set/home-set.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -23,862 +22,238 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } -class _HomePageState extends State - with TickerProviderStateMixin, NetworkListenerMixin { - PoetryData? _poetryData; - List _keywordList = []; - bool _loading = false; - bool _isLiked = false; - bool _isLoadingLike = false; - String _errorMessage = ''; - String _starDisplay = ''; - List> _historyList = []; - int _currentHistoryIndex = -1; - late AnimationController _fadeController; - late AnimationController _slideController; - late Animation _fadeAnimation; - late Animation _slideAnimation; - - final GlobalKey _repaintKey = GlobalKey(); - - // 动态加载状态 - bool _isLoadingNext = false; - Map _sectionLoadingStates = { - 'title': false, - 'content': false, - 'name': false, - 'keywords': false, - 'introduction': false, - }; +class _HomePageState extends State { + final GlobalKey repaintKey = GlobalKey(); + final SecondaryButtonsManager _secondaryButtonsManager = + SecondaryButtonsManager(); + final FloatingButtonsVisibilityManager _floatingButtonsVisibilityManager = + FloatingButtonsVisibilityManager(); @override void initState() { super.initState(); - _initAnimations(); - _loadHistory(); - _initAutoRefresh(); - _initDebugInfo(); - _initOfflineDataManager(); - _initAudioManager(); - _initSecondaryButtonsManager(); - // 延迟加载诗词,确保页面先显示 - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadPoetry(); - }); - } - - Future _initSecondaryButtonsManager() async { - await SecondaryButtonsManager().init(); - } - - Future _initAudioManager() async { - await AudioManager().init(); - } - - Future _initOfflineDataManager() async { - final offlineDataManager = OfflineDataManager(); - await offlineDataManager.init(); - } - - Future _initAutoRefresh() async { - final autoRefreshManager = AutoRefreshManager(); - await autoRefreshManager.init(); - autoRefreshManager.setOnRefresh(() { - if (mounted) { - _loadNextPoetry(); - } - }); - if (autoRefreshManager.isEnabled) { - autoRefreshManager.setEnabled(true); - } - } - - Future _initDebugInfo() async { - final debugInfoManager = DebugInfoManager(); - await debugInfoManager.init(); - } - - void _initAnimations() { - _fadeController = AnimationUtils.createFadeController(this); - _slideController = AnimationUtils.createSlideController(this); - _fadeAnimation = AnimationUtils.createFadeAnimation(_fadeController); - _slideAnimation = AnimationUtils.createSlideAnimation(_slideController); + _secondaryButtonsManager.init(); + _floatingButtonsVisibilityManager.init(); } @override void dispose() { - _fadeController.dispose(); - _slideController.dispose(); - // 只停止定时器,不释放单例资源 - AutoRefreshManager().stopTimer(); - // 不调用DebugInfoManager的dispose,因为它是单例,其他页面可能还在使用 + _floatingButtonsVisibilityManager.dispose(); super.dispose(); } - Future _loadPoetry() async { - if (_loading) return; - - setState(() => _loading = true); - PoetryStateManager.triggerHapticFeedback(); - - try { - final offlineDataManager = OfflineDataManager(); - final isOnline = await offlineDataManager.isOnline(); - final hasCachedData = await offlineDataManager.hasCachedData(); - - if (isOnline) { - // 在线状态:从网络加载 - final response = await PoetryApi.getRandomPoetry(); - - if (mounted && response.data != null) { - // 记录浏览统计 - try { - await StatisticsManager().recordView(); - await StatisticsManager().recordFirstUse(); - await StatisticsManager().recordTotalView(); - } catch (e) { - // 忽略错误 - } - - setState(() { - _poetryData = response.data; - _keywordList = PoetryDataUtils.extractKeywords(response.data); - _starDisplay = PoetryDataUtils.getStarDisplay(response.data); - _isLiked = false; - _loading = false; - _errorMessage = ''; - }); - - _fadeController.forward(); - _slideController.forward(); - _checkIfLiked(); - await _saveToHistory(response.data!); - DebugInfoManager().showRefreshSuccess(); - } else { - // 数据为空时,显示默认内容 - if (mounted) { - setState(() { - _poetryData = _createDefaultPoetryData(); - _keywordList = []; - _starDisplay = '⭐⭐⭐⭐⭐'; - _isLiked = false; - _loading = false; - _errorMessage = ''; - }); - - _fadeController.forward(); - _slideController.forward(); - } - DebugInfoManager().showRefreshFailed(); - } - } else { - // 离线状态:从本地缓存加载 - if (hasCachedData) { - final poetryData = await offlineDataManager.getNextPoetry(); - - if (mounted && poetryData != null) { - // 记录浏览统计 - try { - await StatisticsManager().recordView(); - await StatisticsManager().recordFirstUse(); - await StatisticsManager().recordTotalView(); - } catch (e) { - // 忽略错误 - } - - setState(() { - _poetryData = poetryData; - _keywordList = PoetryDataUtils.extractKeywords(poetryData); - _starDisplay = PoetryDataUtils.getStarDisplay(poetryData); - _isLiked = false; - _loading = false; - _errorMessage = ''; - }); - - _fadeController.forward(); - _slideController.forward(); - _checkIfLiked(); - DebugInfoManager().showRefreshSuccess(); - } else { - // 缓存为空时,显示错误信息 - if (mounted) { - setState(() { - _loading = false; - _errorMessage = '离线模式下无缓存数据,请先在线下载'; - }); - } - DebugInfoManager().showRefreshFailed(); - } - } else { - // 离线且无缓存时,显示错误信息 - if (mounted) { - setState(() { - _loading = false; - _errorMessage = '离线模式下无缓存数据,请先在线下载'; - }); - } - DebugInfoManager().showRefreshFailed(); - } - } - } catch (e) { - if (mounted) { - setState(() { - _loading = false; - _errorMessage = '加载失败,请检查网络连接'; - }); - } - DebugInfoManager().showRefreshFailed(); - } - } - - // 创建默认诗词数据,确保页面始终有内容显示 - PoetryData _createDefaultPoetryData() { - final now = DateTime.now(); - final dateStr = now.toString().substring(0, 10); - final monthStr = dateStr.substring(0, 7); - final timeStr = now.toString().substring(11, 19); - - return PoetryData( - id: 1, - name: '静夜思', - alias: '李白', - keywords: '思乡,月亮,静夜', - introduce: '床前明月光,疑是地上霜。举头望明月,低头思故乡。', - drtime: '唐·李白《静夜思》', - like: 0, - url: '李白-静夜思', - tui: 1, - star: 5, - hitsTotal: 10000, - hitsMonth: 1000, - hitsDay: 100, - date: dateStr, - datem: monthStr, - time: timeStr, - createTime: timeStr, - updateTime: timeStr, - ); - } - - Future _loadPoetryById(int poetryId) async { - if (_loading) return; - - setState(() => _loading = true); - - try { - final response = await PoetryApi.getPoetryById(poetryId); - - if (mounted && response.data != null) { - // 记录浏览统计 - try { - await StatisticsManager().recordView(); - await StatisticsManager().recordFirstUse(); - await StatisticsManager().recordTotalView(); - } catch (e) { - // 忽略错误 - } - - setState(() { - _poetryData = response.data; - _keywordList = PoetryDataUtils.extractKeywords(response.data); - _starDisplay = PoetryDataUtils.getStarDisplay(response.data); - _loading = false; - _errorMessage = ''; - }); - - _fadeController.forward(); - _slideController.forward(); - _checkIfLiked(); - _updateCurrentHistoryIndex(); - } else { - // 数据为空时,显示默认内容 - if (mounted) { - setState(() { - _poetryData = _createDefaultPoetryData(); - _keywordList = []; - _starDisplay = '⭐⭐⭐⭐⭐'; - _isLiked = false; - _loading = false; - _errorMessage = ''; - }); - - _fadeController.forward(); - _slideController.forward(); - } - } - } catch (e) { - if (mounted) { - setState(() { - _poetryData = _createDefaultPoetryData(); - _keywordList = []; - _starDisplay = '⭐⭐⭐⭐⭐'; - _isLiked = false; - _loading = false; - _errorMessage = ''; - }); - - _fadeController.forward(); - _slideController.forward(); - } - } - } - - Future _toggleLike() async { - if (_poetryData == null || _isLoadingLike) return; - - // 播放点赞音效(不等待完成) - AudioManager().playLikeSound(); - - // 立即切换按钮状态和显示加载 - setState(() { - _isLoadingLike = true; - }); - - // 发送网络加载开始事件 - startNetworkLoading('toggle_like'); - - try { - final response = await PoetryApi.toggleLike(_poetryData!.id); - - if (mounted) { - setState(() { - // 根据API响应消息直接判断状态 - _isLiked = response.message.contains('点赞成功'); - - // 更新诗词数据 - if (response.data != null) { - _poetryData = PoetryData( - id: _poetryData!.id, - name: _poetryData!.name, - alias: _poetryData!.alias, - keywords: _poetryData!.keywords, - introduce: _poetryData!.introduce, - drtime: _poetryData!.drtime, - like: response.data!.like, - url: _poetryData!.url, - tui: _poetryData!.tui, - star: _poetryData!.star, - hitsTotal: _poetryData!.hitsTotal, - hitsMonth: _poetryData!.hitsMonth, - hitsDay: _poetryData!.hitsDay, - date: _poetryData!.date, - datem: _poetryData!.datem, - time: _poetryData!.time, - createTime: _poetryData!.createTime, - updateTime: _poetryData!.updateTime, - ); - } - }); - - // 管理点赞存储 - if (_isLiked) { - // 添加到点赞列表 - await HistoryController.addToLiked(_poetryData!.toJson()); - // 记录今日点赞 - await StatisticsManager().recordTodayLike(); - // 记录累计点赞 - await StatisticsManager().recordTotalLike(); - } else { - // 从点赞列表移除 - await HistoryController.removeLikedPoetry(_poetryData!.id.toString()); - } - - // 发送点赞事件通知其他页面 - sendLikeEvent(_poetryData!.id.toString(), _isLiked); - - if (_isLiked) { - DebugInfoManager().showLiked(); - } else { - DebugInfoManager().showUnliked(); - } - - PoetryStateManager.showSnackBar( - context, - response.message, - backgroundColor: AppConstants.successColor, - duration: const Duration(milliseconds: 200), - ); - } - } catch (e) { - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '操作失败,请重试', - backgroundColor: AppConstants.errorColor, - duration: const Duration(milliseconds: 200), - ); - } - } finally { - if (mounted) { - setState(() => _isLoadingLike = false); - // 发送网络加载结束事件 - endNetworkLoading('toggle_like'); - } - } - } - - Future _loadHistory() async { - try { - final historyJson = await HistoryController.getHistory(); - if (mounted) { - setState(() { - _historyList = historyJson; - // 重新计算当前诗词在历史记录中的索引 - if (_poetryData != null && _historyList.isNotEmpty) { - _currentHistoryIndex = _historyList.indexWhere( - (item) => item['id'] == _poetryData!.id, - ); - } else { - _currentHistoryIndex = -1; - } - }); - } - } catch (e) { - // 加载失败 - } - } - - Future _saveToHistory(PoetryData poetryData) async { - try { - final poetryMap = { - 'id': poetryData.id, - 'name': poetryData.name, // 诗句名称/标题 - 'alias': poetryData.alias, // 诗句朝代/作者 - 'introduce': poetryData.introduce, // 诗句译文/解释 - 'drtime': poetryData.drtime, // 诗句原文/内容 - }; - - await HistoryController.addToHistory(poetryMap); - } catch (e) { - // 保存失败 - } - } - - void _updateCurrentHistoryIndex() { - if (_poetryData?.id != null) { - final index = _historyList.indexWhere( - (item) => item['id'] == _poetryData!.id, - ); - _currentHistoryIndex = index >= 0 ? index : -1; - } - } - - Future _checkIfLiked() async { - // 这里可以实现检查收藏状态的逻辑 - // 暂时使用简单的模拟逻辑 - } - - void _loadNextPoetry() async { - if (_isLoadingNext) return; - - // 播放下一条音效(不等待完成) - AudioManager().playNextSound(); - - setState(() { - _isLoadingNext = true; - // 设置所有区域为加载状态 - _sectionLoadingStates = { - 'title': true, - 'content': true, - 'name': true, - 'keywords': true, - 'introduction': true, - }; - }); - - try { - final offlineDataManager = OfflineDataManager(); - final isOnline = await offlineDataManager.isOnline(); - final hasCachedData = await offlineDataManager.hasCachedData(); - - PoetryData? newPoetryData; - - if (isOnline) { - // 在线状态:从网络加载 - // 确保历史记录已加载 - if (_historyList.isEmpty) { - await _loadHistory(); - } - - if (_currentHistoryIndex < 0 || - _currentHistoryIndex >= _historyList.length - 1) { - // 如果没有下一条了,加载新的诗词 - final response = await PoetryApi.getRandomPoetry(); - newPoetryData = response.data; - - if (mounted && newPoetryData != null) { - await _saveToHistory(newPoetryData); - } - } else { - // 如果有下一条,加载下一条 - final nextPoetry = _historyList[_currentHistoryIndex + 1]; - final response = await PoetryApi.getPoetryById(nextPoetry['id']); - newPoetryData = response.data; - } - } else { - // 离线状态:从本地缓存加载 - if (hasCachedData) { - newPoetryData = await offlineDataManager.getNextPoetry(); - } else { - // 离线且无缓存时,显示错误信息 - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '离线模式下无缓存数据,请先在线下载', - backgroundColor: AppConstants.errorColor, - duration: const Duration(milliseconds: 200), - ); - // 重置所有加载状态 - setState(() { - _sectionLoadingStates.updateAll((key, value) => false); - }); - } - DebugInfoManager().showNextFailed(); - return; - } - } - - if (mounted && newPoetryData != null) { - // 模拟分步加载 - await _simulateSectionLoading(newPoetryData); - DebugInfoManager().showNextSuccess(); - } - } catch (e) { - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '加载下一条失败,建议开启离线模式', - backgroundColor: AppConstants.errorColor, - duration: const Duration(milliseconds: 200), - ); - // 重置所有加载状态 - setState(() { - _sectionLoadingStates.updateAll((key, value) => false); - }); - } - DebugInfoManager().showNextFailed(); - } finally { - if (mounted) { - setState(() { - _isLoadingNext = false; - }); - } - } - } - - // 模拟分步加载过程 - Future _simulateSectionLoading(PoetryData newPoetryData) async { - // 记录浏览统计 - try { - await StatisticsManager().recordView(); - await StatisticsManager().recordFirstUse(); - await StatisticsManager().recordTotalView(); - } catch (e) { - // 忽略错误 - } - - // 1. 加载标题区域 - setState(() { - _sectionLoadingStates['title'] = false; - _poetryData = newPoetryData; - }); - await Future.delayed(const Duration(milliseconds: 200)); - - // 2. 加载诗句区域 - setState(() { - _sectionLoadingStates['name'] = false; - }); - await Future.delayed(const Duration(milliseconds: 200)); - - // 3. 加载原文区域 - setState(() { - _sectionLoadingStates['content'] = false; - }); - await Future.delayed(const Duration(milliseconds: 200)); - - // 4. 加载关键词区域 - setState(() { - _sectionLoadingStates['keywords'] = false; - _keywordList = PoetryDataUtils.extractKeywords(newPoetryData); - }); - await Future.delayed(const Duration(milliseconds: 200)); - - // 5. 加载译文区域 - setState(() { - _sectionLoadingStates['introduction'] = false; - _starDisplay = PoetryDataUtils.getStarDisplay(newPoetryData); - _isLiked = false; - _errorMessage = ''; - }); - - _checkIfLiked(); - _updateCurrentHistoryIndex(); - } - - void _loadPreviousPoetry() async { - try { - final offlineDataManager = OfflineDataManager(); - final isOnline = await offlineDataManager.isOnline(); - final hasCachedData = await offlineDataManager.hasCachedData(); - - if (isOnline) { - // 在线状态:从历史记录加载 - // 确保历史记录已加载 - if (_historyList.isEmpty) { - await _loadHistory(); - } - - if (_currentHistoryIndex <= 0) { - // 如果当前索引无效或已经是第一条,则重新加载历史记录 - await _loadHistory(); - - // 如果历史记录为空,显示提示 - if (_historyList.isEmpty) { - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '暂无历史记录', - backgroundColor: AppConstants.errorColor, - duration: const Duration(milliseconds: 200), - ); - } - return; - } - - // 如果当前索引无效,设置为最新的一条 - if (_currentHistoryIndex < 0) { - _currentHistoryIndex = 0; - } - } - - // 检查是否有上一条 - if (_currentHistoryIndex > 0) { - final previousPoetry = _historyList[_currentHistoryIndex - 1]; - await _loadPoetryById(previousPoetry['id']); - } else { - // 如果没有上一条了,显示提示 - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '已经是第一条了', - backgroundColor: AppConstants.infoColor, - duration: const Duration(milliseconds: 200), - ); - } - } - } else { - // 离线状态:从本地缓存加载 - if (hasCachedData) { - final poetryData = await offlineDataManager.getPreviousPoetry(); - - if (mounted && poetryData != null) { - setState(() { - _poetryData = poetryData; - _keywordList = PoetryDataUtils.extractKeywords(poetryData); - _starDisplay = PoetryDataUtils.getStarDisplay(poetryData); - _isLiked = false; - _errorMessage = ''; - }); - - _fadeController.forward(); - _slideController.forward(); - _checkIfLiked(); - DebugInfoManager().showPreviousSuccess(); - } else { - // 缓存为空时,显示错误信息 - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '离线模式下无缓存数据,请先在线下载', - backgroundColor: AppConstants.errorColor, - duration: const Duration(milliseconds: 200), - ); - } - DebugInfoManager().showPreviousFailed(); - } - } else { - // 离线且无缓存时,显示错误信息 - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '离线模式下无缓存数据,请先在线下载', - backgroundColor: AppConstants.errorColor, - duration: const Duration(milliseconds: 200), - ); - } - DebugInfoManager().showPreviousFailed(); - } - } - } catch (e) { - if (mounted) { - PoetryStateManager.showSnackBar( - context, - '加载上一条失败', - backgroundColor: AppConstants.errorColor, - duration: const Duration(milliseconds: 200), - ); - } - } - } - - Future _refreshPoetry() async { - if (_poetryData?.id != null) { - // 如果有当前诗词ID,则重新加载当前诗词 - await _loadPoetryById(_poetryData!.id); - } else { - // 如果没有当前诗词ID,则加载新的诗词 - await _loadPoetry(); - } - } - - Future _sharePoetryImage() async { - if (_poetryData == null) return; - await ShareImageUtils.captureAndShare( - context, - _repaintKey, - subject: _poetryData!.name, - ); - } - @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[50], - body: SafeArea(child: _buildBody()), - ); + Get.lazyPut(() => HomeController()); + final controller = Get.find(); + final themeController = Get.find(); + + return Obx(() { + final isDark = themeController.isDarkModeRx.value; + return Scaffold( + backgroundColor: isDark ? const Color(0xFF121212) : Colors.grey[50], + body: SafeArea(child: _buildBody(controller, isDark)), + ); + }); } - Widget _buildBody() { - if (_loading) { - return const LoadingWidget(); + Widget _buildBody(HomeController controller, bool isDark) { + if (controller.loading.value) { + return LoadingWidget(isDark: isDark); } - if (_errorMessage.isNotEmpty) { + if (controller.errorMessage.value.isNotEmpty) { return CustomErrorWidget( - errorMessage: _errorMessage, - onRetry: _loadPoetry, + errorMessage: controller.errorMessage.value, + onRetry: controller.loadPoetry, + isDark: isDark, ); } - if (!PoetryDataUtils.isValidPoetryData(_poetryData)) { - return const EmptyWidget(); + if (!PoetryDataUtils.isValidPoetryData(controller.poetryData.value)) { + return EmptyWidget(isDark: isDark); } - return _buildContent(); + return _buildContent(controller, isDark); } - Widget _buildContent() { + Widget _buildContent(HomeController controller, bool isDark) { return FutureBuilder( future: OfflineDataManager().isOnline(), builder: (context, snapshot) { final isOnline = snapshot.data ?? true; - return RefreshIndicator( - onRefresh: _refreshPoetry, - child: FadeTransition( - opacity: _fadeAnimation, - child: ValueListenableBuilder( - valueListenable: SecondaryButtonsManager().hiddenNotifier, - builder: (context, hideSecondaryButtons, child) { - return Stack( + return Stack( + children: [ + RefreshIndicator( + onRefresh: controller.refreshPoetry, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.only( + top: 48, + bottom: AppConfig.liquidGlassTotalHeight + 16, + ), + child: Column( children: [ - SingleChildScrollView( - physics: - const AlwaysScrollableScrollPhysics(), // 确保可以下拉刷新 - padding: const EdgeInsets.symmetric(vertical: 16), - child: Column( - children: [ - PoetryCard( - poetryData: _poetryData!, - keywordList: _keywordList, - onTap: _loadNextPoetry, - sectionLoadingStates: _sectionLoadingStates, - repaintKey: _repaintKey, - ), - const SizedBox(height: 160), // 为悬浮按钮留出更多空间 - ], - ), - ), - // 调试信息气泡 - Positioned( - bottom: 66, - left: 0, - right: 0, - child: Center(child: _buildDebugInfoBubble()), - ), - // 悬浮上一条按钮 - 左边上方(根据设置显示/隐藏) - if (!hideSecondaryButtons) - Positioned( - left: 16, - bottom: 100, - child: FloatingPreviousButton( - onPrevious: _loadPreviousPoetry, + Obx( + () => PoetryCard( + poetryData: controller.poetryData.value!, + keywordList: List.from(controller.keywordList), + onTap: controller.loadNextPoetry, + sectionLoadingStates: Map.from( + controller.sectionLoadingStates, ), + repaintKey: repaintKey, + isDark: isDark, ), - // 悬浮下一条按钮 - 左边下方 - Positioned( - left: 16, - bottom: 32, - child: FloatingNextButton(onNext: _loadNextPoetry), ), - // 悬浮分享按钮 - 右边上方(根据设置显示/隐藏) - if (!hideSecondaryButtons) - Positioned( - right: 16, - bottom: 100, - child: FloatingShareButton(onShare: _sharePoetryImage), - ), - // 悬浮点赞按钮 - 右边(仅在线状态显示) - if (isOnline) - Positioned( - right: 16, - bottom: 32, - child: FloatingLikeButton( - isLiked: _isLiked, - isLoadingLike: _isLoadingLike, - onToggleLike: _toggleLike, - ), - ), + const SizedBox(height: 160), ], + ), + ), + ), + // 收起/恢复按钮 - 右上角(与下方分享按钮对齐) + Positioned( + top: 8, + right: 16, + child: FloatingButtonsToggleButton( + manager: _floatingButtonsVisibilityManager, + isDark: isDark, + ), + ), + // 左侧按钮组 - 固定位置,紧贴底栏bar + ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.flashingNotifier, + builder: (context, isFlashing, child) { + return ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.visibleNotifier, + builder: (context, isVisible, child) { + if (!isVisible && !isFlashing) { + return const SizedBox.shrink(); + } + return ValueListenableBuilder( + valueListenable: _secondaryButtonsManager.hiddenNotifier, + builder: (context, isHidden, child) { + if (isHidden) return const SizedBox.shrink(); + return Positioned( + left: 16, + bottom: + AppConfig.liquidGlassHeight + + AppConfig.liquidGlassBottomMargin + + 68, + child: FloatingPreviousButton( + onPrevious: controller.loadPreviousPoetry, + isDark: isDark, + ), + ); + }, + ); + }, ); }, ), - ), - ); - }, - ); - } - - Widget _buildDebugInfoBubble() { - return ValueListenableBuilder( - valueListenable: DebugInfoManager().messageNotifier, - builder: (context, message, child) { - if (message.isEmpty) { - return const SizedBox.shrink(); - } - - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: AppConstants.primaryColor.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Text( - message, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w500, + ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.flashingNotifier, + builder: (context, isFlashing, child) { + return ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.visibleNotifier, + builder: (context, isVisible, child) { + if (!isVisible && !isFlashing) { + return const SizedBox.shrink(); + } + return Positioned( + left: 16, + bottom: + AppConfig.liquidGlassHeight + + AppConfig.liquidGlassBottomMargin + + 8, + child: FloatingNextButton( + onNext: controller.loadNextPoetry, + isDark: isDark, + ), + ); + }, + ); + }, ), - ), + // 右侧按钮组 - 固定位置,紧贴底栏bar + ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.flashingNotifier, + builder: (context, isFlashing, child) { + return ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.visibleNotifier, + builder: (context, isVisible, child) { + if (!isVisible && !isFlashing) { + return const SizedBox.shrink(); + } + return ValueListenableBuilder( + valueListenable: _secondaryButtonsManager.hiddenNotifier, + builder: (context, isHidden, child) { + if (isHidden) return const SizedBox.shrink(); + return Positioned( + right: 16, + bottom: + AppConfig.liquidGlassHeight + + AppConfig.liquidGlassBottomMargin + + 68, + child: FloatingShareButton( + onShare: () async { + if (controller.poetryData.value != null) { + await ShareImageUtils.captureAndShare( + context, + repaintKey, + subject: controller.poetryData.value!.name, + ); + } + }, + isDark: isDark, + ), + ); + }, + ); + }, + ); + }, + ), + if (isOnline) + ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.flashingNotifier, + builder: (context, isFlashing, child) { + return ValueListenableBuilder( + valueListenable: + _floatingButtonsVisibilityManager.visibleNotifier, + builder: (context, isVisible, child) { + if (!isVisible && !isFlashing) { + return const SizedBox.shrink(); + } + return Positioned( + right: 16, + bottom: + AppConfig.liquidGlassHeight + + AppConfig.liquidGlassBottomMargin + + 8, + child: Obx( + () => FloatingLikeButton( + isLiked: controller.isLiked.value, + isLoadingLike: controller.isLoadingLike.value, + onToggleLike: controller.toggleLike, + isDark: isDark, + ), + ), + ); + }, + ); + }, + ), + ], ); }, ); diff --git a/lib/views/home/home_part.dart b/lib/views/home/home_part.dart index 9ea3a64..f288048 100644 --- a/lib/views/home/home_part.dart +++ b/lib/views/home/home_part.dart @@ -1,15 +1,14 @@ /// 时间: 2025-03-22 /// 功能: 诗词页面组件部分 /// 介绍: 包含诗词卡片、操作按钮、统计信息等组件的独立文件 -/// 最新变化: 重构代码结构,使用工具类简化代码 +/// 最新变化: 2026-04-02 支持深色模式 import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/poetry_api.dart'; import '../../../utils/audio_manager.dart'; -import 'home_components.dart'; +import 'set/home_components.dart'; /// 诗词卡片组件 - 优化版本,防止拉伸和处理文本溢出 class PoetryCard extends StatefulWidget { @@ -18,6 +17,7 @@ class PoetryCard extends StatefulWidget { final VoidCallback? onTap; final Map? sectionLoadingStates; final GlobalKey? repaintKey; + final bool isDark; const PoetryCard({ super.key, @@ -26,6 +26,7 @@ class PoetryCard extends StatefulWidget { this.onTap, this.sectionLoadingStates, this.repaintKey, + this.isDark = false, }); @override @@ -35,12 +36,12 @@ class PoetryCard extends StatefulWidget { class _PoetryCardState extends State { bool _showCopyTip = true; bool _showRecommendation = false; - bool _globalTipsEnabled = true; // 添加全局Tips开关状态 + bool _globalTipsEnabled = true; @override void initState() { super.initState(); - _loadGlobalTipsSettings(); // 加载全局Tips设置 + _loadGlobalTipsSettings(); } String _getTimeOfDayGreeting() { @@ -85,7 +86,6 @@ class _PoetryCardState extends State { } } - // 加载全局Tips设置 Future _loadGlobalTipsSettings() async { try { final prefs = await SharedPreferences.getInstance(); @@ -95,7 +95,6 @@ class _PoetryCardState extends State { }); } } catch (e) { - // 如果加载失败,默认开启 if (mounted) { setState(() { _globalTipsEnabled = true; @@ -106,11 +105,10 @@ class _PoetryCardState extends State { @override Widget build(BuildContext context) { + final isDark = widget.isDark; final card = GestureDetector( onTap: () { - // 播放点击音效(不等待完成) AudioManager().playClickSound(); - // 调用原始的onTap回调 widget.onTap?.call(); }, child: Stack( @@ -118,11 +116,11 @@ class _PoetryCardState extends State { Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withAlpha(10), + color: Colors.black.withAlpha(isDark ? 40 : 10), blurRadius: 10, offset: const Offset(0, 2), ), @@ -131,19 +129,18 @@ class _PoetryCardState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - _buildTimeBar(), + _buildTimeBar(isDark), Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _buildTitleSection(), + _buildTitleSection(isDark), if (widget.poetryData.drtime.isNotEmpty) ...[ - _buildContentSection(context), + _buildContentSection(context, isDark), const SizedBox(height: 3), ], - // 精选诗句标签 - 放在缝隙位置 Align( alignment: Alignment.centerRight, child: Container( @@ -173,14 +170,14 @@ class _PoetryCardState extends State { ), ), ), - _buildNameSection(), + _buildNameSection(isDark), const SizedBox(height: 12), if (widget.keywordList.isNotEmpty) ...[ - _buildKeywordSection(), + _buildKeywordSection(isDark), const SizedBox(height: 16), ], if (widget.poetryData.introduce.isNotEmpty) ...[ - _buildIntroductionSection(context), + _buildIntroductionSection(context, isDark), ], ], ), @@ -188,7 +185,7 @@ class _PoetryCardState extends State { ], ), ), - _buildCopyTip(), + _buildCopyTip(isDark), ], ), ); @@ -199,7 +196,7 @@ class _PoetryCardState extends State { return card; } - Widget _buildTimeBar() { + Widget _buildTimeBar(bool isDark) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { @@ -254,7 +251,7 @@ class _PoetryCardState extends State { ); } - Widget _buildCopyTip() { + Widget _buildCopyTip(bool isDark) { if (!_globalTipsEnabled || !_showCopyTip) { return const SizedBox.shrink(); } @@ -299,7 +296,7 @@ class _PoetryCardState extends State { ); } - Widget _buildTitleSection() { + Widget _buildTitleSection(bool isDark) { final isLoading = widget.sectionLoadingStates?['title'] ?? false; return SizedBox( @@ -313,6 +310,7 @@ class _PoetryCardState extends State { context, widget.poetryData.url, '诗人和标题', + isDark: isDark, ), child: isLoading ? Row( @@ -332,7 +330,9 @@ class _PoetryCardState extends State { '出处加载中...', style: TextStyle( fontSize: 14, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], fontStyle: FontStyle.italic, ), ), @@ -340,10 +340,10 @@ class _PoetryCardState extends State { ) : Text( "出处: ${widget.poetryData.url}", - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.normal, - color: Colors.black, + color: isDark ? Colors.grey[200] : Colors.black, fontStyle: FontStyle.italic, ), overflow: TextOverflow.ellipsis, @@ -357,7 +357,7 @@ class _PoetryCardState extends State { ); } - Widget _buildNameSection() { + Widget _buildNameSection(bool isDark) { final isLoading = widget.sectionLoadingStates?['name'] ?? false; return Container( @@ -366,21 +366,27 @@ class _PoetryCardState extends State { margin: EdgeInsets.zero, decoration: BoxDecoration( gradient: LinearGradient( - colors: [ - AppConstants.primaryColor.withAlpha(26), - AppConstants.primaryColor.withAlpha(13), - ], + colors: isDark + ? [const Color(0xFF2A2A2A), const Color(0xFF252525)] + : [ + AppConstants.primaryColor.withAlpha(26), + AppConstants.primaryColor.withAlpha(13), + ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), border: Border.all( - color: AppConstants.primaryColor.withAlpha(51), + color: isDark + ? Colors.grey[700]! + : AppConstants.primaryColor.withAlpha(51), width: 1, ), boxShadow: [ BoxShadow( - color: AppConstants.primaryColor.withAlpha(26), + color: isDark + ? Colors.black.withAlpha(40) + : AppConstants.primaryColor.withAlpha(26), blurRadius: 8, offset: const Offset(0, 2), ), @@ -390,8 +396,12 @@ class _PoetryCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( - onLongPress: () => - CopyUtils.showCopyDialog(context, widget.poetryData.name, '诗词'), + onLongPress: () => CopyUtils.showCopyDialog( + context, + widget.poetryData.name, + '诗词', + isDark: isDark, + ), child: isLoading ? Center( child: Column( @@ -411,7 +421,7 @@ class _PoetryCardState extends State { '诗句加载中...', style: TextStyle( fontSize: 14, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), ], @@ -419,10 +429,10 @@ class _PoetryCardState extends State { ) : Text( _formatPoetryText(widget.poetryData.name), - style: const TextStyle( + style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.grey[100] : Colors.black87, height: 1.4, ), textAlign: TextAlign.center, @@ -434,30 +444,25 @@ class _PoetryCardState extends State { } String _formatPoetryText(String text) { - // 检查文本长度(文字+符号) if (text.length < 10) { - return text; // 小于10个字,不换行 + return text; } - // 找到第一个逗号的位置 final commaIndex = text.indexOf(','); if (commaIndex != -1) { - // 在第一个逗号处添加换行 return text.replaceFirst(',', ',\n'); } - return text; // 没有逗号,保持原样 + return text; } - Widget _buildKeywordSection() { + Widget _buildKeywordSection(bool isDark) { final isLoading = widget.sectionLoadingStates?['keywords'] ?? false; return Column( children: [ - // 第一行:关键词和朝代(左边)vs 星星和点赞(右边) Row( children: [ - // 左边:关键词和朝代 Expanded( child: isLoading ? Center( @@ -479,7 +484,9 @@ class _PoetryCardState extends State { '关键词加载中...', style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], ), ), ], @@ -490,39 +497,43 @@ class _PoetryCardState extends State { runSpacing: 4, children: [ if (widget.keywordList.isNotEmpty) - ...widget.keywordList - .map( - (keyword) => Builder( - builder: (context) => GestureDetector( - onLongPress: () => CopyUtils.showCopyDialog( - context, - keyword, - '关键词', - ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: AppConstants.secondaryColor - .withAlpha(26), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - keyword, - style: TextStyle( - color: AppConstants.secondaryColor, - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + ...widget.keywordList.map( + (keyword) => Builder( + builder: (context) => GestureDetector( + onLongPress: () => CopyUtils.showCopyDialog( + context, + keyword, + '关键词', + isDark: isDark, + ), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: isDark + ? AppConstants.secondaryColor.withAlpha( + 40, + ) + : AppConstants.secondaryColor.withAlpha( + 26, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + keyword, + style: TextStyle( + color: AppConstants.secondaryColor, + fontSize: 12, ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), ), - ) - .toList(), + ), + ), + ), if (widget.poetryData.alias.isNotEmpty) Builder( builder: (context) => GestureDetector( @@ -530,6 +541,7 @@ class _PoetryCardState extends State { context, widget.poetryData.alias, '朝代', + isDark: isDark, ), child: Container( padding: const EdgeInsets.symmetric( @@ -537,9 +549,9 @@ class _PoetryCardState extends State { vertical: 4, ), decoration: BoxDecoration( - color: AppConstants.primaryColor.withAlpha( - 26, - ), + color: isDark + ? AppConstants.primaryColor.withAlpha(40) + : AppConstants.primaryColor.withAlpha(26), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -558,7 +570,6 @@ class _PoetryCardState extends State { ], ), ), - // 右边:星星和点赞 if (!isLoading) Row( mainAxisSize: MainAxisSize.min, @@ -570,14 +581,20 @@ class _PoetryCardState extends State { const SizedBox(width: 4), Text( PoetryDataUtils.generateLikeText(widget.poetryData.like), - style: const TextStyle(fontSize: 12, color: Colors.grey), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey, + ), ), const SizedBox(width: 4), Text( PoetryDataUtils.generateViewText( widget.poetryData.hitsTotal, ), - style: const TextStyle(fontSize: 12, color: Colors.grey), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey, + ), ), ], ), @@ -587,344 +604,162 @@ class _PoetryCardState extends State { ); } - Widget _buildContentSection(BuildContext context) { + Widget _buildContentSection(BuildContext context, bool isDark) { final isLoading = widget.sectionLoadingStates?['content'] ?? false; - return GestureDetector( - onLongPress: () => - CopyUtils.showCopyDialog(context, widget.poetryData.drtime, '原文'), - child: Container( - width: double.infinity, - constraints: const BoxConstraints(maxHeight: 200), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), - borderRadius: BorderRadius.circular(12), + return Builder( + builder: (context) => GestureDetector( + onLongPress: () => CopyUtils.showCopyDialog( + context, + widget.poetryData.drtime, + '时间', + isDark: isDark, ), - child: isLoading - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - AppConstants.primaryColor, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + margin: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: isDark + ? Colors.grey[800]!.withAlpha(80) + : AppConstants.primaryColor.withAlpha(20), + borderRadius: BorderRadius.circular(8), + ), + child: isLoading + ? Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + AppConstants.primaryColor, + ), ), ), + const SizedBox(width: 8), + Text( + '时间加载中...', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '诗词原文', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark + ? Colors.grey[300] + : AppConstants.primaryColor, + ), ), const SizedBox(height: 8), Text( - '原文加载中...', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + widget.poetryData.drtime, + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + textAlign: TextAlign.center, ), ], ), - ) - : SingleChildScrollView( - child: Text( - widget.poetryData.drtime, - style: const TextStyle( - fontSize: 16, - height: 1.6, - color: Colors.black87, - ), - ), - ), + ), ), ); } - Widget _buildIntroductionSection(BuildContext context) { + Widget _buildIntroductionSection(BuildContext context, bool isDark) { final isLoading = widget.sectionLoadingStates?['introduction'] ?? false; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '译文', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppConstants.primaryColor, - ), + return Builder( + builder: (context) => GestureDetector( + onLongPress: () => CopyUtils.showCopyDialog( + context, + widget.poetryData.introduce, + '简介', + isDark: isDark, ), - const SizedBox(height: 8), - GestureDetector( - onLongPress: () => CopyUtils.showCopyDialog( - context, - widget.poetryData.introduce, - '译文', - ), - child: Container( - width: double.infinity, - constraints: const BoxConstraints(maxHeight: 150), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppConstants.infoColor.withAlpha(13), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppConstants.infoColor.withAlpha(51)), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF252525) : Colors.grey[50], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[200]!, ), - child: isLoading - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - AppConstants.primaryColor, - ), + ), + child: isLoading + ? Center( + child: Column( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + AppConstants.primaryColor, ), ), - const SizedBox(height: 8), + ), + const SizedBox(height: 8), + Text( + '简介加载中...', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + const SizedBox(width: 4), Text( - '译文加载中...', + '简介', style: TextStyle( fontSize: 14, - color: Colors.grey[600], + fontWeight: FontWeight.w500, + color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ], ), - ) - : SingleChildScrollView( - child: Text( + const SizedBox(height: 8), + Text( widget.poetryData.introduce, - style: const TextStyle( + style: TextStyle( fontSize: 14, - height: 1.6, - color: Colors.black87, + height: 1.5, + color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), - ), - ), - ), - ], - ); - } -} - -/// 悬浮上一条按钮组件 -class FloatingPreviousButton extends StatelessWidget { - final VoidCallback onPrevious; - - const FloatingPreviousButton({super.key, required this.onPrevious}); - - @override - Widget build(BuildContext context) { - return Tooltip( - message: 'beta功能', - child: Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: AppConstants.secondaryColor, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: AppConstants.secondaryColor.withAlpha(76), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(28), - onTap: () { - HapticFeedback.lightImpact(); - AudioManager().playClickSound(); - onPrevious(); - }, - child: const Center( - child: Icon(Icons.arrow_back, color: Colors.white, size: 28), - ), - ), + ], + ), ), ), ); } } - -/// 悬浮下一条按钮组件 -class FloatingNextButton extends StatelessWidget { - final VoidCallback onNext; - - const FloatingNextButton({super.key, required this.onNext}); - - @override - Widget build(BuildContext context) { - return Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: AppConstants.primaryColor, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: AppConstants.primaryColor.withAlpha(76), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(28), - onTap: () { - HapticFeedback.lightImpact(); - AudioManager().playNextSound(); - onNext(); - }, - child: const Center( - child: Icon(Icons.arrow_forward, color: Colors.white, size: 28), - ), - ), - ), - ); - } -} - -/// 悬浮点赞按钮组件 -class FloatingLikeButton extends StatelessWidget { - final bool isLiked; - final bool isLoadingLike; - final VoidCallback onToggleLike; - - const FloatingLikeButton({ - super.key, - required this.isLiked, - required this.isLoadingLike, - required this.onToggleLike, - }); - - @override - Widget build(BuildContext context) { - return Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: isLiked ? AppConstants.errorColor : AppConstants.primaryColor, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: - (isLiked ? AppConstants.errorColor : AppConstants.primaryColor) - .withAlpha(76), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(28), - onTap: isLoadingLike - ? null - : () { - HapticFeedback.mediumImpact(); - AudioManager().playLikeSound(); - onToggleLike(); - }, - child: Center( - child: isLoadingLike - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white70), - ), - ) - : Icon( - isLiked ? Icons.favorite : Icons.favorite_border, - color: Colors.white, - size: 28, - ), - ), - ), - ), - ); - } -} - -/// 统计信息卡片组件 -class StatsCard extends StatelessWidget { - final PoetryData poetryData; - - const StatsCard({super.key, required this.poetryData}); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(5), - blurRadius: 5, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '统计信息', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppConstants.primaryColor, - ), - ), - const SizedBox(height: 12), - Row( - children: [ - _buildStatItem('今日', poetryData.hitsDay.toString()), - const SizedBox(width: 20), - _buildStatItem('本月', poetryData.hitsMonth.toString()), - const SizedBox(width: 20), - _buildStatItem('总计', poetryData.hitsTotal.toString()), - ], - ), - ], - ), - ); - } - - Widget _buildStatItem(String label, String value) { - return Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - value, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), - ], - ), - ); - } -} diff --git a/lib/views/home/home-load.dart b/lib/views/home/set/home-load.dart similarity index 77% rename from lib/views/home/home-load.dart rename to lib/views/home/set/home-load.dart index 02b5eda..a93ba29 100644 --- a/lib/views/home/home-load.dart +++ b/lib/views/home/set/home-load.dart @@ -4,9 +4,13 @@ /// 最新变化: 新建文件 import 'dart:async'; +import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../../utils/http/poetry_api.dart'; +import '../../../../utils/http/poetry_api.dart'; +import '../../../../constants/app_constants.dart'; class SecondaryButtonsManager { static const String _hideSecondaryButtonsKey = 'hide_secondary_buttons'; @@ -51,6 +55,7 @@ class AutoRefreshManager { Timer? _refreshTimer; bool _isEnabled = false; VoidCallback? _onRefresh; + final ValueNotifier _enabledNotifier = ValueNotifier(false); AutoRefreshManager._internal(); @@ -62,12 +67,19 @@ class AutoRefreshManager { Future init() async { final prefs = await SharedPreferences.getInstance(); _isEnabled = prefs.getBool(_autoRefreshKey) ?? false; + _enabledNotifier.value = _isEnabled; + + if (_isEnabled) { + _startTimer(); + } } bool get isEnabled => _isEnabled; + ValueNotifier get enabledNotifier => _enabledNotifier; Future setEnabled(bool enabled) async { _isEnabled = enabled; + _enabledNotifier.value = enabled; final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_autoRefreshKey, enabled); @@ -113,8 +125,7 @@ class DebugInfoManager { static DebugInfoManager? _instance; bool _isEnabled = false; - final ValueNotifier _messageNotifier = ValueNotifier(''); - Timer? _messageTimer; + final ValueNotifier _enabledNotifier = ValueNotifier(false); DebugInfoManager._internal(); @@ -126,30 +137,42 @@ class DebugInfoManager { Future init() async { final prefs = await SharedPreferences.getInstance(); _isEnabled = prefs.getBool(_debugInfoKey) ?? false; + _enabledNotifier.value = _isEnabled; } bool get isEnabled => _isEnabled; - ValueNotifier get messageNotifier => _messageNotifier; + ValueNotifier get enabledNotifier => _enabledNotifier; Future setEnabled(bool enabled) async { _isEnabled = enabled; + _enabledNotifier.value = enabled; final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_debugInfoKey, enabled); - - if (!enabled) { - _messageNotifier.value = ''; - } } void showMessage(String message) { if (!_isEnabled) return; - _messageNotifier.value = message; - - _messageTimer?.cancel(); - _messageTimer = Timer(const Duration(seconds: 2), () { - _messageNotifier.value = ''; - }); + Get.snackbar( + '调试信息', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.transparent, + colorText: Colors.white, + duration: const Duration(seconds: 2), + margin: const EdgeInsets.all(16), + borderRadius: 20, + animationDuration: const Duration(milliseconds: 300), + snackbarStatus: (status) {}, + maxWidth: Get.width - 32, + overlayBlur: 0.1, + overlayColor: Colors.transparent, + isDismissible: true, + padding: EdgeInsets.zero, + shouldIconPulse: false, + barBlur: 20, + // backgroundColor: Colors.black.withValues(alpha: 0.15), + ); } void showRefreshSuccess() { @@ -192,10 +215,7 @@ class DebugInfoManager { showMessage('复制失败'); } - void dispose() { - _messageTimer?.cancel(); - _messageNotifier.value = ''; - } + void dispose() {} } class OfflineDataManager { @@ -337,3 +357,38 @@ class OfflineDataManager { } } } + +class GlobalTipsManager { + static const String _globalTipsKey = 'global_tips_enabled'; + + static GlobalTipsManager? _instance; + bool _isEnabled = true; + final ValueNotifier _enabledNotifier = ValueNotifier(true); + + GlobalTipsManager._internal(); + + factory GlobalTipsManager() { + _instance ??= GlobalTipsManager._internal(); + return _instance!; + } + + Future init() async { + final prefs = await SharedPreferences.getInstance(); + _isEnabled = prefs.getBool(_globalTipsKey) ?? true; + _enabledNotifier.value = _isEnabled; + } + + bool get isEnabled => _isEnabled; + ValueNotifier get enabledNotifier => _enabledNotifier; + + Future setEnabled(bool enabled) async { + _isEnabled = enabled; + _enabledNotifier.value = enabled; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_globalTipsKey, enabled); + } + + void dispose() { + _enabledNotifier.value = true; + } +} diff --git a/lib/views/home/set/home-set.dart b/lib/views/home/set/home-set.dart new file mode 100644 index 0000000..af6200d --- /dev/null +++ b/lib/views/home/set/home-set.dart @@ -0,0 +1,190 @@ +/// 时间: 2026-04-02 +/// 功能: 主页设置和控制组件 +/// 介绍: 包含收起/恢复悬浮按钮的管理器和组件 +/// 最新变化: 2026-04-02 初始创建 + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../../../constants/app_constants.dart'; + +/// 悬浮按钮收起管理器 +class FloatingButtonsVisibilityManager { + static const String _key = 'floating_buttons_visible'; + + static FloatingButtonsVisibilityManager? _instance; + bool _isVisible = true; + bool _isFlashing = false; + int _flashCount = 0; + final ValueNotifier _visibleNotifier = ValueNotifier(true); + final ValueNotifier _flashingNotifier = ValueNotifier(false); + Timer? _hideTimer; + Timer? _flashTimer; + + FloatingButtonsVisibilityManager._internal(); + + factory FloatingButtonsVisibilityManager() { + _instance ??= FloatingButtonsVisibilityManager._internal(); + return _instance!; + } + + Future init() async { + final prefs = await SharedPreferences.getInstance(); + _isVisible = prefs.getBool(_key) ?? true; + _visibleNotifier.value = _isVisible; + } + + bool get isVisible => _isVisible; + bool get isFlashing => _isFlashing; + ValueNotifier get visibleNotifier => _visibleNotifier; + ValueNotifier get flashingNotifier => _flashingNotifier; + + Future toggle() async { + if (_isFlashing) { + await _restoreFromFlashing(); + return; + } + + _isVisible = !_isVisible; + _visibleNotifier.value = _isVisible; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_key, _isVisible); + + if (!_isVisible) { + _startHideTimer(); + } else { + _cancelAllTimers(); + } + } + + Future _restoreFromFlashing() async { + _cancelAllTimers(); + _isFlashing = false; + _flashingNotifier.value = false; + _isVisible = true; + _visibleNotifier.value = true; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_key, true); + } + + void _startHideTimer() { + _cancelAllTimers(); + _hideTimer = Timer(const Duration(seconds: 5), () { + _startFlashing(); + }); + } + + void _startFlashing() { + _isFlashing = true; + _flashingNotifier.value = true; + _flashCount = 0; + _flashTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + _flashCount++; + _flashingNotifier.value = !_flashingNotifier.value; + + if (_flashCount >= 6) { + timer.cancel(); + _isFlashing = false; + _flashingNotifier.value = false; + _isVisible = false; + _visibleNotifier.value = false; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_key, false); + } + }); + } + + void _cancelAllTimers() { + if (_hideTimer != null) { + _hideTimer!.cancel(); + _hideTimer = null; + } + if (_flashTimer != null) { + _flashTimer!.cancel(); + _flashTimer = null; + } + } + + void dispose() { + _cancelAllTimers(); + _visibleNotifier.value = true; + _flashingNotifier.value = false; + } +} + +/// 收起/恢复悬浮按钮组件 +class FloatingButtonsToggleButton extends StatelessWidget { + final FloatingButtonsVisibilityManager manager; + final bool isDark; + + const FloatingButtonsToggleButton({ + super.key, + required this.manager, + required this.isDark, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: manager.flashingNotifier, + builder: (context, isFlashing, child) { + return ValueListenableBuilder( + valueListenable: manager.visibleNotifier, + builder: (context, isVisible, child) { + final shouldShow = isFlashing ? !isFlashing : isVisible; + + return SizedBox( + width: 44, + height: 44, + child: shouldShow || isFlashing + ? Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(isDark ? 40 : 20), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () { + manager.toggle(); + }, + child: Center( + child: Icon( + isFlashing + ? Icons.visibility + : Icons.visibility_off, + color: isFlashing + ? AppConstants.primaryColor + : (isDark + ? Colors.grey[300] + : AppConstants.primaryColor), + size: 24, + ), + ), + ), + ), + ) + : Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () { + manager.toggle(); + }, + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/views/home/home_components.dart b/lib/views/home/set/home_components.dart similarity index 53% rename from lib/views/home/home_components.dart rename to lib/views/home/set/home_components.dart index 4034425..ff80553 100644 --- a/lib/views/home/home_components.dart +++ b/lib/views/home/set/home_components.dart @@ -1,25 +1,29 @@ /// 时间: 2025-03-22 /// 功能: 诗词页面通用组件和工具函数 /// 介绍: 从 home_page.dart 和 home_part.dart 中提取的公共组件,用于代码复用和简化 +/// 最新变化: 2026-04-02 支持深色模式 import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; -import '../../constants/app_constants.dart'; -import '../../utils/http/poetry_api.dart'; +import '../../../constants/app_constants.dart'; +import '../../../utils/http/poetry_api.dart'; import 'home-load.dart'; /// 加载状态组件 class LoadingWidget extends StatelessWidget { - const LoadingWidget({super.key}); + final bool isDark; + + const LoadingWidget({super.key, this.isDark = false}); @override Widget build(BuildContext context) { - return const Center( + return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -28,10 +32,13 @@ class LoadingWidget extends StatelessWidget { AppConstants.primaryColor, ), ), - SizedBox(height: 16), + const SizedBox(height: 16), Text( '加载中...', - style: TextStyle(fontSize: 16, color: const Color(0xFF757575)), + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : const Color(0xFF757575), + ), ), ], ), @@ -41,78 +48,62 @@ class LoadingWidget extends StatelessWidget { /// 截图和分享工具类 class ShareImageUtils { - /// 将 Widget 截图并分享 static Future captureAndShare( BuildContext context, GlobalKey repaintKey, { String? subject, }) async { try { - PoetryStateManager.showSnackBar( - context, - '正在生成图片...', - backgroundColor: AppConstants.primaryColor, - ); + Get.snackbar('提示', '正在生成图片...'); final boundary = repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { - if (context.mounted) { - PoetryStateManager.showSnackBar( - context, - '生成图片失败', - backgroundColor: AppConstants.errorColor, - ); - } + Get.snackbar('错误', '生成图片失败'); return; } - ui.Image image = await boundary.toImage(pixelRatio: 3.0); - ByteData? byteData = await image.toByteData( - format: ui.ImageByteFormat.png, - ); - + final image = await boundary.toImage(pixelRatio: 3.0); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { - if (context.mounted) { - PoetryStateManager.showSnackBar( - context, - '生成图片失败', - backgroundColor: AppConstants.errorColor, - ); - } + Get.snackbar('错误', '生成图片失败'); return; } - Uint8List pngBytes = byteData.buffer.asUint8List(); - + final pngBytes = byteData.buffer.asUint8List(); final directory = await getTemporaryDirectory(); final file = File( '${directory.path}/poetry_${DateTime.now().millisecondsSinceEpoch}.png', ); await file.writeAsBytes(pngBytes); - final result = await Share.shareXFiles([ - XFile(file.path), - ], subject: subject ?? '诗词分享'); + await Share.shareXFiles([XFile(file.path)], subject: subject ?? '诗词分享'); - if (result.status == ShareResultStatus.success) { - if (context.mounted) { - PoetryStateManager.showSnackBar( - context, - '分享成功!', - backgroundColor: AppConstants.successColor, - ); - } - } + Get.snackbar( + '成功', + '分享成功!', + + // snackPosition: SnackPosition.TOP, + // backgroundColor: Colors.transparent, + // colorText: Colors.white, + // duration: const Duration(seconds: 2), + // margin: const EdgeInsets.all(16), + // borderRadius: 20, + // animationDuration: const Duration(milliseconds: 300), + // maxWidth: Get.width - 32, + // overlayBlur: 0.1, + // overlayColor: Colors.transparent, + // isDismissible: true, + // padding: EdgeInsets.zero, + // shouldIconPulse: false, + // barBlur: 20, + ); } catch (e) { - if (context.mounted) { - PoetryStateManager.showSnackBar( - context, - '分享失败:${e.toString().substring(0, e.toString().length > 50 ? 50 : e.toString().length)}', - backgroundColor: AppConstants.errorColor, - ); - } + Get.snackbar( + '错误', + '分享失败:${e.toString().substring(0, e.toString().length > 50 ? 50 : e.toString().length)}', + ); } } } @@ -121,11 +112,13 @@ class ShareImageUtils { class CustomErrorWidget extends StatelessWidget { final String errorMessage; final VoidCallback onRetry; + final bool isDark; const CustomErrorWidget({ super.key, required this.errorMessage, required this.onRetry, + this.isDark = false, }); @override @@ -134,11 +127,18 @@ class CustomErrorWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), + Icon( + Icons.error_outline, + size: 64, + color: isDark ? Colors.grey[600] : Colors.grey[400], + ), const SizedBox(height: 16), Text( errorMessage, - style: const TextStyle(fontSize: 16, color: Colors.grey), + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey, + ), textAlign: TextAlign.center, ), const SizedBox(height: 16), @@ -157,12 +157,20 @@ class CustomErrorWidget extends StatelessWidget { /// 空状态组件 class EmptyWidget extends StatelessWidget { - const EmptyWidget({super.key}); + final bool isDark; + + const EmptyWidget({super.key, this.isDark = false}); @override Widget build(BuildContext context) { - return const Center( - child: Text('暂无诗词内容', style: TextStyle(fontSize: 16, color: Colors.grey)), + return Center( + child: Text( + '暂无诗词内容', + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey, + ), + ), ); } } @@ -283,18 +291,15 @@ class CopyUtils { String content, String contentType, { VoidCallback? onSuccess, + bool isDark = false, }) { try { Clipboard.setData(ClipboardData(text: content)); - PoetryStateManager.showSnackBar(context, '已复制$contentType'); + Get.snackbar('提示', '已复制$contentType'); DebugInfoManager().showCopySuccess(); onSuccess?.call(); } catch (e) { - PoetryStateManager.showSnackBar( - context, - '复制失败', - backgroundColor: AppConstants.errorColor, - ); + Get.snackbar('错误', '复制失败'); DebugInfoManager().showCopyFailed(); } } @@ -302,39 +307,56 @@ class CopyUtils { static void showCopyDialog( BuildContext context, String content, - String contentType, - ) { + String contentType, { + bool isDark = false, + }) { showDialog( context: context, builder: (context) => AlertDialog( - title: Text('复制$contentType'), + backgroundColor: isDark ? const Color(0xFF1E1E1E) : Colors.white, + title: Text( + '复制$contentType', + style: TextStyle(color: isDark ? Colors.white : Colors.black87), + ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '受隐私权限约束,频繁写入剪切板需告知用户', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: isDark ? Colors.grey[300] : Colors.black87, + ), ), const SizedBox(height: 12), Text( '预览内容:', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), const SizedBox(height: 4), Container( width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? const Color(0xFF2A2A2A) : Colors.grey[100], borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.grey[300]!), + border: Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[300]!, + ), ), child: Text( content.length > 50 ? '${content.substring(0, 50)}...' : content, - style: const TextStyle(fontSize: 12, color: Colors.black87), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[300] : Colors.black87, + ), ), ), ], @@ -342,12 +364,17 @@ class CopyUtils { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('返回'), + child: Text( + '返回', + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); - copyToClipboard(context, content, contentType); + copyToClipboard(context, content, contentType, isDark: isDark); }, style: ElevatedButton.styleFrom( backgroundColor: AppConstants.primaryColor, @@ -364,8 +391,13 @@ class CopyUtils { /// 悬浮分享按钮组件 class FloatingShareButton extends StatelessWidget { final VoidCallback onShare; + final bool isDark; - const FloatingShareButton({super.key, required this.onShare}); + const FloatingShareButton({ + super.key, + required this.onShare, + this.isDark = false, + }); @override Widget build(BuildContext context) { @@ -400,11 +432,179 @@ class FloatingShareButton extends StatelessWidget { } } +/// 悬浮上一条按钮 +class FloatingPreviousButton extends StatelessWidget { + final VoidCallback onPrevious; + final bool isDark; + + const FloatingPreviousButton({ + super.key, + required this.onPrevious, + this.isDark = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(isDark ? 40 : 20), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () { + HapticFeedback.lightImpact(); + onPrevious(); + }, + child: Center( + child: Icon( + Icons.arrow_back, + color: isDark ? Colors.grey[300] : AppConstants.primaryColor, + size: 28, + ), + ), + ), + ), + ); + } +} + +/// 悬浮下一条按钮 +class FloatingNextButton extends StatelessWidget { + final VoidCallback onNext; + final bool isDark; + + const FloatingNextButton({ + super.key, + required this.onNext, + this.isDark = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(isDark ? 40 : 20), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () { + HapticFeedback.lightImpact(); + onNext(); + }, + child: Center( + child: Icon( + Icons.arrow_forward, + color: isDark ? Colors.grey[300] : AppConstants.primaryColor, + size: 28, + ), + ), + ), + ), + ); + } +} + +/// 悬浮点赞按钮 +class FloatingLikeButton extends StatelessWidget { + final bool isLiked; + final bool isLoadingLike; + final VoidCallback onToggleLike; + final bool isDark; + + const FloatingLikeButton({ + super.key, + required this.isLiked, + required this.isLoadingLike, + required this.onToggleLike, + this.isDark = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: isLiked + ? Colors.red + : (isDark ? const Color(0xFF2A2A2A) : Colors.white), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: (isLiked ? Colors.red : Colors.black).withAlpha( + isDark ? 40 : 20, + ), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: isLoadingLike + ? null + : () { + HapticFeedback.lightImpact(); + onToggleLike(); + }, + child: Center( + child: isLoadingLike + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + isDark ? Colors.grey[300]! : AppConstants.primaryColor, + ), + ), + ) + : Icon( + isLiked ? Icons.favorite : Icons.favorite_border, + color: isLiked + ? Colors.white + : (isDark ? Colors.grey[300] : Colors.grey), + size: 28, + ), + ), + ), + ), + ); + } +} + /// 统计信息卡片组件 class StatsCard extends StatelessWidget { final PoetryData poetryData; + final bool isDark; - const StatsCard({super.key, required this.poetryData}); + const StatsCard({super.key, required this.poetryData, this.isDark = false}); @override Widget build(BuildContext context) { @@ -412,11 +612,11 @@ class StatsCard extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withAlpha(5), + color: Colors.black.withAlpha(isDark ? 40 : 5), blurRadius: 5, offset: const Offset(0, 1), ), @@ -456,14 +656,20 @@ class StatsCard extends StatelessWidget { children: [ Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.grey[200] : Colors.black87, ), ), const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), + Text( + label, + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey, + ), + ), ], ), ); diff --git a/lib/views/main_navigation.dart b/lib/views/main_navigation.dart deleted file mode 100644 index 60e6813..0000000 --- a/lib/views/main_navigation.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import '../constants/app_constants.dart'; -import './home/home_page.dart'; -import './discover_page.dart'; -import './favorites_page.dart'; -import './profile/profile_page.dart'; - -/// 时间: 2025-03-21 -/// 功能: 主导航页面,包含底部导航栏和4个主要页面 -/// 介绍: 这是应用的主要导航容器,提供底部导航栏来切换主页、发现、收藏和个人页面 -/// 最新变化: 新创建文件,实现了底部导航栏功能,添加黑屏检查 - -class MainNavigation extends StatefulWidget { - const MainNavigation({super.key}); - - @override - State createState() => _MainNavigationState(); -} - -class _MainNavigationState extends State { - int _currentIndex = 0; - final GlobalKey> _profileKey = - GlobalKey>(); - - late final List _pages = [ - const HomePage(), - const DiscoverPage(), - const FavoritesPage(), - ProfilePage(key: _profileKey), - ]; - - final List _bottomNavItems = [ - const BottomNavigationBarItem( - icon: Icon(Icons.home), - activeIcon: Icon(Icons.home, color: AppConstants.primaryColor), - label: '主页', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.explore), - activeIcon: Icon(Icons.explore, color: AppConstants.primaryColor), - label: '发现', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.favorite_border), - activeIcon: Icon(Icons.favorite, color: AppConstants.primaryColor), - label: '收藏', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.person_outline), - activeIcon: Icon(Icons.person, color: AppConstants.primaryColor), - label: '个人', - ), - ]; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: IndexedStack(index: _currentIndex, children: _pages), - bottomNavigationBar: Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - offset: const Offset(0, -2), - ), - ], - ), - child: SafeArea( - top: false, - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - setState(() { - _currentIndex = index; - }); - // 切换到个人页面时刷新数据 - if (index == 3) { - final profileState = _profileKey.currentState; - if (profileState != null && profileState.mounted) { - (profileState as dynamic).refreshData(); - } - } - }, - type: BottomNavigationBarType.fixed, - selectedItemColor: AppConstants.primaryColor, - unselectedItemColor: Colors.grey[600], - backgroundColor: Colors.white, - elevation: 0, - items: _bottomNavItems, - selectedFontSize: 12, - unselectedFontSize: 12, - ), - ), - ), - ); - } -} diff --git a/lib/views/profile/app-info.dart b/lib/views/profile/app-info.dart index a1a441c..c3ded83 100644 --- a/lib/views/profile/app-info.dart +++ b/lib/views/profile/app-info.dart @@ -3,11 +3,13 @@ import 'dart:io' as io show Platform; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:platform_info/platform_info.dart'; import 'package:flutter_udid/flutter_udid.dart'; import '../../../config/app_config.dart'; import '../../../constants/app_constants.dart'; import '../../../controllers/shared_preferences_storage_controller.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 应用信息页面 @@ -26,6 +28,7 @@ class _AppInfoPageState extends State { bool _isDeveloperMode = false; int _tapCount = 0; DateTime? _lastTapTime; + final ThemeController _themeController = Get.find(); @override void initState() { @@ -97,59 +100,64 @@ class _AppInfoPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '应用信息', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), - ), - actions: [ - if (_isDeveloperMode) - IconButton( - icon: const Icon(Icons.bug_report, color: Colors.green), - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('调试信息已激活'), - duration: Duration(seconds: 2), - ), - ); - }, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '应用信息', + style: TextStyle( + color: AppConstants.primaryColor, + fontWeight: FontWeight.bold, ), - ], - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildHeaderCard(), - const SizedBox(height: 16), - _buildTechStackCard(), - const SizedBox(height: 16), - _buildBuildInfoCard(context), - const SizedBox(height: 16), - _buildServerInfoCard(), - const SizedBox(height: 16), - _buildDeviceInfoCard(context), - const SizedBox(height: 16), - _buildUpdateLogCard(), - const SizedBox(height: 16), - _buildDesignStyleCard(), - const SizedBox(height: 24), - _buildBottomIndicator(), - ], - ), - ); + ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + if (_isDeveloperMode) + IconButton( + icon: const Icon(Icons.bug_report, color: Colors.green), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('调试信息已激活'), + duration: Duration(seconds: 2), + ), + ); + }, + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildHeaderCard(), + const SizedBox(height: 16), + _buildTechStackCard(isDark), + const SizedBox(height: 16), + _buildBuildInfoCard(context, isDark), + const SizedBox(height: 16), + _buildServerInfoCard(isDark), + const SizedBox(height: 16), + _buildDeviceInfoCard(context, isDark), + const SizedBox(height: 16), + _buildUpdateLogCard(isDark), + const SizedBox(height: 16), + _buildDesignStyleCard(isDark), + const SizedBox(height: 24), + _buildBottomIndicator(isDark), + ], + ), + ); + }); } Widget _buildHeaderCard() { @@ -289,14 +297,16 @@ class _AppInfoPageState extends State { ); } - Widget _buildTechStackCard() { + Widget _buildTechStackCard(bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -318,15 +328,18 @@ class _AppInfoPageState extends State { child: Icon(Icons.code, color: Colors.blue[700], size: 20), ), const SizedBox(width: 12), - const Text( + Text( '技术栈', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), - // 2x2 网格布局展示技术栈 + Divider(height: 1, color: isDark ? Colors.grey[800] : null), Padding( padding: const EdgeInsets.all(16), child: GridView.count( @@ -342,14 +355,28 @@ class _AppInfoPageState extends State { '跨平台UI框架', Icons.flutter_dash, Colors.blue, + isDark, + ), + _buildTechStackItem( + 'Dart', + '编程语言', + Icons.code, + Colors.teal, + isDark, + ), + _buildTechStackItem( + 'SP', + '本地存储', + Icons.storage, + Colors.orange, + isDark, ), - _buildTechStackItem('Dart', '编程语言', Icons.code, Colors.teal), - _buildTechStackItem('SP', '本地存储', Icons.storage, Colors.orange), _buildTechStackItem( 'dio', '网络处理', Icons.network_check, Colors.purple, + isDark, ), ], ), @@ -359,7 +386,7 @@ class _AppInfoPageState extends State { ); } - Widget _buildBuildInfoCard(BuildContext context) { + Widget _buildBuildInfoCard(BuildContext context, bool isDark) { String buildSdk = 'Unknown'; try { final String osName = io.Platform.operatingSystem; @@ -386,11 +413,13 @@ class _AppInfoPageState extends State { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -412,72 +441,94 @@ class _AppInfoPageState extends State { child: Icon(Icons.build, color: Colors.green[700], size: 20), ), const SizedBox(width: 12), - const Text( + Text( '构建信息', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), _buildCopyableItem( context, '版本号', AppConfig.appVersion, Icons.verified, + isDark, ), _buildCopyableItem( context, 'Builder version', '26d03a26', Icons.developer_mode, + isDark, ), _buildInfoItem( '打包时间', DateTime.now().toString().split(' ').first, Icons.schedule, + isDark, ), - _buildInfoItem('Build SDK', buildSdk, Icons.new_releases), - _buildLicenseItem(context), + _buildInfoItem('Build SDK', buildSdk, Icons.new_releases, isDark), + _buildLicenseItem(context, isDark), ], ), ); } - Widget _buildLicenseItem(BuildContext context) { + Widget _buildLicenseItem(BuildContext context, bool isDark) { return InkWell( - onTap: () => _showFlutterLicense(context), + onTap: () => _showFlutterLicense(context, isDark), borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Icon(Icons.gavel, size: 20, color: Colors.grey[600]), + Icon( + Icons.gavel, + size: 20, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '开源框架', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), const SizedBox(height: 2), Text( 'Flutter SDK', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ), ], ), ), - Icon(Icons.chevron_right, size: 20, color: Colors.grey[400]), + Icon( + Icons.chevron_right, + size: 20, + color: isDark ? Colors.grey[600] : Colors.grey[400], + ), ], ), ), ); } - void _showFlutterLicense(BuildContext context) { + void _showFlutterLicense(BuildContext context, bool isDark) { final licenses = [ {'name': 'Flutter SDK', 'license': 'BSD 3-Clause'}, {'name': 'OpenHarmony SDK', 'license': 'Apache 2.0'}, @@ -491,11 +542,15 @@ class _AppInfoPageState extends State { showDialog( context: context, builder: (context) => AlertDialog( + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, title: Row( children: [ Icon(Icons.description, color: AppConstants.primaryColor), const SizedBox(width: 8), - const Text('开源框架'), + Text( + '开源框架', + style: TextStyle(color: isDark ? Colors.white : Colors.black), + ), ], ), content: SingleChildScrollView( @@ -506,9 +561,13 @@ class _AppInfoPageState extends State { margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[50], borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), + border: Border.all( + color: isDark + ? Colors.grey[700]!.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + ), ), child: Row( children: [ @@ -531,9 +590,10 @@ class _AppInfoPageState extends State { children: [ Text( item['name']!, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 2), @@ -541,7 +601,9 @@ class _AppInfoPageState extends State { item['license']!, style: TextStyle( fontSize: 12, - color: Colors.grey[500], + color: isDark + ? Colors.grey[400] + : Colors.grey[500], ), ), ], @@ -566,14 +628,16 @@ class _AppInfoPageState extends State { ); } - Widget _buildServerInfoCard() { + Widget _buildServerInfoCard(bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -595,22 +659,26 @@ class _AppInfoPageState extends State { child: Icon(Icons.dns, color: Colors.orange[700], size: 20), ), const SizedBox(width: 12), - const Text( + Text( '后端服务', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), - _buildInfoItem('后端语言', 'PHP', Icons.php), - _buildInfoItem('Web服务器', 'Nginx', Icons.web), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), + _buildInfoItem('后端语言', 'PHP', Icons.php, isDark), + _buildInfoItem('Web服务器', 'Nginx', Icons.web, isDark), ], ), ); } - Widget _buildDeviceInfoCard(BuildContext context) { + Widget _buildDeviceInfoCard(BuildContext context, bool isDark) { final platform = Platform.instance; bool isHarmonyOS = false; @@ -703,11 +771,13 @@ class _AppInfoPageState extends State { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -733,14 +803,18 @@ class _AppInfoPageState extends State { ), ), const SizedBox(width: 12), - const Text( + Text( '设备信息', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: GridView.count( @@ -751,18 +825,29 @@ class _AppInfoPageState extends State { crossAxisSpacing: 12, childAspectRatio: 2.0, children: [ - _buildGridInfoItem('操作系统', platformName, Icons.phone_iphone), + _buildGridInfoItem( + '操作系统', + platformName, + Icons.phone_iphone, + isDark, + ), if (!isHarmonyOS) ...[ - _buildGridInfoItem('设计风格', designStyle, Icons.palette), + _buildGridInfoItem( + '设计风格', + designStyle, + Icons.palette, + isDark, + ), ], - _buildGridInfoItem('设备类型', deviceType, Icons.devices), - _buildGridInfoItem('构建模式', buildMode, Icons.build), - _buildGridInfoItem('运行环境', runtimeEnv, Icons.code), + _buildGridInfoItem('设备类型', deviceType, Icons.devices, isDark), + _buildGridInfoItem('构建模式', buildMode, Icons.build, isDark), + _buildGridInfoItem('运行环境', runtimeEnv, Icons.code, isDark), _buildGridCopyableItem( context, 'Flutter UUID', _udid, Icons.perm_identity, + isDark, ), ], ), @@ -773,9 +858,13 @@ class _AppInfoPageState extends State { width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[50], borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), + border: Border.all( + color: isDark + ? Colors.grey[700]!.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -785,7 +874,7 @@ class _AppInfoPageState extends State { Icon( Icons.info_outline, size: 16, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], ), const SizedBox(width: 8), Text( @@ -793,7 +882,7 @@ class _AppInfoPageState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ], @@ -801,12 +890,18 @@ class _AppInfoPageState extends State { const SizedBox(height: 8), Text( '窗口尺寸: ${MediaQuery.of(context).size.width.toStringAsFixed(0)} x ${MediaQuery.of(context).size.height.toStringAsFixed(0)}', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), const SizedBox(height: 4), Text( '像素密度: ${MediaQuery.of(context).devicePixelRatio.toStringAsFixed(2)} (越大越好)', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -817,14 +912,16 @@ class _AppInfoPageState extends State { ); } - Widget _buildUpdateLogCard() { + Widget _buildUpdateLogCard(bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -850,14 +947,18 @@ class _AppInfoPageState extends State { ), ), const SizedBox(width: 12), - const Text( + Text( '软件更新日志', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), Padding( padding: const EdgeInsets.all(16), child: Column( @@ -867,19 +968,13 @@ class _AppInfoPageState extends State { '新增:主题个性化页面', '新增:设计风格卡片', '优化:界面布局和响应式设计', - ]), + ], isDark), const SizedBox(height: 16), _buildUpdateItem('版本 1.2.39', '2026-03-27', [ '修复:引导页滑动问题', '优化:左侧进度条位置', '新增:协议内容焦点功能', - ]), - // const SizedBox(height: 16), - // _buildUpdateItem('版本 1.2.38', '2026-03-27', [ - // '优化:引导页滑动逻辑', - // '新增:页面进度指示器', - // '更新:滑动提示文本', - // ]), + ], isDark), ], ), ), @@ -888,13 +983,22 @@ class _AppInfoPageState extends State { ); } - Widget _buildUpdateItem(String version, String date, List changes) { + Widget _buildUpdateItem( + String version, + String date, + List changes, + bool isDark, + ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[50], borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), + border: Border.all( + color: isDark + ? Colors.grey[700]!.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -904,15 +1008,18 @@ class _AppInfoPageState extends State { children: [ Text( version, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), Text( date, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -926,11 +1033,19 @@ class _AppInfoPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(width: 8), - const Text('• ', style: TextStyle(color: Colors.blue)), + Text( + '• ', + style: TextStyle( + color: isDark ? Colors.blue[300] : Colors.blue, + ), + ), Expanded( child: Text( change, - style: TextStyle(fontSize: 12, color: Colors.grey[700]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), ), ), ], @@ -943,20 +1058,63 @@ class _AppInfoPageState extends State { ); } - Widget _buildDesignStyleCard() { + Widget _buildDesignStyleCard(bool isDark) { + final platform = Platform.instance; + final String designStyle = + platform.when( + material: () => 'Material Design', + cupertino: () => 'Cupertino Design', + orElse: () => null, + ) ?? + 'Unknown'; + return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: []), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.pink.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.palette, color: Colors.pink[700], size: 20), + ), + const SizedBox(width: 12), + Text( + '设计风格', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), + _buildInfoItem('当前风格', designStyle, Icons.style, isDark), + _buildInfoItem('设计语言', 'Flutter', Icons.design_services, isDark), + _buildInfoItem('交互模式', '触摸优先', Icons.touch_app, isDark), + ], + ), ); } @@ -966,23 +1124,24 @@ class _AppInfoPageState extends State { String value, IconData icon, Color color, + bool isDark, ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: color.withAlpha(20), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withAlpha(50), width: 1), + border: Border.all(color: color.withValues(alpha: 0.2)), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: color.withAlpha(30), + color: color.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), - child: Icon(icon, size: 20, color: color), + child: Icon(icon, color: color, size: 20), ), const SizedBox(width: 10), Expanded( @@ -995,14 +1154,17 @@ class _AppInfoPageState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: color, + color: isDark ? Colors.white : Colors.black, ), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( value, - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + style: TextStyle( + fontSize: 11, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), overflow: TextOverflow.ellipsis, ), ], @@ -1014,13 +1176,22 @@ class _AppInfoPageState extends State { } // 网格布局版本的信息项 - Widget _buildGridInfoItem(String title, String value, IconData icon) { + Widget _buildGridInfoItem( + String title, + String value, + IconData icon, + bool isDark, + ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[50], borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.withValues(alpha: 0.1)), + border: Border.all( + color: isDark + ? Colors.grey[700]!.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1035,7 +1206,7 @@ class _AppInfoPageState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: Colors.grey[700], + color: isDark ? Colors.grey[400] : Colors.grey[700], ), overflow: TextOverflow.ellipsis, ), @@ -1048,7 +1219,7 @@ class _AppInfoPageState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), overflow: TextOverflow.ellipsis, maxLines: 1, @@ -1064,30 +1235,41 @@ class _AppInfoPageState extends State { String title, String value, IconData icon, + bool isDark, ) { - return GestureDetector( + return InkWell( onTap: () => _copyToClipboard(context, value), + borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[50], borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.withValues(alpha: 0.1)), + border: Border.all( + color: isDark + ? Colors.grey[700]!.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ - Icon(icon, size: 18, color: AppConstants.primaryColor), - const SizedBox(width: 8), + Icon( + icon, + size: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + const SizedBox(width: 6), Expanded( child: Text( title, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: Colors.grey[700], + color: isDark ? Colors.grey[400] : Colors.grey[700], ), overflow: TextOverflow.ellipsis, ), @@ -1105,10 +1287,9 @@ class _AppInfoPageState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), overflow: TextOverflow.ellipsis, - maxLines: 1, ), ], ), @@ -1116,12 +1297,21 @@ class _AppInfoPageState extends State { ); } - Widget _buildInfoItem(String title, String value, IconData icon) { + Widget _buildInfoItem( + String title, + String value, + IconData icon, + bool isDark, + ) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon( + icon, + size: 20, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 12), Expanded( child: Column( @@ -1129,15 +1319,19 @@ class _AppInfoPageState extends State { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 2), Text( value, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), @@ -1152,6 +1346,7 @@ class _AppInfoPageState extends State { String title, String value, IconData icon, + bool isDark, ) { return InkWell( onTap: () => _copyToClipboard(context, value), @@ -1160,7 +1355,11 @@ class _AppInfoPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon( + icon, + size: 20, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 12), Expanded( child: Column( @@ -1168,15 +1367,19 @@ class _AppInfoPageState extends State { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 2), Text( value, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), @@ -1184,7 +1387,7 @@ class _AppInfoPageState extends State { Icon( Icons.content_copy, size: 16, - color: AppConstants.primaryColor.withValues(alpha: 0.6), + color: isDark ? Colors.grey[600] : Colors.grey[400], ), ], ), @@ -1211,21 +1414,46 @@ class _AppInfoPageState extends State { ); } - Widget _buildBottomIndicator() { + Widget _buildBottomIndicator(bool isDark) { return Container( - padding: const EdgeInsets.symmetric(vertical: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( children: [ - Container(width: 10, height: 1, color: Colors.grey[300]), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - '若有卡顿闪退信息,请将此页面长截图发给开发者', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + Icon( + Icons.code, + size: 24, + color: isDark ? Colors.grey[600] : Colors.grey[400], + ), + const SizedBox(height: 8), + Text( + '用心开发,只为更好的体验', + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), + ), + const SizedBox(height: 4), + Text( + '© 2026 ${AppConfig.appName}', + style: TextStyle( + fontSize: 11, + color: isDark ? Colors.grey[600] : Colors.grey[400], ), ), - Container(width: 10, height: 1, color: Colors.grey[300]), ], ), ); diff --git a/lib/views/profile/components/bug_list_page.dart b/lib/views/profile/components/bug_list_page.dart index cb58555..428465e 100644 --- a/lib/views/profile/components/bug_list_page.dart +++ b/lib/views/profile/components/bug_list_page.dart @@ -5,8 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; class BugListPage extends StatefulWidget { const BugListPage({super.key}); @@ -16,6 +18,7 @@ class BugListPage extends StatefulWidget { } class _BugListPageState extends State { + final ThemeController _themeController = Get.find(); // 模拟bug数据 final List> _bugs = [ { @@ -178,7 +181,6 @@ class _BugListPageState extends State { ]; final ScrollController _scrollController = ScrollController(); - bool _isRefreshing = false; // 切换bug展开状态 void _toggleBugExpanded(int bugId) { @@ -268,57 +270,52 @@ class _BugListPageState extends State { } Future _refresh() async { - setState(() { - _isRefreshing = true; - }); - // 模拟网络请求延迟 await Future.delayed(const Duration(seconds: 1)); - setState(() { - _isRefreshing = false; - }); - HapticFeedback.lightImpact(); } @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - child: Column( - children: [ - // 顶部拖拽条和标题 - _buildHeader(), - // bug列表 - Expanded( - child: RefreshIndicator( - onRefresh: _refresh, - color: AppConstants.primaryColor, - child: ListView.builder( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), - itemCount: _bugs.length + 1, - itemBuilder: (context, index) { - if (index == _bugs.length) { - return _buildFooter(); - } - final bug = _bugs[index]; - return _buildBugItem(bug); - }, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1A1A1A) : Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // 顶部拖拽条和标题 + _buildHeader(isDark), + // bug列表 + Expanded( + child: RefreshIndicator( + onRefresh: _refresh, + color: AppConstants.primaryColor, + child: ListView.builder( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: _bugs.length + 1, + itemBuilder: (context, index) { + if (index == _bugs.length) { + return _buildFooter(isDark); + } + final bug = _bugs[index]; + return _buildBugItem(bug, isDark); + }, + ), ), ), - ), - ], - ), - ); + ], + ), + ); + }); } - Widget _buildHeader() { + Widget _buildHeader(bool isDark) { return Column( children: [ // 拖拽条 @@ -327,7 +324,7 @@ class _BugListPageState extends State { height: 4, margin: const EdgeInsets.only(top: 8), decoration: BoxDecoration( - color: Colors.grey[300], + color: isDark ? Colors.grey[600] : Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), @@ -354,17 +351,20 @@ class _BugListPageState extends State { ), TextButton( onPressed: () => Navigator.pop(context), - child: const Text('关闭'), + child: Text( + '关闭', + style: TextStyle(color: isDark ? Colors.white : null), + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[700] : null), ], ); } - Widget _buildFooter() { + Widget _buildFooter(bool isDark) { return Container( padding: const EdgeInsets.symmetric(vertical: 24), child: Column( @@ -372,39 +372,55 @@ class _BugListPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[600] : Colors.grey[300], + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '已经到底了', - style: TextStyle(fontSize: 13, color: Colors.grey[500]), + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ), - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[600] : Colors.grey[300], + ), ], ), const SizedBox(height: 8), Text( '共 ${_bugs.length} 个已知问题', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ], ), ); } - Widget _buildBugItem(Map bug) { + Widget _buildBugItem(Map bug, bool isDark) { final bool isExpanded = bug['expanded'] ?? false; return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[200]!), + border: Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[200]!, + ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 5, offset: const Offset(0, 2), ), @@ -426,10 +442,10 @@ class _BugListPageState extends State { Expanded( child: Text( bug['title'] ?? '未知问题', - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), ), @@ -464,9 +480,9 @@ class _BugListPageState extends State { // 问题描述 Text( bug['description'] ?? '暂无描述', - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: Colors.black54, + color: isDark ? Colors.grey[400] : Colors.black54, height: 1.4, ), ), @@ -506,11 +522,18 @@ class _BugListPageState extends State { ), ), const SizedBox(width: 12), - Icon(Icons.people, size: 14, color: Colors.grey[600]), + Icon( + Icons.people, + size: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 4), Text( bug['affectedUsers'] ?? '未知用户', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -585,11 +608,11 @@ class _BugListPageState extends State { ), // 解决方案区域(可展开/收起) if (isExpanded) ...[ - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[700] : null), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF333333) : Colors.grey[50], borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), @@ -619,9 +642,9 @@ class _BugListPageState extends State { const SizedBox(height: 8), Text( bug['solution'] ?? '暂无解决方案', - style: const TextStyle( + style: TextStyle( fontSize: 13, - color: Colors.black54, + color: isDark ? Colors.grey[400] : Colors.black54, height: 1.4, ), ), @@ -629,18 +652,32 @@ class _BugListPageState extends State { // 时间信息 Row( children: [ - Icon(Icons.schedule, size: 14, color: Colors.grey[600]), + Icon( + Icons.schedule, + size: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 4), Text( '预计解决: ${bug['resolveTime'] ?? '待定'}', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), const SizedBox(width: 16), - Icon(Icons.report, size: 14, color: Colors.grey[600]), + Icon( + Icons.report, + size: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 4), Text( '报告时间: ${bug['reportTime'] ?? '未知'}', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -650,11 +687,11 @@ class _BugListPageState extends State { ], // 复现步骤区域(可展开/收起) if (bug['reproductionExpanded']) ...[ - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[700] : null), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF333333) : Colors.grey[50], borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), @@ -684,9 +721,9 @@ class _BugListPageState extends State { const SizedBox(height: 8), Text( bug['reproduction'] ?? '暂无复现步骤', - style: const TextStyle( + style: TextStyle( fontSize: 13, - color: Colors.black54, + color: isDark ? Colors.grey[400] : Colors.black54, height: 1.4, ), ), diff --git a/lib/views/profile/components/entire_page.dart b/lib/views/profile/components/entire_page.dart index e5c83db..34e397a 100644 --- a/lib/views/profile/components/entire_page.dart +++ b/lib/views/profile/components/entire_page.dart @@ -6,9 +6,11 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/http_client.dart'; import '../../../services/network_listener_service.dart'; +import '../../../services/get/theme_controller.dart'; import 'server_info_dialog.dart'; class EntirePage extends StatefulWidget { @@ -20,6 +22,7 @@ class EntirePage extends StatefulWidget { class _EntirePageState extends State with NetworkListenerMixin, SingleTickerProviderStateMixin { + final ThemeController _themeController = Get.find(); Map? _statsData; String? _errorMessage; late AnimationController _animationController; @@ -124,31 +127,36 @@ class _EntirePageState extends State @override Widget build(BuildContext context) { - return AnnotatedRegion( - value: SystemUiOverlayStyle.dark, - child: Scaffold( - backgroundColor: const Color(0xFFF2F2F7), - appBar: _buildAppBar(), - body: _buildBody(), - ), - ); + return Obx(() { + final isDark = _themeController.isDarkMode; + return AnnotatedRegion( + value: isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, + child: Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF2F2F7), + appBar: _buildAppBar(isDark), + body: _buildBody(isDark), + ), + ); + }); } - PreferredSizeWidget _buildAppBar() { + PreferredSizeWidget _buildAppBar(bool isDark) { return AppBar( - backgroundColor: Colors.white, + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, elevation: 0, leading: IconButton( - icon: const Icon( + icon: Icon( Icons.arrow_back_ios, - color: AppConstants.primaryColor, + color: isDark ? Colors.white : AppConstants.primaryColor, ), onPressed: () => Navigator.pop(context), ), - title: const Text( + title: Text( '全站统计', style: TextStyle( - color: Colors.black, + color: isDark ? Colors.white : Colors.black, fontSize: 17, fontWeight: FontWeight.w600, ), @@ -156,9 +164,9 @@ class _EntirePageState extends State centerTitle: true, actions: [ IconButton( - icon: const Icon( + icon: Icon( Icons.info_outline, - color: AppConstants.primaryColor, + color: isDark ? Colors.white : AppConstants.primaryColor, ), onPressed: _showServerInfo, tooltip: '服务器信息', @@ -166,64 +174,68 @@ class _EntirePageState extends State ], bottom: PreferredSize( preferredSize: const Size.fromHeight(0.5), - child: Container(height: 0.5, color: const Color(0xFFE5E5EA)), + child: Container( + height: 0.5, + color: isDark ? Colors.grey[700] : const Color(0xFFE5E5EA), + ), ), ); } - Widget _buildBody() { + Widget _buildBody(bool isDark) { if (_errorMessage != null) { - return _buildErrorView(); + return _buildErrorView(isDark); } if (_statsData == null) { - return _buildSkeletonView(); + return _buildSkeletonView(isDark); } - return _buildStatsContent(); + return _buildStatsContent(isDark); } Widget _buildSkeletonBox({ double width = double.infinity, double height = 16, double radius = 8, + bool isDark = false, }) { return Container( width: width, height: height, decoration: BoxDecoration( - color: const Color(0xFFE5E5EA), + color: isDark ? Colors.grey[700] : const Color(0xFFE5E5EA), borderRadius: BorderRadius.circular(radius), ), ); } - Widget _buildSkeletonView() { + Widget _buildSkeletonView(bool isDark) { return FadeTransition( opacity: _fadeAnimation, child: ListView( padding: const EdgeInsets.all(16), children: [ - _buildSkeletonHeaderCard(), + _buildSkeletonHeaderCard(isDark), const SizedBox(height: 16), - _buildSkeletonSection(), + _buildSkeletonSection(isDark), const SizedBox(height: 16), - _buildSkeletonSection(), + _buildSkeletonSection(isDark), const SizedBox(height: 16), - _buildSkeletonSection(), + _buildSkeletonSection(isDark), const SizedBox(height: 16), - _buildSkeletonBuildTimeCard(), + _buildSkeletonBuildTimeCard(isDark), const SizedBox(height: 32), ], ), ); } - Widget _buildSkeletonHeaderCard() { + Widget _buildSkeletonHeaderCard(bool isDark) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFFE5E5EA), + color: isDark ? Colors.grey[700] : const Color(0xFFE5E5EA), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -231,20 +243,25 @@ class _EntirePageState extends State children: [ Row( children: [ - _buildSkeletonBox(width: 28, height: 28, radius: 14), + _buildSkeletonBox( + width: 28, + height: 28, + radius: 14, + isDark: isDark, + ), const SizedBox(width: 12), - _buildSkeletonBox(width: 100, height: 22), + _buildSkeletonBox(width: 100, height: 22, isDark: isDark), ], ), const SizedBox(height: 12), - _buildSkeletonBox(width: 200, height: 14), + _buildSkeletonBox(width: 200, height: 14, isDark: isDark), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildSkeletonBox(width: 60, height: 40), - _buildSkeletonBox(width: 60, height: 40), - _buildSkeletonBox(width: 60, height: 40), + _buildSkeletonBox(width: 60, height: 40, isDark: isDark), + _buildSkeletonBox(width: 60, height: 40, isDark: isDark), + _buildSkeletonBox(width: 60, height: 40, isDark: isDark), ], ), ], @@ -252,15 +269,15 @@ class _EntirePageState extends State ); } - Widget _buildSkeletonSection() { + Widget _buildSkeletonSection(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), @@ -271,9 +288,14 @@ class _EntirePageState extends State children: [ Row( children: [ - _buildSkeletonBox(width: 20, height: 20, radius: 10), + _buildSkeletonBox( + width: 20, + height: 20, + radius: 10, + isDark: isDark, + ), const SizedBox(width: 8), - _buildSkeletonBox(width: 80, height: 16), + _buildSkeletonBox(width: 80, height: 16, isDark: isDark), ], ), const SizedBox(height: 16), @@ -284,18 +306,21 @@ class _EntirePageState extends State childAspectRatio: 1.0, crossAxisSpacing: 12, mainAxisSpacing: 12, - children: List.generate(9, (index) => _buildSkeletonCountItem()), + children: List.generate( + 9, + (index) => _buildSkeletonCountItem(isDark), + ), ), ], ), ); } - Widget _buildSkeletonCountItem() { + Widget _buildSkeletonCountItem(bool isDark) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: const Color(0xFFF2F2F7), + color: isDark ? const Color(0xFF333333) : const Color(0xFFF2F2F7), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -309,31 +334,39 @@ class _EntirePageState extends State width: double.infinity, height: 28, radius: 8, + isDark: isDark, ), ), const SizedBox(width: 8), Expanded( - child: _buildSkeletonBox(width: double.infinity, height: 22), + child: _buildSkeletonBox( + width: double.infinity, + height: 22, + isDark: isDark, + ), ), ], ), ), const SizedBox(height: 4), - Expanded(flex: 1, child: _buildSkeletonBox(width: 50, height: 12)), + Expanded( + flex: 1, + child: _buildSkeletonBox(width: 50, height: 12, isDark: isDark), + ), ], ), ); } - Widget _buildSkeletonBuildTimeCard() { + Widget _buildSkeletonBuildTimeCard(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), @@ -341,15 +374,15 @@ class _EntirePageState extends State ), child: Row( children: [ - _buildSkeletonBox(width: 40, height: 40, radius: 10), + _buildSkeletonBox(width: 40, height: 40, radius: 10, isDark: isDark), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSkeletonBox(width: 60, height: 14), + _buildSkeletonBox(width: 60, height: 14, isDark: isDark), const SizedBox(height: 4), - _buildSkeletonBox(width: 150, height: 16), + _buildSkeletonBox(width: 150, height: 16, isDark: isDark), ], ), ), @@ -358,7 +391,7 @@ class _EntirePageState extends State ); } - Widget _buildErrorView() { + Widget _buildErrorView(bool isDark) { return Center( child: Padding( padding: const EdgeInsets.all(24), @@ -381,7 +414,10 @@ class _EntirePageState extends State const SizedBox(height: 16), Text( _errorMessage ?? '加载失败', - style: const TextStyle(color: Color(0xFF8E8E93), fontSize: 14), + style: TextStyle( + color: isDark ? Colors.grey[400] : const Color(0xFF8E8E93), + fontSize: 14, + ), textAlign: TextAlign.center, ), const SizedBox(height: 24), @@ -406,7 +442,7 @@ class _EntirePageState extends State ); } - Widget _buildStatsContent() { + Widget _buildStatsContent(bool isDark) { return FadeTransition( opacity: _fadeAnimation, child: RefreshIndicator( @@ -417,13 +453,13 @@ class _EntirePageState extends State children: [ _buildHeaderCard(), const SizedBox(height: 16), - _buildHotSection(), + _buildHotSection(isDark), const SizedBox(height: 16), - _buildCountSection(), + _buildCountSection(isDark), const SizedBox(height: 16), - _buildTopContentSection(), + _buildTopContentSection(isDark), const SizedBox(height: 16), - _buildBuildTimeCard(), + _buildBuildTimeCard(isDark), const SizedBox(height: 32), ], ), @@ -543,13 +579,13 @@ class _EntirePageState extends State ); } - Widget _buildCountSection() { + Widget _buildCountSection(bool isDark) { return _buildSection('数量统计', Icons.format_list_numbered, [ - _buildCountGrid(), - ]); + _buildCountGrid(isDark), + ], isDark); } - Widget _buildCountGrid() { + Widget _buildCountGrid(bool isDark) { final counts = [ { 'label': '项目', @@ -634,6 +670,7 @@ class _EntirePageState extends State item['icon'] as IconData, item['color'] as Color, item['showIcon'] as bool, + isDark, ); }, ); @@ -645,15 +682,16 @@ class _EntirePageState extends State IconData icon, Color color, bool showIcon, + bool isDark, ) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), @@ -682,10 +720,10 @@ class _EntirePageState extends State Expanded( child: Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), textAlign: TextAlign.center, ), @@ -700,7 +738,10 @@ class _EntirePageState extends State child: Center( child: Text( label, - style: const TextStyle(fontSize: 12, color: Color(0xFF3C3C43)), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : const Color(0xFF3C3C43), + ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, @@ -712,13 +753,14 @@ class _EntirePageState extends State ); } - Widget _buildHotSection() { + Widget _buildHotSection(bool isDark) { return _buildSection('热度统计', Icons.trending_up, [ _buildHotItem( '累计热度', _statsData?['cumulative_hits']?.toString() ?? '0', Icons.local_fire_department, const Color(0xFFFF9500), + isDark, ), const SizedBox(height: 12), _buildHotItem( @@ -726,19 +768,26 @@ class _EntirePageState extends State _statsData?['cumulative_likes']?.toString() ?? '0', Icons.favorite, const Color(0xFFFF2D55), + isDark, ), - ]); + ], isDark); } - Widget _buildHotItem(String label, String value, IconData icon, Color color) { + Widget _buildHotItem( + String label, + String value, + IconData icon, + Color color, + bool isDark, + ) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), @@ -762,18 +811,18 @@ class _EntirePageState extends State children: [ Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: Color(0xFF8E8E93), + color: isDark ? Colors.grey[400] : const Color(0xFF8E8E93), ), ), const SizedBox(height: 4), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), ), ], @@ -784,13 +833,14 @@ class _EntirePageState extends State ); } - Widget _buildTopContentSection() { + Widget _buildTopContentSection(bool isDark) { return _buildSection('热门内容', Icons.star, [ _buildTopContentItem( '今日热门', _statsData?['top_hits_day'], Icons.today, const Color(0xFFFF9500), + isDark, ), const SizedBox(height: 12), _buildTopContentItem( @@ -798,6 +848,7 @@ class _EntirePageState extends State _statsData?['top_hits_month'], Icons.calendar_month, const Color(0xFF007AFF), + isDark, ), const SizedBox(height: 12), _buildTopContentItem( @@ -805,6 +856,7 @@ class _EntirePageState extends State _statsData?['top_hits_total'], Icons.history, const Color(0xFF5856D6), + isDark, ), const SizedBox(height: 12), _buildTopContentItem( @@ -812,8 +864,9 @@ class _EntirePageState extends State _statsData?['top_like'], Icons.thumb_up, const Color(0xFF34C759), + isDark, ), - ]); + ], isDark); } Widget _buildTopContentItem( @@ -821,6 +874,7 @@ class _EntirePageState extends State dynamic data, IconData icon, Color color, + bool isDark, ) { final hasData = data != null && data is Map; final content = hasData ? data['name']?.toString() ?? '暂无数据' : '暂无数据'; @@ -828,11 +882,11 @@ class _EntirePageState extends State return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), @@ -857,10 +911,10 @@ class _EntirePageState extends State children: [ Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 8), @@ -869,8 +923,8 @@ class _EntirePageState extends State style: TextStyle( fontSize: 13, color: hasData - ? const Color(0xFF3C3C43) - : const Color(0xFF8E8E93), + ? (isDark ? Colors.grey[400] : const Color(0xFF3C3C43)) + : (isDark ? Colors.grey[500] : const Color(0xFF8E8E93)), height: 1.5, ), maxLines: 3, @@ -884,7 +938,7 @@ class _EntirePageState extends State ); } - Widget _buildBuildTimeCard() { + Widget _buildBuildTimeCard(bool isDark) { final buildTime = _statsData?['build_time']?.toString() ?? '未知'; int days = 0; @@ -900,11 +954,11 @@ class _EntirePageState extends State return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), @@ -930,12 +984,12 @@ class _EntirePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '建站时间', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 4), @@ -943,9 +997,11 @@ class _EntirePageState extends State children: [ Text( buildTime, - style: const TextStyle( + style: TextStyle( fontSize: 16, - color: Color(0xFF3C3C43), + color: isDark + ? Colors.grey[400] + : const Color(0xFF3C3C43), ), ), const SizedBox(width: 8), @@ -977,14 +1033,19 @@ class _EntirePageState extends State ); } - Widget _buildSection(String title, IconData icon, List children) { + Widget _buildSection( + String title, + IconData icon, + List children, + bool isDark, + ) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.04), blurRadius: 8, offset: const Offset(0, 2), ), @@ -1001,10 +1062,10 @@ class _EntirePageState extends State const SizedBox(width: 8), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), ), ], diff --git a/lib/views/profile/components/pop-menu.dart b/lib/views/profile/components/pop-menu.dart index 481b6f4..5fdbc1b 100644 --- a/lib/views/profile/components/pop-menu.dart +++ b/lib/views/profile/components/pop-menu.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:share_plus/share_plus.dart'; import '../../../constants/app_constants.dart'; -import '../../../services/wakelock_service.dart'; +import '../../../services/isweb/wakelock_service.dart'; import '../guide/beginner_page.dart'; import 'dart:io' as io; diff --git a/lib/views/profile/expand/tougao.dart b/lib/views/profile/expand/tougao.dart index e6ac3ae..b9d24ef 100644 --- a/lib/views/profile/expand/tougao.dart +++ b/lib/views/profile/expand/tougao.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; class ManuscriptRecord { final String name; @@ -57,6 +59,7 @@ class TougaoPage extends StatefulWidget { } class _TougaoPageState extends State { + final ThemeController _themeController = Get.find(); List _records = []; bool _isLoading = true; @@ -135,67 +138,89 @@ class _TougaoPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('投稿记录'), - actions: [ - if (_records.isNotEmpty) - IconButton( - icon: const Icon(Icons.delete_sweep_outlined), - onPressed: _clearAllRecords, - tooltip: '清空记录', - ), - ], - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _records.isEmpty - ? _buildEmptyState() - : _buildRecordsList(), - ); + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.white, + appBar: AppBar( + title: Text( + '投稿记录', + style: TextStyle(color: isDark ? Colors.white : Colors.black87), + ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + actions: [ + if (_records.isNotEmpty) + IconButton( + icon: Icon( + Icons.delete_sweep_outlined, + color: isDark ? Colors.white : Colors.black87, + ), + onPressed: _clearAllRecords, + tooltip: '清空记录', + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _records.isEmpty + ? _buildEmptyState(isDark) + : _buildRecordsList(isDark), + ); + }); } - Widget _buildEmptyState() { + Widget _buildEmptyState(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.history_outlined, size: 80, color: Colors.grey[300]), + Icon( + Icons.history_outlined, + size: 80, + color: isDark ? Colors.grey[600] : Colors.grey[300], + ), const SizedBox(height: 16), Text( '暂无投稿记录', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), ); } - Widget _buildRecordsList() { + Widget _buildRecordsList(bool isDark) { return ListView.builder( padding: const EdgeInsets.all(16), itemCount: _records.length, itemBuilder: (context, index) { final record = _records[index]; - return _buildRecordCard(record); + return _buildRecordCard(record, isDark); }, ); } - Widget _buildRecordCard(ManuscriptRecord record) { + Widget _buildRecordCard(ManuscriptRecord record, bool isDark) { return Card( elevation: 0, - color: Colors.grey[50], + color: isDark ? const Color(0xFF2A2A2A) : Colors.grey[50], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: ExpansionTile( + iconColor: isDark ? Colors.white : Colors.black87, + collapsedIconColor: isDark ? Colors.white : Colors.black87, title: Row( children: [ Expanded( child: Text( record.name, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, + color: isDark ? Colors.white : Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -205,7 +230,7 @@ class _TougaoPageState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: AppConstants.primaryColor.withAlpha(20), + color: AppConstants.primaryColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -223,27 +248,39 @@ class _TougaoPageState extends State { padding: const EdgeInsets.only(top: 4), child: Row( children: [ - Icon(Icons.access_time, size: 14, color: Colors.grey[500]), + Icon( + Icons.access_time, + size: 14, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), const SizedBox(width: 4), Text( _formatDate(record.submitTime), - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), ), childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), children: [ - _buildDetailItem('诗人和标题', record.url), - _buildDetailItem('关键词', record.keywords), - _buildDetailItem('平台', record.platform), - _buildDetailItem('诗词介绍', record.introduce, maxLines: 3), + _buildDetailItem('诗人和标题', record.url, isDark), + _buildDetailItem('关键词', record.keywords, isDark), + _buildDetailItem('平台', record.platform, isDark), + _buildDetailItem('诗词介绍', record.introduce, isDark, maxLines: 3), ], ), ); } - Widget _buildDetailItem(String label, String value, {int maxLines = 1}) { + Widget _buildDetailItem( + String label, + String value, + bool isDark, { + int maxLines = 1, + }) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( @@ -253,14 +290,17 @@ class _TougaoPageState extends State { label, style: TextStyle( fontSize: 13, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], fontWeight: FontWeight.w500, ), ), const SizedBox(height: 4), Text( value, - style: const TextStyle(fontSize: 14), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.white : Colors.black87, + ), maxLines: maxLines, overflow: TextOverflow.ellipsis, ), diff --git a/lib/views/profile/expand/vote.dart b/lib/views/profile/expand/vote.dart index d681def..c14a43b 100644 --- a/lib/views/profile/expand/vote.dart +++ b/lib/views/profile/expand/vote.dart @@ -3,8 +3,10 @@ /// 介绍: 提供投票功能,显示投票列表,查看投票详情,提交投票 import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/vote_api.dart'; +import '../../../services/get/theme_controller.dart'; import '../components/login_register_dialog.dart'; class VotePage extends StatefulWidget { @@ -15,6 +17,7 @@ class VotePage extends StatefulWidget { } class _VotePageState extends State { + final ThemeController _themeController = Get.find(); List _voteList = []; bool _isLoading = true; bool _isLoggedIn = false; @@ -144,134 +147,156 @@ class _VotePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('参与投票'), - backgroundColor: AppConstants.primaryColor, - actions: [ - if (_isLoggedIn) - IconButton( - icon: const Icon(Icons.logout), - onPressed: () async { - await VoteApi.clearUserLogin(); - await _checkLoginStatus(); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('已清除投票凭证'), - backgroundColor: AppConstants.successColor, - ), - ); - } - }, - ), - ], - ), - body: RefreshIndicator( - onRefresh: _loadVoteList, - child: _isLoading && _voteList.isEmpty - ? const Center(child: CircularProgressIndicator()) - : Column( - children: [ - Expanded( - child: _voteList.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.poll_outlined, - size: 64, - color: Colors.grey, - ), - const SizedBox(height: 16), - const Text( - '暂无投票', - style: TextStyle( - color: Colors.grey, - fontSize: 16, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.white, + appBar: AppBar( + title: Text( + '参与投票', + style: TextStyle(color: isDark ? Colors.white : Colors.black87), + ), + backgroundColor: isDark + ? const Color(0xFF2A2A2A) + : AppConstants.primaryColor, + elevation: 0, + actions: [ + if (_isLoggedIn) + IconButton( + icon: Icon( + Icons.logout, + color: isDark ? Colors.white : Colors.black87, + ), + onPressed: () async { + await VoteApi.clearUserLogin(); + await _checkLoginStatus(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('已清除投票凭证'), + backgroundColor: AppConstants.successColor, + ), + ); + } + }, + ), + ], + ), + body: RefreshIndicator( + onRefresh: _loadVoteList, + child: _isLoading && _voteList.isEmpty + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + Expanded( + child: _voteList.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.poll_outlined, + size: 64, + color: isDark + ? Colors.grey[600] + : Colors.grey, ), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadVoteList, - style: ElevatedButton.styleFrom( - backgroundColor: AppConstants.primaryColor, + const SizedBox(height: 16), + Text( + '暂无投票', + style: TextStyle( + color: isDark + ? Colors.grey[400] + : Colors.grey, + fontSize: 16, + ), ), - child: const Text('刷新'), - ), - ], + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadVoteList, + style: ElevatedButton.styleFrom( + backgroundColor: + AppConstants.primaryColor, + ), + child: const Text('刷新'), + ), + ], + ), + ) + : ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: _voteList.length + (_hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == _voteList.length) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ), + ); + } + return _buildVoteItem(_voteList[index], isDark); + }, ), - ) - : ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(16), - itemCount: _voteList.length + (_hasMore ? 1 : 0), - itemBuilder: (context, index) { - if (index == _voteList.length) { - return const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(), - ), - ); - } - return _buildVoteItem(_voteList[index]); - }, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, ), - child: Column( - children: [ - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - setState(() { - _showDebugInfo = !_showDebugInfo; - }); - }, - icon: Icon( - _showDebugInfo - ? Icons.visibility_off - : Icons.bug_report, - ), - label: Text(_showDebugInfo ? '隐藏调试信息' : '显示调试信息'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[700], - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Column( + children: [ + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + setState(() { + _showDebugInfo = !_showDebugInfo; + }); + }, + icon: Icon( + _showDebugInfo + ? Icons.visibility_off + : Icons.bug_report, + ), + label: Text(_showDebugInfo ? '隐藏调试信息' : '显示调试信息'), + style: ElevatedButton.styleFrom( + backgroundColor: isDark + ? Colors.grey[700] + : Colors.grey[700], + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), ), - ), - if (_showDebugInfo) _buildDebugInfo(), - ], + if (_showDebugInfo) _buildDebugInfo(isDark), + ], + ), ), - ), - ], - ), - ), - ); + ], + ), + ), + ); + }); } - Widget _buildVoteItem(VoteItem vote) { + Widget _buildVoteItem(VoteItem vote, bool isDark) { final statusColor = _getStatusColor(vote); final statusText = _getStatusText(vote); return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.08), blurRadius: 8, offset: const Offset(0, 2), ), @@ -314,9 +339,10 @@ class _VotePageState extends State { children: [ Text( vote.title, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -327,7 +353,7 @@ class _VotePageState extends State { ? '单选投票' : '多选投票 (最多${vote.maxtime}项)', style: TextStyle( - color: Colors.grey[500], + color: isDark ? Colors.grey[400] : Colors.grey[500], fontSize: 12, ), ), @@ -359,7 +385,7 @@ class _VotePageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF333333) : Colors.grey[50], borderRadius: BorderRadius.circular(8), ), child: Row( @@ -367,14 +393,14 @@ class _VotePageState extends State { Icon( Icons.description_outlined, size: 16, - color: Colors.grey[400], + color: isDark ? Colors.grey[400] : Colors.grey[400], ), const SizedBox(width: 8), Expanded( child: Text( vote.idesc!, style: TextStyle( - color: Colors.grey[600], + color: isDark ? Colors.grey[300] : Colors.grey[600], fontSize: 13, ), maxLines: 2, @@ -389,7 +415,7 @@ class _VotePageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF333333) : Colors.grey[50], borderRadius: BorderRadius.circular(8), ), child: Column( @@ -400,17 +426,19 @@ class _VotePageState extends State { icon: Icons.play_arrow_outlined, label: '开始', value: _formatDate(vote.statime), + isDark: isDark, ), Container( height: 24, width: 1, - color: Colors.grey[200], + color: isDark ? Colors.grey[700] : Colors.grey[200], margin: const EdgeInsets.symmetric(horizontal: 12), ), _buildInfoItem( icon: Icons.stop_outlined, label: '结束', value: _formatDate(vote.endtime), + isDark: isDark, ), ], ), @@ -421,17 +449,19 @@ class _VotePageState extends State { icon: Icons.visibility_outlined, label: '浏览', value: '${vote.iview}次', + isDark: isDark, ), Container( height: 24, width: 1, - color: Colors.grey[200], + color: isDark ? Colors.grey[700] : Colors.grey[200], margin: const EdgeInsets.symmetric(horizontal: 12), ), _buildInfoItem( icon: Icons.add_circle_outline, label: '创建', value: _formatDate(vote.addtime), + isDark: isDark, ), ], ), @@ -449,23 +479,31 @@ class _VotePageState extends State { required IconData icon, required String label, required String value, + required bool isDark, }) { return Expanded( child: Row( children: [ - Icon(icon, size: 16, color: Colors.grey[400]), + Icon( + icon, + size: 16, + color: isDark ? Colors.grey[400] : Colors.grey[400], + ), const SizedBox(width: 6), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, - style: TextStyle(color: Colors.grey[400], fontSize: 11), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[400], + fontSize: 11, + ), ), Text( value, style: TextStyle( - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], fontSize: 13, fontWeight: FontWeight.w500, ), @@ -517,7 +555,7 @@ class _VotePageState extends State { }); } - Widget _buildDebugInfo() { + Widget _buildDebugInfo(bool isDark) { return FutureBuilder?>( future: VoteApi.getUserInfo(), builder: (context, snapshot) { @@ -526,31 +564,35 @@ class _VotePageState extends State { margin: const EdgeInsets.only(top: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[900], + color: isDark ? Colors.grey[800] : Colors.grey[900], borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '调试信息', style: TextStyle( - color: Colors.white, + color: isDark ? Colors.white : Colors.white, fontSize: 14, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), - _buildDebugItem('登录状态', _isLoggedIn ? '已验证' : '未验证'), - _buildDebugItem('投票数量', '${_voteList.length}'), - _buildDebugItem('当前页码', '$_currentPage'), - _buildDebugItem('是否有更多', _hasMore ? '是' : '否'), + _buildDebugItem('登录状态', _isLoggedIn ? '已验证' : '未验证', isDark), + _buildDebugItem('投票数量', '${_voteList.length}', isDark), + _buildDebugItem('当前页码', '$_currentPage', isDark), + _buildDebugItem('是否有更多', _hasMore ? '是' : '否', isDark), if (userInfo != null) ...[ - const Divider(color: Colors.grey, height: 16), + Divider( + color: isDark ? Colors.grey[600] : Colors.grey, + height: 16, + ), ...userInfo.entries.map((entry) { return _buildDebugItem( entry.key, entry.value?.toString() ?? 'null', + isDark, ); }), ], @@ -561,7 +603,7 @@ class _VotePageState extends State { ); } - Widget _buildDebugItem(String label, String value) { + Widget _buildDebugItem(String label, String value, bool isDark) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( @@ -571,13 +613,19 @@ class _VotePageState extends State { width: 100, child: Text( '$label:', - style: TextStyle(color: Colors.grey[400], fontSize: 12), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[400], + fontSize: 12, + ), ), ), Expanded( child: Text( value, - style: const TextStyle(color: Colors.white, fontSize: 12), + style: TextStyle( + color: isDark ? Colors.white : Colors.white, + fontSize: 12, + ), ), ), ], @@ -596,6 +644,7 @@ class VoteDetailPage extends StatefulWidget { } class _VoteDetailPageState extends State { + final ThemeController _themeController = Get.find(); VoteDetailResponse? _voteDetail; VoteResultResponse? _voteResult; List _selectedOptions = []; @@ -742,189 +791,214 @@ class _VoteDetailPageState extends State { @override Widget build(BuildContext context) { - final statusColor = _getDetailStatusColor(); - final statusText = _getDetailStatusText(); + return Obx(() { + final isDark = _themeController.isDarkMode; + final statusColor = _getDetailStatusColor(); + final statusText = _getDetailStatusText(); - return Scaffold( - appBar: AppBar( - title: Text(widget.vote.title), - backgroundColor: AppConstants.primaryColor, - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppConstants.primaryColor.withValues(alpha: 0.1), - AppConstants.primaryColor.withValues(alpha: 0.05), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppConstants.primaryColor.withValues( - alpha: 0.15, - ), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - widget.vote.isSingleChoice - ? Icons.radio_button_checked - : Icons.check_box, - color: AppConstants.primaryColor, - size: 24, - ), + return Scaffold( + backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.white, + appBar: AppBar( + title: Text( + widget.vote.title, + style: TextStyle(color: isDark ? Colors.white : Colors.black87), + ), + backgroundColor: isDark + ? const Color(0xFF2A2A2A) + : AppConstants.primaryColor, + elevation: 0, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppConstants.primaryColor.withValues( + alpha: isDark ? 0.3 : 0.1, ), - const SizedBox(width: 12), - Expanded( - child: Column( + AppConstants.primaryColor.withValues( + alpha: isDark ? 0.15 : 0.05, + ), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues( + alpha: 0.15, + ), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + widget.vote.isSingleChoice + ? Icons.radio_button_checked + : Icons.check_box, + color: AppConstants.primaryColor, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.vote.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isDark + ? Colors.white + : Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + widget.vote.isSingleChoice + ? '单选投票' + : '多选投票 (最多${widget.vote.maxtime}项)', + style: TextStyle( + color: isDark + ? Colors.grey[400] + : Colors.grey[600], + fontSize: 13, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + statusText, + style: TextStyle( + color: statusColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + if (widget.vote.idesc != null && + widget.vote.idesc!.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDark + ? const Color(0xFF333333) + : Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.vote.title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + Icon( + Icons.description_outlined, + size: 18, + color: isDark + ? Colors.grey[400] + : Colors.grey[400], ), - const SizedBox(height: 4), - Text( - widget.vote.isSingleChoice - ? '单选投票' - : '多选投票 (最多${widget.vote.maxtime}项)', - style: TextStyle( - color: Colors.grey[600], - fontSize: 13, + const SizedBox(width: 8), + Expanded( + child: Text( + widget.vote.idesc!, + style: TextStyle( + color: isDark + ? Colors.grey[300] + : Colors.grey[600], + fontSize: 14, + ), ), ), ], ), ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(20), - ), + ], + ], + ), + ), + const SizedBox(height: 16), + _buildTimeInfoCard(isDark), + const SizedBox(height: 16), + if (_voteDetail != null && !_voteDetail!.canVote) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.orange.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.orange[700], + size: 20, + ), + const SizedBox(width: 8), + Expanded( child: Text( - statusText, + _voteDetail!.hasVoted ? '您已参与过此投票' : '当前无法参与投票', style: TextStyle( - color: statusColor, - fontSize: 12, - fontWeight: FontWeight.w600, + color: Colors.orange[700], + fontSize: 13, ), ), ), ], ), - if (widget.vote.idesc != null && - widget.vote.idesc!.isNotEmpty) ...[ - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.description_outlined, - size: 18, - color: Colors.grey[400], - ), - const SizedBox(width: 8), - Expanded( - child: Text( - widget.vote.idesc!, - style: TextStyle( - color: Colors.grey[600], - fontSize: 14, - ), - ), - ), - ], - ), - ), - ], - ], - ), - ), - const SizedBox(height: 16), - _buildTimeInfoCard(), - const SizedBox(height: 16), - if (_voteDetail != null && !_voteDetail!.canVote) - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.orange.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.orange.withValues(alpha: 0.3), - ), ), - child: Row( - children: [ - Icon( - Icons.info_outline, - color: Colors.orange[700], - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _voteDetail!.hasVoted ? '您已参与过此投票' : '当前无法参与投票', - style: TextStyle( - color: Colors.orange[700], - fontSize: 13, - ), - ), - ), - ], - ), - ), - if (_showResult && _voteResult != null) - _buildResultView() - else - _buildVoteView(), - ], + if (_showResult && _voteResult != null) + _buildResultView(isDark) + else + _buildVoteView(isDark), + ], + ), ), - ), - ); + ); + }); } - Widget _buildTimeInfoCard() { + Widget _buildTimeInfoCard(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 8, offset: const Offset(0, 2), ), @@ -939,11 +1013,12 @@ class _VoteDetailPageState extends State { label: '开始时间', value: widget.vote.statime, color: AppConstants.successColor, + isDark: isDark, ), Container( height: 40, width: 1, - color: Colors.grey[200], + color: isDark ? Colors.grey[700] : Colors.grey[200], margin: const EdgeInsets.symmetric(horizontal: 16), ), _buildTimeItem( @@ -951,10 +1026,11 @@ class _VoteDetailPageState extends State { label: '结束时间', value: widget.vote.endtime, color: Colors.red, + isDark: isDark, ), ], ), - const Divider(height: 24), + Divider(height: 24, color: isDark ? Colors.grey[700] : null), Row( children: [ _buildTimeItem( @@ -962,11 +1038,12 @@ class _VoteDetailPageState extends State { label: '创建时间', value: widget.vote.addtime, color: Colors.blue, + isDark: isDark, ), Container( height: 40, width: 1, - color: Colors.grey[200], + color: isDark ? Colors.grey[700] : Colors.grey[200], margin: const EdgeInsets.symmetric(horizontal: 16), ), _buildTimeItem( @@ -974,6 +1051,7 @@ class _VoteDetailPageState extends State { label: '浏览次数', value: '${widget.vote.iview} 次', color: Colors.purple, + isDark: isDark, ), ], ), @@ -987,6 +1065,7 @@ class _VoteDetailPageState extends State { required String label, required String value, required Color color, + required bool isDark, }) { return Expanded( child: Row( @@ -1006,14 +1085,18 @@ class _VoteDetailPageState extends State { children: [ Text( label, - style: TextStyle(color: Colors.grey[500], fontSize: 11), + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[500], + fontSize: 11, + ), ), const SizedBox(height: 2), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black87, ), overflow: TextOverflow.ellipsis, ), @@ -1040,7 +1123,7 @@ class _VoteDetailPageState extends State { return widget.vote.isActive ? '进行中' : '已结束'; } - Widget _buildVoteView() { + Widget _buildVoteView(bool isDark) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1052,13 +1135,13 @@ class _VoteDetailPageState extends State { border: Border.all( color: isSelected ? AppConstants.primaryColor - : Colors.grey[300]!, + : (isDark ? Colors.grey[600]! : Colors.grey[300]!), width: 2, ), borderRadius: BorderRadius.circular(12), color: isSelected ? AppConstants.primaryColor.withValues(alpha: 0.05) - : Colors.white, + : (isDark ? const Color(0xFF2A2A2A) : Colors.white), ), child: InkWell( onTap: () => _toggleOption(option.id), @@ -1077,7 +1160,7 @@ class _VoteDetailPageState extends State { : Icons.check_box_outline_blank), color: isSelected ? AppConstants.primaryColor - : Colors.grey, + : (isDark ? Colors.grey[400] : Colors.grey), ), const SizedBox(width: 12), Expanded( @@ -1093,7 +1176,7 @@ class _VoteDetailPageState extends State { : FontWeight.normal, color: isSelected ? AppConstants.primaryColor - : Colors.black, + : (isDark ? Colors.white : Colors.black), ), ), if (option.idesc != null && @@ -1102,7 +1185,9 @@ class _VoteDetailPageState extends State { Text( option.idesc!, style: TextStyle( - color: Colors.grey[600], + color: isDark + ? Colors.grey[400] + : Colors.grey[600], fontSize: 12, ), ), @@ -1144,7 +1229,7 @@ class _VoteDetailPageState extends State { ); } - Widget _buildResultView() { + Widget _buildResultView(bool isDark) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1154,8 +1239,12 @@ class _VoteDetailPageState extends State { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - AppConstants.successColor.withValues(alpha: 0.15), - AppConstants.successColor.withValues(alpha: 0.05), + AppConstants.successColor.withValues( + alpha: isDark ? 0.3 : 0.15, + ), + AppConstants.successColor.withValues( + alpha: isDark ? 0.15 : 0.05, + ), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -1198,7 +1287,7 @@ class _VoteDetailPageState extends State { Text( '您已参与投票', style: TextStyle( - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], fontSize: 13, ), ), @@ -1211,7 +1300,7 @@ class _VoteDetailPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF333333) : Colors.white, borderRadius: BorderRadius.circular(8), ), child: Row( @@ -1221,20 +1310,31 @@ class _VoteDetailPageState extends State { label: '总票数', value: '${_voteResult!.totalVotes}', color: AppConstants.primaryColor, + isDark: isDark, + ), + Container( + height: 36, + width: 1, + color: isDark ? Colors.grey[700] : Colors.grey[200], ), - Container(height: 36, width: 1, color: Colors.grey[200]), _buildResultStat( icon: Icons.how_to_vote_outlined, label: '选项数', value: '${_voteResult!.options.length}', color: Colors.blue, + isDark: isDark, + ), + Container( + height: 36, + width: 1, + color: isDark ? Colors.grey[700] : Colors.grey[200], ), - Container(height: 36, width: 1, color: Colors.grey[200]), _buildResultStat( icon: Icons.check_circle_outline, label: '我的选择', value: '${_voteResult!.userVotes.length}', color: AppConstants.successColor, + isDark: isDark, ), ], ), @@ -1247,14 +1347,18 @@ class _VoteDetailPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: Row( children: [ - Icon(Icons.bar_chart_rounded, size: 18, color: Colors.grey[500]), + Icon( + Icons.bar_chart_rounded, + size: 18, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), const SizedBox(width: 8), Text( '投票详情', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ], @@ -1269,17 +1373,17 @@ class _VoteDetailPageState extends State { decoration: BoxDecoration( color: isVoted ? AppConstants.successColor.withValues(alpha: 0.05) - : Colors.white, + : (isDark ? const Color(0xFF2A2A2A) : Colors.white), borderRadius: BorderRadius.circular(12), border: Border.all( color: isVoted ? AppConstants.successColor.withValues(alpha: 0.3) - : Colors.grey[200]!, + : (isDark ? Colors.grey[600]! : Colors.grey[200]!), width: 1, ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.03), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.03), blurRadius: 4, offset: const Offset(0, 2), ), @@ -1315,6 +1419,7 @@ class _VoteDetailPageState extends State { fontWeight: isVoted ? FontWeight.bold : FontWeight.w500, + color: isDark ? Colors.white : Colors.black87, ), ), if (option.idesc != null && option.idesc!.isNotEmpty) @@ -1323,7 +1428,9 @@ class _VoteDetailPageState extends State { child: Text( option.idesc!, style: TextStyle( - color: Colors.grey[500], + color: isDark + ? Colors.grey[400] + : Colors.grey[500], fontSize: 12, ), ), @@ -1363,7 +1470,9 @@ class _VoteDetailPageState extends State { borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: option.percentage / 100, - backgroundColor: Colors.grey[200], + backgroundColor: isDark + ? Colors.grey[700] + : Colors.grey[200], valueColor: AlwaysStoppedAnimation( isVoted ? AppConstants.successColor @@ -1377,7 +1486,7 @@ class _VoteDetailPageState extends State { Text( '${option.count}票', style: TextStyle( - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], fontSize: 13, fontWeight: FontWeight.w500, ), @@ -1397,6 +1506,7 @@ class _VoteDetailPageState extends State { required String label, required String value, required Color color, + required bool isDark, }) { return Expanded( child: Column( @@ -1411,7 +1521,13 @@ class _VoteDetailPageState extends State { color: color, ), ), - Text(label, style: TextStyle(color: Colors.grey[500], fontSize: 11)), + Text( + label, + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[500], + fontSize: 11, + ), + ), ], ), ); diff --git a/lib/views/profile/guide/beginner_page.dart b/lib/views/profile/guide/beginner_page.dart index 62de321..3e0fe5f 100644 --- a/lib/views/profile/guide/beginner_page.dart +++ b/lib/views/profile/guide/beginner_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; import 'sp-guide.dart'; class BeginnerPage extends StatefulWidget { @@ -12,6 +14,7 @@ class BeginnerPage extends StatefulWidget { class _BeginnerPageState extends State with TickerProviderStateMixin { final ScrollController _scrollController = ScrollController(); + final ThemeController _themeController = Get.find(); late AnimationController _fadeController; late Animation _fadeAnimation; double _progress = 0.0; @@ -266,65 +269,78 @@ class _BeginnerPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[50], - body: Stack( - children: [ - FadeTransition( - opacity: _fadeAnimation, - child: CustomScrollView( - controller: _scrollController, - slivers: [ - SliverAppBar( - title: const Text( - '软件功能', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 17, - color: AppConstants.primaryColor, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.grey[50], + body: Stack( + children: [ + FadeTransition( + opacity: _fadeAnimation, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + title: Text( + '软件功能', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 17, + color: AppConstants.primaryColor, + ), ), + backgroundColor: isDark + ? const Color(0xFF2A2A2A) + : Colors.white, + foregroundColor: AppConstants.primaryColor, + elevation: 0, + centerTitle: true, + floating: true, + snap: true, + pinned: false, + actions: [ + IconButton( + icon: const Icon(Icons.help_outline), + color: AppConstants.primaryColor, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const SpGuidePage(fromSettings: true), + ), + ); + }, + ), + ], ), - backgroundColor: Colors.white, - foregroundColor: AppConstants.primaryColor, - elevation: 0, - centerTitle: true, - floating: true, - snap: true, - pinned: false, - actions: [ - IconButton( - icon: const Icon(Icons.help_outline), - color: AppConstants.primaryColor, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const SpGuidePage(fromSettings: true), - ), + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 100), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return _buildSectionCard( + _tutorialSections[index], + index, + isDark, ); - }, + }, childCount: _tutorialSections.length), ), - ], - ), - SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 100), - sliver: SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return _buildSectionCard(_tutorialSections[index], index); - }, childCount: _tutorialSections.length), ), - ), - ], + ], + ), ), - ), - _buildProgressIndicator(), - ], - ), - ); + _buildProgressIndicator(isDark), + ], + ), + ); + }); } - Widget _buildSectionCard(Map section, int index) { + Widget _buildSectionCard( + Map section, + int index, + bool isDark, + ) { return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 300 + (index * 50)), @@ -338,11 +354,13 @@ class _BeginnerPageState extends State child: Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 12, offset: const Offset(0, 2), ), @@ -376,10 +394,10 @@ class _BeginnerPageState extends State children: [ Text( section['title'], - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, letterSpacing: -0.3, ), ), @@ -388,7 +406,7 @@ class _BeginnerPageState extends State '第 ${index + 1} 部分', style: TextStyle( fontSize: 13, - color: Colors.grey[500], + color: isDark ? Colors.grey[400] : Colors.grey[500], fontWeight: FontWeight.w400, ), ), @@ -398,12 +416,19 @@ class _BeginnerPageState extends State ], ), const SizedBox(height: 20), - _buildSectionContent(section['features'], section['color']), + _buildSectionContent( + section['features'], + section['color'], + isDark, + ), const SizedBox(height: 16), if (index < 4) ...[ - const Divider(height: 1, color: Color(0xFFF5F5F5)), + Divider( + height: 1, + color: isDark ? Colors.grey[700] : const Color(0xFFF5F5F5), + ), const SizedBox(height: 16), - _buildPreviewSection(section['title']), + _buildPreviewSection(section['title'], isDark), ], ], ), @@ -412,7 +437,7 @@ class _BeginnerPageState extends State ); } - Widget _buildSectionContent(List features, Color color) { + Widget _buildSectionContent(List features, Color color, bool isDark) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: features.map((feature) { @@ -432,7 +457,7 @@ class _BeginnerPageState extends State feature, style: TextStyle( fontSize: 15, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], height: 1.5, fontWeight: FontWeight.w400, ), @@ -445,20 +470,20 @@ class _BeginnerPageState extends State ); } - Widget _buildPreviewSection(String title) { + Widget _buildPreviewSection(String title, bool isDark) { Widget preview; switch (title) { case '首页功能': - preview = _buildHomePreview(); + preview = _buildHomePreview(isDark); break; case '发现页面': - preview = _buildDiscoverPreview(); + preview = _buildDiscoverPreview(isDark); break; case '足迹页面': - preview = _buildFootprintPreview(); + preview = _buildFootprintPreview(isDark); break; case '个人中心': - preview = _buildProfilePreview(); + preview = _buildProfilePreview(isDark); break; default: preview = const SizedBox.shrink(); @@ -491,11 +516,11 @@ class _BeginnerPageState extends State ); } - Widget _buildHomePreview() { + Widget _buildHomePreview(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[100], borderRadius: BorderRadius.circular(12), ), child: Column( @@ -525,12 +550,15 @@ class _BeginnerPageState extends State style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: isDark ? Colors.white : Colors.grey[800], ), ), Text( '唐·李白', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), @@ -540,14 +568,14 @@ class _BeginnerPageState extends State Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(8), ), child: Text( '床前明月光,疑是地上霜。\n举头望明月,低头思故乡。', style: TextStyle( fontSize: 14, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], height: 1.6, ), textAlign: TextAlign.center, @@ -557,9 +585,9 @@ class _BeginnerPageState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildPreviewAction(Icons.favorite_border, '点赞'), - _buildPreviewAction(Icons.bookmark_border, '收藏'), - _buildPreviewAction(Icons.share_outlined, '分享'), + _buildPreviewAction(Icons.favorite_border, '点赞', isDark), + _buildPreviewAction(Icons.bookmark_border, '收藏', isDark), + _buildPreviewAction(Icons.share_outlined, '分享', isDark), ], ), ], @@ -567,11 +595,11 @@ class _BeginnerPageState extends State ); } - Widget _buildDiscoverPreview() { + Widget _buildDiscoverPreview(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[100], borderRadius: BorderRadius.circular(12), ), child: Column( @@ -580,16 +608,23 @@ class _BeginnerPageState extends State Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ - Icon(Icons.search, size: 18, color: Colors.grey[400]), + Icon( + Icons.search, + size: 18, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), const SizedBox(width: 8), Text( '搜索诗词...', - style: TextStyle(fontSize: 14, color: Colors.grey[400]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ], ), @@ -597,27 +632,27 @@ class _BeginnerPageState extends State const SizedBox(height: 12), Row( children: [ - _buildPreviewTag('唐诗'), + _buildPreviewTag('唐诗', isDark), const SizedBox(width: 8), - _buildPreviewTag('宋词'), + _buildPreviewTag('宋词', isDark), const SizedBox(width: 8), - _buildPreviewTag('元曲'), + _buildPreviewTag('元曲', isDark), ], ), const SizedBox(height: 12), - _buildPreviewListItem('将进酒', '李白'), + _buildPreviewListItem('将进酒', '李白', isDark), const SizedBox(height: 8), - _buildPreviewListItem('水调歌头', '苏轼'), + _buildPreviewListItem('水调歌头', '苏轼', isDark), ], ), ); } - Widget _buildFootprintPreview() { + Widget _buildFootprintPreview(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[100], borderRadius: BorderRadius.circular(12), ), child: Column( @@ -625,25 +660,25 @@ class _BeginnerPageState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildPreviewStat('128', '收藏'), - _buildPreviewStat('45', '笔记'), - _buildPreviewStat('89', '点赞'), + _buildPreviewStat('128', '收藏', isDark), + _buildPreviewStat('45', '笔记', isDark), + _buildPreviewStat('89', '点赞', isDark), ], ), const SizedBox(height: 12), - _buildPreviewListItem('静夜思', '已收藏'), + _buildPreviewListItem('静夜思', '已收藏', isDark), const SizedBox(height: 8), - _buildPreviewListItem('春晓', '有笔记'), + _buildPreviewListItem('春晓', '有笔记', isDark), ], ), ); } - Widget _buildProfilePreview() { + Widget _buildProfilePreview(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[100], borderRadius: BorderRadius.circular(12), ), child: Column( @@ -670,12 +705,15 @@ class _BeginnerPageState extends State style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: isDark ? Colors.white : Colors.grey[800], ), ), Text( 'Lv.12 · 诗意生活', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), @@ -685,9 +723,9 @@ class _BeginnerPageState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildPreviewStat('156', '浏览'), - _buildPreviewStat('89', '点赞'), - _buildPreviewStat('45', '答题'), + _buildPreviewStat('156', '浏览', isDark), + _buildPreviewStat('89', '点赞', isDark), + _buildPreviewStat('45', '答题', isDark), ], ), ], @@ -695,21 +733,31 @@ class _BeginnerPageState extends State ); } - Widget _buildPreviewAction(IconData icon, String label) { + Widget _buildPreviewAction(IconData icon, String label, bool isDark) { return Column( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon( + icon, + size: 20, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(height: 4), - Text(label, style: TextStyle(fontSize: 11, color: Colors.grey[600])), + Text( + label, + style: TextStyle( + fontSize: 11, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), ], ); } - Widget _buildPreviewTag(String text) { + Widget _buildPreviewTag(String text, bool isDark) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: AppConstants.primaryColor.withValues(alpha: 0.3), @@ -726,11 +774,11 @@ class _BeginnerPageState extends State ); } - Widget _buildPreviewListItem(String title, String subtitle) { + Widget _buildPreviewListItem(String title, String subtitle, bool isDark) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(8), ), child: Row( @@ -740,20 +788,23 @@ class _BeginnerPageState extends State title, style: TextStyle( fontSize: 14, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], fontWeight: FontWeight.w500, ), ), Text( subtitle, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), ); } - Widget _buildPreviewStat(String value, String label) { + Widget _buildPreviewStat(String value, String label, bool isDark) { return Column( children: [ Text( @@ -765,12 +816,18 @@ class _BeginnerPageState extends State ), ), const SizedBox(height: 2), - Text(label, style: TextStyle(fontSize: 11, color: Colors.grey[500])), + Text( + label, + style: TextStyle( + fontSize: 11, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), + ), ], ); } - Widget _buildProgressIndicator() { + Widget _buildProgressIndicator(bool isDark) { return Positioned( left: 0, top: 0, @@ -783,9 +840,15 @@ class _BeginnerPageState extends State begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ - Colors.white.withValues(alpha: 0.95), - Colors.white.withValues(alpha: 0.85), - Colors.white.withValues(alpha: 0.0), + (isDark ? const Color(0xFF1A1A1A) : Colors.white).withValues( + alpha: 0.95, + ), + (isDark ? const Color(0xFF1A1A1A) : Colors.white).withValues( + alpha: 0.85, + ), + (isDark ? const Color(0xFF1A1A1A) : Colors.white).withValues( + alpha: 0.0, + ), ], stops: const [0.0, 0.7, 1.0], ), diff --git a/lib/views/profile/guide/permission.dart b/lib/views/profile/guide/permission.dart index 2c82981..0971a12 100644 --- a/lib/views/profile/guide/permission.dart +++ b/lib/views/profile/guide/permission.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-27 /// 功能: 权限管理页面 @@ -15,6 +17,7 @@ class PermissionPage extends StatefulWidget { } class _PermissionPageState extends State { + final ThemeController _themeController = Get.find(); bool _vibrationEnabled = true; bool _networkEnabled = true; bool _clipboardEnabled = true; @@ -36,98 +39,114 @@ class _PermissionPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '权限管理', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.pop(context), - ), - actions: [ - IconButton( - icon: Icon(Icons.info_outline, color: AppConstants.primaryColor), - onPressed: _showPermissionInfoDialog, - ), - ], - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildPermissionGroup('权限列表', [ - _buildPermissionItem( - '网络访问', - '访问网络获取诗词内容和数据', - Icons.wifi, - _networkEnabled, - null, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '权限管理', + style: TextStyle( + color: isDark ? Colors.white : AppConstants.primaryColor, + fontWeight: FontWeight.bold, ), - _buildPermissionItem( - '震动反馈', - '操作时的震动反馈,提升交互体验', - Icons.vibration, - _vibrationEnabled, - null, + ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: isDark ? Colors.white : AppConstants.primaryColor, ), + onPressed: () => Navigator.pop(context), + ), + actions: [ + IconButton( + icon: Icon( + Icons.info_outline, + color: isDark ? Colors.white : AppConstants.primaryColor, + ), + onPressed: _showPermissionInfoDialog, + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildPermissionGroup('权限列表', [ + _buildPermissionItem( + '网络访问', + '访问网络获取诗词内容和数据', + Icons.wifi, + _networkEnabled, + null, + isDark, + ), + _buildPermissionItem( + '震动反馈', + '操作时的震动反馈,提升交互体验', + Icons.vibration, + _vibrationEnabled, + null, + isDark, + ), - _buildPermissionItem( - '剪切板', - '复制诗词内容到剪切板', - Icons.content_copy, - _clipboardEnabled, - null, - ), - _buildPermissionItem( - '播放声音', - '播放内置提示音', - Icons.audio_file, - _clipboardEnabled, - null, - ), - _buildPermissionItem( - '分享能力', - '调用系统分享接口', - Icons.share, - _clipboardEnabled, - null, - ), - ]), - const SizedBox(height: 16), - _buildPermissionGroup('权限说明', [ - _buildInfoItem('震动反馈', '用于点赞、收藏等操作的触觉反馈,提升用户体验。'), - _buildInfoItem('网络访问', '用于获取诗词内容、排行榜数据、用户信息等。'), - _buildInfoItem('剪切板', '用于复制诗词内容,方便用户分享和记录。'), - _buildInfoItem('播放声音', '用于主页点击提示音,提升用户体验。'), - _buildInfoItem('分享能力', '用于分享诗词内容到社交媒体平台。'), - ]), - const SizedBox(height: 16), - _buildSandboxInfoCard(), - const SizedBox(height: 16), - _buildProjectSupplement(), - const SizedBox(height: 24), - _buildBottomTip(), - ], - ), - ); + _buildPermissionItem( + '剪切板', + '复制诗词内容到剪切板', + Icons.content_copy, + _clipboardEnabled, + null, + isDark, + ), + _buildPermissionItem( + '播放声音', + '播放内置提示音', + Icons.audio_file, + _clipboardEnabled, + null, + isDark, + ), + _buildPermissionItem( + '分享能力', + '调用系统分享接口', + Icons.share, + _clipboardEnabled, + null, + isDark, + ), + ], isDark), + const SizedBox(height: 16), + _buildPermissionGroup('权限说明', [ + _buildInfoItem('震动反馈', '用于点赞、收藏等操作的触觉反馈,提升用户体验。', isDark), + _buildInfoItem('网络访问', '用于获取诗词内容、排行榜数据、用户信息等。', isDark), + _buildInfoItem('剪切板', '用于复制诗词内容,方便用户分享和记录。', isDark), + _buildInfoItem('播放声音', '用于主页点击提示音,提升用户体验。', isDark), + _buildInfoItem('分享能力', '用于分享诗词内容到社交媒体平台。', isDark), + ], isDark), + const SizedBox(height: 16), + _buildSandboxInfoCard(isDark), + const SizedBox(height: 16), + _buildProjectSupplement(isDark), + const SizedBox(height: 24), + _buildBottomTip(isDark), + ], + ), + ); + }); } - Widget _buildPermissionGroup(String title, List items) { + Widget _buildPermissionGroup(String title, List items, bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -157,7 +176,7 @@ class _PermissionPageState extends State { ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[700] : null), ...items, ], ), @@ -170,6 +189,7 @@ class _PermissionPageState extends State { IconData icon, bool enabled, Function(bool)? onToggle, + bool isDark, ) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -193,9 +213,10 @@ class _PermissionPageState extends State { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black87, ), ), const SizedBox(width: 8), @@ -205,12 +226,15 @@ class _PermissionPageState extends State { vertical: 2, ), decoration: BoxDecoration( - color: Colors.grey[200], + color: isDark ? Colors.grey[700] : Colors.grey[200], borderRadius: BorderRadius.circular(4), ), child: Text( '已开启', - style: TextStyle(fontSize: 10, color: Colors.grey[600]), + style: TextStyle( + fontSize: 10, + color: isDark ? Colors.grey[300] : Colors.grey[600], + ), ), ), ], @@ -218,18 +242,25 @@ class _PermissionPageState extends State { const SizedBox(height: 2), Text( description, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), ), - Icon(Icons.lock, color: Colors.grey[400], size: 20), + Icon( + Icons.lock, + color: isDark ? Colors.grey[500] : Colors.grey[400], + size: 20, + ), ], ), ); } - Widget _buildInfoItem(String title, String description) { + Widget _buildInfoItem(String title, String description, bool isDark) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( @@ -251,9 +282,10 @@ class _PermissionPageState extends State { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black87, ), ), // const SizedBox(height: 4), @@ -261,7 +293,7 @@ class _PermissionPageState extends State { description, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], height: 1.5, ), ), @@ -273,14 +305,14 @@ class _PermissionPageState extends State { ); } - Widget _buildProjectSupplement() { + Widget _buildProjectSupplement(bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -306,30 +338,34 @@ class _PermissionPageState extends State { ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[700] : null), _buildSupplementItem( '用户反馈', '帮助我们改进产品', Icons.feedback, () => _showFeedbackDialog(), + isDark, ), _buildSupplementItem( '功能建议', '提出您希望的功能', Icons.lightbulb, () => _showSuggestionDialog(), + isDark, ), // _buildSupplementItem( // 'Bug报告', // '报告遇到的问题', // Icons.bug_report, // () => _showBugReportDialog(), + // isDark, // ), // _buildSupplementItem( // '参与开发', // '成为贡献者', // Icons.code, // () => _showContributionDialog(), + // isDark, // ), ], ), @@ -341,6 +377,7 @@ class _PermissionPageState extends State { String description, IconData icon, VoidCallback onTap, + bool isDark, ) { return InkWell( onTap: onTap, @@ -364,20 +401,27 @@ class _PermissionPageState extends State { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black87, ), ), const SizedBox(height: 2), Text( description, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), ), - Icon(Icons.chevron_right, color: Colors.grey[400]), + Icon( + Icons.chevron_right, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ], ), ), @@ -533,15 +577,15 @@ class _PermissionPageState extends State { ); } - Widget _buildSandboxInfoCard() { + Widget _buildSandboxInfoCard(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -576,30 +620,34 @@ class _PermissionPageState extends State { ], ), const SizedBox(height: 12), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[700] : null), const SizedBox(height: 12), Text( '本软件严格遵循移动平台沙盒机制运行,确保您的数据安全:', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Colors.grey[800], + color: isDark ? Colors.grey[300] : Colors.grey[800], ), ), const SizedBox(height: 12), - _buildSandboxItem('📱 沙盒隔离', '软件在独立沙盒环境中运行,无法访问系统其他应用数据'), + _buildSandboxItem('📱 沙盒隔离', '软件在独立沙盒环境中运行,无法访问系统其他应用数据', isDark), const SizedBox(height: 8), - _buildSandboxItem('📄 无文件创建', '不会在设备上创建额外文件,所有数据通过网络获取'), + _buildSandboxItem('📄 无文件创建', '不会在设备上创建额外文件,所有数据通过网络获取', isDark), const SizedBox(height: 8), - _buildSandboxItem('🔒 权限透明', '仅使用必要权限,此类权限均为基础权限'), + _buildSandboxItem('🔒 权限透明', '仅使用必要权限,此类权限均为基础权限', isDark), const SizedBox(height: 8), - _buildSandboxItem('💾 本地存储', '仅使用 SharedPreferences 存储少量用户偏好设置'), + _buildSandboxItem( + '💾 本地存储', + '仅使用 SharedPreferences 存储少量用户偏好设置', + isDark, + ), ], ), ); } - Widget _buildSandboxItem(String title, String description) { + Widget _buildSandboxItem(String title, String description, bool isDark) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -622,7 +670,7 @@ class _PermissionPageState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Colors.grey[800], + color: isDark ? Colors.white : Colors.grey[800], ), ), const SizedBox(height: 4), @@ -630,7 +678,7 @@ class _PermissionPageState extends State { description, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], height: 1.5, ), ), @@ -641,7 +689,7 @@ class _PermissionPageState extends State { ); } - Widget _buildBottomTip() { + Widget _buildBottomTip(bool isDark) { return Center( child: Padding( padding: const EdgeInsets.only(bottom: 32), @@ -649,7 +697,7 @@ class _PermissionPageState extends State { '到底了', style: TextStyle( fontSize: 14, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], fontWeight: FontWeight.w500, ), ), diff --git a/lib/views/profile/guide/sp-guide.dart b/lib/views/profile/guide/sp-guide.dart index 873d91d..a0416d1 100644 --- a/lib/views/profile/guide/sp-guide.dart +++ b/lib/views/profile/guide/sp-guide.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; import '../../../config/app_config.dart'; +import '../../../services/get/theme_controller.dart'; import '../settings/privacy.dart'; import '../settings/user-plan.dart'; import 'permission.dart'; @@ -24,6 +26,7 @@ class SpGuidePage extends StatefulWidget { class _SpGuidePageState extends State with TickerProviderStateMixin { final PageController _pageController = PageController(); + final ThemeController _themeController = Get.find(); TabController? _tabController; int _currentPage = 0; bool _firstLaunch = true; @@ -31,7 +34,6 @@ class _SpGuidePageState extends State bool _agreementAccepted = false; bool _userPlanJoined = false; - // 协议焦点状态 bool _isAgreementFocused = false; final int _totalPages = 3; @@ -217,58 +219,55 @@ class _SpGuidePageState extends State @override Widget build(BuildContext context) { - return WillPopScope( - // 拦截返回按钮,如果不是从设置页面进入,则阻止返回 - onWillPop: () async { - if (widget.fromSettings) { - return true; // 允许返回 - } - // 首次启动时,阻止返回,必须完成引导流程 - return false; - }, - child: Scaffold( - backgroundColor: Colors.white, - appBar: _buildAppBar(), - body: Stack( - children: [ - PageView( - controller: _pageController, - scrollDirection: Axis.vertical, - physics: const PageScrollPhysics(), - onPageChanged: (index) { - setState(() { - _currentPage = index; - }); - }, - children: [ - _buildWelcomePage(), - _buildPrivacyPage(), - _buildFeaturePage(), - ], - ), - _buildPageIndicator(), - _buildBottomNavigation(), - ], + return Obx(() { + final isDark = _themeController.isDarkMode; + return WillPopScope( + onWillPop: () async { + if (widget.fromSettings) { + return true; + } + return false; + }, + child: Scaffold( + backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.white, + appBar: _buildAppBar(isDark), + body: Stack( + children: [ + PageView( + controller: _pageController, + scrollDirection: Axis.vertical, + physics: const PageScrollPhysics(), + onPageChanged: (index) { + setState(() { + _currentPage = index; + }); + }, + children: [ + _buildWelcomePage(isDark), + _buildPrivacyPage(isDark), + _buildFeaturePage(isDark), + ], + ), + _buildPageIndicator(isDark), + _buildBottomNavigation(isDark), + ], + ), ), - ), - ); + ); + }); } - Widget _buildPageIndicator() { - // 根据当前页面计算进度条位置 - // 第1页:屏幕上方靠近顶部 - // 第2页:屏幕中间(不变) - // 第3页:屏幕下方靠近底部 + Widget _buildPageIndicator(bool isDark) { double alignmentY; switch (_currentPage) { case 0: - alignmentY = -0.7; // 上方靠近顶部 + alignmentY = -0.7; break; case 1: - alignmentY = 0.0; // 中间 + alignmentY = 0.0; break; case 2: - alignmentY = 0.7; // 下方靠近底部 + alignmentY = 0.7; break; default: alignmentY = 0.0; @@ -283,11 +282,15 @@ class _SpGuidePageState extends State child: Container( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.9), + color: (isDark ? const Color(0xFF2A2A2A) : Colors.white).withValues( + alpha: 0.9, + ), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.1), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(2, 0), ), @@ -318,7 +321,7 @@ class _SpGuidePageState extends State ? AppConstants.primaryColor : isCompleted ? AppConstants.primaryColor.withValues(alpha: 0.5) - : Colors.grey[300], + : (isDark ? Colors.grey[600] : Colors.grey[300]), border: isActive ? Border.all(color: AppConstants.primaryColor, width: 3) : null, @@ -343,7 +346,7 @@ class _SpGuidePageState extends State ); } - PreferredSizeWidget _buildAppBar() { + PreferredSizeWidget _buildAppBar(bool isDark) { return AppBar( title: Text( _pageTitles[_currentPage], @@ -352,7 +355,7 @@ class _SpGuidePageState extends State fontWeight: FontWeight.bold, ), ), - backgroundColor: Colors.white, + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, elevation: 0, centerTitle: true, leading: widget.fromSettings @@ -365,7 +368,7 @@ class _SpGuidePageState extends State ); } - Widget _buildWelcomePage() { + Widget _buildWelcomePage(bool isDark) { return Container( padding: const EdgeInsets.all(32), child: Column( @@ -388,12 +391,12 @@ class _SpGuidePageState extends State ), ), const SizedBox(height: 40), - const Text( + Text( '欢迎使用', style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), const SizedBox(height: 16), @@ -409,18 +412,18 @@ class _SpGuidePageState extends State Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF2A2A2A) : Colors.grey[50], borderRadius: BorderRadius.circular(16), ), child: Column( children: [ - _buildWelcomeItem('🌟', '在诗词里旅行,在文化中生长'), + _buildWelcomeItem('🌟', '在诗词里旅行,在文化中生长', isDark), const SizedBox(height: 16), - _buildWelcomeItem('📚', '海量诗词,随心阅读'), + _buildWelcomeItem('📚', '海量诗词,随心阅读', isDark), const SizedBox(height: 16), - _buildWelcomeItem('❤️', '收藏喜爱,记录感悟'), + _buildWelcomeItem('❤️', '收藏喜爱,记录感悟', isDark), const SizedBox(height: 16), - _buildWelcomeItem('🌙', '每日推荐,发现美好'), + _buildWelcomeItem('🌙', '每日推荐,发现美好', isDark), ], ), ), @@ -429,12 +432,15 @@ class _SpGuidePageState extends State margin: const EdgeInsets.only(left: 16), height: 1, width: double.infinity, - color: Colors.grey[300], + color: isDark ? Colors.grey[700] : Colors.grey[300], ), const SizedBox(height: 16), Text( '向上滑动继续 ↓', - style: TextStyle(fontSize: 14, color: Colors.grey[400]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), const SizedBox(height: 16), Row( @@ -475,7 +481,6 @@ class _SpGuidePageState extends State ), ), const SizedBox(width: 8), - // _buildShowGuideCheckbox(), ], ), ], @@ -483,7 +488,7 @@ class _SpGuidePageState extends State ); } - Widget _buildWelcomeItem(String emoji, String text) { + Widget _buildWelcomeItem(String emoji, String text, bool isDark) { return Row( children: [ Text(emoji, style: const TextStyle(fontSize: 20)), @@ -491,44 +496,17 @@ class _SpGuidePageState extends State Expanded( child: Text( text, - style: const TextStyle(fontSize: 15, color: Colors.black87), + style: TextStyle( + fontSize: 15, + color: isDark ? Colors.white : Colors.black87, + ), ), ), ], ); } - Widget _buildShowGuideCheckbox() { - return GestureDetector( - onTap: () { - _toggleShowGuide(!_showGuideOnStartup); - _showGuideStatusPopup(); - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Icon( - _showGuideOnStartup ? Icons.check_box : Icons.check_box_outline_blank, - color: _showGuideOnStartup - ? AppConstants.primaryColor - : Colors.grey[400], - size: 24, - ), - ), - ); - } - - void _showGuideStatusPopup() { + void _showGuideStatusPopup(bool isDark) { showDialog( context: context, builder: (context) => Dialog( @@ -536,11 +514,13 @@ class _SpGuidePageState extends State child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF1A1A1A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.15), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.15), blurRadius: 20, offset: const Offset(0, 4), ), @@ -557,13 +537,17 @@ class _SpGuidePageState extends State : Icons.info_outline, color: _showGuideOnStartup ? AppConstants.primaryColor - : Colors.grey[600], + : (isDark ? Colors.grey[400] : Colors.grey[600]), size: 24, ), const SizedBox(width: 8), - const Text( + Text( '通知设置', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, + ), ), ], ), @@ -573,7 +557,7 @@ class _SpGuidePageState extends State decoration: BoxDecoration( color: _showGuideOnStartup ? AppConstants.primaryColor.withValues(alpha: 0.1) - : Colors.grey[50], + : (isDark ? const Color(0xFF2A2A2A) : Colors.grey[50]), borderRadius: BorderRadius.circular(12), ), child: Column( @@ -584,7 +568,7 @@ class _SpGuidePageState extends State : Icons.notifications_off, color: _showGuideOnStartup ? AppConstants.primaryColor - : Colors.grey[500], + : (isDark ? Colors.grey[400] : Colors.grey[500]), size: 40, ), const SizedBox(height: 12), @@ -595,13 +579,16 @@ class _SpGuidePageState extends State fontWeight: FontWeight.w600, color: _showGuideOnStartup ? AppConstants.primaryColor - : Colors.grey[700], + : (isDark ? Colors.white : Colors.grey[700]), ), ), const SizedBox(height: 8), Text( _showGuideOnStartup ? '下次启动时将显示欢迎页' : '下次启动时将不显示欢迎页', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), textAlign: TextAlign.center, ), ], @@ -630,18 +617,20 @@ class _SpGuidePageState extends State ); } - Widget _buildPrivacyPage() { + Widget _buildPrivacyPage(bool isDark) { _initTabController(); return Stack( children: [ Column( children: [ Container( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, child: TabBar( controller: _tabController!, labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], + unselectedLabelColor: isDark + ? Colors.grey[400] + : Colors.grey[600], indicatorColor: AppConstants.primaryColor, indicatorWeight: 2, tabs: const [ @@ -653,19 +642,16 @@ class _SpGuidePageState extends State Expanded( child: Stack( children: [ - // 协议内容区域 TabBarView( controller: _tabController!, physics: _isAgreementFocused ? const AlwaysScrollableScrollPhysics() : const NeverScrollableScrollPhysics(), children: [ - // 隐私政策 - 使用 NotificationListener 监听滚动 NotificationListener( onNotification: (notification) { if (notification is ScrollEndNotification) { final metrics = notification.metrics; - // 只有在到达顶部或底部时才取消焦点 if (metrics.pixels <= metrics.minScrollExtent || metrics.pixels >= metrics.maxScrollExtent) { setState(() { @@ -691,12 +677,10 @@ class _SpGuidePageState extends State ), ), ), - // 用户协议 - 使用 NotificationListener 监听滚动 NotificationListener( onNotification: (notification) { if (notification is ScrollEndNotification) { final metrics = notification.metrics; - // 只有在到达顶部或底部时才取消焦点 if (metrics.pixels <= metrics.minScrollExtent || metrics.pixels >= metrics.maxScrollExtent) { setState(() { @@ -724,7 +708,6 @@ class _SpGuidePageState extends State ), ], ), - // 左右焦点竖线 if (_isAgreementFocused) Positioned( left: 0, @@ -750,7 +733,6 @@ class _SpGuidePageState extends State ), ], ), - // 底部按钮区域 - 点击时取消协议焦点 Positioned( left: 0, right: 0, @@ -766,10 +748,12 @@ class _SpGuidePageState extends State child: Container( padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -2), ), @@ -781,7 +765,7 @@ class _SpGuidePageState extends State Container( height: 1, width: double.infinity, - color: Colors.grey[300], + color: isDark ? Colors.grey[700] : Colors.grey[300], ), const SizedBox(height: 16), Row( @@ -812,7 +796,9 @@ class _SpGuidePageState extends State fontSize: 14, color: _agreementAccepted ? AppConstants.primaryColor - : Colors.grey[700], + : (isDark + ? Colors.grey[300] + : Colors.grey[700]), ), ), ), @@ -829,9 +815,11 @@ class _SpGuidePageState extends State style: ElevatedButton.styleFrom( backgroundColor: _agreementAccepted ? AppConstants.primaryColor - : Colors.grey[600], + : (isDark ? Colors.grey[700] : Colors.grey[600]), foregroundColor: Colors.white, - disabledBackgroundColor: Colors.grey[300], + disabledBackgroundColor: isDark + ? Colors.grey[800] + : Colors.grey[300], padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -855,41 +843,19 @@ class _SpGuidePageState extends State ); } - Widget _buildFeaturePage() { + Widget _buildFeaturePage(bool isDark) { return Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 100), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Center( - // child: Container( - // width: 80, - // height: 80, - // decoration: BoxDecoration( - // color: Colors.purple.withValues(alpha: 0.1), - // borderRadius: BorderRadius.circular(20), - // ), - // child: const Center( - // child: Text('✨', style: TextStyle(fontSize: 40)), - // ), - // ), - // ), - // const SizedBox(height: 24), - // const Center( - // child: Text( - // '功能介绍', - // style: TextStyle( - // fontSize: 26, - // fontWeight: FontWeight.bold, - // color: Colors.black87, - // ), - // ), - // ), - // const SizedBox(height: 8), Center( child: Text( '探索丰富的诗词世界', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ), const SizedBox(height: 16), @@ -903,6 +869,7 @@ class _SpGuidePageState extends State '每日推荐精选诗词', Icons.home, Colors.blue, + isDark, ), ), const SizedBox(width: 8), @@ -912,6 +879,7 @@ class _SpGuidePageState extends State '浏览排行榜、分类', Icons.explore, Colors.green, + isDark, ), ), ], @@ -925,6 +893,7 @@ class _SpGuidePageState extends State '点赞收藏、记笔记', Icons.favorite, Colors.red, + isDark, ), ), const SizedBox(width: 8), @@ -934,6 +903,7 @@ class _SpGuidePageState extends State '管理数据、设置', Icons.person, Colors.purple, + isDark, ), ), ], @@ -945,7 +915,7 @@ class _SpGuidePageState extends State width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF2A2A2A) : Colors.grey[50], borderRadius: BorderRadius.circular(12), ), child: Row( @@ -993,7 +963,7 @@ class _SpGuidePageState extends State Switch( value: _userPlanJoined, onChanged: _toggleUserPlan, - activeColor: AppConstants.primaryColor, + activeThumbColor: AppConstants.primaryColor, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], @@ -1099,16 +1069,23 @@ class _SpGuidePageState extends State String desc, IconData icon, Color color, + bool isDark, ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), + border: Border.all( + color: isDark + ? Colors.grey[700]! + : Colors.grey.withValues(alpha: 0.2), + ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.03), + color: isDark + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2), ), @@ -1130,14 +1107,18 @@ class _SpGuidePageState extends State const SizedBox(height: 8), Text( title, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), ), const SizedBox(height: 2), Text( desc, style: TextStyle( fontSize: 11, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], height: 1.3, ), ), @@ -1146,7 +1127,7 @@ class _SpGuidePageState extends State ); } - Widget _buildBottomNavigation() { + Widget _buildBottomNavigation(bool isDark) { return Positioned( bottom: 0, left: 0, @@ -1161,6 +1142,7 @@ class _SpGuidePageState extends State icon: Icons.arrow_back_ios, label: '上一页', onPressed: _previousPage, + isDark: isDark, ) : const SizedBox(width: 100), _currentPage < _totalPages - 1 @@ -1170,11 +1152,13 @@ class _SpGuidePageState extends State onPressed: _currentPage == 1 && !_agreementAccepted ? null : _nextPage, + isDark: isDark, ) : _buildNavButton( icon: Icons.check, label: '完成', onPressed: _agreementAccepted ? _finishGuide : null, + isDark: isDark, ), ], ), @@ -1186,10 +1170,13 @@ class _SpGuidePageState extends State required IconData icon, required String label, VoidCallback? onPressed, + required bool isDark, }) { return Container( decoration: BoxDecoration( - color: onPressed != null ? AppConstants.primaryColor : Colors.grey[300], + color: onPressed != null + ? AppConstants.primaryColor + : (isDark ? Colors.grey[700] : Colors.grey[300]), borderRadius: BorderRadius.circular(12), boxShadow: onPressed != null ? [ @@ -1213,14 +1200,18 @@ class _SpGuidePageState extends State children: [ Icon( icon, - color: onPressed != null ? Colors.white : Colors.grey[500], + color: onPressed != null + ? Colors.white + : (isDark ? Colors.grey[400] : Colors.grey[500]), size: 20, ), const SizedBox(width: 8), Text( label, style: TextStyle( - color: onPressed != null ? Colors.white : Colors.grey[500], + color: onPressed != null + ? Colors.white + : (isDark ? Colors.grey[400] : Colors.grey[500]), fontSize: 14, fontWeight: FontWeight.w500, ), diff --git a/lib/views/profile/history_page.dart b/lib/views/profile/history_page.dart index de00063..212d8af 100644 --- a/lib/views/profile/history_page.dart +++ b/lib/views/profile/history_page.dart @@ -1,370 +1,218 @@ -/// 时间: 2025-03-21 -/// 功能: 历史记录页面 -/// 介绍: 独立的历史记录管理页面,支持查看、搜索、删除和导出功能 -/// 最新变化: 新创建文件,实现历史记录的独立页面功能 - +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; -import '../../controllers/history_controller.dart'; +import '../../services/get/history_controller.dart'; +import '../../services/get/theme_controller.dart'; -class HistoryPage extends StatefulWidget { +class HistoryPage extends StatelessWidget { const HistoryPage({super.key}); @override - State createState() => _HistoryPageState(); -} + Widget build(BuildContext context) { + final controller = Get.put(HistoryController()); + final themeController = Get.find(); -class _HistoryPageState extends State { - List> _historyList = []; - List> _filteredHistoryList = []; - bool _isLoading = true; - String _searchKeyword = ''; - int _selectedSortType = 0; // 0: 时间倒序, 1: 时间正序, 2: 按名称排序 - final List _sortTypes = ['时间倒序', '时间正序', '按名称排序']; - - @override - void initState() { - super.initState(); - _loadHistory(); - } - - // === 加载历史记录 === - Future _loadHistory() async { - setState(() { - _isLoading = true; - }); - - try { - final history = await HistoryController.getHistory(); - setState(() { - _historyList = history; - _filteredHistoryList = history; - _isLoading = false; - }); - } catch (e) { - // print('加载历史记录失败: $e'); - setState(() { - _isLoading = false; - }); - } - } - - // === 搜索历史记录 === - void _searchHistory(String keyword) { - setState(() { - _searchKeyword = keyword; - }); - - if (keyword.isEmpty) { - setState(() { - _filteredHistoryList = _historyList; - }); - return; - } - - _performSearch(keyword); - } - - Future _performSearch(String keyword) async { - try { - final searchResults = await HistoryController.searchHistory(keyword); - setState(() { - _filteredHistoryList = searchResults; - }); - } catch (e) { - // print('搜索历史记录失败: $e'); - } - } - - // === 排序历史记录 === - void _sortHistory(int sortType) { - setState(() { - _selectedSortType = sortType; - }); - - final sortedList = List>.from(_filteredHistoryList); - - switch (sortType) { - case 0: // 时间倒序 - sortedList.sort( - (a, b) => (b['timestamp'] ?? 0).compareTo(a['timestamp'] ?? 0), - ); - break; - case 1: // 时间正序 - sortedList.sort( - (a, b) => (a['timestamp'] ?? 0).compareTo(b['timestamp'] ?? 0), - ); - break; - case 2: // 按名称排序 - sortedList.sort((a, b) => (a['name'] ?? '').compareTo(b['name'] ?? '')); - break; - } - - setState(() { - _filteredHistoryList = sortedList; - }); - } - - // === 清空历史记录 === - Future _clearHistory() async { - final confirmed = await _showConfirmDialog( - '清空历史记录', - '确定要清空所有历史记录吗?此操作不可撤销。', - ); - - if (confirmed == null || !confirmed) return; - - try { - final success = await HistoryController.clearHistory(); - - if (success) { - setState(() { - _historyList.clear(); - _filteredHistoryList.clear(); - }); - - _showSnackBar('历史记录已清空'); - } else { - _showSnackBar('清空失败'); - } - } catch (e) { - //('清空历史记录失败: $e'); - _showSnackBar('清空失败'); - } - } - - // === 删除单条记录 === - Future _deleteHistoryItem(int index, Map item) async { - final confirmed = await _showConfirmDialog('删除记录', '确定要删除这条历史记录吗?'); - - if (confirmed == null || !confirmed) return; - - try { - final poetryId = item['id'] as int; - final success = await HistoryController.removeFromHistory(poetryId); - - if (success) { - // 重新加载历史记录,避免索引不匹配问题 - await _loadHistory(); - _showSnackBar('删除成功'); - } else { - _showSnackBar('删除失败'); - } - } catch (e) { - _showSnackBar('删除失败'); - } - } - - // === 导出历史记录 === - Future _exportHistory() async { - try { - final exportData = await HistoryController.exportHistory(format: 'json'); - - if (exportData.isNotEmpty) { - // 这里可以实现文件保存功能 - _showSnackBar('导出功能开发中...'); - } else { - _showSnackBar('无数据可导出'); - } - } catch (e) { - _showSnackBar('导出失败'); - } - } - - // === 显示确认对话框 === - Future _showConfirmDialog(String title, String content) async { - return await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(content), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('取消'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('确定'), - ), - ], - ), - ); - } - - // === 显示历史记录统计 === - void _showStatsDialog() async { - try { - final stats = await HistoryController.getHistoryStats(); - - if (stats.isEmpty) { - _showSnackBar('暂无统计数据'); - return; - } - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('历史记录统计'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('总数: ${stats['totalCount']}'), - Text('今日: ${stats['todayCount']}'), - Text('本周: ${stats['thisWeekCount']}'), - Text('本月: ${stats['thisMonthCount']}'), - const SizedBox(height: 16), - const Text('热门朝代:'), - if (stats['topDynasties'] != null && - stats['topDynasties'] is Map) ...[ - ...(stats['topDynasties'] as Map).entries - .map((entry) => Text('${entry.key}: ${entry.value}')) - .toList(), - ], - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('关闭'), + return Obx(() { + final isDark = themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF2F2F7), + appBar: _buildAppBar(controller, isDark), + body: Column( + children: [ + _buildSearchBar(controller, isDark), + _buildSortOptions(controller, isDark), + _buildCountInfo(controller, isDark), + Expanded( + child: Obx( + () => controller.isLoading.value + ? _buildLoadingWidget(isDark) + : controller.filteredHistoryList.isEmpty + ? _buildEmptyWidget(isDark) + : _buildHistoryList(controller, isDark), + ), ), + _buildPagination(controller, isDark), ], ), ); - } catch (e) { - _showSnackBar('获取统计失败'); - } + }); } - void _showSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: AppConstants.primaryColor, - duration: const Duration(seconds: 2), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: _buildAppBar(), - body: Column( - children: [ - _buildSearchBar(), - _buildSortOptions(), - Expanded( - child: _isLoading - ? _buildLoadingWidget() - : _filteredHistoryList.isEmpty - ? _buildEmptyWidget() - : _buildHistoryList(), - ), - ], - ), - ); - } - - // === 顶部导航栏 === - PreferredSizeWidget _buildAppBar() { + PreferredSizeWidget _buildAppBar(HistoryController controller, bool isDark) { return AppBar( title: Text( '历史记录', style: TextStyle( color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, + fontSize: 17, ), ), - backgroundColor: Colors.white, + backgroundColor: isDark + ? const Color(0xFF2A2A2A) + : const Color(0xFFF2F2F7), elevation: 0, centerTitle: true, actions: [ - IconButton( - icon: Icon(Icons.delete_sweep, color: AppConstants.primaryColor), - onPressed: _historyList.isEmpty ? null : _clearHistory, + Obx( + () => CupertinoButton( + padding: const EdgeInsets.symmetric(horizontal: 8), + onPressed: controller.historyList.isEmpty + ? null + : () async { + final confirmed = await Get.dialog( + CupertinoAlertDialog( + title: const Text('确认清空'), + content: const Text('确定要清空所有历史记录吗?'), + actions: [ + CupertinoDialogAction( + child: const Text('取消'), + onPressed: () => Get.back(result: false), + ), + CupertinoDialogAction( + isDestructiveAction: true, + child: const Text('清空'), + onPressed: () => Get.back(result: true), + ), + ], + ), + ); + if (confirmed == true) { + controller.clearHistory(); + } + }, + child: Icon( + CupertinoIcons.delete, + color: controller.historyList.isEmpty + ? CupertinoColors.systemGrey + : CupertinoColors.systemRed, + ), + ), ), - IconButton( - icon: Icon(Icons.bar_chart, color: AppConstants.primaryColor), - onPressed: _showStatsDialog, + CupertinoButton( + padding: const EdgeInsets.symmetric(horizontal: 8), + onPressed: controller.showStatsDialog, + child: const Icon( + CupertinoIcons.chart_bar, + color: AppConstants.primaryColor, + ), ), - IconButton( - icon: Icon(Icons.file_download, color: AppConstants.primaryColor), - onPressed: _exportHistory, + CupertinoButton( + padding: const EdgeInsets.symmetric(horizontal: 8), + onPressed: controller.exportHistory, + child: const Icon( + CupertinoIcons.share, + color: AppConstants.primaryColor, + ), ), ], ); } - // === 搜索栏 === - Widget _buildSearchBar() { + Widget _buildSearchBar(HistoryController controller, bool isDark) { return Container( - padding: const EdgeInsets.all(16), - child: TextField( - onChanged: _searchHistory, - decoration: InputDecoration( - hintText: '搜索历史记录...', - prefixIcon: const Icon(Icons.search), - suffixIcon: _searchKeyword.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: () => _searchHistory(''), - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: BorderSide(color: Colors.grey[300]!), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF3A3A3A) : CupertinoColors.white, + borderRadius: BorderRadius.circular(10), + ), + child: TextField( + onChanged: controller.searchHistory, + decoration: InputDecoration( + hintText: '搜索历史记录...', + hintStyle: TextStyle( + color: isDark + ? Colors.grey[500] + : CupertinoColors.placeholderText, + ), + prefixIcon: Icon( + CupertinoIcons.search, + color: isDark ? Colors.grey[400] : CupertinoColors.systemGrey, + ), + suffixIcon: Obx( + () => controller.searchKeyword.value.isNotEmpty + ? CupertinoButton( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + onPressed: () => controller.searchHistory(''), + child: Icon( + CupertinoIcons.clear_circled_solid, + color: isDark + ? Colors.grey[400] + : CupertinoColors.systemGrey, + size: 20, + ), + ) + : const SizedBox.shrink(), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: BorderSide(color: AppConstants.primaryColor), - ), - filled: true, - fillColor: Colors.white, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + style: TextStyle( + color: isDark ? Colors.white : CupertinoColors.label, + fontSize: 16, ), ), ), ); } - // === 排序选项 === - Widget _buildSortOptions() { + Widget _buildSortOptions(HistoryController controller, bool isDark) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ - const Text('排序方式:'), - const SizedBox(width: 16), + Text( + '排序', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : CupertinoColors.secondaryLabel, + ), + ), + const SizedBox(width: 12), Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey[300]!), - borderRadius: BorderRadius.circular(20), - ), - child: DropdownButton( - value: _sortTypes[_selectedSortType], - isExpanded: true, - underline: Container(), - items: _sortTypes.map((type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - _sortHistory(_sortTypes.indexOf(value)); - } - }, + child: GestureDetector( + onTap: () => _showSortActionSheet(controller), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: isDark + ? const Color(0xFF3A3A3A) + : CupertinoColors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Obx( + () => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + controller.sortTypes[controller.selectedSortType.value], + style: TextStyle( + fontSize: 15, + color: isDark ? Colors.white : CupertinoColors.label, + ), + ), + Icon( + CupertinoIcons.chevron_down, + size: 16, + color: isDark + ? Colors.grey[400] + : CupertinoColors.systemGrey, + ), + ], + ), + ), ), ), ), @@ -373,146 +221,419 @@ class _HistoryPageState extends State { ); } - // === 加载状态 === - Widget _buildLoadingWidget() { + // === 显示排序选项 === + void _showSortActionSheet(HistoryController controller) { + showCupertinoModalPopup( + context: Get.context!, + builder: (BuildContext context) => CupertinoActionSheet( + title: const Text('选择排序方式'), + actions: [ + CupertinoActionSheetAction( + onPressed: () { + Get.back(); + controller.sortHistory(0); + }, + child: const Text('时间倒序'), + ), + CupertinoActionSheetAction( + onPressed: () { + Get.back(); + controller.sortHistory(1); + }, + child: const Text('时间正序'), + ), + CupertinoActionSheetAction( + onPressed: () { + Get.back(); + controller.sortHistory(2); + }, + child: const Text('按名称排序'), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + ), + ); + } + + Widget _buildCountInfo(HistoryController controller, bool isDark) { + return Obx( + () => Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${controller.totalCount.value} 条记录', + style: TextStyle( + fontSize: 13, + color: isDark + ? Colors.grey[400] + : CupertinoColors.secondaryLabel, + ), + ), + Text( + '第 ${controller.currentPage.value} / ${controller.totalPages.value} 页', + style: TextStyle( + fontSize: 13, + color: isDark + ? Colors.grey[400] + : CupertinoColors.secondaryLabel, + ), + ), + ], + ), + ), + ); + } + + Widget _buildLoadingWidget(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), - SizedBox(height: 16), + CupertinoActivityIndicator( + radius: 20, + color: isDark ? Colors.white : null, + ), + const SizedBox(height: 16), Text( '加载历史记录...', - style: TextStyle(fontSize: 16, color: Colors.grey[600]!), + style: TextStyle( + fontSize: 15, + color: isDark + ? Colors.grey[400] + : CupertinoColors.secondaryLabel.resolveFrom(Get.context!), + ), ), ], ), ); } - // === 空状态 === - Widget _buildEmptyWidget() { + Widget _buildEmptyWidget(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.history, size: 64, color: Colors.grey[400]!), - SizedBox(height: 16), + Icon( + CupertinoIcons.time, + size: 64, + color: isDark + ? Colors.grey[600] + : CupertinoColors.systemGrey3.resolveFrom(Get.context!), + ), + const SizedBox(height: 16), Text( '暂无历史记录', - style: TextStyle(fontSize: 16, color: Colors.grey[600]!), - ), - SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: AppConstants.primaryColor, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: isDark + ? Colors.white + : CupertinoColors.label.resolveFrom(Get.context!), + ), + ), + const SizedBox(height: 8), + Text( + '浏览诗词后会自动记录在这里', + style: TextStyle( + fontSize: 14, + color: isDark + ? Colors.grey[400] + : CupertinoColors.secondaryLabel.resolveFrom(Get.context!), + ), + ), + const SizedBox(height: 24), + CupertinoButton.filled( + onPressed: () => Get.back(), + borderRadius: BorderRadius.circular(8), + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + child: const Text( + '返回', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), - child: Text('返回'), ), ], ), ); } - // === 历史记录列表 === - Widget _buildHistoryList() { - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _filteredHistoryList.length, + Widget _buildHistoryList(HistoryController controller, bool isDark) { + final currentPageData = controller.getCurrentPageData(); + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: currentPageData.length, + separatorBuilder: (context, index) => const SizedBox(height: 8), itemBuilder: (context, index) { - final item = _filteredHistoryList[index]; - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: CircleAvatar( - radius: 20, - backgroundColor: AppConstants.primaryColor.withOpacity(0.1), - child: Text( - '${index + 1}', - style: TextStyle( - fontSize: 12, - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ), - title: Text( - item['name'] ?? '未知诗词', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${item['alias'] ?? '未知朝代'} • ${item['date'] ?? ''}', - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - if (item['introduce']?.toString().isNotEmpty == true) - Text( - item['introduce']?.toString() ?? '', - style: TextStyle(fontSize: 11, color: Colors.grey[600]!), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - trailing: PopupMenuButton( - onSelected: (value) { - switch (value) { - case 'delete': - _deleteHistoryItem(index, item); - break; - case 'share': - _showSnackBar('分享功能开发中...'); - break; - case 'view': - _showSnackBar('查看详情功能开发中...'); - break; - } - }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete, color: Colors.red), - SizedBox(width: 8), - Text('删除'), - ], - ), - ), - const PopupMenuItem( - value: 'share', - child: Row( - children: [ - Icon(Icons.share, color: AppConstants.primaryColor), - SizedBox(width: 8), - Text('分享'), - ], - ), - ), - const PopupMenuItem( - value: 'view', - child: Row( - children: [ - Icon(Icons.visibility, color: AppConstants.primaryColor), - SizedBox(width: 8), - Text('查看'), - ], - ), - ), - ], - ), - onTap: () { - // 可以添加点击查看详情的功能 - HapticFeedback.lightImpact(); - }, - ), + final item = currentPageData[index]; + final startIndex = + (controller.currentPage.value - 1) * controller.pageSize; + final actualIndex = startIndex + index; + + return _buildHistoryItem( + context, + controller, + item, + actualIndex, + isDark, ); }, ); } + + Widget _buildHistoryItem( + BuildContext context, + HistoryController controller, + Map item, + int actualIndex, + bool isDark, + ) { + return Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : CupertinoColors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : CupertinoColors.systemGrey4.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: CupertinoListTile( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(22), + ), + child: Center( + child: Text( + '${actualIndex + 1}', + style: TextStyle( + fontSize: 16, + color: AppConstants.primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + title: Text( + item['name'] ?? '未知诗词', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : CupertinoColors.label, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + '${item['alias'] ?? '未知朝代'} • ${item['date'] ?? ''}', + style: TextStyle( + fontSize: 13, + color: isDark + ? Colors.grey[400] + : CupertinoColors.secondaryLabel, + ), + ), + if (item['introduce']?.toString().isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + item['introduce']?.toString() ?? '', + style: TextStyle( + fontSize: 12, + color: isDark + ? Colors.grey[500] + : CupertinoColors.tertiaryLabel, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + onPressed: () => + _showActionSheet(context, controller, actualIndex, item), + child: Icon( + CupertinoIcons.ellipsis, + color: isDark ? Colors.grey[400] : CupertinoColors.systemGrey, + ), + ), + onTap: () { + HapticFeedback.lightImpact(); + }, + ), + ); + } + + // === 操作菜单 === + void _showActionSheet( + BuildContext context, + HistoryController controller, + int index, + Map item, + ) { + showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => CupertinoActionSheet( + title: const Text('操作'), + actions: [ + CupertinoActionSheetAction( + onPressed: () { + Get.back(); + controller.deleteHistoryItem(index, item); + }, + isDestructiveAction: true, + child: const Text('删除'), + ), + CupertinoActionSheetAction( + onPressed: () { + Get.back(); + Get.snackbar('提示', '分享功能开发中...'); + }, + child: const Text('分享'), + ), + CupertinoActionSheetAction( + onPressed: () { + Get.back(); + Get.snackbar('提示', '查看详情功能开发中...'); + }, + child: const Text('查看详情'), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + ), + ); + } + + Widget _buildPagination(HistoryController controller, bool isDark) { + return Obx( + () => Visibility( + visible: controller.totalPages.value > 1, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : CupertinoColors.white, + border: Border( + top: BorderSide( + color: isDark + ? Colors.grey[800]! + : CupertinoColors.separator.resolveFrom(Get.context!), + width: 0.5, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CupertinoButton( + onPressed: controller.currentPage.value > 1 + ? controller.previousPage + : null, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + color: controller.currentPage.value > 1 + ? AppConstants.primaryColor + : (isDark ? Colors.grey[800] : CupertinoColors.systemGrey5), + borderRadius: BorderRadius.circular(8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + CupertinoIcons.chevron_left, + size: 16, + color: CupertinoColors.white, + ), + const SizedBox(width: 4), + Text( + '上一页', + style: TextStyle( + fontSize: 14, + color: controller.currentPage.value > 1 + ? CupertinoColors.white + : (isDark + ? Colors.grey[500] + : CupertinoColors.systemGrey), + ), + ), + ], + ), + ), + Obx( + () => Text( + '${controller.currentPage.value} / ${controller.totalPages.value}', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : CupertinoColors.label, + ), + ), + ), + CupertinoButton( + onPressed: + controller.currentPage.value < controller.totalPages.value + ? controller.nextPage + : null, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + color: + controller.currentPage.value < controller.totalPages.value + ? AppConstants.primaryColor + : (isDark ? Colors.grey[800] : CupertinoColors.systemGrey5), + borderRadius: BorderRadius.circular(8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '下一页', + style: TextStyle( + fontSize: 14, + color: + controller.currentPage.value < + controller.totalPages.value + ? CupertinoColors.white + : (isDark + ? Colors.grey[500] + : CupertinoColors.systemGrey), + ), + ), + const SizedBox(width: 4), + const Icon( + CupertinoIcons.chevron_right, + size: 16, + color: CupertinoColors.white, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/views/profile/level/distinguish.dart b/lib/views/profile/level/distinguish.dart index e20b229..c9676f4 100644 --- a/lib/views/profile/level/distinguish.dart +++ b/lib/views/profile/level/distinguish.dart @@ -2,12 +2,14 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:intl/intl.dart'; import '../../../constants/app_constants.dart'; import '../../../controllers/shared_preferences_storage_controller.dart'; import '../../../controllers/history_controller.dart'; import '../../../services/network_listener_service.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-28 /// 功能: 答题记录页面 @@ -22,11 +24,10 @@ class DistinguishPage extends StatefulWidget { } class _DistinguishPageState extends State { - // 答题记录列表 + final ThemeController _themeController = Get.find(); List> _answerRecords = []; bool _isLoading = true; - // 统计数据 int _totalQuestions = 0; int _correctAnswers = 0; int _wrongAnswers = 0; @@ -140,22 +141,21 @@ class _DistinguishPageState extends State { } } - /// 显示统计弹窗 void _showStatisticsDialog() { + final isDark = _themeController.isDarkMode; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (context) => _buildStatisticsSheet(), + builder: (context) => _buildStatisticsSheet(isDark), ); } - /// 构建统计弹窗 - Widget _buildStatisticsSheet() { + Widget _buildStatisticsSheet(bool isDark) { return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: SingleChildScrollView( child: Padding( @@ -164,20 +164,17 @@ class _DistinguishPageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 顶部拖动条 Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( - color: Colors.grey[300], + color: isDark ? Colors.grey[600] : Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 20), - - // 标题 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -196,17 +193,16 @@ class _DistinguishPageState extends State { ), ), const SizedBox(width: 12), - const Text( + Text( '本次答题记录', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), ], ), - // 复制内容按钮 if (_answerRecords.isNotEmpty) Container( decoration: BoxDecoration( @@ -244,8 +240,6 @@ class _DistinguishPageState extends State { ], ), const SizedBox(height: 24), - - // 统计卡片 Container( width: double.infinity, padding: const EdgeInsets.all(20), @@ -253,7 +247,7 @@ class _DistinguishPageState extends State { gradient: LinearGradient( colors: [ AppConstants.primaryColor.withAlpha(10), - Colors.white, + isDark ? const Color(0xFF2A2A2A) : Colors.white, ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -266,33 +260,52 @@ class _DistinguishPageState extends State { ), child: Column( children: [ - _buildStatRow('已答题', '$_totalQuestions 题'), - _buildStatRow('正确', '$_correctAnswers 题', isGreen: true), - _buildStatRow('错误', '$_wrongAnswers 题', isRed: true), + _buildStatRow('已答题', '$_totalQuestions 题', isDark), + _buildStatRow( + '正确', + '$_correctAnswers 题', + isDark, + isGreen: true, + ), + _buildStatRow( + '错误', + '$_wrongAnswers 题', + isDark, + isRed: true, + ), _buildStatRow( '正确率', '${_correctRate.toStringAsFixed(1)}%', + isDark, isGreen: _correctRate >= 60, ), _buildStatRow( '错误率', '${_wrongRate.toStringAsFixed(1)}%', + isDark, isRed: _wrongRate > 40, ), _buildStatRow( '平均用时', '${_averageTime.toStringAsFixed(1)} 秒', + isDark, + ), + _buildStatRow('提示次数', '$_hintCount 次', isDark), + _buildStatRow('跳过次数', '$_skipCount 次', isDark), + Divider( + height: 24, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), + _buildStatRow( + '诗词水平', + _poetryLevel, + isDark, + isHighlight: true, ), - _buildStatRow('提示次数', '$_hintCount 次'), - _buildStatRow('跳过次数', '$_skipCount 次'), - const Divider(height: 24), - _buildStatRow('诗词水平', _poetryLevel, isHighlight: true), ], ), ), const SizedBox(height: 24), - - // 复制按钮 SizedBox( width: double.infinity, child: Container( @@ -350,15 +363,15 @@ class _DistinguishPageState extends State { ); } - /// 构建统计行 Widget _buildStatRow( String label, - String value, { + String value, + bool isDark, { bool isGreen = false, bool isRed = false, bool isHighlight = false, }) { - Color valueColor = Colors.black87; + Color valueColor = isDark ? Colors.white : Colors.black87; if (isGreen) valueColor = Colors.green; if (isRed) valueColor = Colors.red; if (isHighlight) valueColor = AppConstants.primaryColor; @@ -368,7 +381,13 @@ class _DistinguishPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(label, style: TextStyle(fontSize: 15, color: Colors.grey[700])), + Text( + label, + style: TextStyle( + fontSize: 15, + color: isDark ? Colors.grey[400] : Colors.grey[700], + ), + ), Text( value, style: TextStyle( @@ -483,96 +502,105 @@ $_poetryLevel @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text( - '答题记录', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), - ), - backgroundColor: AppConstants.primaryColor, - foregroundColor: Colors.white, - elevation: 0, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppConstants.primaryColor, - AppConstants.primaryColor.withAlpha(180), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + appBar: AppBar( + title: Text( + '答题记录', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: isDark ? Colors.white : Colors.white, ), ), - ), - actions: [ - // 统计按钮 - IconButton( - onPressed: _showStatisticsDialog, - icon: const Icon(Icons.analytics_outlined), - tooltip: '查看统计', + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + elevation: 0, + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppConstants.primaryColor, + AppConstants.primaryColor.withAlpha(180), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), ), - // 清空按钮 - if (_answerRecords.isNotEmpty) + actions: [ IconButton( - onPressed: _clearRecords, - icon: const Icon(Icons.delete_outline), - tooltip: '清空记录', + onPressed: _showStatisticsDialog, + icon: const Icon(Icons.analytics_outlined), + tooltip: '查看统计', ), - ], - ), - body: Container( - color: Colors.grey[50], - child: SafeArea( - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _answerRecords.isEmpty - ? _buildEmptyView() - : _buildRecordList(), + if (_answerRecords.isNotEmpty) + IconButton( + onPressed: _clearRecords, + icon: const Icon(Icons.delete_outline), + tooltip: '清空记录', + ), + ], ), - ), - ); + body: Container( + color: isDark ? const Color(0xFF1A1A1A) : Colors.grey[50], + child: SafeArea( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _answerRecords.isEmpty + ? _buildEmptyView(isDark) + : _buildRecordList(isDark), + ), + ), + ); + }); } - /// 构建空记录视图 - Widget _buildEmptyView() { + Widget _buildEmptyView(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.menu_book_outlined, size: 80, color: Colors.grey[300]), + Icon( + Icons.menu_book_outlined, + size: 80, + color: isDark ? Colors.grey[600] : Colors.grey[300], + ), const SizedBox(height: 16), Text( '暂无答题记录', style: TextStyle( fontSize: 18, - color: Colors.grey[500], + color: isDark ? Colors.grey[400] : Colors.grey[500], fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( '快去答题吧!', - style: TextStyle(fontSize: 14, color: Colors.grey[400]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ], ), ); } - /// 构建记录列表 - Widget _buildRecordList() { + Widget _buildRecordList(bool isDark) { return ListView.builder( padding: const EdgeInsets.all(16), itemCount: _answerRecords.length, itemBuilder: (context, index) { final record = _answerRecords[index]; - return _buildRecordCard(record, index); + return _buildRecordCard(record, index, isDark); }, ); } - /// 构建记录卡片 - Widget _buildRecordCard(Map record, int index) { + Widget _buildRecordCard(Map record, int index, bool isDark) { final question = record['question'] ?? '未知题目'; final author = record['author'] ?? '未知作者'; final tags = (record['tags'] as List?)?.cast() ?? []; @@ -582,11 +610,13 @@ $_poetryLevel return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withAlpha(10), + color: isDark + ? Colors.black.withAlpha(30) + : Colors.black.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2), ), @@ -597,11 +627,9 @@ $_poetryLevel child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 标题和答对/答错标识 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 序号 Container( width: 28, height: 28, @@ -621,17 +649,16 @@ $_poetryLevel ), ), const SizedBox(width: 12), - // 题目 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( question, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -641,7 +668,7 @@ $_poetryLevel '—— $author', style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], fontStyle: FontStyle.italic, ), ), @@ -649,7 +676,6 @@ $_poetryLevel ), ), const SizedBox(width: 8), - // 答对/答错标识 Container( padding: const EdgeInsets.symmetric( horizontal: 10, @@ -657,8 +683,8 @@ $_poetryLevel ), decoration: BoxDecoration( color: isCorrect - ? Colors.green.withAlpha(20) - : Colors.red.withAlpha(20), + ? Colors.green.withAlpha(isDark ? 40 : 20) + : Colors.red.withAlpha(isDark ? 40 : 20), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -684,10 +710,8 @@ $_poetryLevel ], ), const SizedBox(height: 12), - // 标签和时间 Row( children: [ - // 标签 Expanded( child: tags.isNotEmpty ? Wrap( @@ -724,20 +748,26 @@ $_poetryLevel '暂无标签', style: TextStyle( fontSize: 12, - color: Colors.grey[400], + color: isDark ? Colors.grey[500] : Colors.grey[400], fontStyle: FontStyle.italic, ), ), ), - // 时间 Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.access_time, size: 12, color: Colors.grey[400]), + Icon( + Icons.access_time, + size: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), const SizedBox(width: 4), Text( answerTime, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), diff --git a/lib/views/profile/level/poetry.dart b/lib/views/profile/level/poetry.dart index c247b4f..4f374e0 100644 --- a/lib/views/profile/level/poetry.dart +++ b/lib/views/profile/level/poetry.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; import '../../../controllers/shared_preferences_storage_controller.dart'; +import '../../../services/get/theme_controller.dart'; import '../guide/tongji.dart'; import 'level-jilu.dart'; import 'flow-anim.dart'; @@ -26,8 +28,8 @@ class PoetryLevelPage extends StatefulWidget { class _PoetryLevelPageState extends State with TickerProviderStateMixin { final PoetryLevelManager _manager = PoetryLevelManager(); + final ThemeController _themeController = Get.find(); - // 状态管理 bool _isLoading = true; bool _isSubmitting = false; Map? _currentQuestion; @@ -35,7 +37,6 @@ class _PoetryLevelPageState extends State int _score = 0; bool _autoLoadNext = true; - // 答题状态 int? _selectedAnswer; String? _feedbackMessage; bool _showFeedback = false; @@ -647,742 +648,782 @@ class _PoetryLevelPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text( - '诗词答题', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), - ), - backgroundColor: AppConstants.primaryColor, - foregroundColor: Colors.white, - elevation: 0, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppConstants.primaryColor, - AppConstants.primaryColor.withAlpha(180), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + appBar: AppBar( + title: Text( + '诗词答题', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.white, ), ), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: Container( - decoration: BoxDecoration( - color: Colors.white.withAlpha(30), - borderRadius: BorderRadius.circular(8), + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + elevation: 0, + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppConstants.primaryColor, + AppConstants.primaryColor.withAlpha(180), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), - child: ElevatedButton( - onPressed: _openAnswerRecordPage, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: Container( + decoration: BoxDecoration( + color: Colors.white.withAlpha(30), + borderRadius: BorderRadius.circular(8), ), - child: const Text( - '记录', - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, + child: ElevatedButton( + onPressed: _openAnswerRecordPage, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + child: const Text( + '记录', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), ), ), ), ), - ), - ], - ), - body: Container( - color: Colors.white, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.only( - left: 16.0, - right: 16.0, - top: 8.0, - bottom: 16.0, - ), - child: Stack( - children: [ - Column( - children: [ - const SizedBox(height: 8), // 减少顶部空白 - // 分数显示 - AnimatedBuilder( - animation: _successAnimationController, - builder: (context, child) { - return Transform.scale( - scale: _isAnswerCorrect ? _scaleAnimation.value : 1.0, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppConstants.primaryColor, - AppConstants.primaryColor.withAlpha(200), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + ], + ), + body: Container( + color: isDark ? const Color(0xFF1A1A1A) : Colors.white, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 8.0, + bottom: 16.0, + ), + child: Stack( + children: [ + Column( + children: [ + const SizedBox(height: 8), // 减少顶部空白 + // 分数显示 + AnimatedBuilder( + animation: _successAnimationController, + builder: (context, child) { + return Transform.scale( + scale: _isAnswerCorrect + ? _scaleAnimation.value + : 1.0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: AppConstants.primaryColor.withAlpha( - 80, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppConstants.primaryColor, + AppConstants.primaryColor.withAlpha(200), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppConstants.primaryColor.withAlpha( + 80, + ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withAlpha(30), + borderRadius: BorderRadius.circular( + 8, + ), + ), + child: Icon( + Icons.quiz_outlined, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + '题目: ${_manager.currentIndex + 1}/${_manager.total}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + const SizedBox(width: 12), + // 奖杯图标和分数 + Row( + children: [ + Icon( + Icons.emoji_events_outlined, + color: Colors.amber[300], + size: 24, + ), + const SizedBox(width: 8), + Text( + '$_score', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ], + ), + Row( + children: [ + // 开关按钮 + Tooltip( + message: _autoLoadNext + ? '已开启自动下一题' + : '已关闭自动下一题', + child: Switch( + value: _autoLoadNext, + onChanged: (value) { + setState(() { + _autoLoadNext = value; + }); + }, + activeColor: Colors.white, + activeTrackColor: Colors.white + .withAlpha(128), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + const SizedBox(height: 10), + if (_isLoading) + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + AppConstants.primaryColor, + ), + strokeWidth: 3, + ), + const SizedBox(height: 20), + Text( + '加载题目中...', + style: TextStyle( + fontSize: 16, + color: isDark + ? Colors.grey[400] + : Colors.grey, ), - blurRadius: 12, - offset: const Offset(0, 4), ), ], ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + ), + + if (!_isLoading && _errorMessage != null) + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withAlpha(30), - borderRadius: BorderRadius.circular(8), + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: isDark + ? Colors.red[900]!.withAlpha(30) + : Colors.red[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.error_outline, + size: 64, + color: Colors.red[400], + ), + ), + const SizedBox(height: 24), + Text( + _errorMessage!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: isDark + ? Colors.red[300] + : Colors.red, + ), + ), + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppConstants.primaryColor, + AppConstants.primaryColor.withAlpha( + 200, + ), + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppConstants.primaryColor + .withAlpha(80), + blurRadius: 8, + offset: const Offset(0, 4), ), - child: Icon( - Icons.quiz_outlined, - color: Colors.white, - size: 20, + ], + ), + child: ElevatedButton( + onPressed: () => _loadQuestion(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, ), ), - const SizedBox(width: 12), - Text( - '题目: ${_manager.currentIndex + 1}/${_manager.total}', - style: const TextStyle( + child: const Text( + '重新加载', + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), - const SizedBox(width: 12), - // 奖杯图标和分数 + ), + ), + ], + ), + ), + ), + + if (!_isLoading && _currentQuestion != null) + Expanded( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + AnimatedBuilder( + animation: _shakeAnimationController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + _isAnswerCorrect + ? 0 + : _shakeAnimation.value, + 0, + ), + child: Stack( + children: [ + Positioned.fill( + child: FlowingBorderContainer( + child: Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + 16, + ), + color: isDark + ? const Color( + 0xFF2A2A2A, + ) + : Colors.white, + ), + ), + color: AppConstants + .primaryColor, + width: 4, + ), + ), + Container( + padding: const EdgeInsets.all( + 16, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + isDark + ? const Color( + 0xFF2A2A2A, + ) + : Colors.white, + AppConstants + .primaryColor + .withAlpha(5), + isDark + ? const Color( + 0xFF2A2A2A, + ) + : Colors.white, + ], + begin: Alignment.topLeft, + end: + Alignment.bottomRight, + ), + borderRadius: + BorderRadius.circular( + 16, + ), + backgroundBlendMode: + BlendMode.softLight, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + // 装饰元素 + Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: AppConstants + .primaryColor, + borderRadius: + BorderRadius.circular( + 2, + ), + ), + ), + const SizedBox( + width: 12, + ), + Text( + '诗词挑战', + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight + .w600, + color: AppConstants + .primaryColor, + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + Text( + _currentQuestion!['question'] ?? + '题目加载失败', + style: TextStyle( + fontSize: 20, + fontWeight: + FontWeight.bold, + height: 1.5, + color: isDark + ? Colors.white + : Colors.black87, + ), + ), + // 标签信息 + if (_showTags) + AnimatedOpacity( + duration: + const Duration( + milliseconds: + 500, + ), + opacity: 1, + child: Container( + margin: + const EdgeInsets.only( + top: 12, + ), + padding: + const EdgeInsets.all( + 12, + ), + decoration: BoxDecoration( + color: AppConstants + .primaryColor + .withAlpha( + 10, + ), + borderRadius: + BorderRadius.circular( + 8, + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + _buildTag( + '作者', + _currentQuestion!['author'] ?? + '', + ), + _buildTag( + '年代', + _currentQuestion!['dynasty'] ?? + '', + ), + _buildTag( + '类型', + _currentQuestion!['type'] ?? + '', + ), + _buildTag( + '阶段', + _currentQuestion!['grade'] ?? + '', + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 10), + // 选项 + _buildOptionsLayout(), + ], + ), + ), + ), + // 固定位置的操作按钮卡片(在底部固定,不随内容滚动) + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(10), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + // 操作按钮 - 改为一行显示 Row( children: [ - Icon( - Icons.emoji_events_outlined, - color: Colors.amber[300], - size: 24, + // 上一题按钮 + Expanded( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white, + Colors.grey[50]!, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: + BorderRadius.circular(12), + border: Border.all( + color: AppConstants.primaryColor + .withAlpha(50), + width: 1, + ), + ), + child: OutlinedButton( + onPressed: _previousQuestion, + style: OutlinedButton.styleFrom( + side: BorderSide.none, + padding: + const EdgeInsets.symmetric( + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.arrow_back, + color: AppConstants + .primaryColor, + size: 20, + ), + const SizedBox(width: 8), + const Text( + '上一题', + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight.w500, + color: Colors.black87, + ), + ), + ], + ), + ), + ), ), - const SizedBox(width: 8), - Text( - '$_score', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, + const SizedBox(width: 12), + // 提示按钮 + Expanded( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white, + Colors.grey[50]!, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: + BorderRadius.circular(12), + border: Border.all( + color: + AppConstants.primaryColor, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: AppConstants + .primaryColor + .withAlpha(30), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ElevatedButton( + onPressed: _getHint, + style: ElevatedButton.styleFrom( + backgroundColor: + Colors.transparent, + shadowColor: Colors.transparent, + padding: + const EdgeInsets.symmetric( + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.lightbulb_outline, + color: AppConstants + .primaryColor, + size: 20, + ), + const SizedBox(width: 8), + const Text( + '提示', + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight.w600, + color: Colors.black87, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 12), + // 下一题按钮 + Expanded( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppConstants.primaryColor, + AppConstants.primaryColor + .withAlpha(200), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: + BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppConstants + .primaryColor + .withAlpha(80), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: ElevatedButton( + onPressed: _nextQuestion, + style: ElevatedButton.styleFrom( + backgroundColor: + Colors.transparent, + shadowColor: Colors.transparent, + padding: + const EdgeInsets.symmetric( + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Text( + '下一题', + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight.w600, + color: Colors.white, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.arrow_forward, + color: Colors.white, + size: 20, + ), + ], + ), + ), ), ), ], ), ], ), - Row( - children: [ - // 开关按钮 - Tooltip( - message: _autoLoadNext - ? '已开启自动下一题' - : '已关闭自动下一题', - child: Switch( - value: _autoLoadNext, - onChanged: (value) { - setState(() { - _autoLoadNext = value; - }); - }, - activeColor: Colors.white, - activeTrackColor: Colors.white - .withAlpha(128), - ), - ), - ], - ), - ], + ), + ], + ), + ), + ], + ), + // 反馈信息气泡(不占用布局) + if (_showFeedback && _feedbackMessage != null) + Positioned( + top: 0, + left: 16, + right: 16, + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut, + transform: Matrix4.translationValues(0, 0, 0), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: _isAnswerCorrect + ? [Colors.green[400]!, Colors.green[300]!] + : [Colors.orange[400]!, Colors.orange[300]!], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: + (_isAnswerCorrect + ? Colors.green + : Colors.orange) + .withAlpha(80), + blurRadius: 12, + offset: const Offset(0, 4), ), - ), - ); - }, - ), - // 移除空白间距 - const SizedBox(height: 10), - // 加载状态 - if (_isLoading) - const Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - AppConstants.primaryColor, - ), - strokeWidth: 3, - ), - SizedBox(height: 20), - Text( - '加载题目中...', - style: TextStyle( - fontSize: 16, - color: Colors.grey, - ), - ), - ], - ), + ], ), - ), - - // 错误状态 - if (!_isLoading && _errorMessage != null) - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.red[50], - shape: BoxShape.circle, - ), - child: Icon( - Icons.error_outline, - size: 64, - color: Colors.red[400], - ), - ), - const SizedBox(height: 24), - Text( - _errorMessage!, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - color: Colors.red, - ), - ), - const SizedBox(height: 32), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppConstants.primaryColor, - AppConstants.primaryColor.withAlpha(200), - ], - ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: AppConstants.primaryColor - .withAlpha(80), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: ElevatedButton( - onPressed: () => _loadQuestion(), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - ), - child: const Text( - '重新加载', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ), - ), - - // 题目内容 - if (!_isLoading && _currentQuestion != null) - Expanded( - child: Column( + child: Row( children: [ - // 可滚动区域 - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 题目信息 - AnimatedBuilder( - animation: _shakeAnimationController, - builder: (context, child) { - return Transform.translate( - offset: Offset( - _isAnswerCorrect - ? 0 - : _shakeAnimation.value, - 0, - ), - child: Stack( - children: [ - // 流动边框 - Positioned.fill( - child: FlowingBorderContainer( - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular( - 16, - ), - color: Colors.white, - ), - ), - color: - AppConstants.primaryColor, - width: 4, - ), - ), - // 题目内容 - Container( - padding: const EdgeInsets.all( - 16, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.white, - AppConstants.primaryColor - .withAlpha(5), - Colors.white, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: - BorderRadius.circular(16), - backgroundBlendMode: - BlendMode.softLight, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - // 装饰元素 - Row( - children: [ - Container( - width: 4, - height: 20, - decoration: BoxDecoration( - color: AppConstants - .primaryColor, - borderRadius: - BorderRadius.circular( - 2, - ), - ), - ), - const SizedBox( - width: 12, - ), - Text( - '诗词挑战', - style: TextStyle( - fontSize: 14, - fontWeight: - FontWeight.w600, - color: AppConstants - .primaryColor, - ), - ), - ], - ), - const SizedBox(height: 12), - // 题目 - Text( - _currentQuestion!['question'] ?? - '题目加载失败', - style: const TextStyle( - fontSize: 20, - fontWeight: - FontWeight.bold, - height: 1.5, - color: Colors.black87, - ), - ), - // 标签信息 - if (_showTags) - AnimatedOpacity( - duration: - const Duration( - milliseconds: 500, - ), - opacity: 1, - child: Container( - margin: - const EdgeInsets.only( - top: 12, - ), - padding: - const EdgeInsets.all( - 12, - ), - decoration: BoxDecoration( - color: AppConstants - .primaryColor - .withAlpha(10), - borderRadius: - BorderRadius.circular( - 8, - ), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - _buildTag( - '作者', - _currentQuestion!['author'] ?? - '', - ), - _buildTag( - '年代', - _currentQuestion!['dynasty'] ?? - '', - ), - _buildTag( - '类型', - _currentQuestion!['type'] ?? - '', - ), - _buildTag( - '阶段', - _currentQuestion!['grade'] ?? - '', - ), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ); - }, - ), - const SizedBox(height: 10), - // 选项 - _buildOptionsLayout(), - ], - ), - ), + Icon( + _isAnswerCorrect + ? Icons.celebration + : Icons.lightbulb_outline, + color: Colors.white, + size: 24, ), - // 固定位置的操作按钮卡片(在底部固定,不随内容滚动) - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(10), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - children: [ - // 操作按钮 - 改为一行显示 - Row( - children: [ - // 上一题按钮 - Expanded( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.white, - Colors.grey[50]!, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular( - 12, - ), - border: Border.all( - color: AppConstants.primaryColor - .withAlpha(50), - width: 1, - ), - ), - child: OutlinedButton( - onPressed: _previousQuestion, - style: OutlinedButton.styleFrom( - side: BorderSide.none, - padding: - const EdgeInsets.symmetric( - vertical: 14, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.arrow_back, - color: - AppConstants.primaryColor, - size: 20, - ), - const SizedBox(width: 8), - const Text( - '上一题', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.black87, - ), - ), - ], - ), - ), - ), - ), - const SizedBox(width: 12), - // 提示按钮 - Expanded( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.white, - Colors.grey[50]!, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular( - 12, - ), - border: Border.all( - color: AppConstants.primaryColor, - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppConstants.primaryColor - .withAlpha(30), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: ElevatedButton( - onPressed: _getHint, - style: ElevatedButton.styleFrom( - backgroundColor: - Colors.transparent, - shadowColor: Colors.transparent, - padding: - const EdgeInsets.symmetric( - vertical: 14, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.lightbulb_outline, - color: - AppConstants.primaryColor, - size: 20, - ), - const SizedBox(width: 8), - const Text( - '提示', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - ], - ), - ), - ), - ), - const SizedBox(width: 12), - // 下一题按钮 - Expanded( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppConstants.primaryColor, - AppConstants.primaryColor - .withAlpha(200), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular( - 12, - ), - boxShadow: [ - BoxShadow( - color: AppConstants.primaryColor - .withAlpha(80), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: ElevatedButton( - onPressed: _nextQuestion, - style: ElevatedButton.styleFrom( - backgroundColor: - Colors.transparent, - shadowColor: Colors.transparent, - padding: - const EdgeInsets.symmetric( - vertical: 14, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Text( - '下一题', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - const SizedBox(width: 8), - Icon( - Icons.arrow_forward, - color: Colors.white, - size: 20, - ), - ], - ), - ), - ), - ), - ], - ), - ], + const SizedBox(width: 12), + Expanded( + child: Text( + _feedbackMessage!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), ), ), ], ), ), - ], - ), - // 反馈信息气泡(不占用布局) - if (_showFeedback && _feedbackMessage != null) - Positioned( - top: 0, - left: 16, - right: 16, - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - curve: Curves.easeOut, - transform: Matrix4.translationValues(0, 0, 0), - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: _isAnswerCorrect - ? [Colors.green[400]!, Colors.green[300]!] - : [Colors.orange[400]!, Colors.orange[300]!], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: - (_isAnswerCorrect - ? Colors.green - : Colors.orange) - .withAlpha(80), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - Icon( - _isAnswerCorrect - ? Icons.celebration - : Icons.lightbulb_outline, - color: Colors.white, - size: 24, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - _feedbackMessage!, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ], - ), ), - ), - ], + ], + ), ), ), ), - ), - ); + ); + }); } } diff --git a/lib/views/profile/per_card.dart b/lib/views/profile/per_card.dart index 1c1824c..44ab7b7 100644 --- a/lib/views/profile/per_card.dart +++ b/lib/views/profile/per_card.dart @@ -186,6 +186,12 @@ class PersonalCardState extends State { _isExpanded = newIsExpanded; }); } + // 监听currentPage变化,触发UI更新 + if (oldWidget.currentPage != widget.currentPage) { + setState(() { + // currentPage变化,触发UI重新构建 + }); + } } void _toggleExpand() { diff --git a/lib/views/profile/profile_page.dart b/lib/views/profile/profile_page.dart index a93064a..9b14d47 100644 --- a/lib/views/profile/profile_page.dart +++ b/lib/views/profile/profile_page.dart @@ -1,20 +1,17 @@ /// 时间: 2025.03.21 /// 功能: 个人页面(类似朋友圈布局) /// 介绍: 展示用户头像、个性签名、昵称、统计信息和设置列表,支持左右滑动切换页面 -/// 最新变化: 重新设计布局,实现朋友圈风格的个人页面 +/// 最新变化: 重新设计布局,实现朋友圈风格的个人页面,添加底部内边距适配液态玻璃导航栏 -import 'dart:convert'; -import 'dart:math' show Random; import 'dart:io' as io; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:get/get.dart'; import '../../constants/app_constants.dart'; -import '../../controllers/history_controller.dart'; -import '../../controllers/shared_preferences_storage_controller.dart'; -import '../../services/wakelock_service.dart'; +import '../../config/app_config.dart'; import 'history_page.dart'; import 'per_card.dart'; import 'settings/app_fun.dart'; @@ -26,282 +23,76 @@ import 'app-info.dart'; import 'level/poetry.dart'; import 'guide/permission.dart'; import 'guide/app-data.dart'; -import 'guide/tongji.dart'; import 'theme/app-diy.dart'; import 'expand/vote.dart'; import 'expand/manu-script.dart'; import 'components/bug_list_page.dart'; import 'components/pop-menu.dart'; import 'components/entire_page.dart'; +import '../../services/get/profile_controller.dart'; +import '../../services/get/theme_controller.dart'; -class ProfilePage extends StatefulWidget { +class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); - @override - State createState() => _ProfilePageState(); -} - -class _ProfilePageState extends State - with TickerProviderStateMixin, WidgetsBindingObserver { - late PageController _pageController; - late TabController _tabController; - int _currentPage = 1; // 默认显示第2页(设置) - bool _isCardExpanded = false; // 个人卡片展开状态 - bool _isStatsHidden = false; // 统计数据隐藏状态 - bool _isScreenWakeEnabled = false; // 屏幕常亮状态 - double _startY = 0.0; - // 历史记录相关 - List> _poetryHistory = []; - - // PersonalCard 的 Key,用于调用其刷新方法 - final GlobalKey _personalCardKey = - GlobalKey(); - - // 答题统计数据 - int _correctAnswers = 0; - int _weekQuestions = 0; - - // 统计数据 - int _todayViews = 0; - int _weekViews = 0; - String _firstUseTime = '未记录'; - int _useDays = 1; - String _dataSize = '0 B'; - int _noteCount = 0; - int _totalQuestions = 0; - int _todayQuestions = 0; - int _todayLikes = 0; - - // 可爱的emoji列表 - final List _avatars = [ - '👤', - '😊', - '🎉', - '🌟', - '🔥', - '💎', - '🌈', - '🦋', - '🌸', - '🐱', - '🐶', - '🐼', - '🐨', - '🐵', - '🦄', - '🐸', - '🐹', - '🐰', - '🦊', - '🐻', - ]; - - // 模拟用户数据 - final Map _userData = { - 'avatar': '👤', // 使用emoji代替网络图片 - 'nickname': '诗词爱好者', - 'signature': '人生如诗,岁月如歌', - 'level': 'Lv.12', - 'vip': true, - 'posts': 156, - 'followers': 1280, - 'following': 89, - 'likes': 2560, - 'favorites': 128, - 'views': 3560, - }; - - // 更换头像 - void _changeAvatar() { - final random = Random(); - setState(() { - _userData['avatar'] = _avatars[random.nextInt(_avatars.length)]; - }); - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - _pageController = PageController(initialPage: 1); - _tabController = TabController(length: 3, vsync: this); - _loadPoetryHistory(); - _loadPoetryStatistics(); - _loadStatistics(); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - _pageController.dispose(); - _tabController.dispose(); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - // 应用恢复时刷新数据 - refreshData(); - } - } - - // 刷新所有数据(公共方法,供外部调用) - Future refreshData() async { - await _loadPoetryStatistics(); - await _loadStatistics(); - // 同时刷新个人卡片的数据 - final personalCardState = _personalCardKey.currentState; - if (personalCardState != null && personalCardState.mounted) { - await personalCardState.refreshData(); - } - } - - void _onPageChanged(int page) { - setState(() { - _currentPage = page; - }); - HapticFeedback.lightImpact(); - } - - // === 历史记录相关方法 === - Future _loadPoetryHistory() async { - try { - final history = await HistoryController.getHistory(); - setState(() { - _poetryHistory = history; - }); - } catch (e) {} - } - - // === 答题统计相关方法 === - Future _loadPoetryStatistics() async { - try { - // 加载总体统计 - _correctAnswers = await SharedPreferencesStorageController.getInt( - 'correctAnswers', - defaultValue: 0, - ); - - // 加载答题记录列表来计算今日和本周答题数 - List records = - await SharedPreferencesStorageController.getStringList( - 'poetryAnswerRecords', - defaultValue: [], - ); - - final now = DateTime.now(); - final todayStart = DateTime(now.year, now.month, now.day); - final weekStart = todayStart.subtract( - Duration(days: todayStart.weekday - 1), - ); - - _todayQuestions = 0; - _weekQuestions = 0; - - for (String recordStr in records) { - try { - final record = jsonDecode(recordStr) as Map; - final answerTime = record['answerTime']; - if (answerTime != null) { - final time = DateTime.parse(answerTime); - if (time.isAfter(todayStart)) { - _todayQuestions++; - } - if (time.isAfter(weekStart)) { - _weekQuestions++; - } - } - } catch (e) { - continue; - } - } - - setState(() {}); - } catch (e) { - // 加载失败 - } - } - - // === 加载统计数据 === - Future _loadStatistics() async { - try { - // 加载浏览统计 - _todayViews = await StatisticsManager().getTodayViews(); - _weekViews = await StatisticsManager().getWeekViews(); - _firstUseTime = await StatisticsManager().getFirstUseTime(); - _useDays = await StatisticsManager().getUseDays(); - _dataSize = await StatisticsManager().getDataSize(); - _todayLikes = await StatisticsManager().getTodayLikes(); - _todayQuestions = await StatisticsManager().getTodayQuestions(); - - // 加载笔记总数 - _noteCount = await HistoryController.getNotesCount(); - - // 加载累计答题数 - _totalQuestions = await SharedPreferencesStorageController.getInt( - 'totalQuestions', - defaultValue: 0, - ); - - setState(() {}); - } catch (e) { - // 加载失败 - } - } - @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: _buildAppBar(), - body: GestureDetector( - behavior: HitTestBehavior.opaque, // 确保手势检测能够捕获整个区域的事件 - onVerticalDragStart: (details) { - _startY = details.globalPosition.dy; - }, - onVerticalDragUpdate: (details) { - double currentY = details.globalPosition.dy; - double deltaY = currentY - _startY; + final controller = Get.put(ProfileController()); + final pageController = PageController(initialPage: 1); + final themeController = Get.find(); - if (deltaY > 20) { - // 下拉超过20像素,张开卡片(降低阈值提高灵敏度) - if (!_isCardExpanded) { - setState(() { - _isCardExpanded = true; - }); - } - } else if (deltaY < -60) { - // 上滑超过60像素,收起卡片(降低阈值提高灵敏度) - if (_isCardExpanded) { - setState(() { - _isCardExpanded = false; - }); - } - } - }, - child: Column( - children: [ - _buildProfileHeader(), - Expanded( - child: PageView( - controller: _pageController, - onPageChanged: _onPageChanged, + return Obx(() { + final isDark = themeController.isDarkMode; + return GetBuilder( + builder: (controller) { + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: _buildAppBar(controller, isDark), + body: GestureDetector( + behavior: HitTestBehavior.opaque, + onVerticalDragStart: (details) { + controller.startY.value = details.globalPosition.dy; + }, + onVerticalDragEnd: (details) { + double currentY = details.globalPosition.dy; + double deltaY = currentY - controller.startY.value; + + if (deltaY > 50) { + if (!controller.isCardExpanded.value) { + controller.isCardExpanded.value = true; + } + } else if (deltaY < -50) { + if (controller.isCardExpanded.value) { + controller.isCardExpanded.value = false; + } + } + }, + child: Column( children: [ - _buildPage1(), // 个人信息卡片 - _buildPage2(), // 设置列表 - _buildPage3(), // 其他功能 + _buildProfileHeader(controller, pageController), + Expanded( + child: PageView( + controller: pageController, + onPageChanged: controller.onPageChanged, + children: [ + _buildPage1(controller, isDark), + _buildPage2(controller, isDark), + _buildPage3(controller, isDark), + ], + ), + ), ], ), ), - ], - ), - ), - ); + ); + }, + ); + }); } - PreferredSizeWidget _buildAppBar() { - // === 顶部导航栏:显示页面标题 === + PreferredSizeWidget _buildAppBar(ProfileController controller, bool isDark) { return AppBar( title: Text( '个人', @@ -310,61 +101,68 @@ class _ProfilePageState extends State fontWeight: FontWeight.bold, ), ), - backgroundColor: Colors.white, + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, elevation: 0, centerTitle: true, actions: [ - // 右上角更多按钮 IconButton( icon: Icon(Icons.more_horiz, color: AppConstants.primaryColor), - onPressed: _showMoreOptions, + onPressed: () => _showMoreOptions(controller), ), ], ); } - Widget _buildProfileHeader() { + Widget _buildProfileHeader( + ProfileController controller, + PageController pageController, + ) { // === 个人信息头部:可收起/张开的个人卡片 === - return PersonalCard( - key: _personalCardKey, - userData: _userData, - isExpanded: _isCardExpanded, - onExpandChanged: (value) { - setState(() { - _isCardExpanded = value; - }); - }, - currentPage: _currentPage, - onPageChanged: (page) { - // 添加 mounted 和 hasClients 检查,避免 dispose 后调用导致黑屏 - if (mounted && _pageController.hasClients) { - _pageController.animateToPage( - page, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - }, - onAvatarTap: _changeAvatar, + return Obx( + () => PersonalCard( + userData: controller.userData, + isExpanded: controller.isCardExpanded.value, + onExpandChanged: (value) { + controller.isCardExpanded.value = value; + }, + currentPage: controller.currentPage.value, + onPageChanged: (page) { + // 添加 hasClients 检查,避免 dispose 后调用导致黑屏 + if (pageController.hasClients) { + pageController.animateToPage( + page, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + onAvatarTap: controller.changeAvatar, + ), ); } - Widget _buildPage1() { - // === 第1页:个人信息卡片展示 === + Widget _buildPage1(ProfileController controller, bool isDark) { return SingleChildScrollView( - padding: const EdgeInsets.all(16), + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: AppConfig.liquidGlassTotalHeight + 16, + ), child: Column( children: [ - // === 互动统计卡片 === Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.08), blurRadius: 10, offset: const Offset(0, 2), ), @@ -392,36 +190,49 @@ class _ProfilePageState extends State ], ), const SizedBox(height: 16), - if (!_isStatsHidden) - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildInteractionItem('今日答题', '$_todayQuestions'), - _buildInteractionItem('本周答题', '$_weekQuestions'), - _buildInteractionItem('答对次数', '$_correctAnswers'), - ], - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Text( - '数据已隐藏', - style: TextStyle(color: Colors.grey[500], fontSize: 14), + Obx(() { + if (!controller.isStatsHidden.value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildInteractionItem( + '今日答题', + '${controller.todayQuestions.value}', + isDark, + ), + _buildInteractionItem( + '本周答题', + '${controller.weekQuestions.value}', + isDark, + ), + _buildInteractionItem( + '答对次数', + '${controller.correctAnswers.value}', + isDark, + ), + ], + ); + } else { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + '数据已隐藏', + style: TextStyle( + color: isDark ? Colors.grey[500] : Colors.grey[500], + fontSize: 14, + ), + ), ), - ), - ), + ); + } + }), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const PoetryLevelPage(), - ), - ); + Get.to(const PoetryLevelPage()); }, style: ElevatedButton.styleFrom( backgroundColor: AppConstants.primaryColor, @@ -437,16 +248,17 @@ class _ProfilePageState extends State ), ), const SizedBox(height: 16), - // 个人信息卡片 Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.08), blurRadius: 10, offset: const Offset(0, 2), ), @@ -455,7 +267,6 @@ class _ProfilePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // === 卡片标题 === Row( children: [ Icon( @@ -475,39 +286,56 @@ class _ProfilePageState extends State const Spacer(), IconButton( icon: Icon( - _isStatsHidden + controller.isStatsHidden.value ? Icons.visibility_off_outlined : Icons.visibility_outlined, - color: Colors.grey[600], + color: isDark ? Colors.grey[400] : Colors.grey[600], size: 20, ), - onPressed: () { - setState(() { - _isStatsHidden = !_isStatsHidden; - }); - HapticFeedback.lightImpact(); - }, - tooltip: _isStatsHidden ? '显示数据' : '隐藏数据', + onPressed: controller.toggleStatsHidden, + tooltip: controller.isStatsHidden.value ? '显示数据' : '隐藏数据', ), ], ), const SizedBox(height: 16), - // === 信息列表 === - if (!_isStatsHidden) ...[ - _buildInfoItem('今日浏览', '$_todayViews 条'), - _buildInfoItem('今日点赞', '$_todayLikes 个'), - _buildInfoItem('今日答题', '$_todayQuestions 题'), - _buildInfoItem('本周浏览', '$_weekViews 条'), - _buildInfoItem('数据', _dataSize), - _buildInfoItem('笔记', '$_noteCount 个'), - _buildInfoItem('已用', '$_useDays 天'), + if (!controller.isStatsHidden.value) ...[ + _buildInfoItem( + '今日浏览', + '${controller.todayViews.value} 条', + isDark, + ), + _buildInfoItem( + '今日点赞', + '${controller.todayLikes.value} 个', + isDark, + ), + _buildInfoItem( + '今日答题', + '${controller.todayQuestions.value} 题', + isDark, + ), + _buildInfoItem( + '本周浏览', + '${controller.weekViews.value} 条', + isDark, + ), + _buildInfoItem('数据', controller.dataSize.value, isDark), + _buildInfoItem( + '笔记', + '${controller.noteCount.value} 个', + isDark, + ), + _buildInfoItem('已用', '${controller.useDays.value} 天', isDark), ] else Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( '数据已隐藏', - style: TextStyle(color: Colors.grey[500], fontSize: 14), + style: TextStyle( + color: isDark ? Colors.grey[500] : Colors.grey[500], + fontSize: 14, + ), ), ), ), @@ -519,151 +347,142 @@ class _ProfilePageState extends State ); } - Widget _buildPage2() { - // === 第2页:设置列表 === + Widget _buildPage2(ProfileController controller, bool isDark) { return ListView( - padding: const EdgeInsets.all(16), + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: AppConfig.liquidGlassTotalHeight + 16, + ), children: [ - // === 账户设置组 === _buildSettingsGroup('软件设置', [ _buildSettingsItem( '功能设置', Icons.verified_user, - () => _navigateToAppFunSettings(), + () => Get.to(const AppFunSettingsPage()), + isDark, ), - _buildSettingsItem( '离线使用', Icons.volunteer_activism, - () => _navigateToOfflineDataPage(), + () => Get.to(const OfflineDataPage()), + isDark, ), _buildSettingsItem( '历史记录', Icons.history, - () => _navigateToHistoryPage(), + () => Get.to(const HistoryPage()), + isDark, ), - _buildSettingsItem( '主题风格', Icons.palette, - () => _navigateToAppDiyPage(), + () => Get.to(const AppDiyPage()), + isDark, ), - ]), + ], isDark), const SizedBox(height: 16), - // === 隐私设置组 === _buildSettingsGroup('隐私设置', [ _buildSettingsItem( '权限管理', Icons.block, - () => _navigateToPermissionPage(), + () => Get.to(const PermissionPage()), + isDark, ), _buildSettingsItem( '应用数据', Icons.security, - () => _navigateToAppDataPage(), + () => Get.to(const AppDataPage()), + isDark, ), - //撤回同意 _buildSettingsItem( '软件协议', Icons.description, - () => _navigateToPrivacyPage(), + () => Get.to(const PrivacyPage()), + isDark, ), - ]), + ], isDark), const SizedBox(height: 16), - // === 通用设置组 === _buildSettingsGroup('软件信息', [ _buildSettingsItem( '应用信息', Icons.phone_android, - () => _navigateToAppInfoPage(), + () => Get.to(const AppInfoPage()), + isDark, ), _buildSettingsItem( '了解我们', Icons.info, - () => _navigateToLearnUsPage(), + () => Get.to(const LearnUsPage()), + isDark, ), _buildSettingsItem( '已知bug', Icons.cleaning_services, - () => showBugListBottomSheet(context), + () => showBugListBottomSheet(Get.context!), + isDark, ), - _buildScreenWakeItem(), - ]), + _buildScreenWakeItem(controller, isDark), + ], isDark), ], ); } - Widget _buildPage3() { - // === 第3页:其他功能列表 === + Widget _buildPage3(ProfileController controller, bool isDark) { return ListView( - padding: const EdgeInsets.all(16), + // 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果 + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: AppConfig.liquidGlassTotalHeight + 16, + ), children: [ - // === 功能服务组 === _buildSettingsGroup('用户体验计划(限免)', [ _buildSettingsItem( '加入体验', Icons.edit, - () => _navigateToUserPlanPage(), + () => Get.to(const UserPlanPage()), + isDark, ), _buildSettingsItem( '参与投票', Icons.how_to_vote, - () => _navigateToVotePage(), + () => Get.to(const VotePage()), + isDark, ), _buildSettingsItem( '查看全站统计', Icons.history, - () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const EntirePage()), - ), + () => Get.to(const EntirePage()), + isDark, ), _buildSettingsItem( '开发计划', Icons.analytics, - () => _showSnackBar('首个上线版本暂无计划,待收集整理用户建议'), + () => controller.showSnackBar('首个上线版本暂无计划,待收集整理用户建议'), + isDark, ), - _buildSettingsItem('去投稿', Icons.edit_note, () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ManuscriptPage()), - ); - }), - ]), + _buildSettingsItem( + '去投稿', + Icons.edit_note, + () => Get.to(const ManuscriptPage()), + isDark, + ), + ], isDark), const SizedBox(height: 16), - // === 帮助支持组 === - // _buildSettingsGroup('帮助支持', [ - // _buildSettingsItem( - // '使用教程', - // Icons.help_outline, - // () => _navigateToSpGuidePage(), - // ), - // _buildSettingsItem( - // '意见反馈', - // Icons.feedback, - // () => _showSnackBar('意见反馈'), - // ), - // _buildSettingsItem( - // '联系客服', - // Icons.support_agent, - // () => _showSnackBar('联系客服'), - // ), - // _buildSettingsItem( - // '商务合作', - // Icons.info_outline, - // () => _showSnackBar('商务合作'), - // ), - // ]), - // const SizedBox(height: 16), - // === 关于信息组 === Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.08), blurRadius: 10, offset: const Offset(0, 2), ), @@ -689,22 +508,26 @@ class _ProfilePageState extends State const SizedBox(height: 16), Text( AppConstants.appName, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 8), Text( '版本 ${AppConstants.appVersion}', - style: const TextStyle(fontSize: 14, color: Colors.grey), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey, + ), ), const SizedBox(height: 16), - const Text( + Text( '基于Flutter开发的现代化诗词应用,致力于为用户提供优质的诗词阅读和创作体验。', style: TextStyle( fontSize: 14, - color: Colors.black87, + color: isDark ? Colors.grey[300] : Colors.black87, height: 1.5, ), textAlign: TextAlign.center, @@ -716,8 +539,7 @@ class _ProfilePageState extends State ); } - Widget _buildInfoItem(String label, String value) { - // === 信息项:显示标签和值 === + Widget _buildInfoItem(String label, String value, bool isDark) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( @@ -725,16 +547,16 @@ class _ProfilePageState extends State children: [ Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: Color(0xFF9E9E9E), // 使用具体颜色值代替Colors.grey[600] + color: isDark ? Colors.grey[400] : const Color(0xFF9E9E9E), ), ), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, fontWeight: FontWeight.w500, ), ), @@ -743,15 +565,16 @@ class _ProfilePageState extends State ); } - Widget _buildSettingsGroup(String title, List items) { - // === 设置组:显示组标题和设置项列表 === + Widget _buildSettingsGroup(String title, List items, bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.08), blurRadius: 10, offset: const Offset(0, 2), ), @@ -760,7 +583,6 @@ class _ProfilePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 组标题 Padding( padding: const EdgeInsets.all(16), child: Row( @@ -782,23 +604,33 @@ class _ProfilePageState extends State ], ), ), - const Divider(height: 1), - // 设置项列表 + Divider(height: 1, color: isDark ? Colors.grey[800] : null), ...items, ], ), ); } - Widget _buildSettingsItem(String title, IconData icon, VoidCallback onTap) { - // === 设置项:显示图标、标题和箭头 === + Widget _buildSettingsItem( + String title, + IconData icon, + VoidCallback onTap, + bool isDark, + ) { return ListTile( leading: Icon(icon, color: AppConstants.primaryColor, size: 20), title: Text( title, - style: const TextStyle(fontSize: 14, color: Colors.black87), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.white : Colors.black87, + ), + ), + trailing: Icon( + Icons.chevron_right, + color: isDark ? Colors.grey[600] : Colors.grey, + size: 20, ), - trailing: const Icon(Icons.chevron_right, color: Colors.grey, size: 20), onTap: () { HapticFeedback.lightImpact(); onTap(); @@ -806,13 +638,11 @@ class _ProfilePageState extends State ); } - Widget _buildScreenWakeItem() { - // Web 平台不支持屏幕常亮功能,不显示该项 + Widget _buildScreenWakeItem(ProfileController controller, bool isDark) { if (kIsWeb) { return const SizedBox.shrink(); } - // === 屏幕常亮设置项:显示图标、标题和开关 === return ListTile( leading: Icon( Icons.screen_lock_rotation, @@ -821,175 +651,93 @@ class _ProfilePageState extends State ), title: Text( '屏幕常亮', - style: const TextStyle(fontSize: 14, color: Colors.black87), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.white : Colors.black87, + ), ), - trailing: Switch( - value: _isScreenWakeEnabled, - onChanged: (value) async { - if (value) { - // 显示提示对话框 - showDialog( - context: context, - builder: (context) => AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: Row( - children: [ - Icon( - Icons.info_outline, + trailing: Obx( + () => Switch( + value: controller.isScreenWakeEnabled.value, + onChanged: (value) async { + final originalValue = controller.isScreenWakeEnabled.value; + controller.isScreenWakeEnabled.value = value; + + if (value) { + await Get.dialog( + AlertDialog( + backgroundColor: isDark + ? const Color(0xFF2A2A2A) + : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + title: Text( + '⚠️ 屏幕常亮提示', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, color: AppConstants.primaryColor, - size: 20, ), - const SizedBox(width: 8), - const Text( - '屏幕常亮提示', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + _buildTipItem('OLED屏幕长时间显示相同内容可能会造成不可逆老化', isDark), + const SizedBox(height: 6), + _buildTipItem('开启常亮后,配合自动刷新内容的设置,实现常亮自动加载诗句', isDark), + const SizedBox(height: 6), + _buildTipItem('挂起常亮后,需保持该软件在屏幕中显示,记得关闭常亮哦!', isDark), + ], + ), + actions: [ + TextButton( + onPressed: () { + controller.isScreenWakeEnabled.value = originalValue; + Get.back(result: false); + }, + style: TextButton.styleFrom( + foregroundColor: isDark + ? Colors.grey[400] + : Colors.grey[600], ), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () { + Get.back(result: true); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('确定开启'), ), ], ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - Text( - '⚠️ 注意事项', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.orange[600], - ), - ), - const SizedBox(height: 8), - Text( - '• OLED屏幕长时间显示相同内容可能会造成不可逆老化', - style: const TextStyle( - fontSize: 14, - color: Colors.black87, - ), - ), - const SizedBox(height: 6), - Text( - '• 开启常亮后,配合自动刷新内容的设置,实现常亮自动加载诗句', - style: const TextStyle( - fontSize: 14, - color: Colors.black87, - ), - ), - const SizedBox(height: 6), - Text( - '• 挂起常亮后,需保持该软件在屏幕中显示,记得关闭常亮哦!', - style: const TextStyle( - fontSize: 14, - color: Colors.black87, - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - foregroundColor: Colors.grey[600], - ), - child: const Text('取消'), - ), - TextButton( - onPressed: () async { - Navigator.pop(context); - await _toggleScreenWake(true); - }, - style: TextButton.styleFrom( - foregroundColor: AppConstants.primaryColor, - textStyle: const TextStyle(fontWeight: FontWeight.bold), - ), - child: const Text('确定'), - ), - ], - ), - ); - } else { - await _toggleScreenWake(false); - } - }, - activeColor: AppConstants.primaryColor, + ).then((result) async { + if (result == true) { + await controller.toggleScreenWake(value); + } else { + controller.isScreenWakeEnabled.value = originalValue; + } + }); + } else { + await controller.toggleScreenWake(value); + } + }, + activeColor: AppConstants.primaryColor, + ), ), ); } - Future _toggleScreenWake(bool enable) async { - // Web 平台不支持 wakelock_plus - if (kIsWeb) { - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Web 平台不支持屏幕常亮功能'))); - } - return; - } - - try { - // 使用 io.Platform 检测平台 - final String osName = io.Platform.operatingSystem; - final String osVersion = io.Platform.operatingSystemVersion; - print('Current platform: $osName, version: $osVersion'); - - if (enable) { - await WakelockService.instance.enable(); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('屏幕常亮已开启'))); - } - } else { - await WakelockService.instance.disable(); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('屏幕常亮已关闭'))); - } - } - - setState(() { - _isScreenWakeEnabled = enable; - }); - } catch (e, stackTrace) { - print('WakelockService error: $e'); - print('Stack trace: $stackTrace'); - if (mounted) { - // 检查错误类型,判断是否是设备不支持 - String errorMessage; - if (e.toString().contains('not supported') || - e.toString().contains('unsupported') || - e.toString().contains('不支持')) { - errorMessage = '该设备不支持屏幕常亮功能'; - } else { - errorMessage = '屏幕常亮功能异常: $e'; - } - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('提示'), - content: Text(errorMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('确定'), - ), - ], - ), - ); - } - } - } - - Widget _buildInteractionItem(String label, String value) { - // === 互动统计项:显示标签和数值 === + Widget _buildInteractionItem(String label, String value, bool isDark) { return Expanded( child: Column( children: [ @@ -1002,111 +750,30 @@ class _ProfilePageState extends State ), ), const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), + Text( + label, + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey, + ), + ), ], ), ); } - void _showSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: AppConstants.primaryColor, - duration: const Duration(seconds: 2), - ), - ); - } - - // === 导航到历史记录页面 === - void _navigateToHistoryPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const HistoryPage()), - ); - } - - void _navigateToAppFunSettings() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const AppFunSettingsPage()), - ); - } - - void _navigateToUserPlanPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const UserPlanPage()), - ); - } - - void _navigateToOfflineDataPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const OfflineDataPage()), - ); - } - - void _navigateToPermissionPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const PermissionPage()), - ); - } - - void _navigateToAppDataPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const AppDataPage()), - ); - } - - void _navigateToPrivacyPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const PrivacyPage()), - ); - } - - void _navigateToLearnUsPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const LearnUsPage()), - ); - } - - void _navigateToAppInfoPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const AppInfoPage()), - ); - } - - void _navigateToAppDiyPage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const AppDiyPage()), - ); - } - - void _navigateToVotePage() { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const VotePage()), - ); - } - - void _showMoreOptions() { + void _showMoreOptions(ProfileController controller) { PopMenu.show( - context, - onRefresh: () => refreshData(), - onEdit: () => _showSnackBar('编辑资料'), - onScanQr: () => _showSnackBar('了解软件功能'), - onDarkMode: () => _showSnackBar('夜间模式'), + Get.context!, + onRefresh: () => controller.refreshData(), + onEdit: () => controller.showSnackBar('编辑资料'), + onScanQr: () => controller.showSnackBar('了解软件功能'), + onDarkMode: () => controller.showSnackBar('夜间模式'), onSettings: () { Future.delayed(const Duration(milliseconds: 100), () { - if (mounted && _pageController.hasClients) { - _pageController.animateToPage( + final pageController = PageController(initialPage: 1); + if (pageController.hasClients) { + pageController.animateToPage( 1, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, @@ -1116,4 +783,28 @@ class _ProfilePageState extends State }, ); } + + Widget _buildTipItem(String text, bool isDark) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '• ', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.black87, + ), + ), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.black87, + ), + ), + ), + ], + ); + } } diff --git a/lib/views/profile/settings/app_fun.dart b/lib/views/profile/settings/app_fun.dart index 9eea6f2..af46948 100644 --- a/lib/views/profile/settings/app_fun.dart +++ b/lib/views/profile/settings/app_fun.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; +import '../../../services/get/tap_liquid_glass_controller.dart'; import '../../../utils/audio_manager.dart'; import './widgets.dart'; -import '../../home/home-load.dart'; +import '../../home/set/home-load.dart'; import '../../../controllers/load/locally.dart'; /// 时间: 2026-03-26 @@ -20,6 +23,10 @@ class AppFunSettingsPage extends StatefulWidget { } class _AppFunSettingsPageState extends State { + final ThemeController _themeController = Get.find(); + final TapLiquidGlassController _glassController = Get.put( + TapLiquidGlassController(), + ); bool _autoRefreshEnabled = false; bool _debugInfoEnabled = false; bool _soundEnabled = false; // 默认关闭 @@ -30,7 +37,6 @@ class _AppFunSettingsPageState extends State { static const String _autoRefreshKey = 'auto_refresh_enabled'; static const String _debugInfoKey = 'debug_info_enabled'; - static const String _globalTipsKey = 'global_tips_enabled'; // 添加全局Tips开关key static const String _soundEnabledKey = 'sound_enabled'; // 声音反馈开关key static const String _hideSecondaryButtonsKey = 'hide_secondary_buttons'; // 隐藏次要按钮key @@ -43,12 +49,13 @@ class _AppFunSettingsPageState extends State { Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); + await GlobalTipsManager().init(); if (mounted) { setState(() { _autoRefreshEnabled = prefs.getBool(_autoRefreshKey) ?? false; _debugInfoEnabled = prefs.getBool(_debugInfoKey) ?? false; _globalTipsEnabled = - prefs.getBool(_globalTipsKey) ?? true; // 加载全局Tips开关状态 + GlobalTipsManager().isEnabled; // 从 GlobalTipsManager 加载状态 _preloadEnabled = prefs.getBool('preload_enabled') ?? true; _soundEnabled = prefs.getBool(_soundEnabledKey) ?? false; // 加载声音反馈状态 _hideSecondaryButtons = @@ -160,141 +167,154 @@ class _AppFunSettingsPageState extends State { } } - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '功能设置', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), - ), - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildSettingsGroup('基础功能', [ - _buildSwitchItem( - '自动刷新', - '首页诗句自动刷新(5s) ', - Icons.refresh, - _autoRefreshEnabled, - _setAutoRefresh, - ), - _buildSwitchItem( - '调式信息', - '开启后可加载更多信息', - Icons.bug_report, - _debugInfoEnabled, - _setDebugInfo, - ), - _buildSwitchItem( - '预加载', - _preloadEnabled - ? '开启后 部分数据优先使用本地缓存,减少与服务器的通信次数' - : '关闭后,优先使用云端数据,无延迟,但刷新缓慢', - Icons.storage, - _preloadEnabled, - _setPreload, - ), - ]), - const SizedBox(height: 16), - _buildSettingsGroup('主页显示设置', [ - _buildDevelopmentItem( - 'Tap沉浸光感', - '开启后底栏显示类iOS26 风格', - Icons.dark_mode, - ), - - _buildSwitchItem( - '隐藏次要按钮', - '开启后隐藏上一条和分享按钮', - Icons.share, - _hideSecondaryButtons, - _setHideSecondaryButtons, - ), - - _buildFontSliderItem(), - ]), - const SizedBox(height: 16), - _buildSettingsGroup('交互反馈', [ - _buildSwitchItem( - '全局Tips开关', - '显示一些使用技巧', - Icons.volume_up, - _globalTipsEnabled, - (value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_globalTipsKey, value); - if (mounted) { - setState(() { - _globalTipsEnabled = value; - }); - } - }, - ), - _buildSwitchItem( - '声音反馈', - '操作时播放提示音', - Icons.volume_up, - _soundEnabled, - _setSoundEnabled, - ), - _buildSwitchItem( - '震动反馈', - '操作时震动提示', - Icons.vibration, - _vibrationEnabled, - (value) => _setVibrationEnabled(value), - ), - ]), - const SizedBox(height: 16), - - _buildSettingsGroup('高级设置', [ - // _buildActionItem( - // 'Beta开关 关闭软件', - // '开发者选项', - // Icons.developer_mode, - // () => _showSnackBar('调试模式开发中'), - // ), - _buildActionItem( - '重置设置', - '恢复默认设置', - Icons.restore, - () => _showResetDialog(), - ), - // _buildActionItem( - // '调试模式', - // '开发者选项', - // Icons.developer_mode, - // () => _showSnackBar('调试模式开发中'), - // ), - ]), - const SizedBox(height: 32), - _buildVersionInfo(), - ], - ), - ); + // 设置Tap沉浸光感导航栏 + Future _setTapLiquidGlass(bool value) async { + await _glassController.toggleEnabled(value); } - Widget _buildSettingsGroup(String title, List items) { + @override + Widget build(BuildContext context) { + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '功能设置', + style: TextStyle( + color: AppConstants.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildSettingsGroup('基础功能', [ + _buildSwitchItem( + '自动刷新', + '首页诗句自动刷新(5s) ', + Icons.refresh, + _autoRefreshEnabled, + _setAutoRefresh, + isDark, + ), + _buildSwitchItem( + '调式信息', + '开启后可加载更多信息', + Icons.bug_report, + _debugInfoEnabled, + _setDebugInfo, + isDark, + ), + _buildSwitchItem( + '预加载', + _preloadEnabled + ? '开启后 部分数据优先使用本地缓存,减少与服务器的通信次数' + : '关闭后,优先使用云端数据,无延迟,但刷新缓慢', + Icons.storage, + _preloadEnabled, + _setPreload, + isDark, + ), + ], isDark), + const SizedBox(height: 16), + _buildSettingsGroup('主页显示设置', [ + Obx( + () => _buildSwitchItem( + 'Tap沉浸光感', + '开启后底栏显示类iOS26 风格', + Icons.dark_mode, + _glassController.isEnabled, + _setTapLiquidGlass, + isDark, + ), + ), + Obx(() { + if (!_glassController.isEnabled) return const SizedBox.shrink(); + return _buildTransparencyLevelItem(isDark); + }), + _buildSwitchItem( + '隐藏次要按钮', + '开启后隐藏上一条和分享按钮', + Icons.share, + _hideSecondaryButtons, + _setHideSecondaryButtons, + isDark, + ), + + _buildFontSliderItem(isDark), + ], isDark), + const SizedBox(height: 16), + _buildSettingsGroup('交互反馈', [ + _buildSwitchItem( + '全局Tips开关', + '显示一些使用技巧', + Icons.volume_up, + _globalTipsEnabled, + (value) async { + await GlobalTipsManager().setEnabled(value); + if (mounted) { + setState(() { + _globalTipsEnabled = value; + }); + } + }, + isDark, + ), + _buildSwitchItem( + '声音反馈', + '操作时播放提示音', + Icons.volume_up, + _soundEnabled, + _setSoundEnabled, + isDark, + ), + _buildSwitchItem( + '震动反馈', + '操作时震动提示', + Icons.vibration, + _vibrationEnabled, + (value) => _setVibrationEnabled(value), + isDark, + ), + ], isDark), + const SizedBox(height: 16), + + _buildSettingsGroup('高级设置', [ + _buildActionItem( + '重置设置', + '恢复默认设置', + Icons.restore, + () => _showResetDialog(), + isDark, + ), + ], isDark), + const SizedBox(height: 32), + _buildVersionInfo(isDark), + ], + ), + ); + }); + } + + Widget _buildSettingsGroup(String title, List items, bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -326,6 +346,7 @@ class _AppFunSettingsPageState extends State { IconData icon, bool value, ValueChanged onChanged, + bool isDark, ) { return ListTile( leading: Container( @@ -338,11 +359,18 @@ class _AppFunSettingsPageState extends State { ), title: Text( title, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), subtitle: Text( subtitle, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), trailing: Switch( value: value, @@ -352,7 +380,7 @@ class _AppFunSettingsPageState extends State { ); } - Widget _buildFontSliderItem() { + Widget _buildFontSliderItem(bool isDark) { return ListTile( leading: Container( padding: const EdgeInsets.all(8), @@ -362,43 +390,81 @@ class _AppFunSettingsPageState extends State { ), child: Icon(Icons.widgets, color: AppConstants.primaryColor, size: 20), ), - title: const Text( + title: Text( '桌面卡片', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), subtitle: Text( '使用帮助', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + trailing: Icon( + Icons.chevron_right, + color: isDark ? Colors.grey[500] : Colors.grey[400], ), - trailing: Icon(Icons.chevron_right, color: Colors.grey[400]), onTap: () { - // 显示对话框提示鸿蒙设备设置方法 showDialog( context: context, builder: (BuildContext context) { return AlertDialog( + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, title: Row( children: [ Icon(Icons.info_outline, color: AppConstants.primaryColor), const SizedBox(width: 8), - const Text('桌面卡片设置'), + Text( + '桌面卡片设置', + style: TextStyle( + color: isDark ? Colors.white : Colors.black, + ), + ), ], ), - content: const Column( + content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '鸿蒙设备请在桌面端设置:', - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), - SizedBox(height: 8), - Text('1. 长按桌面空白处'), - Text('2. 选择「情景诗词」卡片'), - Text('3. 添加后点击桌面卡片即可设置'), - Text('4. 后续版本支持在应用内设置卡片'), - SizedBox(height: 12), + const SizedBox(height: 8), Text( + '1. 长按桌面空白处', + style: TextStyle( + color: isDark ? Colors.grey[300] : Colors.black, + ), + ), + Text( + '2. 选择「情景诗词」卡片', + style: TextStyle( + color: isDark ? Colors.grey[300] : Colors.black, + ), + ), + Text( + '3. 添加后点击桌面卡片即可设置', + style: TextStyle( + color: isDark ? Colors.grey[300] : Colors.black, + ), + ), + Text( + '4. 后续版本支持在应用内设置卡片', + style: TextStyle( + color: isDark ? Colors.grey[300] : Colors.black, + ), + ), + const SizedBox(height: 12), + const Text( '注意:该页面设置对鸿蒙设备不生效', style: TextStyle(color: Colors.orange, fontSize: 12), ), @@ -408,7 +474,6 @@ class _AppFunSettingsPageState extends State { TextButton( onPressed: () { Navigator.of(context).pop(); - // 返回桌面 SystemNavigator.pop(); }, child: const Text('前往桌面'), @@ -437,11 +502,116 @@ class _AppFunSettingsPageState extends State { ); } + Widget _buildTransparencyLevelItem(bool isDark) { + return Obx(() { + final currentIndex = _glassController.transparencyLevelIndex; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppConstants.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.opacity, + color: AppConstants.primaryColor, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '高透级别', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), + ), + Text( + '当前: ${_glassController.transparencyLevelLabel}', + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + _buildLevelButton('弱', 0, currentIndex == 0, isDark), + const SizedBox(width: 8), + _buildLevelButton('中', 1, currentIndex == 1, isDark), + const SizedBox(width: 8), + _buildLevelButton('强', 2, currentIndex == 2, isDark), + ], + ), + ), + ], + ); + }); + } + + Widget _buildLevelButton( + String label, + int levelIndex, + bool isSelected, + bool isDark, + ) { + return Expanded( + child: GestureDetector( + onTap: () => _glassController.setTransparencyLevelByIndex(levelIndex), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: isSelected + ? AppConstants.primaryColor + : (isDark ? Colors.grey[800] : Colors.grey[200]), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected + ? AppConstants.primaryColor + : (isDark ? Colors.grey[700]! : Colors.grey[300]!), + width: 1.5, + ), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : (isDark ? Colors.grey[300] : Colors.grey[700]), + ), + ), + ), + ), + ); + } + Widget _buildActionItem( String title, String subtitle, IconData icon, VoidCallback onTap, + bool isDark, ) { return ListTile( leading: Container( @@ -454,66 +624,36 @@ class _AppFunSettingsPageState extends State { ), title: Text( title, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), subtitle: Text( subtitle, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + trailing: Icon( + Icons.chevron_right, + color: isDark ? Colors.grey[500] : Colors.grey[400], ), - trailing: Icon(Icons.chevron_right, color: Colors.grey[400]), onTap: onTap, ); } - Widget _buildDevelopmentItem(String title, String subtitle, IconData icon) { - return ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, color: Colors.grey, size: 20), - ), - title: Text( - title, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Colors.grey[600], - ), - ), - subtitle: Text( - subtitle, - style: TextStyle(fontSize: 12, color: Colors.grey[400]), - ), - trailing: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: Colors.grey.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '开发中', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontWeight: FontWeight.w500, - ), - ), - ), - ); - } - - Widget _buildVersionInfo() { + Widget _buildVersionInfo(bool isDark) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -529,12 +669,19 @@ class _AppFunSettingsPageState extends State { const SizedBox(height: 12), Text( AppConstants.appName, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), const SizedBox(height: 4), Text( '版本 ${AppConstants.appVersion}', - style: TextStyle(fontSize: 13, color: Colors.grey[600]), + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -567,9 +714,9 @@ class _AppFunSettingsPageState extends State { await _setPreload(true); // 预加载:开启 await _setHideSecondaryButtons(false); // 隐藏次要按钮:关闭 await _setSoundEnabled(false); // 声音反馈:关闭 + await _setTapLiquidGlass(true); // Tap沉浸光感:开启 // 全局Tips:开启 - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_globalTipsKey, true); + await GlobalTipsManager().setEnabled(true); // 震动反馈:开启(不需要保存到SharedPreferences,直接更新状态) setState(() { _vibrationEnabled = true; @@ -590,4 +737,5 @@ class _AppFunSettingsPageState extends State { // - 隐藏次要按钮:关闭 // - 声音反馈:关闭 // - 震动反馈:开启 -// - 全局Tips:开启 \ No newline at end of file +// - 全局Tips:开启 +// - tap 沉浸:开启 \ No newline at end of file diff --git a/lib/views/profile/settings/learn-us.dart b/lib/views/profile/settings/learn-us.dart index ad9d4bd..7460067 100644 --- a/lib/views/profile/settings/learn-us.dart +++ b/lib/views/profile/settings/learn-us.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../../config/app_config.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 了解我们页面 @@ -13,51 +15,58 @@ class LearnUsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '了解我们', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, + final themeController = Get.find(); + + return Obx(() { + final isDark = themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '了解我们', + style: TextStyle( + color: AppConstants.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), + onPressed: () => Navigator.of(context).pop(), ), ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildHeaderCard(), + const SizedBox(height: 16), + _buildOfficialSiteCard(isDark), + const SizedBox(height: 16), + _buildQQGroupCard(context, isDark), + const SizedBox(height: 16), + _buildDeveloperCard(context, isDark), + const SizedBox(height: 16), + _buildTeamCard(isDark), + const SizedBox(height: 16), + _buildIcpCard(context, isDark), + const SizedBox(height: 24), + _buildBottomIndicator(isDark), + ], ), - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildHeaderCard(), - const SizedBox(height: 16), - _buildOfficialSiteCard(), - const SizedBox(height: 16), - _buildQQGroupCard(context), - const SizedBox(height: 16), - _buildDeveloperCard(context), - const SizedBox(height: 16), - _buildTeamCard(), - const SizedBox(height: 16), - _buildIcpCard(context), - const SizedBox(height: 24), - _buildBottomIndicator(), - ], - ), - ); + ); + }); } Widget _buildHeaderCard() { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( - gradient: LinearGradient( - colors: const [ + gradient: const LinearGradient( + colors: [ Color.fromARGB(255, 143, 73, 228), Color(0xFF6200EE), Color(0xFF3700B3), @@ -145,14 +154,16 @@ class LearnUsPage extends StatelessWidget { ); } - Widget _buildOfficialSiteCard() { + Widget _buildOfficialSiteCard(bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -178,32 +189,41 @@ class LearnUsPage extends StatelessWidget { ), ), const SizedBox(width: 12), - const Text( + Text( '官方网站', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '访问我们的官方网站了解更多信息', - style: TextStyle(fontSize: 13, color: Colors.grey), + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey, + ), ), const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[50], borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.grey.withValues(alpha: 0.2), + color: isDark + ? Colors.grey[700]!.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), ), ), child: Row( @@ -234,14 +254,16 @@ class LearnUsPage extends StatelessWidget { ); } - Widget _buildQQGroupCard(BuildContext context) { + Widget _buildQQGroupCard(BuildContext context, bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -263,22 +285,29 @@ class LearnUsPage extends StatelessWidget { child: Icon(Icons.group, color: Colors.blue[700], size: 20), ), const SizedBox(width: 12), - const Text( + Text( 'QQ交流群', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '加入我们的QQ交流群,与其他诗词爱好者一起交流', - style: TextStyle(fontSize: 13, color: Colors.grey), + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey, + ), ), const SizedBox(height: 12), InkWell( @@ -288,7 +317,7 @@ class LearnUsPage extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[50], borderRadius: BorderRadius.circular(8), border: Border.all( color: AppConstants.primaryColor.withValues(alpha: 0.3), @@ -318,7 +347,10 @@ class LearnUsPage extends StatelessWidget { const SizedBox(height: 8), Text( '💡 点击群号可复制', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ), ], ), @@ -348,14 +380,16 @@ class LearnUsPage extends StatelessWidget { ); } - Widget _buildDeveloperCard(BuildContext context) { + Widget _buildDeveloperCard(BuildContext context, bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -381,14 +415,18 @@ class LearnUsPage extends StatelessWidget { ), ), const SizedBox(width: 12), - const Text( + Text( '开发者', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), Padding( padding: const EdgeInsets.all(16), child: Row( @@ -414,17 +452,21 @@ class LearnUsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '微风暴工作室', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 4), Text( '专注文字文化领域', - style: TextStyle(fontSize: 13, color: Colors.grey[600]), + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -457,11 +499,12 @@ class LearnUsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '商务合作&侵权申诉&联系我们', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 2), @@ -504,11 +547,12 @@ class LearnUsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '微信公众号', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 4), @@ -573,14 +617,16 @@ class LearnUsPage extends StatelessWidget { ); } - Widget _buildTeamCard() { + Widget _buildTeamCard(bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -602,17 +648,27 @@ class LearnUsPage extends StatelessWidget { child: Icon(Icons.group, color: Colors.purple[700], size: 20), ), const SizedBox(width: 12), - const Text( + Text( '团队信息(Team)', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - _buildTeamMember('💻', '程序设计', '无书的书🤡', '尽毕生所学,取天下之诗集,只为逗她一笑'), - _buildTeamMember('🎨', 'UI/UX/Testing', 'Ayk', '....'), - _buildTeamMember('⚙️', '后端', '伯乐不相马', '真的吗,还是做不到吗?'), - _buildTeamMember('🔧', '技术支持', '闲言app', '闲言app原班人马打造'), + _buildTeamMember( + '💻', + '程序设计', + '无书的书🤡', + '尽毕生所学,取天下之诗集,只为逗她一笑', + isDark, + ), + _buildTeamMember('🎨', 'UI/UX/Testing', 'Ayk', '....', isDark), + _buildTeamMember('⚙️', '后端', '伯乐不相马', '真的吗,还是做不到吗?', isDark), + _buildTeamMember('🔧', '技术支持', '闲言app', '闲言app原班人马打造', isDark), ], ), ); @@ -623,6 +679,7 @@ class LearnUsPage extends StatelessWidget { String role, String name, String signature, + bool isDark, ) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -632,7 +689,7 @@ class LearnUsPage extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[100], borderRadius: BorderRadius.circular(10), ), child: Center( @@ -648,9 +705,10 @@ class LearnUsPage extends StatelessWidget { children: [ Text( role, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(width: 8), @@ -676,7 +734,10 @@ class LearnUsPage extends StatelessWidget { const SizedBox(height: 4), Text( signature, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -686,14 +747,16 @@ class LearnUsPage extends StatelessWidget { ); } - Widget _buildIcpCard(BuildContext context) { + Widget _buildIcpCard(BuildContext context, bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: isDark + ? Colors.black.withValues(alpha: 0.3) + : Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -713,68 +776,43 @@ class LearnUsPage extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Icon( - Icons.verified_user, + Icons.verified, color: Colors.orange[700], size: 20, ), ), const SizedBox(width: 12), - const Text( - 'ICP备案信息', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + Text( + '备案信息', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), - const Divider(height: 1), + Divider(height: 1, color: isDark ? Colors.grey[800] : null), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'APP核准备案号', - style: TextStyle(fontSize: 13, color: Colors.grey), - ), - const SizedBox(height: 12), - InkWell( - onTap: () => _copyIcpNumber(context), - borderRadius: BorderRadius.circular(8), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey[50], - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppConstants.primaryColor.withValues(alpha: 0.3), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.content_copy, - size: 16, - color: AppConstants.primaryColor, - ), - const SizedBox(width: 8), - Text( - '滇ICP备2022000863号-15A', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppConstants.primaryColor, - ), - ), - ], - ), + Text( + '滇ICP备2025002147号-2A', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Text( - '💡 点击备案号可复制', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + '弥勒市朋普镇微风暴网络科技工作室', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), @@ -784,41 +822,32 @@ class LearnUsPage extends StatelessWidget { ); } - void _copyIcpNumber(BuildContext context) { - Clipboard.setData(const ClipboardData(text: '滇ICP备2022000863号-15A')); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( - children: [ - Icon(Icons.check_circle, color: Colors.white, size: 20), - SizedBox(width: 8), - Text('备案号已复制到剪贴板'), - ], - ), - backgroundColor: AppConstants.primaryColor, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - duration: const Duration(seconds: 2), - ), - ); - } - - Widget _buildBottomIndicator() { + Widget _buildBottomIndicator(bool isDark) { return Container( padding: const EdgeInsets.symmetric(vertical: 24), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '到底了', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ), - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), ], ), ); diff --git a/lib/views/profile/settings/offline-data.dart b/lib/views/profile/settings/offline-data.dart index c79a657..7bb1136 100644 --- a/lib/views/profile/settings/offline-data.dart +++ b/lib/views/profile/settings/offline-data.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; import '../../../utils/http/http_client.dart'; import 'user-plan.dart'; import '../components/server_info_dialog.dart'; @@ -25,6 +27,7 @@ enum DownloadType { } class _OfflineDataPageState extends State { + final ThemeController _themeController = Get.find(); DownloadType _selectedType = DownloadType.poetry; int _selectedCount = 30; bool _isLoading = false; @@ -510,262 +513,109 @@ class _OfflineDataPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '离线使用', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '离线使用', + style: TextStyle( + color: AppConstants.primaryColor, + fontWeight: FontWeight.bold, + ), ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + IconButton( + icon: Icon( + Icons.cloud_outlined, + color: AppConstants.primaryColor, + ), + onPressed: _showServerInfo, + tooltip: '服务器信息', + ), + ], ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), - ), - actions: [ - IconButton( - icon: Icon(Icons.cloud_outlined, color: AppConstants.primaryColor), - onPressed: _showServerInfo, - tooltip: '服务器信息', - ), - ], - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - // 缓存状态卡片 - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - Row( - children: [ - Icon( - Icons.download_done, - color: AppConstants.primaryColor, - size: 24, - ), - const SizedBox(width: 12), - const Text( - '缓存状态', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '精选诗句: $_poetryCount 条', - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - const SizedBox(height: 8), - Text( - '答题挑战: $_quizCount 条', - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - ], - ), - ], - ), - ), - - const SizedBox(height: 20), - - // 下载类型选择 - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.category, - color: AppConstants.primaryColor, - size: 20, - ), - const SizedBox(width: 8), - const Text( - '下载类型', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildTypeOption(DownloadType.poetry, '精选诗句'), - _buildTypeOption(DownloadType.quiz, '答题挑战'), - ], - ), - ], - ), - ), - - const SizedBox(height: 20), - - // 下载数量选择 - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.settings, - color: AppConstants.primaryColor, - size: 20, - ), - const SizedBox(width: 8), - const Text( - '下载数量', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: _selectedType == DownloadType.poetry - ? [ - _buildCountOption(20), - _buildCountOption(30), - _buildCountOption(60), - _buildCountOption(100), - ] - : [ - _buildCountOption(20), - _buildCountOption(50), - _buildCountOption(80), - _buildCountOption(100), - ], - ), - ], - ), - ), - - const SizedBox(height: 20), - - // 下载/取消按钮 - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _isLoading - ? (_isCancelling ? null : _cancelDownload) - : _downloadOfflineData, - style: ElevatedButton.styleFrom( - backgroundColor: _isCancelling - ? Colors.red - : AppConstants.primaryColor, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isCancelling - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - _isLoading ? '取消下载' : '开始下载', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - - const SizedBox(height: 16), - - // 清空按钮 - SizedBox( - width: double.infinity, - child: OutlinedButton( - onPressed: _clearOfflineData, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.red, - side: const BorderSide(color: Colors.red), - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text( - '清空缓存', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), - ), - ), - - const SizedBox(height: 20), - - // 进度条 - if (_isLoading) ...[ + body: ListView( + padding: const EdgeInsets.all(16), + children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.download_done, + color: AppConstants.primaryColor, + size: 24, + ), + const SizedBox(width: 12), + Text( + '缓存状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '精选诗句: $_poetryCount 条', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey, + ), + ), + const SizedBox(height: 8), + Text( + '答题挑战: $_quizCount 条', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey, + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -774,145 +624,351 @@ class _OfflineDataPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - _status, - style: const TextStyle(fontSize: 14, color: Colors.grey), + Row( + children: [ + Icon( + Icons.category, + color: AppConstants.primaryColor, + size: 20, + ), + const SizedBox(width: 8), + Text( + '下载类型', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildTypeOption(DownloadType.poetry, '精选诗句', isDark), + _buildTypeOption(DownloadType.quiz, '答题挑战', isDark), + ], + ), + ], + ), + ), + + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.settings, + color: AppConstants.primaryColor, + size: 20, + ), + const SizedBox(width: 8), + Text( + '下载数量', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _selectedType == DownloadType.poetry + ? [ + _buildCountOption(20, isDark), + _buildCountOption(30, isDark), + _buildCountOption(60, isDark), + _buildCountOption(100, isDark), + ] + : [ + _buildCountOption(20, isDark), + _buildCountOption(50, isDark), + _buildCountOption(80, isDark), + _buildCountOption(100, isDark), + ], + ), + ], + ), + ), + + const SizedBox(height: 20), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading + ? (_isCancelling ? null : _cancelDownload) + : _downloadOfflineData, + style: ElevatedButton.styleFrom( + backgroundColor: _isCancelling + ? Colors.red + : AppConstants.primaryColor, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isCancelling + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : Text( + _isLoading ? '取消下载' : '开始下载', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + const SizedBox(height: 16), + + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: _clearOfflineData, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red, + side: const BorderSide(color: Colors.red), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + '清空缓存', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ), + + const SizedBox(height: 20), + + if (_isLoading) ...[ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues( + alpha: isDark ? 0.3 : 0.05, + ), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _status, + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey, + ), + ), + const SizedBox(height: 12), + LinearProgressIndicator( + value: _progress / 100, + backgroundColor: isDark + ? Colors.grey[700] + : Colors.grey[200], + valueColor: AlwaysStoppedAnimation( + AppConstants.primaryColor, + ), + borderRadius: BorderRadius.circular(10), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: Text( + '$_progress%', + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey, + ), + ), + ), + ], + ), + ), + ], + + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark + ? Colors.blue.withValues(alpha: 0.15) + : Colors.blue.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark + ? Colors.blue.withValues(alpha: 0.3) + : Colors.blue.withValues(alpha: 0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.blue[600], + size: 20, + ), + const SizedBox(width: 8), + Text( + '温馨提示', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue[600], + ), + ), + ], ), const SizedBox(height: 12), - LinearProgressIndicator( - value: _progress / 100, - backgroundColor: Colors.grey[200], - valueColor: AlwaysStoppedAnimation( - AppConstants.primaryColor, - ), - borderRadius: BorderRadius.circular(10), - ), - const SizedBox(height: 8), - Align( - alignment: Alignment.centerRight, - child: Text( - '$_progress%', - style: const TextStyle(fontSize: 12, color: Colors.grey), + Padding( + padding: const EdgeInsets.only(left: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '• 数据下载后 需手动开启离线状态', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + '方法:个人 → 下拉 点击头像下面关闭按钮 头像显示离线', + style: TextStyle( + fontSize: 13, + color: isDark + ? Colors.grey[400] + : Colors.grey[600], + ), + ), + ), + const SizedBox(height: 8), + Text( + '• 开启离线模式后,将会循环加载本地的数据源', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), + ), + const SizedBox(height: 8), + Text( + '• 下载的数据将保存在本地,可在无网络时使用', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), + ), + const SizedBox(height: 8), + Text( + '• 下载过程中请保持网络连接', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), + ), + const SizedBox(height: 8), + Text( + '• 缓存数据不会写入历史记录', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), + ), + const SizedBox(height: 8), + Text( + '• 建议在WiFi环境下下载较多数据', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), + ), + ], ), ), ], ), ), ], - - const SizedBox(height: 20), - - // 说明信息 - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.blue.withValues(alpha: 0.2)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.info_outline, color: Colors.blue[600], size: 20), - const SizedBox(width: 8), - Text( - '温馨提示', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.blue[600], - ), - ), - ], - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.only(left: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '• 数据下载后 需手动开启离线状态', - style: TextStyle( - fontSize: 14, - color: Colors.grey[700], - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.only(left: 20), - child: Text( - '方法:个人 → 下拉 点击头像下面关闭按钮 头像显示离线', - style: TextStyle( - fontSize: 13, - color: Colors.grey[600], - ), - ), - ), - const SizedBox(height: 8), - Text( - '• 开启离线模式后,将会循环加载本地的数据源', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - const SizedBox(height: 8), - Text( - '• 下载的数据将保存在本地,可在无网络时使用', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - const SizedBox(height: 8), - Text( - '• 下载过程中请保持网络连接', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - const SizedBox(height: 8), - Text( - '• 缓存数据不会写入历史记录', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - const SizedBox(height: 8), - Text( - '• 建议在WiFi环境下下载较多数据', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); + ), + ); + }); } - Widget _buildTypeOption(DownloadType type, String label) { + Widget _buildTypeOption(DownloadType type, String label, bool isDark) { final isSelected = _selectedType == type; return GestureDetector( onTap: () { setState(() { _selectedType = type; - // 重置为默认数量 _selectedCount = type == DownloadType.poetry ? 30 : 50; }); - // 重新加载缓存数量 _loadCachedCount(); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( - color: isSelected ? AppConstants.primaryColor : Colors.grey[100], + color: isSelected + ? AppConstants.primaryColor + : (isDark ? Colors.grey[800] : Colors.grey[100]), borderRadius: BorderRadius.circular(8), border: Border.all( - color: isSelected ? AppConstants.primaryColor : Colors.grey[300]!, + color: isSelected + ? AppConstants.primaryColor + : (isDark ? Colors.grey[700]! : Colors.grey[300]!), ), ), child: Text( label, style: TextStyle( - color: isSelected ? Colors.white : Colors.black, + color: isSelected + ? Colors.white + : (isDark ? Colors.white : Colors.black), fontSize: 14, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), @@ -921,7 +977,7 @@ class _OfflineDataPageState extends State { ); } - Widget _buildCountOption(int count) { + Widget _buildCountOption(int count, bool isDark) { final isSelected = _selectedCount == count; return Stack( children: [ @@ -934,25 +990,28 @@ class _OfflineDataPageState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - color: isSelected ? AppConstants.primaryColor : Colors.grey[100], + color: isSelected + ? AppConstants.primaryColor + : (isDark ? Colors.grey[800] : Colors.grey[100]), borderRadius: BorderRadius.circular(8), border: Border.all( color: isSelected ? AppConstants.primaryColor - : Colors.grey[300]!, + : (isDark ? Colors.grey[700]! : Colors.grey[300]!), ), ), child: Text( '$count条', style: TextStyle( - color: isSelected ? Colors.white : Colors.black, + color: isSelected + ? Colors.white + : (isDark ? Colors.white : Colors.black), fontSize: 12, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ), - // 为100条添加标记(在卡片外面右上角) if (count == 100) Positioned( top: -6, diff --git a/lib/views/profile/settings/privacy.dart b/lib/views/profile/settings/privacy.dart index 1603b7d..192115e 100644 --- a/lib/views/profile/settings/privacy.dart +++ b/lib/views/profile/settings/privacy.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 隐私政策与软件协议页面 @@ -9,7 +11,8 @@ import '../../../constants/app_constants.dart'; /// 公共协议内容组件 - 隐私政策 class PrivacyPolicyContent extends StatelessWidget { - const PrivacyPolicyContent({super.key}); + final bool isDark; + const PrivacyPolicyContent({super.key, this.isDark = false}); @override Widget build(BuildContext context) { @@ -87,10 +90,10 @@ class PrivacyPolicyContent extends StatelessWidget { padding: const EdgeInsets.only(bottom: 8), child: Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), ); @@ -101,10 +104,10 @@ class PrivacyPolicyContent extends StatelessWidget { padding: const EdgeInsets.only(bottom: 8), child: Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), ); @@ -113,7 +116,11 @@ class PrivacyPolicyContent extends StatelessWidget { Widget _buildParagraph(String text) { return Text( text, - style: TextStyle(fontSize: 14, color: Colors.grey[700], height: 1.6), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + height: 1.6, + ), ); } @@ -137,7 +144,7 @@ class PrivacyPolicyContent extends StatelessWidget { text, style: TextStyle( fontSize: 14, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], height: 1.5, ), ), @@ -152,19 +159,33 @@ class PrivacyPolicyContent extends StatelessWidget { margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), + border: Border.all( + color: isDark + ? Colors.grey[700]!.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), const SizedBox(height: 4), - Text(desc, style: TextStyle(fontSize: 13, color: Colors.grey[600])), + Text( + desc, + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), ], ), ); @@ -179,13 +200,20 @@ class PrivacyPolicyContent extends StatelessWidget { width: 80, child: Text( '$label:', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), ), Expanded( child: Text( value, - style: TextStyle(fontSize: 14, color: Colors.grey[700]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), ), ), ], @@ -196,14 +224,20 @@ class PrivacyPolicyContent extends StatelessWidget { Widget _buildUpdateDate(String date) { return Text( '更新日期:$date', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ); } Widget _buildEffectiveDate(String date) { return Text( '生效日期:$date', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ); } @@ -213,15 +247,26 @@ class PrivacyPolicyContent extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '到底了', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ), - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), ], ), ); @@ -230,7 +275,8 @@ class PrivacyPolicyContent extends StatelessWidget { /// 公共协议内容组件 - 用户协议 class UserAgreementContent extends StatelessWidget { - const UserAgreementContent({super.key}); + final bool isDark; + const UserAgreementContent({super.key, this.isDark = false}); @override Widget build(BuildContext context) { @@ -320,10 +366,10 @@ class UserAgreementContent extends StatelessWidget { padding: const EdgeInsets.only(bottom: 8), child: Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), ); @@ -332,7 +378,11 @@ class UserAgreementContent extends StatelessWidget { Widget _buildParagraph(String text) { return Text( text, - style: TextStyle(fontSize: 14, color: Colors.grey[700], height: 1.6), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + height: 1.6, + ), ); } @@ -345,13 +395,20 @@ class UserAgreementContent extends StatelessWidget { width: 80, child: Text( '$label:', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), ), Expanded( child: Text( value, - style: TextStyle(fontSize: 14, color: Colors.grey[700]), + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.grey[700], + ), ), ), ], @@ -362,7 +419,10 @@ class UserAgreementContent extends StatelessWidget { Widget _buildEffectiveDate(String date) { return Text( '生效日期:$date', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), ); } @@ -372,15 +432,26 @@ class UserAgreementContent extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '到底了', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ), - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), ], ), ); @@ -398,6 +469,7 @@ class PrivacyPage extends StatefulWidget { class _PrivacyPageState extends State with TickerProviderStateMixin { late TabController _tabController; + final ThemeController _themeController = Get.find(); @override void initState() { @@ -413,73 +485,93 @@ class _PrivacyPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '隐私与协议', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '隐私与协议', + style: TextStyle( + color: AppConstants.primaryColor, + fontWeight: FontWeight.bold, + ), ), - ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - bottom: TabBar( - controller: _tabController, - labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], - indicatorColor: AppConstants.primaryColor, - indicatorWeight: 2, - tabs: const [ - Tab(text: '隐私政策'), - Tab(text: '用户协议'), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + bottom: TabBar( + controller: _tabController, + labelColor: AppConstants.primaryColor, + unselectedLabelColor: isDark ? Colors.grey[400] : Colors.grey[600], + indicatorColor: AppConstants.primaryColor, + indicatorWeight: 2, + tabs: const [ + Tab(text: '隐私政策'), + Tab(text: '用户协议'), + ], + ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + IconButton( + icon: Icon(Icons.link, color: AppConstants.primaryColor), + onPressed: () => _showOnlineLinkDialog(isDark), + tooltip: '在线版本', + ), ], ), - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), + body: TabBarView( + controller: _tabController, + children: [ + PrivacyPolicyContent(isDark: isDark), + UserAgreementContent(isDark: isDark), + ], ), - actions: [ - IconButton( - icon: Icon(Icons.link, color: AppConstants.primaryColor), - onPressed: () => _showOnlineLinkDialog(), - tooltip: '在线版本', - ), - ], - ), - body: TabBarView( - controller: _tabController, - children: const [PrivacyPolicyContent(), UserAgreementContent()], - ), - ); + ); + }); } - void _showOnlineLinkDialog() { + void _showOnlineLinkDialog(bool isDark) { showDialog( context: context, builder: (context) => AlertDialog( + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Row( children: [ Icon(Icons.public, color: AppConstants.primaryColor), const SizedBox(width: 8), - const Text('在线版本'), + Text( + '在线版本', + style: TextStyle(color: isDark ? Colors.white : Colors.black), + ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('您可以访问以下链接查看最新版本的协议:', style: TextStyle(fontSize: 14)), + Text( + '您可以访问以下链接查看最新版本的协议:', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[300] : Colors.black, + ), + ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey[100], + color: isDark ? const Color(0xFF3A3A3A) : Colors.grey[100], borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey[300]!), + border: Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[300]!, + ), ), child: SelectableText( 'https://poe.vogov.cn/privacy.html', @@ -493,14 +585,20 @@ class _PrivacyPageState extends State const SizedBox(height: 12), Text( '💡 点击下方按钮复制链接,在浏览器中打开查看', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('关闭'), + child: Text( + '关闭', + style: TextStyle(color: isDark ? Colors.grey[400] : null), + ), ), ElevatedButton.icon( onPressed: () { diff --git a/lib/views/profile/settings/user-plan.dart b/lib/views/profile/settings/user-plan.dart index cb5517c..27e2b1d 100644 --- a/lib/views/profile/settings/user-plan.dart +++ b/lib/views/profile/settings/user-plan.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-26 /// 功能: 用户体验计划页面 @@ -15,6 +17,7 @@ class UserPlanPage extends StatefulWidget { } class _UserPlanPageState extends State { + final ThemeController _themeController = Get.find(); bool _isJoined = false; bool _isLoading = true; @@ -187,46 +190,51 @@ class _UserPlanPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '用户体验计划', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '用户体验计划', + style: TextStyle( + color: AppConstants.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), + onPressed: () => Navigator.of(context).pop(), ), ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), - ), - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildStatusCard(), - const SizedBox(height: 16), - _buildCollectInfoSection(), - const SizedBox(height: 16), - _buildBenefitsSection(), - const SizedBox(height: 16), - _buildPrivacyNotice(), - const SizedBox(height: 24), - _buildActionButton(), - const SizedBox(height: 16), - _buildBottomIndicator(), - ], - ), - ); + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildStatusCard(isDark), + const SizedBox(height: 16), + _buildCollectInfoSection(isDark), + const SizedBox(height: 16), + _buildBenefitsSection(isDark), + const SizedBox(height: 16), + _buildPrivacyNotice(isDark), + const SizedBox(height: 24), + _buildActionButton(isDark), + const SizedBox(height: 16), + _buildBottomIndicator(isDark), + ], + ), + ); + }); } - Widget _buildStatusCard() { + Widget _buildStatusCard(bool isDark) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -299,14 +307,14 @@ class _UserPlanPageState extends State { ); } - Widget _buildCollectInfoSection() { + Widget _buildCollectInfoSection(bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -332,26 +340,32 @@ class _UserPlanPageState extends State { ), ), const SizedBox(width: 12), - const Text( + Text( '收集的信息', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), ), const Divider(height: 1), - _buildInfoItem(Icons.timer, '软件使用时长', '统计每日使用时长及时段'), - _buildInfoItem(Icons.visibility, '不同页面停留时间', '了解用户关注的页面'), - _buildInfoItem(Icons.favorite, '个人喜爱诗词', '推荐更精准的内容'), - _buildInfoItem(Icons.error, '错误信息', '优化软件功能体验'), - _buildInfoItem(Icons.location_on, 'IP归属地', '群体用户画像生成分析'), - _buildInfoItem(Icons.devices, '设备信息', '适配更多设备型号'), + _buildInfoItem(Icons.timer, '软件使用时长', '统计每日使用时长及时段', isDark), + _buildInfoItem(Icons.visibility, '不同页面停留时间', '了解用户关注的页面', isDark), + _buildInfoItem(Icons.favorite, '个人喜爱诗词', '推荐更精准的内容', isDark), + _buildInfoItem(Icons.error, '错误信息', '优化软件功能体验', isDark), + _buildInfoItem(Icons.location_on, 'IP归属地', '群体用户画像生成分析', isDark), + _buildInfoItem(Icons.devices, '设备信息', '适配更多设备型号', isDark), Padding( padding: const EdgeInsets.all(16), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.05), + color: isDark + ? Colors.blue.withValues(alpha: 0.15) + : Colors.blue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), ), child: Row( @@ -373,12 +387,21 @@ class _UserPlanPageState extends State { ); } - Widget _buildInfoItem(IconData icon, String title, String subtitle) { + Widget _buildInfoItem( + IconData icon, + String title, + String subtitle, + bool isDark, + ) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon( + icon, + size: 20, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), const SizedBox(width: 12), Expanded( child: Column( @@ -386,15 +409,19 @@ class _UserPlanPageState extends State { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 2), Text( subtitle, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[500], + ), ), ], ), @@ -404,7 +431,7 @@ class _UserPlanPageState extends State { ); } - Widget _buildBenefitsSection() { + Widget _buildBenefitsSection(bool isDark) { final benefits = [ {'icon': Icons.how_to_vote, 'title': '参与投票', 'desc': '对产品功能进行投票'}, {'icon': Icons.bar_chart, 'title': '查看统计数据', 'desc': '查看全站使用统计'}, @@ -416,11 +443,11 @@ class _UserPlanPageState extends State { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -450,11 +477,12 @@ class _UserPlanPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '加入后的权益', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 2), @@ -464,7 +492,9 @@ class _UserPlanPageState extends State { '体验更多受限制的功能', style: TextStyle( fontSize: 11, - color: Colors.grey[500], + color: isDark + ? Colors.grey[400] + : Colors.grey[500], ), ), const SizedBox(width: 4), @@ -473,7 +503,9 @@ class _UserPlanPageState extends State { child: Icon( Icons.info_outline, size: 14, - color: Colors.grey[500], + color: isDark + ? Colors.grey[400] + : Colors.grey[500], ), ), ], @@ -502,10 +534,12 @@ class _UserPlanPageState extends State { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.grey[50], + color: isDark ? Colors.grey[800] : Colors.grey[50], borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.grey.withValues(alpha: 0.2), + color: isDark + ? Colors.grey[700]! + : Colors.grey.withValues(alpha: 0.2), ), ), child: Row( @@ -523,16 +557,19 @@ class _UserPlanPageState extends State { children: [ Text( benefit['title'] as String, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, ), ), Text( benefit['desc'] as String, style: TextStyle( fontSize: 10, - color: Colors.grey[500], + color: isDark + ? Colors.grey[400] + : Colors.grey[500], ), ), ], @@ -549,15 +586,15 @@ class _UserPlanPageState extends State { ); } - Widget _buildPrivacyNotice() { + Widget _buildPrivacyNotice(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -581,23 +618,27 @@ class _UserPlanPageState extends State { ), ), const SizedBox(width: 12), - const Text( + Text( '隐私保护说明', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black, + ), ), ], ), const SizedBox(height: 16), - _buildPrivacyItem('🔒 数据匿名化处理,无法追溯到个人'), - _buildPrivacyItem('🛡️ 数据仅用于产品改进,不会出售'), - _buildPrivacyItem('📋 您可以随时退出计划'), - _buildPrivacyItem('🗑️ 部分数据将被删除'), + _buildPrivacyItem('🔒 数据匿名化处理,无法追溯到个人', isDark), + _buildPrivacyItem('🛡️ 数据仅用于产品改进,不会出售', isDark), + _buildPrivacyItem('📋 您可以随时退出计划', isDark), + _buildPrivacyItem('🗑️ 部分数据将被删除', isDark), ], ), ); } - Widget _buildPrivacyItem(String text) { + Widget _buildPrivacyItem(String text, bool isDark) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( @@ -608,7 +649,7 @@ class _UserPlanPageState extends State { text, style: TextStyle( fontSize: 13, - color: Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], height: 1.4, ), ), @@ -618,7 +659,7 @@ class _UserPlanPageState extends State { ); } - Widget _buildActionButton() { + Widget _buildActionButton(bool isDark) { return Container( width: double.infinity, height: 50, @@ -631,7 +672,9 @@ class _UserPlanPageState extends State { AppConstants.primaryColor.withBlue(180), ], ), - color: _isJoined ? Colors.grey[300] : null, + color: _isJoined + ? (isDark ? Colors.grey[700] : Colors.grey[300]) + : null, borderRadius: BorderRadius.circular(12), boxShadow: _isJoined ? null @@ -663,7 +706,9 @@ class _UserPlanPageState extends State { children: [ Icon( _isJoined ? Icons.exit_to_app : Icons.volunteer_activism, - color: _isJoined ? Colors.grey[600] : Colors.white, + color: _isJoined + ? (isDark ? Colors.grey[300] : Colors.grey[600]) + : Colors.white, ), const SizedBox(width: 8), Text( @@ -671,7 +716,9 @@ class _UserPlanPageState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: _isJoined ? Colors.grey[600] : Colors.white, + color: _isJoined + ? (isDark ? Colors.grey[300] : Colors.grey[600]) + : Colors.white, ), ), ], @@ -680,21 +727,32 @@ class _UserPlanPageState extends State { ); } - Widget _buildBottomIndicator() { + Widget _buildBottomIndicator(bool isDark) { return Container( padding: const EdgeInsets.symmetric(vertical: 24), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '到底了', - style: TextStyle(fontSize: 12, color: Colors.grey[400]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), ), ), - Container(width: 40, height: 1, color: Colors.grey[300]), + Container( + width: 40, + height: 1, + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), ], ), ); diff --git a/lib/views/profile/settings/widgets.dart b/lib/views/profile/settings/widgets.dart index e7be9d9..82b9100 100644 --- a/lib/views/profile/settings/widgets.dart +++ b/lib/views/profile/settings/widgets.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-27 /// 功能: 桌面小卡片设置页面 @@ -12,6 +14,7 @@ class WidgetsPage extends StatefulWidget { } class _WidgetsPageState extends State { + final ThemeController _themeController = Get.find(); bool _enableWidget = true; bool _showWeather = true; bool _showQuote = true; @@ -22,79 +25,88 @@ class _WidgetsPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar( - title: Text( - '桌面卡片设置', - style: TextStyle( - color: AppConstants.primaryColor, - fontWeight: FontWeight.bold, + return Obx(() { + final isDark = _themeController.isDarkMode; + return Scaffold( + backgroundColor: isDark + ? const Color(0xFF1A1A1A) + : const Color(0xFFF5F5F5), + appBar: AppBar( + title: Text( + '桌面卡片设置', + style: TextStyle( + color: AppConstants.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), + onPressed: () => Navigator.of(context).pop(), ), ), - backgroundColor: Colors.white, - elevation: 0, - centerTitle: true, - leading: IconButton( - icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), - onPressed: () => Navigator.of(context).pop(), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildSettingsGroup('卡片状态', [ + _buildSwitchItem( + '启用桌面卡片', + '在桌面上显示诗词卡片', + Icons.widgets, + _enableWidget, + (value) => setState(() => _enableWidget = value), + isDark, + ), + ], isDark), + const SizedBox(height: 16), + _buildSettingsGroup('显示内容', [ + _buildSwitchItem( + '显示天气', + '在卡片上显示当前天气', + Icons.cloud, + _showWeather, + (value) => setState(() => _showWeather = value), + isDark, + ), + _buildSwitchItem( + '显示诗句', + '在卡片上显示随机诗句', + Icons.edit_document, + _showQuote, + (value) => setState(() => _showQuote = value), + isDark, + ), + _buildSwitchItem( + '显示时间', + '在卡片上显示当前时间', + Icons.access_time, + _showTime, + (value) => setState(() => _showTime = value), + isDark, + ), + ], isDark), + const SizedBox(height: 16), + _buildSettingsGroup('更新设置', [_buildIntervalItem(isDark)], isDark), + const SizedBox(height: 16), + _buildSettingsGroup('预览', [_buildPreviewCard(isDark)], isDark), + const SizedBox(height: 32), + _buildActionButton(isDark), + ], ), - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildSettingsGroup('卡片状态', [ - _buildSwitchItem( - '启用桌面卡片', - '在桌面上显示诗词卡片', - Icons.widgets, - _enableWidget, - (value) => setState(() => _enableWidget = value), - ), - ]), - const SizedBox(height: 16), - _buildSettingsGroup('显示内容', [ - _buildSwitchItem( - '显示天气', - '在卡片上显示当前天气', - Icons.cloud, - _showWeather, - (value) => setState(() => _showWeather = value), - ), - _buildSwitchItem( - '显示诗句', - '在卡片上显示随机诗句', - Icons.edit_document, - _showQuote, - (value) => setState(() => _showQuote = value), - ), - _buildSwitchItem( - '显示时间', - '在卡片上显示当前时间', - Icons.access_time, - _showTime, - (value) => setState(() => _showTime = value), - ), - ]), - const SizedBox(height: 16), - _buildSettingsGroup('更新设置', [_buildIntervalItem()]), - const SizedBox(height: 16), - _buildSettingsGroup('预览', [_buildPreviewCard()]), - const SizedBox(height: 32), - _buildActionButton(), - ], - ), - ); + ); + }); } - Widget _buildSettingsGroup(String title, List items) { + Widget _buildSettingsGroup(String title, List items, bool isDark) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withAlpha(5), + color: Colors.black.withAlpha(isDark ? 30 : 5), blurRadius: 10, offset: const Offset(0, 2), ), @@ -126,6 +138,7 @@ class _WidgetsPageState extends State { IconData icon, bool value, ValueChanged onChanged, + bool isDark, ) { return ListTile( leading: Container( @@ -138,11 +151,18 @@ class _WidgetsPageState extends State { ), title: Text( title, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), subtitle: Text( subtitle, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), trailing: Switch( value: value, @@ -152,7 +172,7 @@ class _WidgetsPageState extends State { ); } - Widget _buildIntervalItem() { + Widget _buildIntervalItem(bool isDark) { return ListTile( leading: Container( padding: const EdgeInsets.all(8), @@ -162,13 +182,20 @@ class _WidgetsPageState extends State { ), child: Icon(Icons.refresh, color: AppConstants.primaryColor, size: 20), ), - title: const Text( + title: Text( '更新间隔', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black, + ), ), subtitle: Text( '每 $_updateInterval 分钟更新一次', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), ), trailing: SizedBox( width: 150, @@ -188,13 +215,13 @@ class _WidgetsPageState extends State { if (states.contains(WidgetState.selected)) { return AppConstants.primaryColor; } - return Colors.grey[200]; + return isDark ? Colors.grey[700] : Colors.grey[200]; }), foregroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return Colors.white; } - return Colors.black87; + return isDark ? Colors.white : Colors.black87; }), ), ), @@ -202,7 +229,7 @@ class _WidgetsPageState extends State { ); } - Widget _buildPreviewCard() { + Widget _buildPreviewCard(bool isDark) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), @@ -275,7 +302,7 @@ class _WidgetsPageState extends State { ); } - Widget _buildActionButton() { + Widget _buildActionButton(bool isDark) { return SizedBox( width: double.infinity, child: ElevatedButton( diff --git a/lib/views/profile/theme/app-diy.dart b/lib/views/profile/theme/app-diy.dart index fc6cf32..bd4707b 100644 --- a/lib/views/profile/theme/app-diy.dart +++ b/lib/views/profile/theme/app-diy.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:get/get.dart'; import '../../../constants/app_constants.dart'; +import '../../../services/get/theme_controller.dart'; /// 时间: 2026-03-27 /// 功能: 主题个性化设置页面 @@ -14,16 +15,13 @@ class AppDiyPage extends StatefulWidget { } class _AppDiyPageState extends State { - // 主题设置 - bool _isDarkMode = false; - int _themeColorIndex = 0; - int _fontSizeIndex = 1; // 0: 小, 1: 中, 2: 大 + // GetX ThemeController + late ThemeController _themeController; + + // 本地状态(不通过ThemeController管理的设置) bool _showGuideOnStartup = true; int _cardSizeIndex = 1; // 0: 小, 1: 中, 2: 大 - bool _enableAnimation = true; - bool _enableBlurEffect = true; bool _enableSystemNavigation = false; - int _accentColorIndex = 0; // 滚动控制 final ScrollController _scrollController = ScrollController(); @@ -58,7 +56,9 @@ class _AppDiyPageState extends State { @override void initState() { super.initState(); - _loadSettings(); + // 获取 ThemeController + _themeController = Get.find(); + _loadLocalSettings(); _startScrolling(); // 延迟显示开发中提示对话框 WidgetsBinding.instance.addPostFrameCallback((_) { @@ -148,145 +148,185 @@ class _AppDiyPageState extends State { } } - void _loadSettings() async { - final prefs = await SharedPreferences.getInstance(); + /// 加载本地设置(不通过ThemeController管理的设置) + void _loadLocalSettings() async { + // ThemeController 会自动加载主题相关设置 + // 这里只加载本地独有的设置 setState(() { - _isDarkMode = prefs.getBool('darkMode') ?? false; - _themeColorIndex = prefs.getInt('themeColorIndex') ?? 0; - _fontSizeIndex = prefs.getInt('fontSizeIndex') ?? 1; - _showGuideOnStartup = prefs.getBool('showGuideOnStartup') ?? true; - _cardSizeIndex = prefs.getInt('cardSizeIndex') ?? 1; - _enableAnimation = prefs.getBool('enableAnimation') ?? true; - _enableBlurEffect = prefs.getBool('enableBlurEffect') ?? true; + _showGuideOnStartup = + _themeController.prefs?.getBool('showGuideOnStartup') ?? true; + _cardSizeIndex = _themeController.prefs?.getInt('cardSizeIndex') ?? 1; _enableSystemNavigation = - prefs.getBool('enableSystemNavigation') ?? false; - _accentColorIndex = prefs.getInt('accentColorIndex') ?? 0; + _themeController.prefs?.getBool('enableSystemNavigation') ?? false; }); } - void _saveSettings() async { - final prefs = await SharedPreferences.getInstance(); - prefs.setBool('darkMode', _isDarkMode); - prefs.setInt('themeColorIndex', _themeColorIndex); - prefs.setInt('fontSizeIndex', _fontSizeIndex); - prefs.setBool('showGuideOnStartup', _showGuideOnStartup); - prefs.setInt('cardSizeIndex', _cardSizeIndex); - prefs.setBool('enableAnimation', _enableAnimation); - prefs.setBool('enableBlurEffect', _enableBlurEffect); - prefs.setBool('enableSystemNavigation', _enableSystemNavigation); - prefs.setInt('accentColorIndex', _accentColorIndex); + /// 保存本地设置 + void _saveLocalSettings() async { + await _themeController.prefs?.setBool( + 'showGuideOnStartup', + _showGuideOnStartup, + ); + await _themeController.prefs?.setInt('cardSizeIndex', _cardSizeIndex); + await _themeController.prefs?.setBool( + 'enableSystemNavigation', + _enableSystemNavigation, + ); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('个性化'), - backgroundColor: _isDarkMode ? Colors.grey[900] : Colors.white, - foregroundColor: _isDarkMode ? Colors.white : AppConstants.primaryColor, - elevation: 0, - ), - backgroundColor: _isDarkMode ? Colors.grey[900] : Colors.grey[50], - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - // 主题模式 - _buildSection('显示设置'), - _buildSwitchItem('深色模式', _isDarkMode, (value) { - setState(() { - _isDarkMode = value; - _saveSettings(); - }); - }, icon: Icons.dark_mode), + // 使用 Obx 监听 ThemeController 的状态变化 + return Obx(() { + final isDark = _themeController.isDarkModeRx.value; + final themeColorIdx = _themeController.themeColorIndexRx.value; - // 主题颜色 - _buildSection('主题颜色'), - _buildColorSelector('色彩', _themeColors, _themeColorIndex, (index) { - setState(() { - _themeColorIndex = index; - _saveSettings(); - }); - }), + return Scaffold( + appBar: AppBar( + title: const Text('个性化'), + backgroundColor: isDark ? Colors.grey[900] : Colors.white, + foregroundColor: isDark ? Colors.white : AppConstants.primaryColor, + elevation: 0, + ), + backgroundColor: isDark ? Colors.grey[900] : Colors.grey[50], + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // 主题模式 + _buildSection('显示设置', isDark), + _buildSwitchItem( + '深色模式', + _themeController.isDarkModeRx.value, + (value) => _themeController.toggleDarkMode(value), + icon: Icons.dark_mode, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - _buildColorSelector('强调色', _accentColors, _accentColorIndex, (index) { - setState(() { - _accentColorIndex = index; - _saveSettings(); - }); - }), + // 主题颜色 + _buildSection('主题颜色', isDark), + _buildColorSelector( + '色彩', + _themeColors, + _themeController.themeColorIndexRx.value, + (index) => _themeController.setThemeColorIndex(index), + isDark: isDark, + ), - // 字体大小 - _buildSection('字体设置'), - _buildOptionSelector('字体大小', _fontSizes, _fontSizeIndex, (index) { - if (index != null) { - setState(() { - _fontSizeIndex = index; - _saveSettings(); - }); - } - }, icon: Icons.font_download), + _buildColorSelector( + '强调色', + _accentColors, + _themeController.accentColorIndexRx.value, + (index) => _themeController.setAccentColorIndex(index), + isDark: isDark, + ), - // 卡片大小 - _buildSection('界面设置'), - _buildOptionSelector('卡片阴影', _cardSizes, _cardSizeIndex, (index) { - if (index != null) { - setState(() { - _cardSizeIndex = index; - _saveSettings(); - }); - } - }, icon: Icons.crop_square), + // 字体大小 + _buildSection('字体设置', isDark), + _buildOptionSelector( + '字体大小', + _fontSizes, + _themeController.fontSizeIndexRx.value, + (index) { + if (index != null) { + _themeController.setFontSizeIndex(index); + } + }, + icon: Icons.font_download, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - // 启动显示 - _buildSwitchItem('启动时显示引导页', _showGuideOnStartup, (value) { - setState(() { - _showGuideOnStartup = value; - _saveSettings(); - }); - }, icon: Icons.info_outline), + // 卡片大小 + _buildSection('界面设置', isDark), + _buildOptionSelector( + '卡片阴影', + _cardSizes, + _cardSizeIndex, + (index) { + if (index != null) { + setState(() { + _cardSizeIndex = index; + }); + _saveLocalSettings(); + } + }, + icon: Icons.crop_square, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - // 动画效果 - _buildSwitchItem('启用动画效果', _enableAnimation, (value) { - setState(() { - _enableAnimation = value; - _saveSettings(); - }); - }, icon: Icons.animation), + // 启动显示 + _buildSwitchItem( + '启动时显示引导页', + _showGuideOnStartup, + (value) { + setState(() { + _showGuideOnStartup = value; + }); + _saveLocalSettings(); + }, + icon: Icons.info_outline, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - // 模糊效果 - _buildSwitchItem('启用模糊效果', _enableBlurEffect, (value) { - setState(() { - _enableBlurEffect = value; - _saveSettings(); - }); - }, icon: Icons.blur_on), + // 动画效果 + _buildSwitchItem( + '启用动画效果', + _themeController.enableAnimationRx.value, + (value) => _themeController.toggleAnimation(value), + icon: Icons.animation, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - // 系统导航 - _buildSwitchItem('软件悬浮球', _enableSystemNavigation, (value) { - setState(() { - _enableSystemNavigation = value; - _saveSettings(); - }); - }, icon: Icons.navigation), + // 模糊效果 + _buildSwitchItem( + '启用模糊效果', + _themeController.enableBlurEffectRx.value, + (value) => _themeController.toggleBlurEffect(value), + icon: Icons.blur_on, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - _buildSwitchItem('转场动画', _enableAnimation, (value) { - setState(() { - // _enableAnimation = value; - _saveSettings(); - }); - }, icon: Icons.track_changes), + // 系统导航 + _buildSwitchItem( + '软件悬浮球', + _enableSystemNavigation, + (value) { + setState(() { + _enableSystemNavigation = value; + }); + _saveLocalSettings(); + }, + icon: Icons.navigation, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - // 设计风格 - _buildSection('设计风格'), - _buildDesignStyleCard(), + _buildSwitchItem( + '转场动画', + _themeController.enableAnimationRx.value, + (value) => _themeController.toggleAnimation(value), + icon: Icons.track_changes, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), - const SizedBox(height: 40), - ], - ), - ); + // 设计风格 + _buildSection('设计风格', isDark), + _buildDesignStyleCard(isDark, themeColorIdx), + + const SizedBox(height: 40), + ], + ), + ); + }); } - Widget _buildSection(String title) { + Widget _buildSection(String title, bool isDark) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8), child: Text( @@ -294,7 +334,7 @@ class _AppDiyPageState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: _isDarkMode ? Colors.grey[300] : Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ); @@ -305,11 +345,13 @@ class _AppDiyPageState extends State { bool value, ValueChanged onChanged, { required IconData icon, + required bool isDark, + required int themeColorIdx, }) { return Container( margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: _isDarkMode ? Colors.grey[800] : Colors.white, + color: isDark ? Colors.grey[800] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -320,15 +362,15 @@ class _AppDiyPageState extends State { ], ), child: ListTile( - leading: Icon(icon, color: _themeColors[_themeColorIndex]), + leading: Icon(icon, color: _themeColors[themeColorIdx]), title: Text( title, - style: TextStyle(color: _isDarkMode ? Colors.white : Colors.black87), + style: TextStyle(color: isDark ? Colors.white : Colors.black87), ), trailing: Switch( value: value, onChanged: onChanged, - activeThumbColor: _themeColors[_themeColorIndex], + activeThumbColor: _themeColors[themeColorIdx], ), ), ); @@ -340,11 +382,13 @@ class _AppDiyPageState extends State { int selectedIndex, ValueChanged onChanged, { required IconData icon, + required bool isDark, + required int themeColorIdx, }) { return Container( margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: _isDarkMode ? Colors.grey[800] : Colors.white, + color: isDark ? Colors.grey[800] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -355,10 +399,10 @@ class _AppDiyPageState extends State { ], ), child: ListTile( - leading: Icon(icon, color: _themeColors[_themeColorIndex]), + leading: Icon(icon, color: _themeColors[themeColorIdx]), title: Text( title, - style: TextStyle(color: _isDarkMode ? Colors.white : Colors.black87), + style: TextStyle(color: isDark ? Colors.white : Colors.black87), ), trailing: DropdownButton( value: selectedIndex, @@ -372,14 +416,12 @@ class _AppDiyPageState extends State { value: entry.key, child: Text( entry.value, - style: TextStyle( - color: _isDarkMode ? Colors.white : Colors.black87, - ), + style: TextStyle(color: isDark ? Colors.white : Colors.black87), ), ); }).toList(), - dropdownColor: _isDarkMode ? Colors.grey[800] : Colors.white, - style: TextStyle(color: _isDarkMode ? Colors.white : Colors.black87), + dropdownColor: isDark ? Colors.grey[800] : Colors.white, + style: TextStyle(color: isDark ? Colors.white : Colors.black87), ), ), ); @@ -389,12 +431,13 @@ class _AppDiyPageState extends State { String title, List colors, int selectedIndex, - ValueChanged onChanged, - ) { + ValueChanged onChanged, { + required bool isDark, + }) { return Container( margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: _isDarkMode ? Colors.grey[800] : Colors.white, + color: isDark ? Colors.grey[800] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -407,7 +450,7 @@ class _AppDiyPageState extends State { child: ListTile( title: Text( title, - style: TextStyle(color: _isDarkMode ? Colors.white : Colors.black87), + style: TextStyle(color: isDark ? Colors.white : Colors.black87), ), trailing: Row( mainAxisSize: MainAxisSize.min, @@ -423,7 +466,7 @@ class _AppDiyPageState extends State { shape: BoxShape.circle, border: entry.key == selectedIndex ? Border.all( - color: _isDarkMode ? Colors.white : Colors.black, + color: isDark ? Colors.white : Colors.black, width: 2, ) : null, @@ -436,11 +479,11 @@ class _AppDiyPageState extends State { ); } - Widget _buildDesignStyleCard() { + Widget _buildDesignStyleCard(bool isDark, int themeColorIdx) { return Container( margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: _isDarkMode ? Colors.grey[800] : Colors.white, + color: isDark ? Colors.grey[800] : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -464,7 +507,7 @@ class _AppDiyPageState extends State { ), child: Icon( Icons.design_services, - color: _isDarkMode ? Colors.teal[300] : Colors.teal[700], + color: isDark ? Colors.teal[300] : Colors.teal[700], size: 20, ), ), @@ -474,7 +517,7 @@ class _AppDiyPageState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: _isDarkMode ? Colors.white : Colors.black87, + color: isDark ? Colors.white : Colors.black87, ), ), ], @@ -487,7 +530,7 @@ class _AppDiyPageState extends State { children: [ GestureDetector( onTap: _toggleScroll, - child: Container( + child: SizedBox( height: 40, child: SingleChildScrollView( controller: _scrollController, @@ -498,19 +541,38 @@ class _AppDiyPageState extends State { 'Material 2', Icons.style, isMd: true, + isDark: isDark, + themeColorIdx: themeColorIdx, ), const SizedBox(width: 8), _buildStyleChip( 'Material 3', Icons.auto_awesome, isMd: true, + isDark: isDark, + themeColorIdx: themeColorIdx, ), const SizedBox(width: 8), - _buildStyleChip('透明毛玻璃', Icons.blur_on), + _buildStyleChip( + '透明毛玻璃', + Icons.blur_on, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), const SizedBox(width: 8), - _buildStyleChip('沉浸式渐变色', Icons.color_lens), + _buildStyleChip( + '沉浸式渐变色', + Icons.color_lens, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), const SizedBox(width: 8), - _buildStyleChip('动态光感效果', Icons.lightbulb_outline), + _buildStyleChip( + '动态光感效果', + Icons.lightbulb_outline, + isDark: isDark, + themeColorIdx: themeColorIdx, + ), const SizedBox(width: 8), ], ), @@ -523,8 +585,8 @@ class _AppDiyPageState extends State { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - _themeColors[_themeColorIndex].withAlpha(10), - _themeColors[_themeColorIndex].withAlpha(5), + _themeColors[themeColorIdx].withAlpha(10), + _themeColors[themeColorIdx].withAlpha(5), ], ), borderRadius: BorderRadius.circular(8), @@ -534,7 +596,7 @@ class _AppDiyPageState extends State { Icon( Icons.lightbulb_outline, size: 16, - color: _themeColors[_themeColorIndex], + color: _themeColors[themeColorIdx], ), const SizedBox(width: 8), Expanded( @@ -542,9 +604,7 @@ class _AppDiyPageState extends State { '采用现代Material Design设计语言,\n参考透明毛玻璃、沉浸式渐变色和动态光感等效果', style: TextStyle( fontSize: 12, - color: _isDarkMode - ? Colors.grey[300] - : Colors.grey[700], + color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ), @@ -559,10 +619,16 @@ class _AppDiyPageState extends State { ); } - Widget _buildStyleChip(String label, IconData icon, {bool isMd = false}) { + Widget _buildStyleChip( + String label, + IconData icon, { + bool isMd = false, + required bool isDark, + required int themeColorIdx, + }) { final color = isMd - ? _themeColors[_themeColorIndex] - : _isDarkMode + ? _themeColors[themeColorIdx] + : isDark ? Colors.blue[400]! : Colors.blue[600]!; diff --git a/lib/widgets/main_navigation.dart b/lib/widgets/main_navigation.dart new file mode 100644 index 0000000..208c95e --- /dev/null +++ b/lib/widgets/main_navigation.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../constants/app_constants.dart'; +import '../views/home/home_page.dart'; +import '../views/discover_page.dart'; +import '../views/favorites_page.dart'; +import '../views/profile/profile_page.dart'; +import '../services/get/main_navigation_controller.dart'; +import '../services/get/profile_controller.dart'; +import '../services/get/theme_controller.dart'; +import '../services/get/tap_liquid_glass_controller.dart'; +import 'tap-liquid-glass.dart'; + +/// 时间: 2025-03-21 +/// 功能: 主导航页面,包含底部导航栏和4个主要页面 +/// 介绍: 这是应用的主要导航容器,提供底部导航栏来切换主页、发现、收藏和个人页面 +/// 最新变化: 使用 Stack 布局实现 Tap 沉浸光感液态玻璃导航栏悬浮效果 + +class MainNavigation extends StatelessWidget { + const MainNavigation({super.key}); + + @override + Widget build(BuildContext context) { + // 初始化导航控制器(只初始化一次) + Get.lazyPut(() => MainNavigationController()); + Get.lazyPut(() => TapLiquidGlassController()); + final controller = Get.find(); + final themeController = Get.find(); + final glassController = Get.find(); + + final List pages = [ + HomePage(), + const DiscoverPage(), + const FavoritesPage(), + const ProfilePage(), + ]; + + final List bottomNavItems = [ + const BottomNavigationBarItem( + icon: Icon(Icons.home), + activeIcon: Icon(Icons.home, color: AppConstants.primaryColor), + label: '主页', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.explore), + activeIcon: Icon(Icons.explore, color: AppConstants.primaryColor), + label: '发现', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.favorite_border), + activeIcon: Icon(Icons.favorite, color: AppConstants.primaryColor), + label: '收藏', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.person_outline), + activeIcon: Icon(Icons.person, color: AppConstants.primaryColor), + label: '个人', + ), + ]; + + return Obx(() { + final isDark = themeController.isDarkMode; + final useGlass = glassController.isEnabled; + + return Scaffold( + body: useGlass + ? _buildGlassBody(controller, pages) + : _buildClassicBody(controller, pages, isDark, bottomNavItems), + ); + }); + } + + /// 构建液态玻璃导航栏主体 + /// 使用 Stack 布局,让导航栏悬浮在页面内容上方 + Widget _buildGlassBody( + MainNavigationController controller, + List pages, + ) { + return Stack( + children: [ + // 页面内容 - 延伸到屏幕底部 + IndexedStack( + index: controller.currentIndex.value, + children: pages, + ), + // 悬浮的液态玻璃导航栏 + Positioned( + left: 0, + right: 0, + bottom: 0, + child: TapLiquidGlassNavigation( + currentIndex: controller.currentIndex.value, + onTap: (index) { + controller.switchPage(index); + if (index == 3) { + final profileController = Get.find(); + profileController.onInit(); + } + }, + ), + ), + ], + ); + } + + /// 构建经典导航栏主体 + Widget _buildClassicBody( + MainNavigationController controller, + List pages, + bool isDark, + List items, + ) { + return Column( + children: [ + // 页面内容 + Expanded( + child: IndexedStack( + index: controller.currentIndex.value, + children: pages, + ), + ), + // 经典底部导航栏 + Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF2A2A2A) : Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.1), + blurRadius: 8, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + top: false, + child: BottomNavigationBar( + currentIndex: controller.currentIndex.value, + onTap: (index) { + controller.switchPage(index); + if (index == 3) { + final profileController = Get.find(); + profileController.onInit(); + } + }, + type: BottomNavigationBarType.fixed, + selectedItemColor: AppConstants.primaryColor, + unselectedItemColor: isDark ? Colors.grey[400] : Colors.grey[600], + backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, + elevation: 0, + items: items, + selectedFontSize: 12, + unselectedFontSize: 12, + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/tabbed_nav_app_bar.dart b/lib/widgets/tabbed_nav_app_bar.dart index af6ae20..fb715c9 100644 --- a/lib/widgets/tabbed_nav_app_bar.dart +++ b/lib/widgets/tabbed_nav_app_bar.dart @@ -4,7 +4,7 @@ import '../constants/app_constants.dart'; /// 时间: 2026-03-22 /// 功能: 主导航内「标题 + Tab」共用 AppBar 构造 /// 介绍: 压缩工具栏与 Tab 行高度,关闭 M3 卷动 surface tint,统一收藏页与发现页顶部观感 -/// 最新变化: 初始提取,减少两页相同结构的顶部留白 +/// 最新变化: 2026-04-02 支持深色模式 /// 主导航子页(IndexedStack 内)带 [TabBar] 的页面共用 [AppBar] 配置 class TabbedNavAppBar { @@ -19,19 +19,26 @@ class TabbedNavAppBar { bool tabBarScrollable = false, EdgeInsetsGeometry? tabPadding, EdgeInsetsGeometry? tabLabelPadding, + Color? backgroundColor, + Color? foregroundColor, }) { + final isDark = backgroundColor != null && backgroundColor != Colors.white; + return AppBar( title: Text( title, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18, - color: Colors.black87, + color: foregroundColor ?? (isDark ? Colors.white : Colors.black87), ), ), - backgroundColor: Colors.white, - foregroundColor: Colors.black87, - iconTheme: const IconThemeData(color: Colors.black87), + backgroundColor: backgroundColor ?? Colors.white, + foregroundColor: + foregroundColor ?? (isDark ? Colors.white : Colors.black87), + iconTheme: IconThemeData( + color: foregroundColor ?? (isDark ? Colors.white : Colors.black87), + ), elevation: 0, scrolledUnderElevation: 0, surfaceTintColor: Colors.transparent, @@ -49,7 +56,7 @@ class TabbedNavAppBar { dividerColor: Colors.transparent, tabs: tabLabels.map((String e) => Tab(text: e)).toList(), labelColor: AppConstants.primaryColor, - unselectedLabelColor: Colors.grey[600], + unselectedLabelColor: isDark ? Colors.grey[400] : Colors.grey[600], indicator: UnderlineTabIndicator( borderSide: BorderSide(color: AppConstants.primaryColor, width: 3), ), diff --git a/lib/widgets/tap-liquid-glass.dart b/lib/widgets/tap-liquid-glass.dart new file mode 100644 index 0000000..63a075a --- /dev/null +++ b/lib/widgets/tap-liquid-glass.dart @@ -0,0 +1,212 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../constants/app_constants.dart'; +import '../config/app_config.dart'; +import '../services/get/theme_controller.dart'; +import '../services/get/tap_liquid_glass_controller.dart'; + +class TapLiquidGlassNavigation extends StatelessWidget { + final int currentIndex; + final Function(int) onTap; + + const TapLiquidGlassNavigation({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final themeController = Get.find(); + final glassController = Get.find(); + + return Obx(() { + final isDark = themeController.isDarkMode; + final enableBlur = themeController.enableBlurEffect; + final transparencyValues = glassController.transparencyValues; + return _buildGlassBar(context, isDark, enableBlur, transparencyValues); + }); + } + + Widget _buildGlassBar( + BuildContext context, + bool isDark, + bool enableBlur, + Map transparencyValues, + ) { + final backgroundOpacity = transparencyValues['backgroundOpacity']!; + return SafeArea( + top: false, + child: Container( + padding: EdgeInsets.only( + left: AppConfig.liquidGlassHorizontalMargin, + right: AppConfig.liquidGlassHorizontalMargin, + bottom: AppConfig.liquidGlassBottomMargin, + top: 8, + ), + child: Container( + height: AppConfig.liquidGlassHeight, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.liquidGlassCornerRadius, + ), + boxShadow: backgroundOpacity > 0.2 + ? [ + BoxShadow( + color: isDark + ? Colors.black.withValues(alpha: 0.6) + : Colors.black.withValues(alpha: 0.15), + blurRadius: 35, + spreadRadius: -10, + offset: const Offset(0, 12), + ), + ] + : [], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + AppConfig.liquidGlassCornerRadius, + ), + child: enableBlur && backgroundOpacity >= 0.01 + ? BackdropFilter( + filter: ImageFilter.blur( + sigmaX: AppConfig.liquidGlassBlur, + sigmaY: AppConfig.liquidGlassBlur, + ), + child: _buildGlassContent(isDark, backgroundOpacity), + ) + : _buildGlassContent(isDark, backgroundOpacity), + ), + ), + ), + ); + } + + Widget _buildGlassContent(bool isDark, double backgroundOpacity) { + return Stack( + children: [ + if (backgroundOpacity >= 0.1) + Container( + decoration: BoxDecoration( + color: isDark + ? Colors.black.withValues(alpha: backgroundOpacity * 0.4) + : Colors.white.withValues(alpha: backgroundOpacity), + borderRadius: BorderRadius.circular( + AppConfig.liquidGlassCornerRadius, + ), + ), + ), + if (backgroundOpacity >= 0.15) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.liquidGlassCornerRadius, + ), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: backgroundOpacity * 0.2) + : Colors.black.withValues(alpha: backgroundOpacity * 0.1), + width: 0.6, + ), + ), + ), + _buildNavItems(), + ], + ); + } + + Widget _buildNavItems() { + final items = [ + _NavItem(Icons.home_rounded, '🏠', '主页'), + _NavItem(Icons.explore_rounded, '🔍', '发现'), + _NavItem(Icons.favorite_border_rounded, '❤️', '收藏'), + _NavItem(Icons.person_outline_rounded, '👤', '个人'), + ]; + + return Row( + children: List.generate(items.length, (index) { + return Expanded( + child: _buildNavItem( + items[index], + index == currentIndex, + () => onTap(index), + ), + ); + }), + ); + } + + Widget _buildNavItem(_NavItem item, bool isSelected, VoidCallback onTap) { + final themeController = Get.find(); + + return Obx(() { + final isDark = themeController.isDarkMode; + final enableAnimation = themeController.enableAnimation; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + borderRadius: BorderRadius.circular( + AppConfig.liquidGlassCornerRadius, + ), + child: AnimatedContainer( + duration: enableAnimation + ? const Duration(milliseconds: 250) + : Duration.zero, + curve: Curves.easeOutCubic, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + enableAnimation + ? AnimatedScale( + scale: isSelected ? 1.1 : 1.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + child: _buildIcon(item, isSelected, isDark), + ) + : _buildIcon(item, isSelected, isDark), + const SizedBox(height: 3), + AnimatedDefaultTextStyle( + duration: enableAnimation + ? const Duration(milliseconds: 200) + : Duration.zero, + style: TextStyle( + fontSize: 12, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + color: isSelected + ? AppConstants.primaryColor + : (isDark ? Colors.grey[400] : Colors.grey[600]), + letterSpacing: 0.15, + ), + child: Text(item.label), + ), + ], + ), + ), + ), + ); + }); + } + + Widget _buildIcon(_NavItem item, bool isSelected, bool isDark) { + return Icon( + item.icon, + size: 24, + color: isSelected + ? AppConstants.primaryColor + : (isDark ? Colors.grey[400] : Colors.grey[600]), + ); + } +} + +class _NavItem { + final IconData icon; + final String emoji; + final String label; + + _NavItem(this.icon, this.emoji, this.label); +} diff --git a/ohos/AppScope/app.json5 b/ohos/AppScope/app.json5 index 819b4d4..cd55b32 100644 --- a/ohos/AppScope/app.json5 +++ b/ohos/AppScope/app.json5 @@ -3,7 +3,7 @@ "bundleName": "app.wushu.poes", "vendor": "微风暴", "versionCode": 26040101, - "versionName": "1.3.1", + "versionName": "1.4.1", "icon": "$media:app_icon", "label": "$string:app_name" } diff --git a/ohos/build-profile.json5 b/ohos/build-profile.json5 index f599cc9..0fbc844 100644 --- a/ohos/build-profile.json5 +++ b/ohos/build-profile.json5 @@ -5,26 +5,26 @@ "name": "default", "type": "HarmonyOS", "material": { - "certpath": "C:/Users/无书/.ohos/config/default_ohos_hWyPJmkoRvc13RxcUC3NSaiad5bS6MHBrY9nkKd-288=.cer", + "certpath": "C:\\Users\\无书\\.ohos\\config\\default_ohos_hWyPJmkoRvc13RxcUC3NSaiad5bS6MHBrY9nkKd-288=.cer", "keyAlias": "debugKey", - "keyPassword": "0000001B675E31900A26DD43D5BAE7CDC29AC560497DAE61594206507C57058DF31D24E44F7E7847222D72", - "profile": "C:/Users/无书/.ohos/config/default_ohos_hWyPJmkoRvc13RxcUC3NSaiad5bS6MHBrY9nkKd-288=.p7b", + "keyPassword": "0000001BD23894F1C728472C15E1CFD5A35D53C14AAA5CB668AFB407AA9A688319F47FCE244C0526BD541D", + "profile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_hWyPJmkoRvc13RxcUC3NSaiad5bS6MHBrY9nkKd-288=.p7b", "signAlg": "SHA256withECDSA", - "storeFile": "C:/Users/无书/.ohos/config/default_ohos_hWyPJmkoRvc13RxcUC3NSaiad5bS6MHBrY9nkKd-288=.p12", - "storePassword": "0000001BC2CC19FF457937BA7ADA3E0A3DD48065FC3A82D6186E7055C2164A061265A691D49E0783D3A718" + "storeFile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_hWyPJmkoRvc13RxcUC3NSaiad5bS6MHBrY9nkKd-288=.p12", + "storePassword": "0000001B9419E1D96E288BAD3910A8E0B1BE70A8D56BA67EEA0AF12AD6C4B3035E526CA218229DDEEEB875" } }, { "name": "release", "type": "HarmonyOS", "material": { - "certpath": "D:/zhshu/520kiss123.cer", + "certpath": "D:/zhshu/520kiss123Release .cer", "keyAlias": "520kiss123", - "keyPassword": "0000001ADC811A155E80C47E61C324586F0FA08B1F383DB23026B17B7076AAE123458B9A5FE2DE812751", - "profile": "D:/zhshu/520kiss123Release.p7b", + "keyPassword": "0000001A770D27C715C2C76CD225BA13525637D1AB00DD6553534835E2BFB6E3C99FED62A418FF950EE3", + "profile": "D:/zhshu/520kiss123Release .p7b", "signAlg": "SHA256withECDSA", "storeFile": "D:/zhshu/520kiss123.p12", - "storePassword": "0000001AA1C222C2D3454B69525FC0FFF7AD37F3966FCDC782DC9BF88D3F5A241892C1FB35EB36B01888" + "storePassword": "0000001A9357A9E29E7DA423D4BA05B8410FA43D4D4364A3D93B4478502F07FAE3CDFC2EB4CD5B945CF8" } } ], diff --git a/ohos/entry/src/main/ets/pages/WidgetQuoteSettings.ets b/ohos/entry/src/main/ets/pages/WidgetQuoteSettings.ets index 042dff3..a10add9 100644 --- a/ohos/entry/src/main/ets/pages/WidgetQuoteSettings.ets +++ b/ohos/entry/src/main/ets/pages/WidgetQuoteSettings.ets @@ -71,7 +71,7 @@ struct WidgetQuoteSettings { async aboutToAppear() { try { - const params = router.getParams() as RouterParams; + const params = router.getState().params as RouterParams; console.info('[WidgetQuoteSettings] router params: ' + JSON.stringify(params)); if (params && params.formName) { this.formName = params.formName; @@ -143,7 +143,7 @@ struct WidgetQuoteSettings { async loadSettings() { try { - const context = getContext(this); + const context = getContext(this) as common.UIAbilityContext; this.preference = await preferences.getPreferences(context, 'widget_quote_settings'); this.selectedStyle = await this.preference.get('style', 'classic') as string; @@ -199,7 +199,7 @@ struct WidgetQuoteSettings { this.isSaving = true; try { if (!this.preference) { - const context = getContext(this); + const context = getContext(this) as common.UIAbilityContext; this.preference = await preferences.getPreferences(context, 'widget_quote_settings'); } diff --git a/ohos/entry/src/main/ets/pages/WidgetSettings.ets b/ohos/entry/src/main/ets/pages/WidgetSettings.ets index 34eeaaa..36ab688 100644 --- a/ohos/entry/src/main/ets/pages/WidgetSettings.ets +++ b/ohos/entry/src/main/ets/pages/WidgetSettings.ets @@ -1,5 +1,6 @@ import { router } from '@kit.ArkUI'; import { preferences } from '@kit.ArkData'; +import { common } from '@kit.AbilityKit'; //todo 重要 后续 刷新按钮要求固定显示 @Entry @Component @@ -45,7 +46,7 @@ struct WidgetSettings { async saveSettings() { try { if (!this.preference) { - const context = getContext(this); + const context = getContext(this) as common.UIAbilityContext; this.preference = await preferences.getPreferences(context, 'widget_settings'); } console.info('[WidgetSettings] Saving settings - showTime: ' + this.showTime + ', showWeather: ' + this.showWeather); diff --git a/packages/audioplayers/LICENSE b/packages/audioplayers/LICENSE new file mode 100644 index 0000000..1a581b0 --- /dev/null +++ b/packages/audioplayers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Blue Fire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/audioplayers/lib/audioplayers.dart b/packages/audioplayers/lib/audioplayers.dart new file mode 100644 index 0000000..a55f946 --- /dev/null +++ b/packages/audioplayers/lib/audioplayers.dart @@ -0,0 +1,16 @@ +export 'package:audioplayers_platform_interface/src/api/audio_context.dart'; +export 'package:audioplayers_platform_interface/src/api/audio_context_config.dart'; +export 'package:audioplayers_platform_interface/src/api/audio_event.dart'; +export 'package:audioplayers_platform_interface/src/api/global_audio_event.dart'; +export 'package:audioplayers_platform_interface/src/api/player_mode.dart'; +export 'package:audioplayers_platform_interface/src/api/player_state.dart'; +export 'package:audioplayers_platform_interface/src/api/release_mode.dart'; + +export 'src/audio_cache.dart'; +export 'src/audio_log_level.dart'; +export 'src/audio_logger.dart'; +export 'src/audio_pool.dart'; +export 'src/audioplayer.dart'; +export 'src/global_audio_scope.dart'; +export 'src/position_updater.dart'; +export 'src/source.dart'; diff --git a/packages/audioplayers/lib/src/audio_cache.dart b/packages/audioplayers/lib/src/audio_cache.dart new file mode 100644 index 0000000..b619f81 --- /dev/null +++ b/packages/audioplayers/lib/src/audio_cache.dart @@ -0,0 +1,181 @@ +import 'dart:async'; + +import 'package:audioplayers/src/uri_ext.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; + +const _uuid = Uuid(); + +/// This class represents a cache for Local Assets to be played. +/// +/// On desktop/mobile, Flutter can only play audios on device folders, so first +/// this class copies asset files to a temporary folder, and then holds a +/// reference to the file. +/// +/// On web, it just stores a reference to the URL of the audio, but it gets +/// preloaded by making a simple GET request (the browser then takes care of +/// caching). +/// +/// You can pre-cache your audio, or clear the cache, as desired. +/// For most normal uses, the static instance is used. But if you want to +/// control multiple caches, you can create your own instances. +class AudioCache { + /// A globally accessible instance used by default by all players. + static AudioCache instance = AudioCache(); + + @visibleForTesting + static FileSystem fileSystem = const LocalFileSystem(); + + /// A reference to the loaded files absolute URLs. + /// + /// This is a map of fileNames to pre-loaded URIs. + /// On mobile/desktop, the URIs are from local files where the bytes have been + /// copied. + /// On web, the URIs are external links for pre-loaded files. + final Map loadedFiles = {}; + + /// This is the path inside your assets folder where your files lie. + /// + /// For example, Flame uses the prefix 'assets/audio/' + /// (you must include the final slash!). + /// The default prefix (if not provided) is 'assets/' + /// Your files will be found at `` (so the trailing slash is + /// crucial). + String prefix; + + /// An unique ID generated for this instance of [AudioCache]. + /// + /// This is used to load a file into an unique location in the temporary + /// directory. + String? cacheId; + + AudioCache({this.prefix = 'assets/', String? cacheId}) + : cacheId = cacheId ?? _uuid.v4(); + + /// Clears the cache for the file [fileName]. + /// + /// Does nothing if the file was not on cache. + /// Note: web relies on the browser cache which is handled entirely by the + /// browser, thus this will no-op. + Future clear(String fileName) async { + await _clearFile(fileName); + loadedFiles.remove(fileName); + } + + Future _clearFile(String fileName) async { + final uri = loadedFiles[fileName]; + if (uri != null && !kIsWeb) { + await fileSystem.file(uri.toFilePath(windows: false)).delete(); + } + } + + /// Clears the whole cache. + Future clearAll() async { + await Future.wait(loadedFiles.keys.map(_clearFile)); + loadedFiles.clear(); + } + + @visibleForTesting + Future loadAsset(String path) => rootBundle.load(path); + + @visibleForTesting + Future getTempDir() async => (await getTemporaryDirectory()).path; + + Future fetchToMemory(String fileName) async { + if (kIsWeb) { + final uri = _sanitizeURLForWeb(fileName); + // We rely on browser caching here. Once the browser downloads this file, + // the native side implementation should be able to access it from cache. + await http.get(uri); + return uri; + } + + // read local asset from rootBundle + final byteData = await loadAsset('$prefix$fileName'); + + // create a temporary file on the device to be read by the native side + final file = fileSystem.file('${await getTempDir()}/$cacheId/$fileName'); + await file.create(recursive: true); + await file.writeAsBytes(byteData.buffer.asUint8List()); + + // returns the local file uri + return file.uri; + } + + Uri _sanitizeURLForWeb(String fileName) { + final tryAbsolute = Uri.tryParse(fileName); + if (tryAbsolute?.isAbsolute ?? false) { + return tryAbsolute!; + } + + // Relative Asset path + // URL-encode twice, see: + // https://github.com/flutter/engine/blob/2d39e672c95efc6c539d9b48b2cccc65df290cc4/lib/web_ui/lib/ui_web/src/ui_web/asset_manager.dart#L61 + // Parsing an already encoded string to an Uri does not encode it a second + // time, so we have to do it manually: + final encoded = UriCoder.encodeOnce(fileName); + return Uri.parse(Uri.encodeFull('assets/$prefix$encoded')); + } + + /// Loads a single [fileName] to the cache. + /// + /// Returns a [Uri] to access that file. + Future load(String fileName) async { + var needsFetch = !loadedFiles.containsKey(fileName); + + // On Android, verify that the cached file still exists. It can be removed + // by the system when the storage is almost full + // see https://developer.android.com/training/data-storage/app-specific#internal-remove-cache + if (!needsFetch && + defaultTargetPlatform == TargetPlatform.android && + !await fileSystem.file(loadedFiles[fileName]).exists()) { + needsFetch = true; + } + + if (needsFetch) { + loadedFiles[fileName] = await fetchToMemory(fileName); + } + return loadedFiles[fileName]!; + } + + /// Loads a single [fileName] to the cache. + /// + /// Returns a decoded [String] to access that file. + Future loadPath(String fileName) async { + final encodedPath = (await load(fileName)).path; + // Web needs an url double-encoded path. + // Darwin needs a decoded path for local files. + return kIsWeb ? encodedPath : Uri.decodeFull(encodedPath); + } + + /// Loads a single [fileName] to the cache but returns it as a File. + /// + /// Note: this is not available for web, as File doesn't make sense on the + /// browser! + Future loadAsFile(String fileName) async { + if (kIsWeb) { + throw 'This method cannot be used on web!'; + } + final uri = await load(fileName); + return fileSystem.file( + uri.toFilePath(windows: defaultTargetPlatform == TargetPlatform.windows), + ); + } + + /// Loads a single [fileName] to the cache but returns it as a list of bytes. + Future loadAsBytes(String fileName) async { + return (await loadAsFile(fileName)).readAsBytes(); + } + + /// Loads all the [fileNames] provided to the cache. + /// + /// Also returns a list of [Future]s for those files. + Future> loadAll(List fileNames) async { + return Future.wait(fileNames.map(load)); + } +} diff --git a/packages/audioplayers/lib/src/audio_log_level.dart b/packages/audioplayers/lib/src/audio_log_level.dart new file mode 100644 index 0000000..aa71252 --- /dev/null +++ b/packages/audioplayers/lib/src/audio_log_level.dart @@ -0,0 +1,16 @@ +enum AudioLogLevel implements Comparable { + none(0), + error(1), + info(2); + + const AudioLogLevel(this.level); + + factory AudioLogLevel.fromInt(int level) { + return values.firstWhere((e) => e.level == level); + } + + final int level; + + @override + int compareTo(AudioLogLevel other) => level - other.level; +} diff --git a/packages/audioplayers/lib/src/audio_logger.dart b/packages/audioplayers/lib/src/audio_logger.dart new file mode 100644 index 0000000..c629415 --- /dev/null +++ b/packages/audioplayers/lib/src/audio_logger.dart @@ -0,0 +1,46 @@ +import 'package:audioplayers/audioplayers.dart'; + +class AudioLogger { + static AudioLogLevel logLevel = AudioLogLevel.error; + + static void log(String message) { + if (AudioLogLevel.info.level <= logLevel.level) { + // ignore: avoid_print + print('AudioPlayers Log: $message'); + } + } + + static void error(Object o, [StackTrace? stacktrace]) { + if (AudioLogLevel.error.level <= logLevel.level) { + // ignore: avoid_print + print(_errorColor(errorToString(o, stacktrace))); + } + } + + static String errorToString(Object o, [StackTrace? stackTrace]) { + String errStr; + if (o is Error) { + errStr = 'AudioPlayers Error: $o\n${o.stackTrace}'; + } else if (o is Exception) { + errStr = 'AudioPlayers Exception: $o'; + } else { + errStr = 'AudioPlayers throw: $o'; + } + if (stackTrace != null && stackTrace.toString().isNotEmpty) { + errStr += '\n$stackTrace'; + } + return errStr; + } + + static String _errorColor(String text) => '\x1B[31m$text\x1B[0m'; +} + +class AudioPlayerException implements Exception { + Object? cause; + AudioPlayer player; + + AudioPlayerException(this.player, {this.cause}); + + @override + String toString() => 'AudioPlayerException(\n\t${player.source}, \n\t$cause'; +} diff --git a/packages/audioplayers/lib/src/audio_pool.dart b/packages/audioplayers/lib/src/audio_pool.dart new file mode 100644 index 0000000..1d84f26 --- /dev/null +++ b/packages/audioplayers/lib/src/audio_pool.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/foundation.dart'; +import 'package:synchronized/synchronized.dart'; + +/// Represents a function that can stop an audio playing. +typedef StopFunction = Future Function(); + +/// An AudioPool is a provider of AudioPlayers that are pre-loaded with an asset +/// to minimize delays. +/// +/// All AudioPlayers are loaded with the same audio [source]. +/// If you want multiple sounds use multiple [AudioPool]s. +/// +/// Use this class if you for example have extremely quick firing, repetitive +/// or simultaneous sounds. +class AudioPool { + @visibleForTesting + final Map currentPlayers = {}; + @visibleForTesting + final List availablePlayers = []; + + /// Instance of [AudioCache] to be used by all players. + final AudioCache audioCache; + + /// Platform specific configuration. + final AudioContext? audioContext; + + /// The source of the sound for this pool. + final Source source; + + /// The minimum numbers of players, this is the amount of players that the + /// pool is initialized with. + final int minPlayers; + + /// The maximum number of players to be kept in the pool. + /// + /// If `start` is called after the pool is full there will still be new + /// [AudioPlayer]s created, but once they are stopped they will not be + /// returned to the pool. + final int maxPlayers; + + /// Whether the players in this pool use low latency mode. + final PlayerMode playerMode; + + /// Lock to synchronize access to the pool. + final Lock _lock = Lock(); + + AudioPool._({ + required this.minPlayers, + required this.maxPlayers, + required this.source, + required this.audioContext, + this.playerMode = PlayerMode.mediaPlayer, + AudioCache? audioCache, + }) : audioCache = audioCache ?? AudioCache.instance; + + /// Creates an [AudioPool] instance with the given parameters. + /// You will have to manage disposing the players if you choose + /// PlayerMode.lowLatency. + static Future create({ + required Source source, + required int maxPlayers, + AudioCache? audioCache, + AudioContext? audioContext, + int minPlayers = 1, + PlayerMode playerMode = PlayerMode.mediaPlayer, + }) async { + final instance = AudioPool._( + source: source, + audioCache: audioCache, + maxPlayers: maxPlayers, + minPlayers: minPlayers, + playerMode: playerMode, + audioContext: audioContext, + ); + + final players = []; + + for (var i = 0; i < minPlayers; i++) { + players.add(await instance._createNewAudioPlayer()); + } + + return instance..availablePlayers.addAll(players); + } + + /// Creates an [AudioPool] instance with the asset from the given [path]. + static Future createFromAsset({ + required String path, + required int maxPlayers, + AudioCache? audioCache, + int minPlayers = 1, + PlayerMode playerMode = PlayerMode.mediaPlayer, + }) async { + return create( + source: AssetSource(path), + audioCache: audioCache, + minPlayers: minPlayers, + maxPlayers: maxPlayers, + playerMode: playerMode, + ); + } + + /// Starts playing the audio, returns a function that can stop the audio. + /// You must dispose the audio player yourself if using PlayerMode.lowLatency. + Future start({double volume = 1.0}) async { + return _lock.synchronized(() async { + if (availablePlayers.isEmpty) { + availablePlayers.add(await _createNewAudioPlayer()); + } + final player = availablePlayers.removeAt(0); + currentPlayers[player.playerId] = player; + await player.setVolume(volume); + await player.resume(); + + StreamSubscription? subscription; + + Future stop() { + return _lock.synchronized(() async { + final removedPlayer = currentPlayers.remove(player.playerId); + if (removedPlayer != null) { + subscription?.cancel(); + await removedPlayer.stop(); + if (availablePlayers.length >= maxPlayers) { + await removedPlayer.release(); + } else { + availablePlayers.add(removedPlayer); + } + } + }); + } + + if (playerMode != PlayerMode.lowLatency) { + subscription = player.onPlayerComplete.listen((_) => stop()); + } + + return stop; + }); + } + + Future _createNewAudioPlayer() async { + final player = AudioPlayer()..audioCache = audioCache; + + await player.setPlayerMode(playerMode); + + if (audioContext != null) { + await player.setAudioContext(audioContext!); + } + await player.setSource(source); + await player.setReleaseMode(ReleaseMode.stop); + return player; + } + + /// Disposes the audio pool. Then it cannot be used anymore. + Future dispose() async { + // Dispose all players + await Future.wait([ + ...currentPlayers.values.map((e) => e.dispose()), + ...availablePlayers.map((e) => e.dispose()), + ]); + currentPlayers.clear(); + availablePlayers.clear(); + } +} diff --git a/packages/audioplayers/lib/src/audioplayer.dart b/packages/audioplayers/lib/src/audioplayer.dart new file mode 100644 index 0000000..94f9f84 --- /dev/null +++ b/packages/audioplayers/lib/src/audioplayer.dart @@ -0,0 +1,517 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:audioplayers/src/uri_ext.dart'; +import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; + +const _uuid = Uuid(); + +/// This represents a single AudioPlayer, which can play one audio at a time. +/// To play several audios at the same time, you must create several instances +/// of this class. +/// +/// It holds methods to play, loop, pause, stop, seek the audio, and some useful +/// hooks for handlers and callbacks. +class AudioPlayer { + static final global = GlobalAudioScope(); + static Duration preparationTimeout = const Duration(seconds: 30); + static Duration seekingTimeout = const Duration(seconds: 30); + + final _platform = AudioplayersPlatformInterface.instance; + + /// This is the [AudioCache] instance used by this player. + /// Unless you want to control multiple caches separately, you don't need to + /// change anything as the global instance will be used by default. + AudioCache audioCache = AudioCache.instance; + + /// An unique ID generated for this instance of [AudioPlayer]. + /// + /// This is used to properly exchange messages with the [MethodChannel]. + final String playerId; + + Source? _source; + + Source? get source => _source; + + double _volume = 1.0; + + double get volume => _volume; + + double _balance = 0.0; + + double get balance => _balance; + + double _playbackRate = 1.0; + + double get playbackRate => _playbackRate; + + /// Current mode of the audio player. Can be updated at any time, but is going + /// to take effect only at the next time you play the audio. + PlayerMode _mode = PlayerMode.mediaPlayer; + + PlayerMode get mode => _mode; + + ReleaseMode _releaseMode = ReleaseMode.release; + + ReleaseMode get releaseMode => _releaseMode; + + /// Auxiliary variable to re-check the volatile player state during async + /// operations. + @visibleForTesting + PlayerState desiredState = PlayerState.stopped; + + PlayerState _playerState = PlayerState.stopped; + + PlayerState get state => _playerState; + + /// The current playback state. + /// It is only set, when the corresponding action succeeds. + set state(PlayerState state) { + if (_playerState == PlayerState.disposed) { + throw Exception('AudioPlayer has been disposed'); + } + if (!_playerStateController.isClosed) { + _playerStateController.add(state); + } + _playerState = desiredState = state; + } + + PositionUpdater? _positionUpdater; + + /// Completer to wait until the native player and its event stream are + /// created. + @visibleForTesting + final creatingCompleter = Completer(); + + late final StreamSubscription _onPlayerCompleteStreamSubscription; + + late final StreamSubscription _onLogStreamSubscription; + + /// Stream controller to be able to get a stream on initialization, before the + /// native event stream is ready via [_create] method. + final _eventStreamController = StreamController.broadcast(); + late final StreamSubscription _eventStreamSubscription; + + Stream get eventStream => _eventStreamController.stream; + + final StreamController _playerStateController = + StreamController.broadcast(); + + /// Stream of changes on player state. + Stream get onPlayerStateChanged => _playerStateController.stream; + + /// Stream of changes on audio position. + /// + /// Roughly fires every 200 milliseconds. Will continuously update the + /// position of the playback if the status is [PlayerState.playing]. + /// + /// You can use it on a progress bar, for instance. + Stream get onPositionChanged => + _positionUpdater?.positionStream ?? const Stream.empty(); + + /// Stream of changes on audio duration. + /// + /// An event is going to be sent as soon as the audio duration is available + /// (it might take a while to download or buffer it). + Stream get onDurationChanged => eventStream + .where((event) => event.eventType == AudioEventType.duration) + .map((event) => event.duration!); + + /// Stream of player completions. + /// + /// Events are sent every time an audio is finished, therefore no event is + /// sent when an audio is paused or stopped. + /// + /// [ReleaseMode.loop] also sends events to this stream. + Stream get onPlayerComplete => + eventStream.where((event) => event.eventType == AudioEventType.complete); + + /// Stream of seek completions. + /// + /// An event is going to be sent as soon as the audio seek is finished. + Stream get onSeekComplete => eventStream + .where((event) => event.eventType == AudioEventType.seekComplete); + + Stream get _onPrepared => eventStream + .where((event) => event.eventType == AudioEventType.prepared) + .map((event) => event.isPrepared!); + + /// Stream of log events. + Stream get onLog => eventStream + .where((event) => event.eventType == AudioEventType.log) + .map((event) => event.logMessage!); + + /// Creates a new instance and assigns an unique id to it. + AudioPlayer({String? playerId}) : playerId = playerId ?? _uuid.v4() { + _onLogStreamSubscription = onLog.listen( + (log) => AudioLogger.log('$log\nSource: $_source'), + onError: (Object e, [StackTrace? stackTrace]) => AudioLogger.error( + AudioPlayerException(this, cause: e), + stackTrace, + ), + ); + _onPlayerCompleteStreamSubscription = onPlayerComplete.listen( + (_) async { + state = PlayerState.completed; + if (releaseMode == ReleaseMode.release) { + _source = null; + } + await _positionUpdater?.stopAndUpdate(); + }, + onError: (Object _, [StackTrace? __]) { + /* Errors are already handled via log stream */ + }, + ); + _create(); + positionUpdater = FramePositionUpdater( + getPosition: getCurrentPosition, + ); + } + + Future _create() async { + try { + await global.ensureInitialized(); + await _platform.create(playerId); + // Assign the event stream, now that the platform registered this player. + _eventStreamSubscription = _platform.getEventStream(playerId).listen( + _eventStreamController.add, + onError: _eventStreamController.addError, + ); + creatingCompleter.complete(); + } on Exception catch (e, stackTrace) { + creatingCompleter.completeError(e, stackTrace); + } + } + + /// Play an audio [source]. + /// + /// To reduce preparation latency, instead consider calling [setSource] + /// beforehand and then [resume] separately. + Future play( + Source source, { + double? volume, + double? balance, + AudioContext? ctx, + Duration? position, + PlayerMode? mode, + }) async { + desiredState = PlayerState.playing; + + if (mode != null) { + await setPlayerMode(mode); + } + if (volume != null) { + await setVolume(volume); + } + if (balance != null) { + await setBalance(balance); + } + if (ctx != null) { + await setAudioContext(ctx); + } + + await setSource(source); + if (position != null) { + await seek(position); + } + + await _resume(); + } + + Future setAudioContext(AudioContext ctx) async { + await creatingCompleter.future; + return _platform.setAudioContext(playerId, ctx); + } + + Future setPlayerMode(PlayerMode mode) async { + _mode = mode; + await creatingCompleter.future; + return _platform.setPlayerMode(playerId, mode); + } + + /// Pauses the audio that is currently playing. + /// + /// If you call [resume] later, the audio will resume from the point that it + /// has been paused. + Future pause() async { + desiredState = PlayerState.paused; + await creatingCompleter.future; + if (desiredState == PlayerState.paused) { + await _platform.pause(playerId); + state = PlayerState.paused; + await _positionUpdater?.stopAndUpdate(); + } + } + + /// Stops the audio that is currently playing. + /// + /// The position is going to be reset and you will no longer be able to resume + /// from the last point. + Future stop() async { + desiredState = PlayerState.stopped; + await creatingCompleter.future; + if (desiredState == PlayerState.stopped) { + await _platform.stop(playerId); + state = PlayerState.stopped; + await _positionUpdater?.stopAndUpdate(); + } + } + + /// Resumes the audio that has been paused or stopped. + Future resume() async { + desiredState = PlayerState.playing; + await _resume(); + } + + /// Resume without setting the desired state. + Future _resume() async { + await creatingCompleter.future; + if (desiredState == PlayerState.playing) { + await _platform.resume(playerId); + state = PlayerState.playing; + _positionUpdater?.start(); + } + } + + /// Releases the resources associated with this media player. + /// + /// The resources are going to be fetched or buffered again as soon as you + /// call [resume] or change the source. + Future release() async { + await stop(); + await _platform.release(playerId); + // Stop state already set in stop() + _source = null; + } + + /// Moves the cursor to the desired position. + Future seek(Duration position) async { + await creatingCompleter.future; + + final futureSeekComplete = + onSeekComplete.first.timeout(AudioPlayer.seekingTimeout); + final futureSeek = _platform.seek(playerId, position); + // Wait simultaneously to ensure all errors are propagated through the same + // future. + await Future.wait([futureSeek, futureSeekComplete]); + + await _positionUpdater?.update(); + } + + /// Sets the stereo balance. + /// + /// -1 - The left channel is at full volume; the right channel is silent. + /// 1 - The right channel is at full volume; the left channel is silent. + /// 0 - Both channels are at the same volume. + Future setBalance(double balance) async { + _balance = balance; + await creatingCompleter.future; + return _platform.setBalance(playerId, balance); + } + + /// Sets the volume (amplitude). + /// + /// 0 is mute and 1 is the max volume. The values between 0 and 1 are linearly + /// interpolated. + Future setVolume(double volume) async { + _volume = volume; + await creatingCompleter.future; + return _platform.setVolume(playerId, volume); + } + + /// Sets the release mode. + /// + /// Check [ReleaseMode]'s doc to understand the difference between the modes. + Future setReleaseMode(ReleaseMode releaseMode) async { + _releaseMode = releaseMode; + await creatingCompleter.future; + return _platform.setReleaseMode(playerId, releaseMode); + } + + /// Sets the playback rate - call this after first calling play() or resume(). + /// + /// iOS and macOS have limits between 0.5 and 2x + /// Android SDK version should be 23 or higher + Future setPlaybackRate(double playbackRate) async { + _playbackRate = playbackRate; + await creatingCompleter.future; + return _platform.setPlaybackRate(playerId, playbackRate); + } + + /// Sets the audio source for this player. + /// + /// This will delegate to one of the specific methods below depending on + /// the source type. + Future setSource(Source source) async { + // Implementations of setOnPlayer also call `creatingCompleter.future` + await source.setOnPlayer(this); + } + + /// This method helps waiting for a source to be set until it's prepared. + /// This can happen immediately after [setSource] has finished or it needs to + /// wait for the [AudioEvent] [AudioEventType.prepared] to arrive. + Future _completePrepared(Future Function() setSource) async { + await creatingCompleter.future; + + final preparedFuture = _onPrepared + .firstWhere((isPrepared) => isPrepared) + .timeout(AudioPlayer.preparationTimeout); + // Need to await the setting the source to propagate immediate errors. + final setSourceFuture = setSource(); + + // Wait simultaneously to ensure all errors are propagated through the same + // future. + await Future.wait([setSourceFuture, preparedFuture]); + + // Share position once after finished loading + await _positionUpdater?.update(); + } + + /// Sets the URL to a remote link. + /// + /// The resources will start being fetched or buffered as soon as you call + /// this method. + Future setSourceUrl(String url, {String? mimeType}) async { + if (!kIsWeb && + defaultTargetPlatform != TargetPlatform.android && + url.startsWith('data:')) { + // Convert data URI's to bytes (native support for web and android). + final uriData = UriData.fromUri(Uri.parse(url)); + mimeType ??= url.substring(url.indexOf(':') + 1, url.indexOf(';')); + await setSourceBytes(uriData.contentAsBytes(), mimeType: mimeType); + return; + } + + _source = UrlSource(url, mimeType: mimeType); + // Encode remote url to avoid unexpected failures. + await _completePrepared( + () => _platform.setSourceUrl( + playerId, + UriCoder.encodeOnce(url), + mimeType: mimeType, + isLocal: false, + ), + ); + } + + /// Sets the URL to a file in the users device. + /// + /// The resources will start being fetched or buffered as soon as you call + /// this method. + Future setSourceDeviceFile(String path, {String? mimeType}) async { + _source = DeviceFileSource(path, mimeType: mimeType); + await _completePrepared( + () => _platform.setSourceUrl( + playerId, + path, + isLocal: true, + mimeType: mimeType, + ), + ); + } + + /// Sets the URL to an asset in your Flutter application. + /// The global instance of AudioCache will be used by default. + /// + /// The resources will start being fetched or buffered as soon as you call + /// this method. + Future setSourceAsset(String path, {String? mimeType}) async { + _source = AssetSource(path, mimeType: mimeType); + final cachePath = await audioCache.loadPath(path); + await _completePrepared( + () => _platform.setSourceUrl( + playerId, + cachePath, + mimeType: mimeType, + isLocal: true, + ), + ); + } + + Future setSourceBytes(Uint8List bytes, {String? mimeType}) async { + if (!kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux)) { + // Convert to file as workaround + final tempDir = (await getTemporaryDirectory()).path; + final bytesHash = Object.hashAll(bytes) + .toUnsigned(20) + .toRadixString(16) + .padLeft(5, '0'); + final file = File('$tempDir/$bytesHash'); + await file.writeAsBytes(bytes); + await setSourceDeviceFile(file.path, mimeType: mimeType); + } else { + _source = BytesSource(bytes, mimeType: mimeType); + await _completePrepared( + () => _platform.setSourceBytes(playerId, bytes, mimeType: mimeType), + ); + } + } + + /// Set the PositionUpdater to control how often the position stream will be + /// updated. You can use the [FramePositionUpdater], the + /// [TimerPositionUpdater] or write your own implementation of the + /// [PositionUpdater]. + set positionUpdater(PositionUpdater? positionUpdater) { + _positionUpdater?.dispose(); // No need to wait for dispose + _positionUpdater = positionUpdater; + } + + /// Get audio duration after setting url. + /// Use it in conjunction with setUrl. + /// + /// It will be available as soon as the audio duration is available + /// (it might take a while to download or buffer it if file is not local). + Future getDuration() async { + await creatingCompleter.future; + final milliseconds = await _platform.getDuration(playerId); + if (milliseconds == null) { + return null; + } + return Duration(milliseconds: milliseconds); + } + + // Gets audio current playing position + Future getCurrentPosition() async { + await creatingCompleter.future; + final milliseconds = await _platform.getCurrentPosition(playerId); + if (milliseconds == null) { + return null; + } + return Duration(milliseconds: milliseconds); + } + + /// Closes all [StreamController]s. + /// + /// You must call this method when your [AudioPlayer] instance is not going to + /// be used anymore. If you try to use it after this you will get errors. + Future dispose() async { + // First stop and release all native resources. + await release(); + + state = desiredState = PlayerState.disposed; + + final futures = [ + if (_positionUpdater != null) _positionUpdater!.dispose(), + if (!_playerStateController.isClosed) _playerStateController.close(), + _onPlayerCompleteStreamSubscription.cancel(), + _onLogStreamSubscription.cancel(), + _eventStreamSubscription.cancel(), + _eventStreamController.close(), + ]; + + _source = null; + + await Future.wait(futures); + + // Needs to be called after cancelling event stream subscription: + await _platform.dispose(playerId); + } +} diff --git a/packages/audioplayers/lib/src/global_audio_scope.dart b/packages/audioplayers/lib/src/global_audio_scope.dart new file mode 100644 index 0000000..8aeb714 --- /dev/null +++ b/packages/audioplayers/lib/src/global_audio_scope.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:audioplayers/src/audio_logger.dart'; +import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; + +GlobalAudioplayersPlatformInterface? _lastGlobalAudioplayersPlatform; + +/// Handle global audio scope like calls and events concerning all AudioPlayers. +class GlobalAudioScope { + Completer? _initCompleter; + + GlobalAudioplayersPlatformInterface get _platform => + GlobalAudioplayersPlatformInterface.instance; + + /// Stream of global events. + late final Stream eventStream; + + /// Stream of global log events. + Stream get onLog => eventStream + .where((event) => event.eventType == GlobalAudioEventType.log) + .map((event) => event.logMessage!); + + GlobalAudioScope() { + eventStream = _platform.getGlobalEventStream(); + onLog.listen( + AudioLogger.log, + onError: AudioLogger.error, + ); + } + + /// Ensure the global platform is initialized. + Future ensureInitialized() async { + if (_lastGlobalAudioplayersPlatform != _platform) { + // This will clear all open players on the platform when a full restart is + // performed. + _lastGlobalAudioplayersPlatform = _platform; + _initCompleter = Completer(); + try { + await _platform.init(); + _initCompleter?.complete(); + } on Exception catch (e, stackTrace) { + _initCompleter?.completeError(e, stackTrace); + } + } + await _initCompleter?.future; + } + + Future setAudioContext(AudioContext ctx) async { + await ensureInitialized(); + await _platform.setGlobalAudioContext(ctx); + } +} diff --git a/packages/audioplayers/lib/src/position_updater.dart b/packages/audioplayers/lib/src/position_updater.dart new file mode 100644 index 0000000..d3a5771 --- /dev/null +++ b/packages/audioplayers/lib/src/position_updater.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:flutter/scheduler.dart'; + +abstract class PositionUpdater { + /// You can use `player.getCurrentPosition` as the [getPosition] parameter. + PositionUpdater({ + required this.getPosition, + }); + + final Future Function() getPosition; + final _streamController = StreamController.broadcast(); + + Stream get positionStream => _streamController.stream; + + Future update() async { + final position = await getPosition(); + if (position != null) { + _streamController.add(position); + } + } + + void start(); + + void stop(); + + Future stopAndUpdate() async { + stop(); + await update(); + } + + Future dispose() async { + stop(); + await _streamController.close(); + } +} + +class TimerPositionUpdater extends PositionUpdater { + Timer? _positionStreamTimer; + final Duration interval; + + /// Position stream will be updated in the according [interval]. + TimerPositionUpdater({ + required super.getPosition, + required this.interval, + }); + + @override + void start() { + _positionStreamTimer?.cancel(); + _positionStreamTimer = Timer.periodic(interval, (timer) async { + await update(); + }); + } + + @override + void stop() { + _positionStreamTimer?.cancel(); + _positionStreamTimer = null; + } +} + +class FramePositionUpdater extends PositionUpdater { + int? _frameCallbackId; + bool _isRunning = false; + + /// Position stream will be updated at every new frame. + FramePositionUpdater({ + required super.getPosition, + }); + + void _tick(Duration? timestamp) { + if (_isRunning) { + update(); + _frameCallbackId = SchedulerBinding.instance.scheduleFrameCallback(_tick); + } + } + + @override + void start() { + _isRunning = true; + _tick(null); + } + + @override + void stop() { + _isRunning = false; + if (_frameCallbackId != null) { + SchedulerBinding.instance.cancelFrameCallbackWithId(_frameCallbackId!); + } + } +} diff --git a/packages/audioplayers/lib/src/source.dart b/packages/audioplayers/lib/src/source.dart new file mode 100644 index 0000000..7cdb811 --- /dev/null +++ b/packages/audioplayers/lib/src/source.dart @@ -0,0 +1,100 @@ +import 'dart:math'; + +import 'package:audioplayers/src/audioplayer.dart'; +import 'package:flutter/foundation.dart'; + +/// A generic representation of a source from where audio can be pulled. +/// +/// This can be a remote or local URL, an application asset, or the file bytes. +abstract class Source { + String? get mimeType; + + Future setOnPlayer(AudioPlayer player); +} + +/// Source representing a remote URL to be played from the Internet. +/// This can be an audio file to be downloaded or an audio stream. +class UrlSource extends Source { + final String url; + + @override + final String? mimeType; + + UrlSource(this.url, {this.mimeType}); + + @override + Future setOnPlayer(AudioPlayer player) { + return player.setSourceUrl(url, mimeType: mimeType); + } + + @override + String toString() { + return 'UrlSource(url: ${url.substring(0, min(500, url.length))},' + ' mimeType: $mimeType)'; + } +} + +/// Source representing the absolute path of a file in the user's device. +class DeviceFileSource extends Source { + final String path; + + @override + final String? mimeType; + + DeviceFileSource(this.path, {this.mimeType}); + + @override + Future setOnPlayer(AudioPlayer player) { + return player.setSourceDeviceFile(path, mimeType: mimeType); + } + + @override + String toString() { + return 'DeviceFileSource(path: $path, mimeType: $mimeType)'; + } +} + +/// Source representing the path of an application asset in your Flutter +/// "assets" folder. +/// Note that a prefix might be applied by your [AudioPlayer]'s audio cache +/// instance. +class AssetSource extends Source { + final String path; + + @override + final String? mimeType; + + AssetSource(this.path, {this.mimeType}); + + @override + Future setOnPlayer(AudioPlayer player) { + return player.setSourceAsset(path, mimeType: mimeType); + } + + @override + String toString() { + return 'AssetSource(path: $path, mimeType: $mimeType)'; + } +} + +/// Source containing the actual bytes of the media to be played. +class BytesSource extends Source { + final Uint8List bytes; + + @override + final String? mimeType; + + BytesSource(this.bytes, {this.mimeType}); + + @override + Future setOnPlayer(AudioPlayer player) { + return player.setSourceBytes(bytes, mimeType: mimeType); + } + + @override + String toString() { + final bytesHash = + Object.hashAll(bytes).toUnsigned(20).toRadixString(16).padLeft(5, '0'); + return 'BytesSource(bytes: $bytesHash, mimeType: $mimeType)'; + } +} diff --git a/packages/audioplayers/lib/src/uri_ext.dart b/packages/audioplayers/lib/src/uri_ext.dart new file mode 100644 index 0000000..80d6fcc --- /dev/null +++ b/packages/audioplayers/lib/src/uri_ext.dart @@ -0,0 +1,12 @@ +extension UriCoder on Uri { + static String encodeOnce(String uri) { + try { + // If decoded differs, the uri was already encoded. + final decodedUri = Uri.decodeFull(uri); + if (decodedUri != uri) { + return uri; + } + } on ArgumentError catch (_) {} + return Uri.encodeFull(uri); + } +} diff --git a/packages/audioplayers/pubspec.yaml b/packages/audioplayers/pubspec.yaml new file mode 100644 index 0000000..4003730 --- /dev/null +++ b/packages/audioplayers/pubspec.yaml @@ -0,0 +1,58 @@ +name: audioplayers +resolution: workspace +description: A Flutter plugin to play multiple audio files simultaneously +version: 6.6.0 +homepage: https://github.com/bluefireteam/audioplayers +repository: https://github.com/bluefireteam/audioplayers/tree/master/packages/audioplayers + +flutter: + plugin: + platforms: + android: + default_package: audioplayers_android + ios: + default_package: audioplayers_darwin + linux: + default_package: audioplayers_linux + macos: + default_package: audioplayers_darwin + ohos: + default_package: audioplayers_ohos + web: + default_package: audioplayers_web + windows: + default_package: audioplayers_windows + +dependencies: + audioplayers_android: ^5.2.1 + audioplayers_darwin: ^6.4.0 + audioplayers_linux: ^4.2.1 + audioplayers_ohos: + path: ../audioplayers_ohos/audioplayers_ohos + audioplayers_platform_interface: ^7.1.1 + audioplayers_web: ^5.2.0 + audioplayers_windows: ^4.3.0 + file: '>=6.1.0 <8.0.0' + flutter: + sdk: flutter + http: '>=0.13.1 <2.0.0' + meta: ^1.7.0 + path_provider: + git: + url: https://gitcode.com/openharmony-sig/flutter_packages.git + path: packages/path_provider/path_provider + synchronized: ^3.0.0 + uuid: '>=3.0.7 <5.0.0' + +dev_dependencies: + flame_lint: ^1.4.1 + flutter_test: + sdk: flutter + +environment: + sdk: ^3.6.0 + flutter: '>=3.27.0' + +topics: + - audio + - audio-player diff --git a/privacy.html b/privacy.html deleted file mode 100644 index 915649e..0000000 --- a/privacy.html +++ /dev/null @@ -1,468 +0,0 @@ - - - - - - 情景诗词 - 隐私政策与用户协议 - - - -
-

📜 情景诗词

-

隐私政策与用户协议

-
- -
-
-
- - -
- -
-
-

关于情景诗词与隐私的声明

-

更新日期:2026.3.26

-

- 情景诗词 是由 *****工作室 (以下简称"我们")为您提供的,用于在诗词里旅行,在文化中生长的应用。本隐私声明由我们为处理您的个人信息而制定。 -

-

- 我们非常重视您的个人信息和隐私保护,将会按照法律要求和业界成熟的安全标准,为您的个人信息提供相应的安全保护措施。 -

-
- -
-

1. 我们如何收集和使用您的个人信息

-

- 我们仅在有合法性基础的情形下才会使用您的个人信息。根据适用的法律,我们可能会基于您的同意、为履行/订立您与我们的合同所必需、履行法定义务所必需等合法性基础,使用您的个人信息。 -

- -

1.1 基于履行法定义务或其他法律法规规定的情形

-

为了实现应用功能,在获取您的同意后我们需要收集您的以下信息:

-
    -
  • 本地笔记数据,用于保存您创建的诗词笔记
  • -
  • 点赞记录,用于展示您收藏的诗词
  • -
  • 历史记录,用于记录您浏览过的诗词
  • -
-
- -
-

2. 设备权限调用

-
-

📱 存储权限

-

用于保存和读取您的笔记、收藏等本地数据

-
-
-

🌐 网络权限

-

用于获取诗词内容和更新应用信息

-
-
- -
-

3. 管理您的个人信息

-

- 如您对您的数据主体权利有进一步要求或存在任何疑问、意见或建议,可通过本声明中"如何联系我们"章节中所述方式与我们取得联系,并行使您的相关权利。 -

-
- -
-

4. 信息存储地点及期限

-

- 4.1 我们承诺,除法律法规另有规定外,我们对您的信息的保存期限应当为实现处理目的所必要的最短时间。 -

-

- 4.2 上述信息将会传输并保存至中国境内的服务器。 -

-
- -
-

5. 如何联系我们

-

您可通过以下方式联系我们,并行使您的相关权利,我们会尽快回复。

-
-
- 开发者: - *****工作室 -
-
- 地址: - 云南昆明 -
-
- 邮箱: - ********@outlook.com -
-
-

- 如果您对我们的回复不满意,特别是当个人信息处理行为损害了您的合法权益时,您还可以通过向有管辖权的人民法院提起诉讼、向行业自律协会或政府相关管理机构投诉等外部途径进行解决。您也可以向我们了解可能适用的相关投诉途径的信息。 -

-
- -

- 生效日期:2026年3月26日 -

-
- -
-
-

用户协议

-

生效日期:2026-03-26

-

- 欢迎使用 情景诗词(以下简称"本App")。本用户协议由个人开发者 *****工作室 制定。用户在下载、安装、注册、登录、使用本App服务前,应当仔细阅读并充分理解本协议内容。用户开始使用本App,即视为同意本协议全部条款。 -

-
- -
-

一、协议适用范围

-

- 本协议适用于用户与开发者 *****工作室 之间,关于用户使用 情景诗词 产品及服务所建立的权利义务关系。 -

-
- -
-

二、服务内容

-

- 情景诗词 主要提供诗词浏览、收藏、笔记记录、历史记录等功能。具体服务内容以应用内实际展示为准,开发者可根据产品运营情况进行功能优化、升级和调整。 -

-
- -
-

三、账号与安全

-

- 您应当确保使用本App时遵守相关法律法规。本App目前不需要注册账号,您的本地数据存储在您的设备上,请妥善保管您的设备。 -

-
- -
-

四、用户行为规范

-

- 用户在使用本App过程中,应当遵守中华人民共和国法律法规,不得利用本App从事违法违规行为,不得发布或传播侵犯他人合法权益的内容,不得实施影响本App安全和稳定运行的行为。 -

-
- -
-

五、知识产权

-

- 用户知悉并认可,本App(含程序代码、界面设计、功能及其更新、扩展、修复版本)相关权利均归开发者或合法权利人所有。 -

-

- 本条所称"知识产权"包括但不限于著作权、商标权、专利权、商业秘密及反不正当竞争法等法律法规项下的一切相关权利。 -

-

- 除法律法规另有规定或开发者书面授权外,用户仅获得基于本协议的个人、非独占、不可转让、可撤销的使用许可;用户不得对本App实施复制、修改、改编、翻译、出租、出借、出售、传播、反向工程、反编译、反汇编,或以其他方式尝试获取源代码。 -

-
- -
-

六、责任限制

-

- 在法律允许范围内,对于因网络异常、设备故障、不可抗力、第三方服务异常等原因导致的服务中断或数据损失,开发者将在能力范围内及时修复或补救,但不承担超出法定范围的责任。 -

-
- -
-

七、协议更新

-

- 开发者有权根据业务变化、监管要求或法律法规变化更新本协议。更新后的协议将在应用内公示,用户继续使用本App即视为接受更新后的协议内容。 -

-
- -
-

八、适用法律与争议解决

-

- 本协议适用中华人民共和国法律。因本协议引发的争议,双方应先友好协商;协商不成的,提交被告住所地有管辖权的人民法院处理。 -

-
- -
-

九、联系方式

-
-
- 开发者: - *****工作室 -
-
- 应用名称: - 情景诗词 -
-
- 联系邮箱: - ********@outlook.com -
-
-
-
-
-
- - - - - - diff --git a/pubspec.lock b/pubspec.lock index 2f9e845..8f96d0c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,74 +21,64 @@ packages: dependency: "direct main" description: path: "packages/audioplayers" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git - version: "4.1.0" + relative: true + source: path + version: "6.6.0" audioplayers_android: dependency: transitive description: - path: "packages/audioplayers_android" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git - version: "3.0.2" + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" audioplayers_darwin: dependency: transitive description: - path: "packages/audioplayers_darwin" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git - version: "4.1.0" + name: audioplayers_darwin + sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.4.0" audioplayers_linux: dependency: transitive description: - path: "packages/audioplayers_linux" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git - version: "2.1.0" + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.1" audioplayers_ohos: dependency: transitive description: - path: "packages/audioplayers_ohos" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git + path: "packages/audioplayers_ohos/audioplayers_ohos" + relative: true + source: path version: "3.0.2" audioplayers_platform_interface: dependency: transitive description: - path: "packages/audioplayers_platform_interface" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git - version: "5.0.1" + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.1.1" audioplayers_web: dependency: transitive description: - path: "packages/audioplayers_web" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git - version: "3.1.0" + name: audioplayers_web + sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.0" audioplayers_windows: dependency: transitive description: - path: "packages/audioplayers_windows" - ref: HEAD - resolved-ref: e566d678531badc1437ada9fcca87c23d6355ba4 - url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" - source: git - version: "2.0.2" + name: audioplayers_windows + sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.0" boolean_selector: dependency: transitive description: @@ -165,10 +155,10 @@ packages: dependency: transitive description: name: device_info_plus - sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da" + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" url: "https://pub.flutter-io.cn" source: hosted - version: "11.3.0" + version: "11.5.0" device_info_plus_platform_interface: dependency: transitive description: @@ -221,10 +211,18 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.4" + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -255,14 +253,23 @@ packages: description: flutter source: sdk version: "0.0.0" + get: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: be2738d5711fc351eb51d753db1dfef0cfb38fc0 + url: "https://gitcode.com/openharmony-sig/fluttertpc_get" + source: git + version: "4.6.5" http: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.flutter-io.cn" source: hosted - version: "0.13.6" + version: "1.6.0" http_parser: dependency: transitive description: @@ -279,14 +286,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.6.7" leak_tracker: dependency: transitive description: @@ -380,7 +379,7 @@ packages: description: path: "packages/path_provider/path_provider" ref: HEAD - resolved-ref: b7d7f892e446d198c596487fda3c57d8e054f9e0 + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 url: "https://gitcode.com/openharmony-sig/flutter_packages.git" source: git version: "2.1.0" @@ -413,7 +412,7 @@ packages: description: path: "packages/path_provider/path_provider_ohos" ref: HEAD - resolved-ref: b7d7f892e446d198c596487fda3c57d8e054f9e0 + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" source: git version: "2.2.1" @@ -484,7 +483,7 @@ packages: description: path: "packages/shared_preferences/shared_preferences" ref: HEAD - resolved-ref: b7d7f892e446d198c596487fda3c57d8e054f9e0 + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" source: git version: "2.2.0" @@ -517,7 +516,7 @@ packages: description: path: "packages/shared_preferences/shared_preferences_ohos" ref: HEAD - resolved-ref: b7d7f892e446d198c596487fda3c57d8e054f9e0 + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" source: git version: "2.2.0" @@ -658,10 +657,10 @@ packages: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.7" + version: "4.5.3" vector_math: dependency: transitive description: @@ -731,10 +730,10 @@ packages: dependency: transitive description: name: win32_registry - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.5" + version: "2.1.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 00fd1a0..3768436 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,32 +1,11 @@ name: poes description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.3.12+26040101 +publish_to: 'none' +version: 1.4.1+26040202 environment: sdk: ^3.9.2 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter @@ -40,16 +19,13 @@ dependencies: cookie_jar: ^4.0.8 dio_cookie_manager: ^3.1.1 platform_info: ^5.0.0 - vibration: ^2.0.0 #删除 + vibration: ^2.0.0 intl: ^0.20.2 audioplayers: - git: - url: https://gitcode.com/openharmony-sig/flutter_audioplayers.git - path: packages/audioplayers + path: packages/audioplayers + flutter_udid: path: packages/flutter_udid - - share_plus: path: packages/flutter_plus_plugins/packages/share_plus/share_plus @@ -58,56 +34,17 @@ dependencies: url: https://gitcode.com/openharmony-sig/fluttertpc_wakelock_plus.git ref: br_v1.4.0_ohos path: wakelock_plus + get: + git: + url: https://gitcode.com/openharmony-sig/fluttertpc_get + dev_dependencies: flutter_test: sdk: flutter - - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^5.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: assets: - assets/audios/deep.mp3 - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package