Browse Source
- 新增 build_channels.sh 脚本,支持华为、小米、OPPO、vivo、荣耀和谷歌渠道的交互式选择打包 - 脚本提供构建前清理、日志记录、错误处理和构建结果汇总功能 - 删除已过时的 AUDIO_PLAYBACK_FIX.md 文档,该文档记录的音频播放问题已修复 - 在 .gitignore 中添加 .claude/ 目录,避免 Claude Code 生成的文件被提交newdev_shunjiawei
3 changed files with 522 additions and 166 deletions
@ -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 记录关键操作和错误 |
|
||||
|
|
||||
## 总结 |
|
||||
此次修复通过添加完善的状态检查、错误处理和用户反馈机制,彻底解决了音频播放按钮无响应的问题。修复后的代码更加健壮,用户体验显著提升。 |
|
||||
|
|
||||
@ -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…
Reference in new issue