Browse Source

chore: 添加多渠道打包脚本并清理项目文件

- 新增 build_channels.sh 脚本,支持华为、小米、OPPO、vivo、荣耀和谷歌渠道的交互式选择打包
- 脚本提供构建前清理、日志记录、错误处理和构建结果汇总功能
- 删除已过时的 AUDIO_PLAYBACK_FIX.md 文档,该文档记录的音频播放问题已修复
- 在 .gitignore 中添加 .claude/ 目录,避免 Claude Code 生成的文件被提交
weicu
lxm 7 months ago
parent
commit
943d70193f
  1. 3
      .gitignore
  2. 166
      AUDIO_PLAYBACK_FIX.md
  3. 519
      build_channels.sh

3
.gitignore

@ -1,5 +1,8 @@
# See https://www.dartlang.org/guides/libraries/private-files # See https://www.dartlang.org/guides/libraries/private-files
# Claude Code
.claude/
# Files and directories created by pub # Files and directories created by pub
.dart_tool/ .dart_tool/
.packages .packages

166
AUDIO_PLAYBACK_FIX.md

@ -1,166 +0,0 @@
# 音频播放Bug修复报告
## 问题描述
在会议详情页面(meeting detail page)中,点击播放按钮(▶️)时没有任何反应,音频无法播放。
影响两种类型的音频:
1. 录音的音频(recorded audio)
2. 导入的音频(imported audio)
## 根本原因分析
### 1. 缺少播放器准备状态检查
**问题:** `MeetingDetailsController` 中的 `_loadPlayerController()` 方法没有错误处理和状态跟踪。
- 播放器初始化失败时没有任何提示
- UI层无法知道播放器是否准备好
- 文件路径错误或文件不存在时静默失败
### 2. UI层缺少状态验证
**问题:** `meeting_details_view.dart` 中的播放按钮点击事件没有检查播放器状态。
- 直接调用 `startPlayer()` 而不验证播放器是否已初始化
- 没有异常捕获机制
- 用户点击后没有任何反馈
### 3. 缺少日志记录
**问题:** 没有足够的日志来追踪播放器初始化和播放过程。
- 无法诊断失败原因
- 难以定位问题发生的具体环节
## 修复方案
### 修改1: MeetingDetailsController - 添加播放器准备状态
**文件:** `lib/modules/meeting/controllers/meeting_details_controller.dart`
**变更:**
1. 添加 `isPlayerReady` 状态变量来跟踪播放器是否准备好
2. 重写 `_loadPlayerController()` 方法,添加完整的错误处理
3. 添加文件路径验证和文件存在性检查
4. 添加详细的日志记录
5. 添加用户友好的错误提示
**关键代码:**
```dart
// 新增状态变量
RxBool isPlayerReady = false.obs;
// 改进的播放器加载方法
Future<void> _loadPlayerController() async {
try {
final filepath = meetingData.value.filepath;
// 验证文件路径
if (filepath.isEmpty) {
Logger.error('音频文件路径为空,无法加载播放器');
isPlayerReady.value = false;
return;
}
// 验证文件是否存在
final file = File(filepath);
if (!await file.exists()) {
Logger.error('音频文件不存在: $filepath');
isPlayerReady.value = false;
Get.snackbar('error'.tr, '音频文件不存在', ...);
return;
}
Logger.d('MeetingDetails', '开始加载音频播放器: $filepath');
// 准备播放器
await playerController.preparePlayer(...);
// 标记播放器已准备好
isPlayerReady.value = true;
} catch (e, stackTrace) {
Logger.error('加载音频播放器失败: $e');
isPlayerReady.value = false;
Get.snackbar('error'.tr, '音频播放器加载失败', ...);
}
}
```
### 修改2: MeetingDetailsView - 添加播放前验证
**文件:** `lib/modules/meeting/views/meeting_details_view.dart`
**变更:**
1. 在播放按钮点击事件中添加播放器准备状态检查
2. 添加异常捕获和用户提示
3. 使用 async/await 确保异步操作正确执行
**关键代码:**
```dart
GestureDetector(
onTap: () async {
if (controller.meetingData.value.filepath.isEmpty) {
controller.downloadAudio();
} else {
// 检查播放器是否已准备好
if (!controller.isPlayerReady.value) {
Get.snackbar('error'.tr, '音频播放器未准备好,请稍候', ...);
return;
}
try {
if (controller.isPlay.value) {
await controller.playerController.pausePlayer();
} else {
await controller.playerController.startPlayer();
}
} catch (e) {
Get.snackbar('error'.tr, '播放失败: ${e.toString()}', ...);
}
}
},
...
)
```
## 修复效果
### 修复前
- ❌ 点击播放按钮无响应
- ❌ 没有错误提示
- ❌ 无法诊断问题
- ❌ 用户体验差
### 修复后
- ✅ 播放器初始化失败时有明确提示
- ✅ 文件不存在时提示用户
- ✅ 播放器未准备好时阻止播放并提示
- ✅ 播放失败时显示具体错误信息
- ✅ 完整的日志记录便于调试
- ✅ 录音音频和导入音频都能正常播放
## 测试建议
1. **测试录音音频播放**
- 创建新的录音
- 保存录音
- 进入会议详情页面
- 点击播放按钮验证播放功能
2. **测试导入音频播放**
- 导入外部音频文件
- 进入会议详情页面
- 点击播放按钮验证播放功能
3. **测试错误场景**
- 删除音频文件后尝试播放(应提示文件不存在)
- 在播放器未准备好时点击播放(应提示等待)
## 技术细节
### 使用的技术
- **audio_waveforms**: Flutter音频播放库
- **GetX**: 状态管理和依赖注入
- **Logger**: 日志记录工具
### 关键改进点
1. **状态管理**: 使用 `RxBool isPlayerReady` 响应式状态
2. **错误处理**: try-catch 包裹所有异步操作
3. **用户反馈**: Get.snackbar 提供即时反馈
4. **日志记录**: Logger 记录关键操作和错误
## 总结
此次修复通过添加完善的状态检查、错误处理和用户反馈机制,彻底解决了音频播放按钮无响应的问题。修复后的代码更加健壮,用户体验显著提升。

