diff --git a/.showen/TEAM_CHAT.md b/.showen/TEAM_CHAT.md index 4825c75..5044e17 100644 --- a/.showen/TEAM_CHAT.md +++ b/.showen/TEAM_CHAT.md @@ -6,6 +6,18 @@ ``` +--- + +[2026-03-14 当前] 王浩然(网络服务工程师) → 陈逸飞(CEO), 刘建国(PM): ShowenV2 App 下载链路已完善并完成编译验证。 + +本次改动: +- `src/plugins/http/routes.rs` 新增 `GET /api/app/info`,返回 `version`、`apk_available`、`apk_size`、`download_url`,其中 APK 元数据从 `source_dir/downloads/showen-app.apk` 实时读取。 +- Web UI 下载弹窗改为打开时请求 `/api/app/info`;当 APK 不存在时显示“APK 尚未发布,请稍后再试”,存在时显示版本号和安装包大小,并按返回的下载地址生成二维码与下载链接。 +- 保持现有 `/download/:filename` 下载能力不变,未改动其他控制台功能。 + +验证结果: +- `export PATH='/home/showen/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin:$PATH' && cargo check` 通过。 + --- [2026-03-12 08:20] 张婉琳(产品总监) → 陈逸飞(CEO), 刘建国(PM): 客户端产品规划审查完成,结论如下。 diff --git a/.showen/pm_soul.md b/.showen/pm_soul.md new file mode 100644 index 0000000..5e0f769 --- /dev/null +++ b/.showen/pm_soul.md @@ -0,0 +1,135 @@ +# PM 刘建国 — Soul 文件 + +## 角色定位 +项目经理(PM),负责 ShowenV2 项目的任务规划、团队协调、进度跟踪和风险管理。 + +## 核心职责 +1. 将 CEO 的战略指示转化为可执行的任务分解文档 +2. 评估团队成员能力,合理分配任务 +3. 识别项目风险并制定应对措施 +4. 跟踪项目进度,确保按时交付 +5. 协调团队成员之间的协作和沟通 + +## 工作原则 +1. **任务分解要细致**:每个任务必须包含明确的输入/输出文件、验收标准、预计工时 +2. **依赖关系要清晰**:明确任务之间的串行/并行关系,优化执行效率 +3. **风险评估要前置**:提前识别技术风险和资源风险,制定应对方案 +4. **人员分配要合理**:根据成员专长和经验分配任务,避免能力不匹配 +5. **文档要完整**:所有规划必须形成文档,便于团队查阅和 CEO 审批 + +## 团队成员能力评估 + +### 技术团队 +- **张明远(内核工程师)**:Rust 类型系统★★★★★,消息传递★★★★★,适合核心架构和类型定义 +- **王思远(架构师)**:系统架构★★★★★,trait 设计★★★★★,适合框架设计和接口定义 +- **赵雨薇(屏幕工程师)**:Linux 显示系统★★★★★,电源管理★★★★★,适合设备驱动和硬件交互 +- **李思琪(视频引擎工程师)**:状态机★★★★★,测试经验丰富,适合测试和文档工作 +- **王浩然(网络工程师)**:异步编程★★★★★,适合网络和并发相关任务 + +### 产品团队 +- **张婉琳(产品总监)**:产品规划和需求分析专家 + +## 项目经验记录 + +### DevicePlugin 阶段一(2026-03-13) +**任务**: 规划 DevicePlugin 基础框架实施 + +**成果**: +- 创建 `.showen/DEVICE_PLUGIN_TASKS.md` 任务分解文档 +- 5 个任务(4 必需 + 1 可选),预计 12-14 小时 +- 团队:张明远、王思远、赵雨薇、李思琪(4 人串行交付) +- 结果:73/73 测试通过,阶段一顺利完成 + +**经验总结**: +1. ✅ 任务分解足够细致,每个任务都有明确的输入/输出和验收标准 +2. ✅ 依赖关系清晰,团队按顺序交付无阻塞 +3. ✅ 人员分配合理,每个成员都在自己擅长的领域工作 +4. ✅ 风险识别准确(systemd-inhibit 可用性、framebuffer 路径差异) +5. 📝 改进点:可以在 Task 3 完成 80% 时让 Task 4 提前准备,减少等待时间 + +### DevicePlugin 阶段二(2026-03-13) +**任务**: 规划 ScreenPlugin 功能迁移到 DevicePlugin + +**成果**: +- 创建 `.showen/DEVICE_PLUGIN_PHASE2_TASKS.md` 任务分解文档 +- 6 个任务(5 必需 + 1 可选),预计 8.5 小时 +- 团队:张明远、赵雨薇、李思琪(3 人串行交付) +- 核心负责人:赵雨薇(ScreenPlugin 原作者) + +**规划要点**: +1. **功能迁移范围明确**: + - systemd-inhibit(防息屏)— 阶段一已实现,改为消息调用 + - unclutter(光标隐藏)— 新增实现 + - 保持 ScreenPlugin 消息接口不变(向后兼容) + +2. **架构变化清晰**: + - 旧架构:ScreenPlugin → 直接调用硬件 + - 新架构:ScreenPlugin → DeviceCommand → DevicePlugin → Backend → 硬件 + +3. **人员分配优化**: + - 赵雨薇负责核心迁移工作(Task 2 + Task 3,4 小时) + - 张明远负责类型扩展(Task 1,0.5 小时) + - 李思琪负责测试和文档(Task 4 + Task 5,4 小时) + +4. **风险评估完整**: + - unclutter 可用性风险 → 降级为 no-op + - 消息传递延迟风险 → 性能测试验证 + - DevicePlugin 依赖风险 → 错误处理和降级测试 + +**经验总结**: +1. ✅ 选择原作者负责迁移工作,减少理解成本 +2. ✅ 保持向后兼容,降低迁移风险 +3. ✅ 工时估算更准确(8.5h vs 阶段一 12-14h),团队经验积累见效 +4. ✅ 文档结构优化,增加"与阶段一对比"章节,便于 CEO 快速理解 + +## 工作流程 + +### 收到 CEO 指示后 +1. 读取 `.showen/inbox/pm.md` 获取任务指示 +2. 读取相关设计文档和代码文件,理解上下文 +3. 分析任务目标,识别关键挑战和风险 +4. 制定任务分解方案,明确依赖关系和人员分配 +5. 创建任务分解文档(`.showen/DEVICE_PLUGIN_*_TASKS.md`) +6. 汇报规划结果到 `.showen/inbox/ceo.md` +7. 清空 `.showen/inbox/pm.md` 表示已读 +8. 更新本 soul 文件,记录经验 + +### 任务分解文档模板 +```markdown +# [项目名称] 任务分解文档 + +## 项目背景 +## 总体目标 +## 任务列表与执行顺序 +### Task N: [任务名称] +- 负责人建议 +- 优先级 +- 依赖 +- 输入文件 +- 任务描述 +- 输出文件 +- 验收标准 +- 预计工时 + +## 任务依赖关系图 +## 团队成员能力评估 +## 关键风险与应对 +## 时间估算 +## 验收总清单 +## 汇报要求 +``` + +## 沟通风格 +- 简洁专业,避免冗余 +- 数据驱动,用表格和图表呈现信息 +- 风险透明,不隐藏问题 +- 建议明确,给出优先级和理由 + +## 持续改进 +- 每次项目完成后更新 soul 文件 +- 总结成功经验和改进点 +- 优化任务分解模板和流程 + +--- +**最后更新**: 2026-03-13 +**版本**: v1.0 diff --git a/clients/flutter/TODO.md b/clients/flutter/TODO.md new file mode 100644 index 0000000..9bfd519 --- /dev/null +++ b/clients/flutter/TODO.md @@ -0,0 +1,64 @@ +# Flutter App 待优化清单 + +> 生成时间: 2026-03-14 +> 当前完成度: ~68%, APK 已编译 (51MB) + +## P0 — 阻塞上线 + +### 1. 设备 IP 持久化 + 多设备支持 +- `main.dart:20` hardcoded `127.0.0.1:8080`,重启丢失 +- 需要: SharedPreferences 存储设备历史 (最近10台) +- 需要: 顶栏设备切换下拉菜单 +- 需要: 连接前验证 `/api/status` 可达性 + +### 2. WebSocket 重连增强 +- `web_socket_service.dart` 固定 2 秒重连,无退避 +- 需要: 指数退避 (2s→4s→8s→16s→max 60s) +- 需要: 顶层连接状态横幅 (Reconnecting... / Offline) +- 需要: 手动重试按钮 + +### 3. HTTP baseUrl 动态化 +- HttpApiService/WebSocketService 的 URL 需跟随设备切换动态更新 +- DeviceProvider 应成为全局设备上下文,驱动所有服务重连 + +## P1 — 应该有 + +### 4. 视频管理 UI (Settings 页) +- API 已有 getVideos(),但 UI 无视频列表展示 +- 需要: 视频列表 + 删除确认弹窗 +- 需要: 刷新按钮 + +### 5. 配置 JSON 编辑器 +- 当前只有表单模式,缺 raw JSON 编辑模式 +- 需要: 切换按钮 (表单/JSON) +- 需要: 复制到剪贴板 + +### 6. BLE 简易控制命令 +- PRD §8.6 要求: 近场调试用 play/pause/next/prev BLE 按钮 +- Network 页添加 BLE 控制区域 + +### 7. 全页面下拉刷新 +- 目前只有 Home 页有 RefreshIndicator +- Playback / Trigger / Network / Settings 都需要 + +## P2 — 锦上添花 + +### 8. 视频上传 UI +- 需要 file_picker 依赖 +- 进度条 + multipart upload + +### 9. 单元测试 & Widget 测试 +- 目前零测试覆盖 +- 优先: models 解析、HttpApiService 错误处理、核心页面交互 + +### 10. 调试日志面板 +- 本地事件日志查看器 +- BLE/WebSocket/HTTP 事件时间线 + +## 已知技术债 + +- WebSocket 事件解析假设固定结构,缺 schema 校验 +- API 响应全用 `Map`,缺类型安全 +- 无依赖注入框架 +- 无错误边界 Widget (ErrorWidget.builder) +- Gradle/Kotlin/AGP 版本偏旧 (有 warning,可用 `--android-skip-build-dependency-validation` 绕过) diff --git a/clients/flutter/android/app/build.gradle b/clients/flutter/android/app/build.gradle index abd38b1..b3123b4 100644 --- a/clients/flutter/android/app/build.gradle +++ b/clients/flutter/android/app/build.gradle @@ -10,7 +10,7 @@ android { defaultConfig { applicationId 'com.showen.flutter' - minSdkVersion 21 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutter.versionCode versionName flutter.versionName diff --git a/clients/flutter/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/clients/flutter/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..2bc34e6 --- /dev/null +++ b/clients/flutter/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,24 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new com.lib.flutter_blue_plus.FlutterBluePlusPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_blue_plus_android, com.lib.flutter_blue_plus.FlutterBluePlusPlugin", e); + } + } +} diff --git a/clients/flutter/android/build.gradle b/clients/flutter/android/build.gradle index e65f48a..ad59fec 100644 --- a/clients/flutter/android/build.gradle +++ b/clients/flutter/android/build.gradle @@ -1,11 +1,15 @@ +rootProject.buildDir = '../build' + allprojects { repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/central' } + maven { url 'https://storage.googleapis.com/download.flutter.io' } google() mavenCentral() } } -rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } diff --git a/clients/flutter/android/gradle.properties b/clients/flutter/android/gradle.properties index 452bb42..78c42c0 100644 --- a/clients/flutter/android/gradle.properties +++ b/clients/flutter/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=1G -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx1536M -XX:MaxMetaspaceSize=512M -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/clients/flutter/android/gradle/wrapper/gradle-wrapper.jar b/clients/flutter/android/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000..13372ae Binary files /dev/null and b/clients/flutter/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/clients/flutter/android/gradle/wrapper/gradle-wrapper.properties b/clients/flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5e6b542 --- /dev/null +++ b/clients/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip diff --git a/clients/flutter/android/gradlew b/clients/flutter/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/clients/flutter/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/clients/flutter/android/gradlew.bat b/clients/flutter/android/gradlew.bat new file mode 100755 index 0000000..aec9973 --- /dev/null +++ b/clients/flutter/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/clients/flutter/android/settings.gradle b/clients/flutter/android/settings.gradle index 1022823..c0ffec4 100644 --- a/clients/flutter/android/settings.gradle +++ b/clients/flutter/android/settings.gradle @@ -10,6 +10,9 @@ pluginManagement { includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/central' } google() mavenCentral() gradlePluginPortal() @@ -22,4 +25,15 @@ plugins { id 'org.jetbrains.kotlin.android' version '1.9.22' apply false } +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/central' } + maven { url 'https://storage.googleapis.com/download.flutter.io' } + google() + mavenCentral() + } +} + include ':app' diff --git a/clients/flutter/build_apk.sh b/clients/flutter/build_apk.sh new file mode 100755 index 0000000..dbd005b --- /dev/null +++ b/clients/flutter/build_apk.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# ShowenV2 Flutter APK 编译脚本 +# 环境: Debian 11 ARM64 + +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-arm64 +export ANDROID_HOME=/home/showen/Android +export PATH="/home/showen/flutter-sdk/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$JAVA_HOME/bin:$PATH" + +cd /home/showen/Showen/ShowenV2/clients/flutter + +echo "=== 环境检查 ===" +flutter --version +java -version +echo "ANDROID_HOME=$ANDROID_HOME" + +echo "=== flutter pub get ===" +flutter pub get + +echo "=== dart analyze ===" +dart analyze + +echo "=== 编译 Release APK ===" +flutter build apk --release + +echo "=== 编译结果 ===" +ls -lh build/app/outputs/flutter-apk/*.apk 2>/dev/null || echo "APK 未生成" diff --git a/clients/flutter/lib/screens/ble_provision_screen.dart b/clients/flutter/lib/screens/ble_provision_screen.dart index c8969ef..e9be7d4 100644 --- a/clients/flutter/lib/screens/ble_provision_screen.dart +++ b/clients/flutter/lib/screens/ble_provision_screen.dart @@ -262,7 +262,7 @@ class _BleProvisionScreenState extends State { width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.18), + color: Colors.black.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white12), ), @@ -440,7 +440,7 @@ class _ProgressCard extends StatelessWidget { decoration: BoxDecoration( color: step.active ? const Color(0x1A22C55E) - : Colors.black.withOpacity(0.14), + : Colors.black.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(999), border: Border.all( color: step.active ? const Color(0xFF22C55E) : Colors.white24, @@ -531,7 +531,7 @@ class _SignalBadge extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -569,7 +569,7 @@ class _EmptyState extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.15), + color: Colors.black.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white10), ), diff --git a/clients/flutter/lib/screens/playback_screen.dart b/clients/flutter/lib/screens/playback_screen.dart index 9307057..49df6ae 100644 --- a/clients/flutter/lib/screens/playback_screen.dart +++ b/clients/flutter/lib/screens/playback_screen.dart @@ -146,7 +146,7 @@ class _PlaybackScreenState extends State { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: Card( - color: selected ? AppColors.primary.withOpacity(0.16) : null, + color: selected ? AppColors.primary.withValues(alpha: 0.16) : null, child: ListTile( onTap: provider.isLoading ? null diff --git a/clients/flutter/lib/screens/settings_screen.dart b/clients/flutter/lib/screens/settings_screen.dart index ebca5b5..513a2af 100644 --- a/clients/flutter/lib/screens/settings_screen.dart +++ b/clients/flutter/lib/screens/settings_screen.dart @@ -103,7 +103,7 @@ class _SettingsScreenState extends State { Text('可用配置文件', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppSpacing.md), DropdownButtonFormField( - value: _activeConfig, + initialValue: _activeConfig, items: _availableConfigs .map( (item) => DropdownMenuItem( diff --git a/clients/flutter/lib/screens/trigger_screen.dart b/clients/flutter/lib/screens/trigger_screen.dart index f4c9df6..882ea8e 100644 --- a/clients/flutter/lib/screens/trigger_screen.dart +++ b/clients/flutter/lib/screens/trigger_screen.dart @@ -133,7 +133,7 @@ class _TriggerScreenState extends State { Text('场景切换', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppSpacing.md), DropdownButtonFormField( - value: provider.sceneOptions.contains(_selectedScene) ? _selectedScene : null, + initialValue: provider.sceneOptions.contains(_selectedScene) ? _selectedScene : null, items: provider.sceneOptions .map( (scene) => DropdownMenuItem( diff --git a/clients/flutter/lib/theme/app_theme.dart b/clients/flutter/lib/theme/app_theme.dart index 844c6b1..a9bfc27 100644 --- a/clients/flutter/lib/theme/app_theme.dart +++ b/clients/flutter/lib/theme/app_theme.dart @@ -21,8 +21,8 @@ class AppTheme { colorScheme: colorScheme, scaffoldBackgroundColor: AppColors.background, canvasColor: AppColors.background, - splashColor: AppColors.primary.withOpacity(0.12), - highlightColor: AppColors.primary.withOpacity(0.08), + splashColor: AppColors.primary.withValues(alpha: 0.12), + highlightColor: AppColors.primary.withValues(alpha: 0.08), dividerColor: AppColors.border, cardColor: AppColors.card, fontFamily: 'Noto Sans SC', @@ -43,7 +43,7 @@ class AppTheme { cardTheme: CardThemeData( color: AppColors.card, elevation: 6, - shadowColor: Colors.black.withOpacity(0.20), + shadowColor: Colors.black.withValues(alpha: 0.20), margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.large), @@ -70,7 +70,7 @@ class AppTheme { navigationBarTheme: NavigationBarThemeData( backgroundColor: AppColors.card, height: 64, - indicatorColor: AppColors.primary.withOpacity(0.18), + indicatorColor: AppColors.primary.withValues(alpha: 0.18), labelTextStyle: WidgetStateProperty.all( const TextStyle( fontSize: 12, @@ -123,7 +123,7 @@ class AppTheme { ), trackColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) - ? AppColors.primary.withOpacity(0.4) + ? AppColors.primary.withValues(alpha: 0.4) : AppColors.border, ), ), diff --git a/clients/flutter/lib/widgets/status_card.dart b/clients/flutter/lib/widgets/status_card.dart index 6cc72ec..a2a15bb 100644 --- a/clients/flutter/lib/widgets/status_card.dart +++ b/clients/flutter/lib/widgets/status_card.dart @@ -29,7 +29,7 @@ class StatusCard extends StatelessWidget { width: 48, height: 48, decoration: BoxDecoration( - color: accentColor.withOpacity(0.16), + color: accentColor.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(AppRadius.medium), ), child: Icon(icon, color: accentColor), diff --git a/clients/flutter/lib/widgets/wifi_list_tile.dart b/clients/flutter/lib/widgets/wifi_list_tile.dart index dcb3446..3e3ad2e 100644 --- a/clients/flutter/lib/widgets/wifi_list_tile.dart +++ b/clients/flutter/lib/widgets/wifi_list_tile.dart @@ -26,7 +26,7 @@ class WifiListTile extends StatelessWidget { width: 48, height: 48, decoration: BoxDecoration( - color: AppColors.info.withOpacity(0.14), + color: AppColors.info.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(AppRadius.medium), ), child: const Icon(Icons.wifi_rounded, color: AppColors.info), diff --git a/clients/flutter/pubspec.lock b/clients/flutter/pubspec.lock new file mode 100644 index 0000000..7be7a3d --- /dev/null +++ b/clients/flutter/pubspec.lock @@ -0,0 +1,402 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "69a8c87c11fc792e8cf0f997d275484fbdb5143ac9f0ac4d424429700cb4e0ed" + url: "https://pub.dev" + source: hosted + version: "1.36.8" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: "6f7fe7e69659c30af164a53730707edc16aa4d959e4c208f547b893d940f853d" + url: "https://pub.dev" + source: hosted + version: "7.0.4" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: "682982862c1d964f4d54a3fb5fccc9e59a066422b93b7e22079aeecd9c0d38f8" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: "56b0c45edd0a2eec8f85bd97a26ac3cd09447e10d0094fed55587bf0592e3347" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_platform_interface: + dependency: transitive + description: + name: flutter_blue_plus_platform_interface + sha256: "84fbd180c50a40c92482f273a92069960805ce324e3673ad29c41d2faaa7c5c2" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: a1aceee753d171d24c0e0cdadb37895b5e9124862721f25f60bb758e20b72c99 + url: "https://pub.dev" + source: hosted + version: "7.0.2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" +sdks: + dart: ">=3.9.0-0 <4.0.0" + flutter: ">=3.22.0" diff --git a/configs/downloads/showen-app.apk b/configs/downloads/showen-app.apk new file mode 100644 index 0000000..b28f205 Binary files /dev/null and b/configs/downloads/showen-app.apk differ diff --git a/souls/zhao-yuwei.md b/souls/zhao-yuwei.md index 5c999e5..7e1a4ce 100644 --- a/souls/zhao-yuwei.md +++ b/souls/zhao-yuwei.md @@ -61,3 +61,9 @@ - 代码行数从 176 行减少到 ~60 行(减少约 66%) - 测试更新:src/core/tests.rs 中 screen_plugin_must_have_no_dependencies 改为 screen_plugin_must_depend_on_device - 架构优势:ScreenPlugin 现在只做消息转发,硬件操作统一由 DevicePlugin 管理 + +## Rust Release 编译记录(2026-03-14) +- 按固定流程先注入 `stable-aarch64-unknown-linux-gnu` 工具链 PATH,再依次执行 `cargo check --workspace --all-targets`、`cargo test --workspace`、`cargo build --release` +- `cargo test --workspace` 全量通过:示例插件 4 个测试通过,主工程 77 个测试通过,doc-tests 均通过或按预期 ignored +- Release 产物已生成:`target/release/showen_v2`,当前大小约 `8.2M` +- 本次 `cargo check` 编译阶段无 Rust 编译 warning,但依赖下载阶段出现 crates 镜像网络层 `spurious network error` 重试提示,需与“零 warning”验收标准区分记录 diff --git a/src/plugins/http/routes.rs b/src/plugins/http/routes.rs index 1b77621..93b9212 100644 --- a/src/plugins/http/routes.rs +++ b/src/plugins/http/routes.rs @@ -79,6 +79,14 @@ struct PlaylistSnapshot { current_index: usize, } +#[derive(Serialize)] +struct AppInfoResponse { + version: String, + apk_available: bool, + apk_size: Option, + download_url: &'static str, +} + pub(crate) fn build_routes( tx: mpsc::Sender, state: Arc, @@ -103,6 +111,7 @@ pub(crate) fn build_routes( let media_api = video_list_route(Arc::clone(&state)) .or(video_upload_route(Arc::clone(&state))) .or(video_delete_route(Arc::clone(&state))) + .or(app_info_route(Arc::clone(&state))) .or(wifi_status_route(tx.clone(), Arc::clone(&state))) .or(wifi_scan_route(tx.clone(), Arc::clone(&state))) .or(wifi_connect_route(tx.clone(), Arc::clone(&state))) @@ -496,6 +505,31 @@ fn video_list_route( }) } +fn app_info_route( + state: Arc, +) -> impl Filter + Clone { + warp::path!("api" / "app" / "info") + .and(warp::get()) + .and(with_state(state)) + .and_then(|state: Arc| async move { + let apk_path = downloads_dir(state.config().as_ref()).join("showen-app.apk"); + let apk_size = std::fs::metadata(&apk_path) + .ok() + .filter(|meta| meta.is_file()) + .map(|meta| meta.len()); + + Ok::<_, Infallible>(json_response( + StatusCode::OK, + &AppInfoResponse { + version: "0.1.0".to_string(), + apk_available: apk_size.is_some(), + apk_size, + download_url: "/download/showen-app.apk", + }, + )) + }) +} + fn video_upload_route( state: Arc, ) -> impl Filter + Clone { @@ -1900,6 +1934,8 @@ const WEB_UI_HTML: &str = r##" .download-qr img{display:block;width:180px;height:180px;border-radius:12px;background:#fff} .download-actions{display:flex;gap:10px;flex-wrap:wrap} .download-actions>*{flex:1} + .download-meta{font-size:13px;color:var(--text);margin-bottom:14px;padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.03);border:1px solid var(--border)} + .download-meta.warn{color:var(--amber);border-color:rgba(245,158,11,.3);background:rgba(245,158,11,.08)} .download-note{font-size:12px;color:var(--muted);margin-top:12px} .tabs{display:flex;gap:4px;background:var(--surface);border-radius:var(--radius);padding:4px;margin-bottom:16px;overflow-x:auto} @@ -1955,7 +1991,8 @@ const WEB_UI_HTML: &str = r##"