feat: Flutter APK 编译成功 + Gradle 配置修复 + APK 下载部署 + 待优化清单
- 通过 qemu-user-static 实现 ARM64 主机编译 Android APK (51MB) - 修复 Gradle: Aliyun 镜像 + PREFER_SETTINGS + JVM 内存 1536M - 部署 APK 到 configs/downloads/, Web 下载接口已验证 (HTTP 200) - 新增 Flutter TODO.md: 10项待优化 (P0/P1/P2 分级) - 新增 pm_soul.md, 更新 routes.rs APK 下载路由 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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): 客户端产品规划审查完成,结论如下。
|
[2026-03-12 08:20] 张婉琳(产品总监) → 陈逸飞(CEO), 刘建国(PM): 客户端产品规划审查完成,结论如下。
|
||||||
|
|||||||
135
.showen/pm_soul.md
Normal file
135
.showen/pm_soul.md
Normal file
@@ -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
|
||||||
64
clients/flutter/TODO.md
Normal file
64
clients/flutter/TODO.md
Normal file
@@ -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<String, dynamic>`,缺类型安全
|
||||||
|
- 无依赖注入框架
|
||||||
|
- 无错误边界 Widget (ErrorWidget.builder)
|
||||||
|
- Gradle/Kotlin/AGP 版本偏旧 (有 warning,可用 `--android-skip-build-dependency-validation` 绕过)
|
||||||
@@ -10,7 +10,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.showen.flutter'
|
applicationId 'com.showen.flutter'
|
||||||
minSdkVersion 21
|
minSdkVersion flutter.minSdkVersion
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutter.versionCode
|
versionCode flutter.versionCode
|
||||||
versionName flutter.versionName
|
versionName flutter.versionName
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
|
rootProject.buildDir = '../build'
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
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()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|||||||
BIN
clients/flutter/android/gradle/wrapper/gradle-wrapper.jar
vendored
Executable file
BIN
clients/flutter/android/gradle/wrapper/gradle-wrapper.jar
vendored
Executable file
Binary file not shown.
5
clients/flutter/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
clients/flutter/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -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
|
||||||
160
clients/flutter/android/gradlew
vendored
Executable file
160
clients/flutter/android/gradlew
vendored
Executable file
@@ -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 "$@"
|
||||||
90
clients/flutter/android/gradlew.bat
vendored
Executable file
90
clients/flutter/android/gradlew.bat
vendored
Executable file
@@ -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
|
||||||
@@ -10,6 +10,9 @@ pluginManagement {
|
|||||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
repositories {
|
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()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
@@ -22,4 +25,15 @@ plugins {
|
|||||||
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
|
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'
|
include ':app'
|
||||||
|
|||||||
26
clients/flutter/build_apk.sh
Executable file
26
clients/flutter/build_apk.sh
Executable file
@@ -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 未生成"
|
||||||
@@ -262,7 +262,7 @@ class _BleProvisionScreenState extends State<BleProvisionScreen> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.18),
|
color: Colors.black.withValues(alpha: 0.18),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: Colors.white12),
|
border: Border.all(color: Colors.white12),
|
||||||
),
|
),
|
||||||
@@ -440,7 +440,7 @@ class _ProgressCard extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: step.active
|
color: step.active
|
||||||
? const Color(0x1A22C55E)
|
? const Color(0x1A22C55E)
|
||||||
: Colors.black.withOpacity(0.14),
|
: Colors.black.withValues(alpha: 0.14),
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: step.active ? const Color(0xFF22C55E) : Colors.white24,
|
color: step.active ? const Color(0xFF22C55E) : Colors.white24,
|
||||||
@@ -531,7 +531,7 @@ class _SignalBadge extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -569,7 +569,7 @@ class _EmptyState extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.15),
|
color: Colors.black.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: Colors.white10),
|
border: Border.all(color: Colors.white10),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class _PlaybackScreenState extends State<PlaybackScreen> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
|
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
|
||||||
child: Card(
|
child: Card(
|
||||||
color: selected ? AppColors.primary.withOpacity(0.16) : null,
|
color: selected ? AppColors.primary.withValues(alpha: 0.16) : null,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
onTap: provider.isLoading
|
onTap: provider.isLoading
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Text('可用配置文件', style: Theme.of(context).textTheme.titleMedium),
|
Text('可用配置文件', style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _activeConfig,
|
initialValue: _activeConfig,
|
||||||
items: _availableConfigs
|
items: _availableConfigs
|
||||||
.map(
|
.map(
|
||||||
(item) => DropdownMenuItem<String>(
|
(item) => DropdownMenuItem<String>(
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class _TriggerScreenState extends State<TriggerScreen> {
|
|||||||
Text('场景切换', style: Theme.of(context).textTheme.titleMedium),
|
Text('场景切换', style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: provider.sceneOptions.contains(_selectedScene) ? _selectedScene : null,
|
initialValue: provider.sceneOptions.contains(_selectedScene) ? _selectedScene : null,
|
||||||
items: provider.sceneOptions
|
items: provider.sceneOptions
|
||||||
.map(
|
.map(
|
||||||
(scene) => DropdownMenuItem<String>(
|
(scene) => DropdownMenuItem<String>(
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class AppTheme {
|
|||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
scaffoldBackgroundColor: AppColors.background,
|
scaffoldBackgroundColor: AppColors.background,
|
||||||
canvasColor: AppColors.background,
|
canvasColor: AppColors.background,
|
||||||
splashColor: AppColors.primary.withOpacity(0.12),
|
splashColor: AppColors.primary.withValues(alpha: 0.12),
|
||||||
highlightColor: AppColors.primary.withOpacity(0.08),
|
highlightColor: AppColors.primary.withValues(alpha: 0.08),
|
||||||
dividerColor: AppColors.border,
|
dividerColor: AppColors.border,
|
||||||
cardColor: AppColors.card,
|
cardColor: AppColors.card,
|
||||||
fontFamily: 'Noto Sans SC',
|
fontFamily: 'Noto Sans SC',
|
||||||
@@ -43,7 +43,7 @@ class AppTheme {
|
|||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
color: AppColors.card,
|
color: AppColors.card,
|
||||||
elevation: 6,
|
elevation: 6,
|
||||||
shadowColor: Colors.black.withOpacity(0.20),
|
shadowColor: Colors.black.withValues(alpha: 0.20),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.large),
|
borderRadius: BorderRadius.circular(AppRadius.large),
|
||||||
@@ -70,7 +70,7 @@ class AppTheme {
|
|||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: AppColors.card,
|
backgroundColor: AppColors.card,
|
||||||
height: 64,
|
height: 64,
|
||||||
indicatorColor: AppColors.primary.withOpacity(0.18),
|
indicatorColor: AppColors.primary.withValues(alpha: 0.18),
|
||||||
labelTextStyle: WidgetStateProperty.all(
|
labelTextStyle: WidgetStateProperty.all(
|
||||||
const TextStyle(
|
const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -123,7 +123,7 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
trackColor: WidgetStateProperty.resolveWith(
|
trackColor: WidgetStateProperty.resolveWith(
|
||||||
(states) => states.contains(WidgetState.selected)
|
(states) => states.contains(WidgetState.selected)
|
||||||
? AppColors.primary.withOpacity(0.4)
|
? AppColors.primary.withValues(alpha: 0.4)
|
||||||
: AppColors.border,
|
: AppColors.border,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class StatusCard extends StatelessWidget {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: accentColor.withOpacity(0.16),
|
color: accentColor.withValues(alpha: 0.16),
|
||||||
borderRadius: BorderRadius.circular(AppRadius.medium),
|
borderRadius: BorderRadius.circular(AppRadius.medium),
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: accentColor),
|
child: Icon(icon, color: accentColor),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class WifiListTile extends StatelessWidget {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.info.withOpacity(0.14),
|
color: AppColors.info.withValues(alpha: 0.14),
|
||||||
borderRadius: BorderRadius.circular(AppRadius.medium),
|
borderRadius: BorderRadius.circular(AppRadius.medium),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.wifi_rounded, color: AppColors.info),
|
child: const Icon(Icons.wifi_rounded, color: AppColors.info),
|
||||||
|
|||||||
402
clients/flutter/pubspec.lock
Normal file
402
clients/flutter/pubspec.lock
Normal file
@@ -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"
|
||||||
BIN
configs/downloads/showen-app.apk
Normal file
BIN
configs/downloads/showen-app.apk
Normal file
Binary file not shown.
@@ -61,3 +61,9 @@
|
|||||||
- 代码行数从 176 行减少到 ~60 行(减少约 66%)
|
- 代码行数从 176 行减少到 ~60 行(减少约 66%)
|
||||||
- 测试更新:src/core/tests.rs 中 screen_plugin_must_have_no_dependencies 改为 screen_plugin_must_depend_on_device
|
- 测试更新:src/core/tests.rs 中 screen_plugin_must_have_no_dependencies 改为 screen_plugin_must_depend_on_device
|
||||||
- 架构优势:ScreenPlugin 现在只做消息转发,硬件操作统一由 DevicePlugin 管理
|
- 架构优势: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”验收标准区分记录
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ struct PlaylistSnapshot {
|
|||||||
current_index: usize,
|
current_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AppInfoResponse {
|
||||||
|
version: String,
|
||||||
|
apk_available: bool,
|
||||||
|
apk_size: Option<u64>,
|
||||||
|
download_url: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn build_routes(
|
pub(crate) fn build_routes(
|
||||||
tx: mpsc::Sender<Envelope>,
|
tx: mpsc::Sender<Envelope>,
|
||||||
state: Arc<HttpState>,
|
state: Arc<HttpState>,
|
||||||
@@ -103,6 +111,7 @@ pub(crate) fn build_routes(
|
|||||||
let media_api = video_list_route(Arc::clone(&state))
|
let media_api = video_list_route(Arc::clone(&state))
|
||||||
.or(video_upload_route(Arc::clone(&state)))
|
.or(video_upload_route(Arc::clone(&state)))
|
||||||
.or(video_delete_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_status_route(tx.clone(), Arc::clone(&state)))
|
||||||
.or(wifi_scan_route(tx.clone(), Arc::clone(&state)))
|
.or(wifi_scan_route(tx.clone(), Arc::clone(&state)))
|
||||||
.or(wifi_connect_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<HttpState>,
|
||||||
|
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
||||||
|
warp::path!("api" / "app" / "info")
|
||||||
|
.and(warp::get())
|
||||||
|
.and(with_state(state))
|
||||||
|
.and_then(|state: Arc<HttpState>| 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(
|
fn video_upload_route(
|
||||||
state: Arc<HttpState>,
|
state: Arc<HttpState>,
|
||||||
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
|
||||||
@@ -1900,6 +1934,8 @@ const WEB_UI_HTML: &str = r##"<!doctype html>
|
|||||||
.download-qr img{display:block;width:180px;height:180px;border-radius:12px;background:#fff}
|
.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{display:flex;gap:10px;flex-wrap:wrap}
|
||||||
.download-actions>*{flex:1}
|
.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}
|
.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}
|
.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##"<!doctype html>
|
|||||||
<div id="download-modal" class="modal-backdrop" onclick="closeDownloadModal(event)">
|
<div id="download-modal" class="modal-backdrop" onclick="closeDownloadModal(event)">
|
||||||
<div class="download-modal">
|
<div class="download-modal">
|
||||||
<h2>下载 Showen App</h2>
|
<h2>下载 Showen App</h2>
|
||||||
<p>扫描二维码或点击下载,支持蓝牙配网和远程控制</p>
|
<p id="download-modal-desc">扫描二维码或点击下载,支持蓝牙配网和远程控制</p>
|
||||||
|
<div id="download-meta" class="download-meta">正在获取 App 信息...</div>
|
||||||
<div class="download-qr"><img id="download-qr-image" alt="Showen App 下载二维码"></div>
|
<div class="download-qr"><img id="download-qr-image" alt="Showen App 下载二维码"></div>
|
||||||
<div class="download-actions">
|
<div class="download-actions">
|
||||||
<a id="download-apk-link" class="btn header-link" href="/download/showen-app.apk" download>下载 APK</a>
|
<a id="download-apk-link" class="btn header-link" href="/download/showen-app.apk" download>下载 APK</a>
|
||||||
@@ -2136,8 +2173,43 @@ var cachedConfig=null,ws=null,wsReady=false;
|
|||||||
var fmCurrentDir='videos',fmCurrentPath='';
|
var fmCurrentDir='videos',fmCurrentPath='';
|
||||||
function $(id){return document.getElementById(id)}
|
function $(id){return document.getElementById(id)}
|
||||||
function toast(msg,err){var el=$('toast');el.textContent=msg;el.className='toast '+(err?'err':'ok');el.style.display='block';clearTimeout(el._t);el._t=setTimeout(function(){el.style.display='none'},3000)}
|
function toast(msg,err){var el=$('toast');el.textContent=msg;el.className='toast '+(err?'err':'ok');el.style.display='block';clearTimeout(el._t);el._t=setTimeout(function(){el.style.display='none'},3000)}
|
||||||
function appDownloadUrl(){return location.origin+'/download/showen-app.apk'}
|
function formatBytes(size){if(size===undefined||size===null||isNaN(size))return '--';if(size<1024)return size+' B';if(size<1048576)return(size/1024).toFixed(1)+' KB';if(size<1073741824)return(size/1048576).toFixed(1)+' MB';return(size/1073741824).toFixed(1)+' GB'}
|
||||||
function openDownloadModal(){var modal=$('download-modal');var url=appDownloadUrl();$('download-apk-link').href=url;$('download-qr-image').src='https://api.qrserver.com/v1/create-qr-code/?size=180x180&data='+encodeURIComponent(url);modal.classList.add('open')}
|
function updateDownloadModal(info){
|
||||||
|
var meta=$('download-meta'),link=$('download-apk-link'),qr=$('download-qr-image'),desc=$('download-modal-desc');
|
||||||
|
if(!info||!info.apk_available){
|
||||||
|
meta.textContent='APK 尚未发布,请稍后再试';
|
||||||
|
meta.className='download-meta warn';
|
||||||
|
link.style.display='none';
|
||||||
|
qr.style.display='none';
|
||||||
|
qr.removeAttribute('src');
|
||||||
|
desc.textContent='当前暂未提供可下载的 APK 安装包';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var url=location.origin+(info.download_url||'/download/showen-app.apk');
|
||||||
|
meta.textContent='版本 '+(info.version||'0.1.0')+' · '+formatBytes(info.apk_size);
|
||||||
|
meta.className='download-meta';
|
||||||
|
link.style.display='inline-flex';
|
||||||
|
link.href=info.download_url||'/download/showen-app.apk';
|
||||||
|
qr.style.display='block';
|
||||||
|
qr.src='https://api.qrserver.com/v1/create-qr-code/?size=180x180&data='+encodeURIComponent(url);
|
||||||
|
desc.textContent='扫描二维码或点击下载,支持蓝牙配网和远程控制';
|
||||||
|
}
|
||||||
|
function openDownloadModal(){
|
||||||
|
var modal=$('download-modal');
|
||||||
|
$('download-meta').textContent='正在获取 App 信息...';
|
||||||
|
$('download-meta').className='download-meta';
|
||||||
|
$('download-apk-link').style.display='none';
|
||||||
|
$('download-qr-image').style.display='none';
|
||||||
|
$('download-qr-image').removeAttribute('src');
|
||||||
|
$('download-modal-desc').textContent='扫描二维码或点击下载,支持蓝牙配网和远程控制';
|
||||||
|
modal.classList.add('open');
|
||||||
|
fetch('/api/app/info').then(function(r){if(!r.ok)throw new Error('加载 App 信息失败');return r.json()}).then(updateDownloadModal).catch(function(){
|
||||||
|
$('download-meta').textContent='APK 信息加载失败,请稍后再试';
|
||||||
|
$('download-meta').className='download-meta warn';
|
||||||
|
$('download-apk-link').style.display='none';
|
||||||
|
$('download-qr-image').style.display='none';
|
||||||
|
})
|
||||||
|
}
|
||||||
function closeDownloadModal(ev){if(ev&&ev.target!==$('download-modal'))return;$('download-modal').classList.remove('open')}
|
function closeDownloadModal(ev){if(ev&&ev.target!==$('download-modal'))return;$('download-modal').classList.remove('open')}
|
||||||
document.addEventListener('keydown',function(ev){if(ev.key==='Escape')closeDownloadModal()})
|
document.addEventListener('keydown',function(ev){if(ev.key==='Escape')closeDownloadModal()})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user