519
build_channels.sh

@ -0,0 +1,519 @@
#!/bin/bash
# =================================================================
# Voitrans 多渠道 Android 打包脚本
# 用法:
# ./build_channels.sh # 打包所有渠道
# ./build_channels.sh huawei google # 只打包指定渠道
# ./build_channels.sh --clean # 清理后再打包所有渠道
# ./build_channels.sh --list # 列出所有支持的渠道
# =================================================================
set -euo pipefail
# ---------- 常量定义 ----------
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/.env"
PUBSPEC_FILE="${SCRIPT_DIR}/pubspec.yaml"
OUTPUT_DIR="/Users/liuxiamin/work/build_outputs"
APP_NAME="Voitrans"
BUILD_DATE=$(date +"%m%d")
# ---------- 渠道配置 ----------
# 渠道名列表(保证顺序)
CHANNEL_NAMES="huawei xiaomi oppo vivo honor google"
# 根据渠道名获取 .env 中的 Channel 值
get_channel_env_value() {
case "$1" in
huawei) echo "CHINA_HUAWEI" ;;
xiaomi) echo "CHINA_XIAOMI" ;;
oppo) echo "CHINA_OPPO" ;;
vivo) echo "CHINA_VIVO" ;;
honor) echo "CHINA_HONOR" ;;
google) echo "GLOBAL_GOOGLE" ;;
*) echo "" ;;
esac
}
# 根据渠道名获取构建类型
get_build_type() {
case "$1" in
google) echo "aab" ;;
*) echo "apk" ;;
esac
}
# 校验渠道名是否有效
is_valid_channel() {
case "$1" in
huawei|xiaomi|oppo|vivo|honor|google) return 0 ;;
*) return 1 ;;
esac
}
# 构建结果追踪
SUCCESS_CHANNELS=""
FAILED_CHANNELS=""
# ---------- 颜色输出 ----------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# ---------- 辅助函数 ----------
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 提取版本号(从 pubspec.yaml 的 version 字段,去掉构建号)
get_version() {
grep '^version:' "$PUBSPEC_FILE" | sed 's/version: *//' | cut -d'+' -f1
}
# 备份 .env
backup_env() {
cp "$ENV_FILE" "${ENV_FILE}.bak"
log_info "已备份 .env -> .env.bak"
}
# 恢复 .env
restore_env() {
if [ -f "${ENV_FILE}.bak" ]; then
mv "${ENV_FILE}.bak" "$ENV_FILE"
log_info "已恢复 .env 文件"
fi
}
# 修改 .env 中的 Channel 值(兼容 macOS sed)
set_channel() {
local channel_value="$1"
if [ "$(uname)" = "Darwin" ]; then
sed -i '' "s/^Channel=.*/Channel=${channel_value}/" "$ENV_FILE"
else
sed -i "s/^Channel=.*/Channel=${channel_value}/" "$ENV_FILE"
fi
}
# 列出所有渠道
list_channels() {
echo ""
echo "支持的渠道列表:"
echo " 序号 渠道名 .env值 构建格式"
echo " ---- -------- ---------------- --------"
local idx=1
for ch in $CHANNEL_NAMES; do
local env_val
env_val=$(get_channel_env_value "$ch")
local build_type
build_type=$(get_build_type "$ch")
printf " [%d] %-10s %-18s %s\n" "$idx" "$ch" "$env_val" "$build_type"
idx=$((idx + 1))
done
echo ""
}
# 交互式选择渠道(上下键移动,空格勾选,Enter确认)
interactive_select() {
# 构建菜单项数组
local items="全部打包"
for ch in $CHANNEL_NAMES; do
local bt
bt=$(get_build_type "$ch")
items="${items}|${ch} (${bt})"
done
local total_items=7 # 全部打包 + 6个渠道
local cursor=0 # 当前光标位置
# 选中状态:0=未选 1=已选(用空格分隔的列表,索引0-6)
local selected="0 0 0 0 0 0 0"
# 获取第n项的选中状态
get_selected() {
echo "$selected" | cut -d' ' -f$(($1 + 1))
}
# 切换第n项的选中状态
toggle_selected() {
local idx=$1
local new_selected=""
local i=0
for val in $selected; do
if [ $i -eq $idx ]; then
if [ "$val" = "0" ]; then
# 如果选中"全部打包",取消其他所有
if [ $idx -eq 0 ]; then
selected="1 0 0 0 0 0 0"
return
fi
val="1"
else
val="0"
fi
fi
# 选中单个渠道时,取消"全部打包"
if [ $idx -ne 0 ] && [ $i -eq 0 ]; then
val="0"
fi
if [ -z "$new_selected" ]; then
new_selected="$val"
else
new_selected="$new_selected $val"
fi
i=$((i + 1))
done
selected="$new_selected"
}
# 绘制菜单
draw_menu() {
# 移动光标到菜单起始位置
local i=0
printf "\033[${total_items}A" # 向上移动
local item_idx=0
# 保存IFS并用|分割
local OLD_IFS="$IFS"
IFS='|'
for item in $items; do
local marker=" "
local sel
sel=$(get_selected $item_idx)
if [ "$sel" = "1" ]; then
marker="✓"
fi
if [ $item_idx -eq $cursor ]; then
# 高亮当前行
printf "\033[2K ${GREEN}▸ [${marker}] ${item}${NC}\n"
else
printf "\033[2K [${marker}] ${item}\n"
fi
item_idx=$((item_idx + 1))
done
IFS="$OLD_IFS"
}
# 显示标题
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ Voitrans 多渠道打包工具 ║"
echo "╚══════════════════════════════════════════╝"
echo ""
echo " ↑/↓ 移动 空格 勾选 Enter 确认 q 退出"
echo ""
# 首次绘制菜单(先输出占位行)
local i=0
while [ $i -lt $total_items ]; do
echo ""
i=$((i + 1))
done
draw_menu
# 读取键盘输入
while true; do
# 读取单个字符(静默模式)
IFS= read -rsn1 key
case "$key" in
$'\x1b')
# 读取转义序列的剩余部分
read -rsn2 seq
case "$seq" in
'[A') # 上箭头
if [ $cursor -gt 0 ]; then
cursor=$((cursor - 1))
else
cursor=$((total_items - 1))
fi
;;
'[B') # 下箭头
if [ $cursor -lt $((total_items - 1)) ]; then
cursor=$((cursor + 1))
else
cursor=0
fi
;;
esac
;;
' ') # 空格:切换选中
toggle_selected $cursor
;;
'') # Enter:确认
break
;;
'q'|'Q') # 退出
echo ""
echo "已取消"
exit 0
;;
esac
draw_menu
done
# 根据选中状态构建渠道列表
if [ "$(get_selected 0)" = "1" ]; then
SELECTED_CHANNELS="$CHANNEL_NAMES"
else
SELECTED_CHANNELS=""
local idx=1
for ch in $CHANNEL_NAMES; do
if [ "$(get_selected $idx)" = "1" ]; then
SELECTED_CHANNELS="${SELECTED_CHANNELS} ${ch}"
fi
idx=$((idx + 1))
done
SELECTED_CHANNELS=$(echo "$SELECTED_CHANNELS" | sed 's/^ *//')
fi
if [ -z "$SELECTED_CHANNELS" ]; then
log_error "未选择任何渠道"
exit 1
fi
echo ""
log_info "已选择: ${SELECTED_CHANNELS}"
}
# ---------- 核心构建函数 ----------
build_channel() {
local channel_name="$1"
local env_value
env_value=$(get_channel_env_value "$channel_name")
local build_type
build_type=$(get_build_type "$channel_name")
local version
version=$(get_version)
local output_filename="${APP_NAME}_${BUILD_DATE}_${version}_${channel_name}.${build_type}"
echo ""
echo "========================================"
log_info "开始构建渠道: ${channel_name} (${env_value})"
log_info "构建类型: ${build_type}"
log_info "输出文件: ${output_filename}"
echo "========================================"
# 1. 修改 .env 中的 Channel 值
set_channel "$env_value"
log_info "已设置 Channel=${env_value}"
# 2. 执行 flutter build
local build_start
build_start=$(date +%s)
local build_result=0
local source_file=""
if [ "$build_type" = "apk" ]; then
flutter build apk \
--obfuscate \
--split-debug-info=./build/app/outputs/symbols \
2>&1 | tee "${OUTPUT_DIR}/build_${channel_name}.log" || build_result=$?
source_file="${SCRIPT_DIR}/build/app/outputs/flutter-apk/app-release.apk"
else
flutter build aab \
--obfuscate \
--split-debug-info=./build/app/outputs/symbols \
2>&1 | tee "${OUTPUT_DIR}/build_${channel_name}.log" || build_result=$?
source_file="${SCRIPT_DIR}/build/app/outputs/bundle/release/app-release.aab"
fi
local build_end
build_end=$(date +%s)
local build_duration=$((build_end - build_start))
# 3. 检查构建结果
if [ $build_result -ne 0 ]; then
log_error "渠道 ${channel_name} 构建失败!(耗时 ${build_duration}秒)"
FAILED_CHANNELS="${FAILED_CHANNELS} ${channel_name}"
return 1
fi
# 4. 复制并重命名输出文件
if [ -f "$source_file" ]; then
cp "$source_file" "${OUTPUT_DIR}/${output_filename}"
local file_size
file_size=$(du -h "${OUTPUT_DIR}/${output_filename}" | cut -f1)
# 构建成功,删除日志文件
rm -f "${OUTPUT_DIR}/build_${channel_name}.log"
log_success "渠道 ${channel_name} 构建成功!(耗时 ${build_duration}秒, 大小 ${file_size})"
log_success "输出: ${OUTPUT_DIR}/${output_filename}"
SUCCESS_CHANNELS="${SUCCESS_CHANNELS} ${channel_name}:${build_duration}"
else
log_error "构建产物未找到: ${source_file}"
FAILED_CHANNELS="${FAILED_CHANNELS} ${channel_name}"
return 1
fi
}
# ---------- 主流程 ----------
main() {
local total_start
total_start=$(date +%s)
# 参数解析
local do_clean=false
local channels_to_build=""
for arg in "$@"; do
case "$arg" in
--list|-l)
list_channels
exit 0
;;
--clean|-c)
do_clean=true
;;
--help|-h)
echo "用法: $0 [选项] [渠道名...]"
echo ""
echo "选项:"
echo " --list, -l 列出所有支持的渠道"
echo " --clean, -c 构建前先执行 flutter clean"
echo " --help, -h 显示帮助信息"
echo ""
echo "示例:"
echo " $0 # 打包所有渠道"
echo " $0 huawei google # 只打包华为和谷歌"
echo " $0 --clean # 清理后打包所有渠道"
exit 0
;;
*)
if ! is_valid_channel "$arg"; then
log_error "未知渠道: $arg"
list_channels
exit 1
fi
channels_to_build="${channels_to_build} ${arg}"
;;
esac
done
# 如果没有指定渠道,进入交互选择模式
if [ -z "$channels_to_build" ]; then
interactive_select
channels_to_build="$SELECTED_CHANNELS"
fi
# 去掉前导空格
channels_to_build=$(echo "$channels_to_build" | sed 's/^ *//')
local version
version=$(get_version)
# 计算渠道总数
local total=0
for _ in $channels_to_build; do
total=$((total + 1))
done
# 显示构建计划
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ Voitrans 多渠道打包工具 ║"
echo "╚══════════════════════════════════════════╝"
echo ""
log_info "构建渠道: ${channels_to_build}"
log_info "版本号: ${version}"
log_info "构建日期: ${BUILD_DATE}"
log_info "输出目录: ${OUTPUT_DIR}"
log_info "共 ${total} 个渠道"
# 前置检查
if ! command -v flutter &> /dev/null; then
log_error "flutter 命令未找到,请确保 Flutter SDK 已安装并配置 PATH"
exit 1
fi
if [ ! -f "$ENV_FILE" ]; then
log_error ".env 文件不存在: $ENV_FILE"
exit 1
fi
# 创建输出目录
mkdir -p "$OUTPUT_DIR"
# 可选:flutter clean
if [ "$do_clean" = true ]; then
log_info "执行 flutter clean..."
flutter clean
log_info "执行 flutter pub get..."
flutter pub get
fi
# 备份 .env 并注册 trap(确保异常退出也能恢复)
backup_env
trap restore_env EXIT
# 遍历构建
local current=0
for channel in $channels_to_build; do
current=$((current + 1))
echo ""
echo "████████████████████████████████████████"
echo " 进度: [${current}/${total}] 渠道: ${channel}"
echo "████████████████████████████████████████"
build_channel "$channel" || true
done
# ---------- 构建结果汇总 ----------
local total_end
total_end=$(date +%s)
local total_duration=$((total_end - total_start))
local total_min=$((total_duration / 60))
local total_sec=$((total_duration % 60))
# 统计成功/失败数量
local success_count=0
local fail_count=0
for _ in $SUCCESS_CHANNELS; do
success_count=$((success_count + 1))
done
for _ in $FAILED_CHANNELS; do
fail_count=$((fail_count + 1))
done
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ 构 建 结 果 汇 总 ║"
echo "╠══════════════════════════════════════════╣"
if [ $success_count -gt 0 ]; then
echo "║ 成功: ║"
for item in $SUCCESS_CHANNELS; do
local name="${item%%:*}"
local dur="${item##*:}"
printf "║ ✅ %-12s (%s秒) ║\n" "$name" "$dur"
done
fi
if [ $fail_count -gt 0 ]; then
echo "║ 失败: ║"
for name in $FAILED_CHANNELS; do
printf "║ ❌ %-12s ║\n" "$name"
done
fi
echo "╠══════════════════════════════════════════╣"
printf "║ 成功: %d/%d 总耗时: %d分%d秒 ║\n" \
"$success_count" "$total" "$total_min" "$total_sec"
echo "╚══════════════════════════════════════════╝"
# 列出输出目录中的安装包文件
if [ $success_count -gt 0 ]; then
echo ""
log_info "输出文件列表:"
ls -lh "${OUTPUT_DIR}/${APP_NAME}_${BUILD_DATE}"_* 2>/dev/null || true
fi
# 如果有失败的渠道,退出码为 1
if [ $fail_count -gt 0 ]; then
exit 1
fi
}
main "$@"
Loading…
Cancel
Save