You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
602 lines
22 KiB
602 lines
22 KiB
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:audio_session/audio_session.dart';
|
|
import 'package:audio_waveforms/audio_waveforms.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart' show rootBundle;
|
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../../../core/utils/logger.dart';
|
|
import '../../../data/services/db/sqflite_api.dart';
|
|
import '../../../data/services/meeting/meeting_task_service.dart';
|
|
import '../../../data/services/meeting/meeting_upload_service.dart';
|
|
import '../../../data/services/network/api.dart';
|
|
import '../model/meeting_details_model.dart';
|
|
import '../model/meeting_model.dart';
|
|
|
|
/// 会议详情控制器
|
|
class MeetingDetailsController extends GetxController
|
|
with GetTickerProviderStateMixin, WidgetsBindingObserver {
|
|
late TabController tabController;
|
|
final RxInt currentIndex = 0.obs;
|
|
|
|
late AnimationController lottieController;
|
|
|
|
late final Worker _tasktypeListener;
|
|
|
|
final _meetingUploadService = Get.find<MeetingUploadService>();
|
|
final _meetingTaskService = Get.find<MeetingTaskService>();
|
|
|
|
final PlayerController playerController = PlayerController();
|
|
|
|
StreamSubscription? _currentDurationSubscription;
|
|
StreamSubscription? _playerStateSubscription;
|
|
|
|
RxBool isTop = false.obs;
|
|
RxInt durationMax = 0.obs;
|
|
RxDouble dragOffset = 210.w.obs;
|
|
|
|
RxBool isPlay = false.obs;
|
|
RxInt currentDuration = 0.obs;
|
|
|
|
// 播放器准备状态
|
|
RxBool isPlayerReady = false.obs;
|
|
|
|
RxInt speakerAllStarttime = (-1).obs;
|
|
RxInt speakerAllEndtime = (-1).obs;
|
|
|
|
int id = 0;
|
|
|
|
RxBool isLoading = true.obs;
|
|
Rx<MeetingModel> meetingData = MeetingModel().obs;
|
|
Rx<MeetingDetailsModel> meetingDetails = MeetingDetailsModel().obs;
|
|
RxList textList = [].obs;
|
|
RxBool isSpeakerText = false.obs;
|
|
List editSpeakerTextList = [];
|
|
RxInt editSpeakerTextId = 0.obs;
|
|
String editSpeakerTextChange = '';
|
|
|
|
RxInt summaryNumberModifications = 0.obs;
|
|
RxInt personnelNumberModifications = 0.obs;
|
|
|
|
String markmapLibJs = '';
|
|
String d3Js = '';
|
|
String markmapViewJs = '';
|
|
String markmapToolbarJs = '';
|
|
String saveSvgAsPngJs = '';
|
|
|
|
RxBool isDownload = false.obs;
|
|
RxDouble downloadProgress = 0.0.obs;
|
|
|
|
List categoryTemplateList = [];
|
|
Map editTemplate = {};
|
|
int deleteTemplateId = 0;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
tabController = TabController(length: 4, vsync: this);
|
|
tabController.addListener(() {
|
|
if (currentIndex.value == 2 && tabController.index != 2) {
|
|
FocusScope.of(Get.context!).unfocus();
|
|
}
|
|
currentIndex.value = tabController.index;
|
|
});
|
|
lottieController = AnimationController(vsync: this)
|
|
..duration = const Duration(seconds: 2);
|
|
final Map<String, dynamic> args = Get.arguments ?? {};
|
|
id = args['id'];
|
|
_meetingTaskService.meetingId = id;
|
|
WidgetsBinding.instance.addObserver(this);
|
|
_getDataDetails();
|
|
_loadJS();
|
|
_tasktypeListener = ever(_meetingTaskService.tasktype, (value) {
|
|
meetingDetails.value.tasktype = value;
|
|
// ⚠️ 只拿**有内容**的值。MeetingTaskService 那几个字段是「上一次轮询到
|
|
// 本条记录时」填的,没轮询到过就是空的;原来无条件覆盖,refreshFromServer
|
|
// 刚拉回来的总结/转写会被这里的空值当场抹掉(本地库是对的,所以退出重进
|
|
// 又有了)。原先那个 _everType 门槛也一并去掉:它是按「进页面时的状态」定的,
|
|
// 服务端跑得比本地快一步就把数据分支整个跳过。
|
|
if (_meetingTaskService.meetingId == id) {
|
|
if (value >= 3 && value < 10000 && _meetingTaskService.textList.isNotEmpty) {
|
|
textList.value = List.from(_meetingTaskService.textList);
|
|
}
|
|
if (value >= 5 && value < 10000 && _meetingTaskService.summary.isNotEmpty) {
|
|
meetingDetails.value.personnel = _meetingTaskService.personnel;
|
|
meetingDetails.value.summary = _meetingTaskService.summary;
|
|
meetingDetails.value.overview = _meetingTaskService.overview;
|
|
summaryNumberModifications.value++;
|
|
}
|
|
}
|
|
meetingDetails.refresh();
|
|
_syncProgressPolling();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
// 切后台回来补拉一次:全局轮询可能已被冻结/清空,这是最常见的「卡在转写中」场景
|
|
if (state == AppLifecycleState.resumed) unawaited(refreshFromServer());
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_refreshTimer?.cancel();
|
|
_refreshTimer = null;
|
|
_tasktypeListener.dispose();
|
|
_currentDurationSubscription?.cancel();
|
|
_playerStateSubscription?.cancel();
|
|
tabController.dispose();
|
|
playerController.dispose();
|
|
lottieController.dispose();
|
|
super.onClose();
|
|
}
|
|
|
|
void popInvokedWithResult(didPop, result) async {
|
|
_meetingUploadService.removeUpload(id);
|
|
_meetingTaskService.meetingId = 0;
|
|
}
|
|
|
|
void _getDataDetails() async {
|
|
final data = await SqfliteApi.getMeetingDetails(id);
|
|
meetingData.value = MeetingModel.fromJson(data);
|
|
meetingDetails.value = MeetingDetailsModel.fromJson(data['details']);
|
|
// tasktype 在 meeting / meetingdetails 两张表里各存一份
|
|
// (见 [SqfliteApi.setMeetingTaskType]),**以 meeting 表那份为准**:
|
|
// 它是历史上唯一每次状态变化都会写的,meetingdetails 那份在旧版本里
|
|
// 只有里程碑才更新,存量记录可能还停在 0。不取这一份的话,
|
|
// 一条正在转写的记录点进来会既没有等待态、也没有内容,只剩空白。
|
|
meetingDetails.value.tasktype = meetingData.value.tasktype;
|
|
_meetingTaskService.tasktype.value = meetingDetails.value.tasktype;
|
|
if (meetingDetails.value.tasktype == 5) {
|
|
await _meetingTaskService.readTask(id);
|
|
}
|
|
if (meetingDetails.value.tasktype >= 5 &&
|
|
meetingDetails.value.tasktype < 10000) {
|
|
dragOffset.value = 0;
|
|
isTop.value = true;
|
|
}
|
|
if (meetingData.value.filepath.isNotEmpty) {
|
|
await _loadPlayerController();
|
|
}
|
|
if (meetingDetails.value.tasktype >= 3 &&
|
|
meetingDetails.value.tasktype < 10000) {
|
|
List speakerList = await SqfliteApi.getMeetingSpeaker(id);
|
|
textList.value = List.from(speakerList);
|
|
}
|
|
isLoading.value = false;
|
|
_syncProgressPolling();
|
|
// 本地先出,服务端整条再覆盖(不 await:页面已经能用了)
|
|
unawaited(refreshFromServer());
|
|
}
|
|
|
|
// ---------- 进行中状态的自我刷新 ----------
|
|
//
|
|
// 本页原来一辈子只向服务端拉一次数据,之后完全依赖 MeetingTaskService 那个它既不
|
|
// 拥有、也无法重启的全局轮询:轮询队列会被首页退出 (stopTask) 清空、被 Synchrodata
|
|
// 整体替换、进程被冻结时定时器也不走——任何一条命中,页面就永远停在「正在转写…」,
|
|
// 退出重进才好(2026-09-20 苹果测试原话)。现在进行中就自己每 6s 拉一次,
|
|
// 到终态/失败态即停;切后台回来也补拉一次;占位图上还给了一个手动刷新。
|
|
Timer? _refreshTimer;
|
|
bool _refreshing = false;
|
|
static const Duration _progressPollInterval = Duration(seconds: 6);
|
|
|
|
bool get _inProgress {
|
|
final t = meetingDetails.value.tasktype;
|
|
return t > 0 && t < 5;
|
|
}
|
|
|
|
void _syncProgressPolling() {
|
|
if (isClosed) return;
|
|
if (_inProgress) {
|
|
_refreshTimer ??= Timer.periodic(_progressPollInterval, (_) {
|
|
unawaited(refreshFromServer());
|
|
});
|
|
} else {
|
|
_refreshTimer?.cancel();
|
|
_refreshTimer = null;
|
|
}
|
|
}
|
|
|
|
/// 用服务端整条记录刷新本地缓存与页面。
|
|
///
|
|
/// 列表接口只回骨架(见 Synchrodata),转写和总结**只从这里进本地库**:
|
|
/// 每次进详情拉一次 `echomeet_getrecord`,服务端是真源,别的设备重新生成的总结、
|
|
/// 改过的标题都在这一步跟过来。拉不到就沿用本地,不报错。并发调用直接返回。
|
|
Future<void> refreshFromServer() async {
|
|
if (_refreshing || isClosed) return;
|
|
_refreshing = true;
|
|
try {
|
|
await _refreshFromServer();
|
|
} finally {
|
|
_refreshing = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _refreshFromServer() async {
|
|
dynamic resp;
|
|
try {
|
|
resp = await Api.getRecordEchomeet({'id': id});
|
|
} catch (e) {
|
|
Logger.w('MeetingDetails', '拉取整条记录失败(沿用本地): $e');
|
|
return;
|
|
}
|
|
final record = resp?['record'];
|
|
if (record is! Map) return;
|
|
if (isClosed) return;
|
|
|
|
final int state = record['state'] ?? meetingDetails.value.tasktype;
|
|
final String title = record['title'] ?? meetingData.value.title;
|
|
final String audiourl = record['audiourl'] ?? meetingData.value.audiourl;
|
|
final String summary = record['summary'] ?? '';
|
|
final String overview = record['overview'] ?? '';
|
|
final String personnel = record['personnel'] ?? '';
|
|
|
|
await SqfliteApi.editMeetingTitle(id, {
|
|
'title': title,
|
|
'audiourl': audiourl,
|
|
'seconds': record['seconds'] ?? meetingData.value.seconds,
|
|
});
|
|
await SqfliteApi.setMeetingTaskType(id, state);
|
|
// 转写:translate 是服务端翻译/整理后的版本(无翻译时等于 original),
|
|
// 键与 meetingspeaker 列一一对应(meetingid/content/starttime/endtime/speaker)
|
|
final speakers = _decodeSpeakers(record['translate']);
|
|
if (speakers != null) {
|
|
await SqfliteApi.deleteMeetingSpeaker(id);
|
|
await SqfliteApi.insertMeetingSpeaker(speakers);
|
|
}
|
|
if (state >= 5 && state < 10000) {
|
|
await SqfliteApi.editMeeting(id, {
|
|
'personnel': personnel,
|
|
'summary': summary,
|
|
'overview': overview,
|
|
});
|
|
}
|
|
if (isClosed) return;
|
|
|
|
meetingData.value.title = title;
|
|
meetingData.value.audiourl = audiourl;
|
|
meetingData.value.tasktype = state;
|
|
meetingData.refresh();
|
|
if (speakers != null && state >= 3 && state < 10000) {
|
|
textList.value = List.from(await SqfliteApi.getMeetingSpeaker(id));
|
|
}
|
|
final bool summaryChanged = state >= 5 &&
|
|
state < 10000 &&
|
|
(meetingDetails.value.summary != summary ||
|
|
meetingDetails.value.overview != overview ||
|
|
meetingDetails.value.personnel != personnel);
|
|
if (summaryChanged) {
|
|
meetingDetails.value.summary = summary;
|
|
meetingDetails.value.overview = overview;
|
|
meetingDetails.value.personnel = personnel;
|
|
summaryNumberModifications.value++;
|
|
}
|
|
if (meetingDetails.value.tasktype != state) {
|
|
// 先把服务端拿到的内容喂给 MeetingTaskService,再推状态:
|
|
// 推状态会触发 onInit 里那个 ever 监听,它从 service 里取内容——
|
|
// 不先喂的话它取到的是空值(见那里的说明)。
|
|
if (_meetingTaskService.meetingId == id) {
|
|
if (state >= 3 && state < 10000 && textList.isNotEmpty) {
|
|
_meetingTaskService.textList = List.from(textList);
|
|
}
|
|
if (state >= 5 && state < 10000) {
|
|
_meetingTaskService.personnel = personnel;
|
|
_meetingTaskService.summary = summary;
|
|
_meetingTaskService.overview = overview;
|
|
}
|
|
}
|
|
meetingDetails.value.tasktype = state;
|
|
_meetingTaskService.tasktype.value = state;
|
|
}
|
|
meetingDetails.refresh();
|
|
_syncProgressPolling();
|
|
}
|
|
|
|
List<Map<String, Object?>>? _decodeSpeakers(dynamic raw) {
|
|
if (raw is! String || raw.trim().isEmpty) return null;
|
|
try {
|
|
final decoded = jsonDecode(raw);
|
|
if (decoded is! List || decoded.isEmpty) return null;
|
|
return decoded.map((e) => Map<String, Object?>.from(e as Map)).toList();
|
|
} catch (e) {
|
|
Logger.w('MeetingDetails', 'translate 解析失败,转写沿用本地: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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;
|
|
// 本地文件缺失:若云端存在音频,清空本地路径并持久化,
|
|
// 让 UI 自动切换为"下载"按钮,下载完成后即可播放;
|
|
// 仅当云端也没有音频时才提示文件不存在
|
|
if (meetingData.value.audiourl.isNotEmpty) {
|
|
Logger.d('MeetingDetails', '本地音频缺失,切换为下载态(云端URL可用)');
|
|
meetingData.value.filepath = '';
|
|
meetingData.refresh();
|
|
await SqfliteApi.editMeetingTitle(id, {'filePath': ''});
|
|
} else {
|
|
Get.snackbar(
|
|
'error'.tr,
|
|
'audioFileNotFound'.tr, // 音频文件不存在
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
Logger.d('MeetingDetails', '开始加载音频播放器: $filepath');
|
|
|
|
// iOS平台特殊处理:配置音频会话
|
|
if (Platform.isIOS) {
|
|
try {
|
|
Logger.d('MeetingDetails', 'iOS平台:开始配置音频会话');
|
|
final session = await AudioSession.instance;
|
|
await session.configure(const AudioSessionConfiguration(
|
|
avAudioSessionCategory: AVAudioSessionCategory.playback,
|
|
avAudioSessionCategoryOptions:
|
|
AVAudioSessionCategoryOptions.mixWithOthers,
|
|
avAudioSessionMode: AVAudioSessionMode.defaultMode,
|
|
avAudioSessionRouteSharingPolicy:
|
|
AVAudioSessionRouteSharingPolicy.defaultPolicy,
|
|
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
|
|
androidAudioAttributes: AndroidAudioAttributes(
|
|
contentType: AndroidAudioContentType.music,
|
|
usage: AndroidAudioUsage.media,
|
|
),
|
|
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
|
|
androidWillPauseWhenDucked: true,
|
|
));
|
|
Logger.d('MeetingDetails', 'iOS平台:音频会话配置成功');
|
|
} catch (e) {
|
|
Logger.error('iOS平台:音频会话配置失败: $e');
|
|
// 继续执行,尝试播放
|
|
}
|
|
}
|
|
|
|
// 准备播放器
|
|
await playerController.preparePlayer(
|
|
path: filepath,
|
|
shouldExtractWaveform: false,
|
|
noOfSamples: 88,
|
|
);
|
|
|
|
// 获取音频时长
|
|
final duration = await playerController.getDuration(DurationType.max);
|
|
// 校验时长有效性,防止无效值导致 DateUtil.formatDateMs 报错
|
|
// 有效的毫秒时间戳应该在合理范围内(大于0且小于未来100年)
|
|
const int maxValidDuration = 315360000000; // 约10年的毫秒数
|
|
if (duration > 0 && duration < maxValidDuration) {
|
|
durationMax.value = duration;
|
|
} else {
|
|
durationMax.value = 0;
|
|
Logger.w('MeetingDetails', '获取到无效的音频时长: $duration,已重置为0');
|
|
}
|
|
|
|
Logger.d('MeetingDetails', '音频播放器加载成功,时长: ${durationMax.value}ms');
|
|
|
|
// 设置播放完成模式
|
|
playerController.setFinishMode(finishMode: FinishMode.pause);
|
|
|
|
// 监听播放进度
|
|
_currentDurationSubscription =
|
|
playerController.onCurrentDurationChanged.listen((int position) {
|
|
currentDuration.value = position;
|
|
if (speakerAllEndtime.value > 0) {
|
|
if (position >= speakerAllEndtime.value) {
|
|
playerController.pausePlayer();
|
|
speakerAllStarttime.value = -1;
|
|
speakerAllEndtime.value = -1;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 监听播放状态
|
|
_playerStateSubscription = playerController.onPlayerStateChanged
|
|
.listen((PlayerState playerState) {
|
|
isPlay.value = playerState.isPlaying;
|
|
if (playerState.isPlaying) {
|
|
lottieController.repeat();
|
|
} else {
|
|
lottieController.stop();
|
|
}
|
|
});
|
|
|
|
// 标记播放器已准备好
|
|
isPlayerReady.value = true;
|
|
} catch (e, stackTrace) {
|
|
Logger.error('加载音频播放器失败: $e');
|
|
Logger.error('堆栈跟踪: $stackTrace');
|
|
isPlayerReady.value = false;
|
|
|
|
Get.snackbar(
|
|
'error'.tr,
|
|
'playerLoadFailed'.tr, // 音频播放器加载失败
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _loadJS() async {
|
|
markmapLibJs = await rootBundle.loadString('assets/js/markmap-lib.js');
|
|
d3Js = await rootBundle.loadString('assets/js/d3@7.js');
|
|
markmapViewJs = await rootBundle.loadString('assets/js/markmap-view.js');
|
|
markmapToolbarJs =
|
|
await rootBundle.loadString('assets/js/markmap-toolbar.js');
|
|
saveSvgAsPngJs =
|
|
await rootBundle.loadString('assets/js/saveSvgAsPng.min.js');
|
|
}
|
|
|
|
void downloadAudio() async {
|
|
if (isDownload.value) return;
|
|
isDownload.value = true;
|
|
final directory = await getApplicationDocumentsDirectory();
|
|
final directoryDir = Directory('${directory.path}/DownloadAudio');
|
|
if (!await directoryDir.exists()) {
|
|
await directoryDir.create(recursive: true);
|
|
}
|
|
final fileName = meetingData.value.audiourl.split('/').last;
|
|
final String filePath = '${directory.path}/DownloadAudio/$fileName';
|
|
await Dio().download(
|
|
meetingData.value.audiourl,
|
|
filePath,
|
|
onReceiveProgress: (int received, int total) {
|
|
downloadProgress.value = total > 0 ? received / total : 0.0;
|
|
},
|
|
);
|
|
meetingData.value.filepath = filePath;
|
|
meetingData.refresh();
|
|
_loadPlayerController();
|
|
SqfliteApi.editMeetingTitle(id, {'filePath': filePath});
|
|
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 {
|
|
if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) {
|
|
Map? foundElement = editSpeakerTextList.firstWhere(
|
|
(element) => element['id'] == editSpeakerTextId.value,
|
|
orElse: () => null,
|
|
);
|
|
if (foundElement != null) {
|
|
foundElement['content'] = editSpeakerTextChange;
|
|
} else {
|
|
editSpeakerTextList.add({
|
|
'id': editSpeakerTextId.value,
|
|
'content': editSpeakerTextChange,
|
|
});
|
|
}
|
|
}
|
|
if (editSpeakerTextList.isNotEmpty) {
|
|
EasyLoading.show();
|
|
var result = await SqfliteApi.editMeetingSpeaker(editSpeakerTextList);
|
|
if (result != null) {
|
|
for (var element in editSpeakerTextList) {
|
|
int index =
|
|
textList.indexWhere((item) => item['id'] == element['id']);
|
|
if (index != -1) {
|
|
final dataMap = Map<String, dynamic>.from(textList[index]);
|
|
dataMap['content'] = element['content'];
|
|
textList[index] = dataMap;
|
|
}
|
|
}
|
|
await Api.modifyRecordEchomeet({
|
|
'id': id,
|
|
'translate': jsonEncode(textList),
|
|
});
|
|
isSpeakerText.value = false;
|
|
editSpeakerTextId.value = 0;
|
|
editSpeakerTextList = [];
|
|
}
|
|
EasyLoading.dismiss();
|
|
} else {
|
|
isSpeakerText.value = false;
|
|
editSpeakerTextId.value = 0;
|
|
editSpeakerTextList = [];
|
|
}
|
|
}
|
|
|
|
void summaryCreate(
|
|
String formlanguage,
|
|
String tolanguage,
|
|
bool isdistinguishspeaker,
|
|
int templateid,
|
|
String tid,
|
|
) async {
|
|
EasyLoading.show();
|
|
final String audiourl = meetingData.value.audiourl;
|
|
final bool accepted = await _meetingTaskService.addTask(
|
|
id,
|
|
audiourl,
|
|
formlanguage,
|
|
tolanguage,
|
|
isdistinguishspeaker,
|
|
templateid,
|
|
tid,
|
|
);
|
|
EasyLoading.dismiss();
|
|
// ⚠️ 没受理(服务端拒绝 / 上传队列里找不到这条)就别把页面切成「等待转写」:
|
|
// 那样页面会永远停在占位图上等一个根本不存在的任务。
|
|
if (!accepted) {
|
|
Logger.w('MeetingDetails', '生成任务未受理 id=$id,页面保持未处理');
|
|
Get.snackbar('error'.tr, 'networkErrorRetry'.tr,
|
|
snackPosition: SnackPosition.TOP);
|
|
return;
|
|
}
|
|
meetingDetails.value.tasktype = 1;
|
|
meetingDetails.refresh();
|
|
_syncProgressPolling();
|
|
}
|
|
|
|
void refreshSummary(int templateid, String tid, String tolanguage) async {
|
|
EasyLoading.show();
|
|
await _meetingTaskService.refreshTask(id, templateid, tid, tolanguage);
|
|
meetingDetails.value.tasktype = 3;
|
|
meetingDetails.refresh();
|
|
_syncProgressPolling();
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|