Browse Source

feat(meeting): 增加云端音频下载及导出分享支持

- 新增提示文案“正在从云端获取音频...”用于本地音频缺失时下载提示
- 在MeetingDetailsController中添加downloadAudioForShare方法,实现云端音频的异步下载并返回本地路径
- 在分享底部弹窗增加导出音频功能,优先使用本地文件,若缺失则尝试从云端下载
- 下载成功后自动更新本地文件路径并触发分享,失败则显示文件不存在提示
- 分享方法中增加本地文件有效性检查,提升分享流程的鲁棒性
- 优化日志输出格式,统一错误日志信息,便于排查问题
- 修改导出PDF等其他功能的错误日志格式,使日志信息更清晰明确
newdev_shunjiawei
lxm 7 months ago
parent
commit
f6db7ad075
  1. 1
      lib/core/translations/language/en_us.dart
  2. 1
      lib/core/translations/language/zh_cn.dart
  3. 41
      lib/modules/meeting/controllers/meeting_details_controller.dart
  4. 61
      lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart

1
lib/core/translations/language/en_us.dart

@ -699,6 +699,7 @@ const Map<String, String> enUS = {
'playerNotReady': 'Audio player is not ready, please wait', 'playerNotReady': 'Audio player is not ready, please wait',
'playFailed': 'Playback failed', 'playFailed': 'Playback failed',
'audioFileNotFound': 'Audio file not found', 'audioFileNotFound': 'Audio file not found',
'fetchingAudio': 'Fetching audio from cloud...', // Prompt shown when local audio file is missing and downloading from cloud
'playerLoadFailed': 'Audio player failed to load', 'playerLoadFailed': 'Audio player failed to load',
// Meeting模块 - 录音语言相关 // Meeting模块 - 录音语言相关

1
lib/core/translations/language/zh_cn.dart

@ -648,6 +648,7 @@ const Map<String, String> zhCN = {
'playerNotReady': '音频播放器未准备好,请稍候', 'playerNotReady': '音频播放器未准备好,请稍候',
'playFailed': '播放失败', 'playFailed': '播放失败',
'audioFileNotFound': '音频文件不存在', 'audioFileNotFound': '音频文件不存在',
'fetchingAudio': '正在从云端获取音频...', // 导出音频时本地文件不存在,尝试从云端下载的提示
'playerLoadFailed': '音频播放器加载失败', 'playerLoadFailed': '音频播放器加载失败',
// Meeting模块 - 录音语言相关 // Meeting模块 - 录音语言相关

41
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -306,6 +306,47 @@ class MeetingDetailsController extends GetxController
isDownload.value = false; isDownload.value = false;
} }
/// 从云端下载音频文件(用于分享/导出场景)
/// 与 [downloadAudio] 的区别:有返回值,便于调用方判断是否成功
///
/// 返回值:下载成功的本地文件路径;失败时返回 null
Future<String?> downloadAudioForShare() async {
final audioUrl = meetingData.value.audiourl;
if (audioUrl.isEmpty) {
Logger.error('云端音频URL为空,无法从云端下载');
return null;
}
try {
final directory = await getApplicationDocumentsDirectory();
final directoryDir = Directory('${directory.path}/DownloadAudio');
if (!await directoryDir.exists()) {
await directoryDir.create(recursive: true);
}
final fileName = audioUrl.split('/').last;
final String filePath = '${directory.path}/DownloadAudio/$fileName';
await Dio().download(
audioUrl,
filePath,
onReceiveProgress: (int received, int total) {
downloadProgress.value = total > 0 ? received / total : 0.0;
},
);
// 更新本地路径并持久化
meetingData.value.filepath = filePath;
meetingData.refresh();
SqfliteApi.editMeetingTitle(id, {'filePath': filePath});
// 若播放器尚未初始化,顺带加载(异步,不阻塞分享流程)
if (!isPlayerReady.value) {
_loadPlayerController();
}
Logger.d('MeetingDetails', '云端音频下载成功: $filePath');
return filePath;
} catch (e) {
Logger.error('从云端下载音频失败: $e');
return null;
}
}
void editSpeakerText() async { void editSpeakerText() async {
if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) { if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) {
Map? foundElement = editSpeakerTextList.firstWhere( Map? foundElement = editSpeakerTextList.firstWhere(

61
lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart

@ -241,7 +241,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
'shareSuccess'.tr); // 对应中文:分享完成 'shareSuccess'.tr); // 对应中文:分享完成
} }
} catch (e) { } catch (e) {
Logger.error('分享链接失败', e); // 技术日志 Logger.error('分享链接失败: $e'); // 技术日志
EasyLoading.showToast( EasyLoading.showToast(
'operationFailedPleaseRetry'.tr); // 对应中文:操作失败,请重试 'operationFailedPleaseRetry'.tr); // 对应中文:操作失败,请重试
} }
@ -296,10 +296,9 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
// - title: 分享时显示的标题文本 // - title: 分享时显示的标题文本
_shareItem( _shareItem(
Icons.multitrack_audio, 'exportAudio'.tr, // 对应中文:导出音频 Icons.multitrack_audio, 'exportAudio'.tr, // 对应中文:导出音频
onTap: () { onTap: () async {
Get.back(); Get.back();
_share(_controller.meetingData.value.filepath, await _exportAudio();
'shareMeetingAudio'.tr); // 对应中文:分享会议音频
}), }),
// 【核心功能模块5:导出转写】 // 【核心功能模块5:导出转写】
// 功能说明:将转写内容导出为TXT文件并分享 // 功能说明:将转写内容导出为TXT文件并分享
@ -543,6 +542,48 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
/// 核心功能方法 /// 核心功能方法
/// ========================================================================== /// ==========================================================================
/// 导出音频方法
/// 优先使用本地文件;本地不存在时尝试从云端下载,下载成功后继续分享
///
/// 执行流程:
/// 1. 检查本地文件是否存在 → 直接分享
/// 2. 本地不存在 → 检查是否有云端 URL → 无 URL 则显示"文件不存在"
/// 3. 有云端 URL → 显示加载提示并执行下载
/// 4. 下载成功 → 继续分享;下载失败 → 显示"文件不存在"提示
Future<void> _exportAudio() async {
final filepath = _controller.meetingData.value.filepath;
final audioUrl = _controller.meetingData.value.audiourl;
// 本地文件存在,直接分享
if (filepath.isNotEmpty && File(filepath).existsSync()) {
_share(filepath, 'shareMeetingAudio'.tr); // 对应中文:分享会议音频
return;
}
// 本地不存在,检查是否有云端 URL 可以下载
if (audioUrl.isEmpty) {
Logger.error('导出音频失败: 本地文件不存在且云端URL为空');
EasyLoading.showToast('audioFileNotFound'.tr); // 对应中文:音频文件不存在
return;
}
// 从云端下载
EasyLoading.show(status: 'fetchingAudio'.tr); // 对应中文:正在从云端获取音频...
final downloadedPath = await _controller.downloadAudioForShare();
EasyLoading.dismiss();
if (downloadedPath != null &&
downloadedPath.isNotEmpty &&
File(downloadedPath).existsSync()) {
// 下载成功,继续分享
_share(downloadedPath, 'shareMeetingAudio'.tr); // 对应中文:分享会议音频
} else {
// 下载失败,显示兜底提示
Logger.error('导出音频失败: 云端下载失败 (url: $audioUrl)');
EasyLoading.showToast('audioFileNotFound'.tr); // 对应中文:音频文件不存在
}
}
/// 分享文件方法 /// 分享文件方法
/// 使用 SharePlus 插件分享文件到系统分享面板 /// 使用 SharePlus 插件分享文件到系统分享面板
/// ///
@ -558,6 +599,13 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
/// 返回值:Future<void> - 异步操作 /// 返回值:Future<void> - 异步操作
Future<void> _share(String filepath, String title) async { Future<void> _share(String filepath, String title) async {
try { try {
// 检查文件路径是否为空或文件是否存在
if (filepath.isEmpty || !File(filepath).existsSync()) {
Logger.error('分享文件失败: 文件不存在 (path: $filepath)');
EasyLoading.showToast(
'audioFileNotFound'.tr); // 对应中文:音频文件不存在
return;
}
final result = await SharePlus.instance.share(ShareParams( final result = await SharePlus.instance.share(ShareParams(
text: title, text: title,
files: [XFile(filepath)], files: [XFile(filepath)],
@ -567,7 +615,8 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
EasyLoading.showToast('shareSuccess'.tr); // 对应中文:分享完成! EasyLoading.showToast('shareSuccess'.tr); // 对应中文:分享完成!
} }
} catch (e) { } catch (e) {
Logger.error('分享文件失败: $filepath', e); // 技术日志 // 将错误信息合并到单条日志消息中,避免多行日志被截断
Logger.error('分享文件失败 (path: $filepath, 错误: $e)');
EasyLoading.showToast('operationFailedPleaseRetry'.tr); // 对应中文:操作失败,请重试 EasyLoading.showToast('operationFailedPleaseRetry'.tr); // 对应中文:操作失败,请重试
} }
} }
@ -781,7 +830,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
await file.writeAsBytes(await markdownPdf.save()); await file.writeAsBytes(await markdownPdf.save());
await _share(file.path, 'exportSummary'.tr); // 对应中文:分享会议总结 await _share(file.path, 'exportSummary'.tr); // 对应中文:分享会议总结
} catch (e) { } catch (e) {
Logger.error('导出PDF失败', e); // 技术日志 Logger.error('导出PDF失败: $e'); // 技术日志
EasyLoading.showToast('operationFailedPleaseRetry'.tr); // 对应中文:操作失败,请重试 EasyLoading.showToast('operationFailedPleaseRetry'.tr); // 对应中文:操作失败,请重试
} finally { } finally {
EasyLoading.dismiss(); EasyLoading.dismiss();

Loading…
Cancel
Save