controller.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. import 'dart:async';
  2. import 'dart:developer';
  3. import 'dart:io';
  4. import 'package:connectivity_plus/connectivity_plus.dart';
  5. import 'package:electronic_assistant/base/base_controller.dart';
  6. import 'package:electronic_assistant/data/consts/event_report_id.dart';
  7. import 'package:electronic_assistant/data/repositories/account_repository.dart';
  8. import 'package:electronic_assistant/data/repositories/task_repository.dart';
  9. import 'package:electronic_assistant/handler/event_handler.dart';
  10. import 'package:electronic_assistant/module/chat/view.dart';
  11. import 'package:electronic_assistant/module/login/view.dart';
  12. import 'package:electronic_assistant/module/record/record_handler.dart';
  13. import 'package:electronic_assistant/module/store/view.dart';
  14. import 'package:electronic_assistant/module/talk/summary/view.dart';
  15. import 'package:electronic_assistant/module/talk/todo/controller.dart';
  16. import 'package:electronic_assistant/module/talk/todo/view.dart';
  17. import 'package:electronic_assistant/module/talk/view.dart';
  18. import 'package:electronic_assistant/resource/assets.gen.dart';
  19. import 'package:electronic_assistant/resource/colors.gen.dart';
  20. import 'package:electronic_assistant/resource/string.gen.dart';
  21. import 'package:electronic_assistant/router/app_pages.dart';
  22. import 'package:electronic_assistant/utils/audio_picker_utils.dart';
  23. import 'package:electronic_assistant/utils/error_handler.dart';
  24. import 'package:electronic_assistant/utils/expand.dart';
  25. import 'package:electronic_assistant/utils/file_upload_check_helper.dart';
  26. import 'package:electronic_assistant/utils/mmkv_util.dart';
  27. import 'package:flutter/cupertino.dart';
  28. import 'package:flutter/material.dart';
  29. import 'package:flutter_screenutil/flutter_screenutil.dart';
  30. import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
  31. import 'package:get/get.dart';
  32. import 'package:just_audio/just_audio.dart';
  33. import 'package:share_plus/share_plus.dart';
  34. import 'package:wakelock_plus/wakelock_plus.dart';
  35. import '../../data/api/request/agenda_update_bean.dart';
  36. import '../../data/bean/agenda.dart';
  37. import '../../data/bean/agenda_list_all_bean.dart';
  38. import '../../data/bean/talks.dart';
  39. import '../../data/repositories/agenda_repository.dart';
  40. import '../../data/repositories/talk_repository.dart';
  41. import '../../dialog/add_agenda_dialog.dart';
  42. import '../../dialog/alert_dialog.dart';
  43. import '../../dialog/talk_share_dialog.dart';
  44. import '../../utils/common_utils.dart';
  45. import '../../utils/event_bus.dart';
  46. import '../../utils/system_share_util.dart';
  47. import '../../utils/toast_util.dart';
  48. import 'original/view.dart';
  49. class TalkController extends BaseController {
  50. static const String argumentItem = 'argument_item';
  51. static const String argumentTalkId = 'argument_talk_id';
  52. static const String argumentEventTag = 'argument_event_tag';
  53. final String uploadNoPrompts = "UPLOAD_NO_PROMPTS";
  54. final Rxn<TalkBean> talkBean = Rxn();
  55. StreamSubscription? _talkUploadListener;
  56. final RxDouble uploadProgress = RxDouble(0);
  57. final isShowElectricLow = false.obs;
  58. bool isAudioLoading = false;
  59. final double sliderMax = 1;
  60. bool? audioFileIsExist;
  61. bool? isUploadedFile;
  62. Rxn<bool> isUploading = Rxn();
  63. final isAudioPlaying = false.obs;
  64. final audioProgressValue = 0.0.obs;
  65. final audioDuration = Duration.zero.obs;
  66. final agendaOriginalAllList = <AgendaListAllBean>[];
  67. final agendaAllList = <AgendaListAllBean>[].obs;
  68. final _isEditModel = false.obs;
  69. final TextEditingController editTalkNameController = TextEditingController();
  70. final tabIndex = 0.obs;
  71. final List<String> tabBeans = [
  72. StringName.talkTabSummary.tr,
  73. '思维导图',
  74. StringName.talkTabMyTask.tr,
  75. StringName.talkTabOriginal.tr
  76. ];
  77. bool get isEditModel => _isEditModel.value;
  78. RxBool get isEditModelRx => _isEditModel;
  79. final _audioPlayer = AudioPlayer();
  80. StreamSubscription? _talkBeanListener;
  81. TextEditingController? _agendaContentController;
  82. TextEditingController? _agendaNameController;
  83. TextEditingController get agendaContentController {
  84. _agendaContentController ??= TextEditingController();
  85. return _agendaContentController!;
  86. }
  87. TextEditingController get agendaNameController {
  88. _agendaNameController ??= TextEditingController();
  89. return _agendaNameController!;
  90. }
  91. String? paramId;
  92. String? eventTag;
  93. bool isLocalFileHas = false;
  94. final Rxn<Duration> playingDuration = Rxn();
  95. @override
  96. void onReady() {
  97. super.onReady();
  98. _initAudioPlayer();
  99. _getArguments();
  100. eventReport(EventId.event_101001, params: {EventId.id: eventTag});
  101. }
  102. void eventReport(String eventId, {Map<String, dynamic>? params}) {
  103. if (talkBean.value == null || talkBean.value?.isExample == true) {
  104. return;
  105. }
  106. EventHandler.report(eventId, params: params);
  107. }
  108. void _initAudioPlayer() {
  109. _audioPlayer.playerStateStream.listen((playerState) {
  110. if (playerState.processingState == ProcessingState.loading ||
  111. playerState.processingState == ProcessingState.buffering) {
  112. isAudioLoading = true;
  113. debugPrint('音频load = true');
  114. } else {
  115. debugPrint('音频load = false');
  116. isAudioLoading = false;
  117. if (playerState.processingState == ProcessingState.completed) {
  118. _audioPlayer.stop();
  119. _audioPlayer.seek(Duration.zero);
  120. isAudioPlaying.value = false;
  121. debugPrint('音频 播放结束了');
  122. }
  123. }
  124. isAudioPlaying.value = playerState.playing;
  125. }, onError: (Object e, StackTrace stackTrace) {
  126. debugPrint('音频加载异常 == $e');
  127. });
  128. _audioPlayer.durationStream.listen((duration) {
  129. if (duration != null) {
  130. debugPrint('音频总播放时长 == ${duration.inMilliseconds}');
  131. audioDuration.value = duration;
  132. }
  133. });
  134. _audioPlayer.positionStream.listen((duration) {
  135. debugPrint('音频播放时长 == ${duration.inMilliseconds}');
  136. playingDuration.value = duration;
  137. if (audioDuration.value.inMilliseconds > 0) {
  138. audioProgressValue.value =
  139. (duration.inMilliseconds / audioDuration.value.inMilliseconds)
  140. .clamp(0.0, sliderMax);
  141. }
  142. });
  143. }
  144. void _dealTalk(TalkBean? bean) async {
  145. debugPrint('talkBean == $bean');
  146. String? id = bean?.id;
  147. if (id == null) {
  148. return;
  149. }
  150. _loadAudioFile(bean);
  151. if (bean?.status.value == TalkStatus.notAnalysis) {
  152. setUploadingProgress(id);
  153. }
  154. if (bean?.status.value == TalkStatus.notAnalysis &&
  155. talkRepository.isUploadingTalk(id)) {
  156. isUploading.value = true;
  157. } else {
  158. isUploading.value = false;
  159. }
  160. }
  161. void setUploadingProgress(String id) {
  162. talkRepository.getUploadProgress(id).listen((progress) {
  163. uploadProgress.value = (progress * 20).toFormattedDouble(1);
  164. });
  165. }
  166. Future<void> _loadAudioFile(TalkBean? bean, {bool? loadPlay}) async {
  167. try {
  168. Uri? uri;
  169. if (bean?.isExample == true && bean?.audioUrl != null) {
  170. uri = Uri.parse(bean!.audioUrl!);
  171. } else {
  172. File? file = await getFileByTalk(talkBean.value);
  173. if (file?.existsSync() == true) {
  174. uri = file?.uri;
  175. }
  176. }
  177. if (uri == null) {
  178. throw '音频文件不存在';
  179. }
  180. await _audioPlayer.setAudioSource(AudioSource.uri(uri));
  181. if (loadPlay == true) {
  182. clickPlayAudio();
  183. }
  184. audioFileIsExist = true;
  185. } catch (e) {
  186. audioFileIsExist = false;
  187. debugPrint('音频设置异常 == $e');
  188. }
  189. }
  190. void _getArguments() {
  191. TalkBean? bean = parameters?[argumentItem];
  192. if (bean != null) {
  193. talkBean.value = bean;
  194. _dealTalk(bean);
  195. } else {
  196. paramId = parameters?[argumentTalkId];
  197. if (paramId != null) {
  198. talkRepository.talkInfo(paramId!).then((data) {
  199. talkBean.value = data.talkInfo;
  200. _dealTalk(data.talkInfo);
  201. }).catchError((error) {
  202. ErrorHandler.toastError(error);
  203. });
  204. }
  205. }
  206. eventTag = parameters?[argumentEventTag];
  207. }
  208. void updateProgress(double value) {
  209. final newPosition = Duration(
  210. milliseconds: (value * audioDuration.value.inMilliseconds).toInt());
  211. _audioPlayer.seek(newPosition);
  212. }
  213. void clickPlayAudio() async {
  214. if (audioFileIsExist != true && isLocalFileHas == false) {
  215. ToastUtil.showToast(StringName.talkFileNotFind.tr);
  216. return;
  217. }
  218. if (isLocalFileHas == true &&
  219. audioFileIsExist == false &&
  220. !await AudioPickerUtils.hasPermission()) {
  221. bool has = await AudioPickerUtils.requestPermissionExtend();
  222. if (has == false) {
  223. ToastUtil.showToast(StringName.authorizationFailed.tr);
  224. return;
  225. }
  226. //重新加载
  227. _loadAudioFile(talkBean.value, loadPlay: true);
  228. return;
  229. }
  230. if (isAudioLoading) {
  231. ToastUtil.showToast(StringName.talkAudioLoading.tr);
  232. return;
  233. }
  234. if (_audioPlayer.playing) {
  235. _audioPlayer.pause();
  236. isAudioPlaying.value = false;
  237. } else {
  238. _audioPlayer.play();
  239. isAudioPlaying.value = true;
  240. }
  241. }
  242. void _checkFileSizeAndNet() async {
  243. String? id = talkBean.value?.id;
  244. if (id == null) {
  245. return;
  246. }
  247. File? file = await getFileByTalk(talkBean.value);
  248. if (isLocalFileHas == true &&
  249. audioFileIsExist == false &&
  250. !await AudioPickerUtils.hasPermission()) {
  251. bool has = await AudioPickerUtils.requestPermissionExtend();
  252. if (has == false) {
  253. ToastUtil.showToast(StringName.authorizationFailed.tr);
  254. return;
  255. }
  256. //重新上传
  257. _checkFileSizeAndNet();
  258. return;
  259. }
  260. if (file == null || !file.existsSync()) {
  261. ToastUtil.showToast(StringName.talkUploadFileNotExist.tr);
  262. return;
  263. }
  264. bool isCheckRemind = KVUtil.getBool(uploadNoPrompts, false);
  265. if (isCheckRemind) {
  266. _requestAnalyze(file);
  267. return;
  268. }
  269. //如果文件大小低于250MB 不弹窗提醒
  270. if (file.lengthSync() < 250 * 1024 * 1024) {
  271. _requestAnalyze(file);
  272. return;
  273. }
  274. final List<ConnectivityResult> connectivityResult =
  275. await (Connectivity().checkConnectivity());
  276. if (connectivityResult.contains(ConnectivityResult.wifi)) {
  277. _requestAnalyze(file);
  278. } else {
  279. _showTrafficRemindDialog(file.lengthSync().toReadableSize(),
  280. confirmOnTap: (isCheckRemind) {
  281. if (isCheckRemind) {
  282. KVUtil.putBool(uploadNoPrompts, true);
  283. }
  284. _requestAnalyze(file);
  285. });
  286. }
  287. }
  288. void _showTrafficRemindDialog(String holderTxt,
  289. {void Function(bool isCheckRemind)? confirmOnTap}) {
  290. final remindTrafficConsume = false.obs;
  291. Widget getSelectIcon() {
  292. return Obx(() {
  293. return remindTrafficConsume.value
  294. ? Assets.images.iconSelectTrue.image()
  295. : Assets.images.iconSelectFalse.image();
  296. });
  297. }
  298. Assets.images.iconSelectTrue.image();
  299. EAAlertDialog.show(
  300. contentWidget: Column(
  301. children: [
  302. Text(
  303. StringName.talkTrafficRemindTitle.tr
  304. .replacePlaceholders([holderTxt]),
  305. style:
  306. TextStyle(fontSize: 15.sp, color: ColorName.primaryTextColor),
  307. ),
  308. SizedBox(height: 8.h),
  309. GestureDetector(
  310. onTap: () {
  311. remindTrafficConsume.value = !remindTrafficConsume.value;
  312. },
  313. child: IntrinsicWidth(
  314. child: Row(
  315. children: [
  316. SizedBox(width: 20.w, height: 20.w, child: getSelectIcon()),
  317. SizedBox(width: 5.w),
  318. Text(
  319. StringName.talkTrafficRemindTips.tr,
  320. style: TextStyle(
  321. fontSize: 15.sp, color: ColorName.tertiaryTextColor),
  322. )
  323. ],
  324. ),
  325. ),
  326. )
  327. ],
  328. ),
  329. cancelText: StringName.cancel.tr,
  330. confirmText: StringName.sure.tr,
  331. confirmOnTap: () {
  332. confirmOnTap?.call(remindTrafficConsume.value);
  333. });
  334. }
  335. void checkCanAnalyze() {
  336. String? id = talkBean.value?.id;
  337. double? duration = talkBean.value?.duration;
  338. if (id == null || duration == null) {
  339. return;
  340. }
  341. eventReport(EventId.event_101002);
  342. talkRepository.checkElectric(duration).then((data) {
  343. if (data.enough) {
  344. //检查网络以及文件大小
  345. _checkFileSizeAndNet();
  346. } else {
  347. ToastUtil.showToast(StringName.talkAnalyseLowToast.tr);
  348. isShowElectricLow.value = true;
  349. }
  350. }).catchError((error) {
  351. ErrorHandler.toastError(error);
  352. });
  353. }
  354. void _requestAnalyze(File file) {
  355. String? talkId = talkBean.value?.id;
  356. double? duration = talkBean.value?.duration;
  357. if (talkId == null || duration == null || isUploadedFile == true) {
  358. return;
  359. }
  360. isUploading.value = true;
  361. WakelockPlus.enable();
  362. talkRepository.uploadTalkFile(talkId, duration, file).then((taskId) {
  363. isUploadedFile = true;
  364. isUploading.value = false;
  365. talkBean.value?.progressContent.value = '录音上传中,请勿关闭小听';
  366. talkBean.value?.progress.value = 20;
  367. talkBean.value?.status.value = TalkStatus.analysing;
  368. taskRepository.addTask(taskId);
  369. }).catchError((error) {
  370. isUploading.value = false;
  371. ErrorHandler.toastError(error);
  372. }).whenComplete(() => WakelockPlus.disable());
  373. }
  374. void refreshAgendaAllData({bool isForceRefresh = false}) {
  375. String? id = talkBean.value?.id;
  376. if (id == null || (!isForceRefresh && agendaAllList.isNotEmpty)) {
  377. return;
  378. }
  379. agendaRepository.agendaListAll(id).then((agenda) {
  380. agendaAllList.clear();
  381. agendaOriginalAllList.clear();
  382. if (agenda.list != null) {
  383. agendaOriginalAllList.addAll(
  384. agenda.list!.map((item) => AgendaListAllBean.from(item)).toList());
  385. agendaAllList.addAll(agenda.list!);
  386. }
  387. });
  388. }
  389. void clickAIAnalysis() async {
  390. if (!await checkLogin()) {
  391. return;
  392. }
  393. if (talkBean.value != null) {
  394. eventReport(EventId.event_101003);
  395. ChatPage.startByTalk(
  396. talkBean.value!.isExample == true
  397. ? ChatFromType.fromTalkExample
  398. : ChatFromType.fromTalkDetail,
  399. talkBean.value!);
  400. }
  401. }
  402. void onGoElectricStore() {
  403. StorePage.start(fromType: StoreFromType.analyse);
  404. Future.delayed(const Duration(milliseconds: 250), () {
  405. isShowElectricLow.value = false;
  406. });
  407. }
  408. void onEditModelClick() async {
  409. if (!await checkLogin()) {
  410. return;
  411. }
  412. _isEditModel.value = true;
  413. if (_audioPlayer.playing) {
  414. _audioPlayer.pause();
  415. isAudioPlaying.value = false;
  416. }
  417. editTalkNameController.text = talkBean.value?.title.value ?? '';
  418. }
  419. void updateTabIndex(int index) {
  420. tabIndex.value = index;
  421. }
  422. void onEditCancel() {
  423. _isEditModel.value = false;
  424. agendaAllList.assignAll(agendaOriginalAllList
  425. .map((item) => AgendaListAllBean.from(item))
  426. .toList());
  427. }
  428. void onEditDoneClick() {
  429. if (talkBean.value == null) {
  430. return;
  431. }
  432. List<AgendaUpdateBean> list = [];
  433. for (AgendaListAllBean item in agendaAllList) {
  434. if (item.list != null) {
  435. for (Agenda agenda in item.list!) {
  436. list.add(AgendaUpdateBean(agenda.id, agenda.name, agenda.content));
  437. }
  438. }
  439. }
  440. agendaRepository.agendaUpdate(talkBean.value!.id, list).then((data) {
  441. refreshAgendaAllData(isForceRefresh: true);
  442. eventBus.emit(TodoController.refreshTalkMineTask);
  443. isEditModelRx.value = false;
  444. }).catchError((error) {
  445. ErrorHandler.toastError(error);
  446. });
  447. if (tabIndex.value == 0) {
  448. String updateName = editTalkNameController.text;
  449. talkRepository.talkRename(talkBean.value!.id, updateName).then((data) {
  450. talkBean.value?.title.value = updateName;
  451. }).catchError((error) {
  452. ErrorHandler.toastError(error);
  453. });
  454. }
  455. }
  456. void removeTalkAgenda(List<Agenda>? list, Agenda agenda) {
  457. list?.remove(agenda);
  458. agendaAllList.refresh();
  459. }
  460. void showSingleAddAgendaDialog(BuildContext context) {
  461. showAddAgendaDialog(context, agendaContentController, agendaNameController,
  462. list: agendaAllList.map((e) => e.name ?? "").toList(), callback: () {
  463. if (agendaContentController.text.isEmpty) {
  464. ToastUtil.showToast(StringName.talkAddAgendaContentHint.tr);
  465. return;
  466. }
  467. if (agendaNameController.text.isEmpty) {
  468. ToastUtil.showToast(StringName.talkAddAgendaNameHint.tr);
  469. return;
  470. }
  471. Get.back();
  472. _dealAddProcedureList();
  473. });
  474. }
  475. void _dealAddProcedureList() {
  476. String name = agendaNameController.text;
  477. final addItem = Agenda(
  478. id: "",
  479. talkId: "",
  480. name: name,
  481. );
  482. addItem.content = agendaContentController.text;
  483. for (AgendaListAllBean item in agendaAllList) {
  484. if (item.name == name) {
  485. List<Agenda> list = item.list ?? [];
  486. list.add(addItem);
  487. item.list = list;
  488. agendaAllList.refresh();
  489. agendaContentController.clear();
  490. agendaNameController.clear();
  491. return;
  492. }
  493. }
  494. agendaAllList.add(AgendaListAllBean(name: name, list: [addItem]));
  495. agendaContentController.clear();
  496. agendaNameController.clear();
  497. }
  498. Future<File?> getFileByTalk(TalkBean? bean) async {
  499. isLocalFileHas = false;
  500. if (bean == null) {
  501. return null;
  502. }
  503. if (bean.uploadType == TalkUploadType.localUpload) {
  504. String? audioId =
  505. FileUploadCheckHelper.getLocalAudioId(bean.localAudioUrl);
  506. if (audioId != null && audioId.isNotEmpty) {
  507. isLocalFileHas = true;
  508. return await AudioPickerUtils.getAssetFile(audioId);
  509. } else {
  510. return await FileUploadCheckHelper.getChoiceUploadFile(bean.id);
  511. }
  512. } else {
  513. return await RecordHandler.getRecordFile(bean.id);
  514. }
  515. }
  516. Future<bool> checkLogin() async {
  517. if (!accountRepository.isLogin.value) {
  518. bool isLogin = await LoginPage.start(fromType: LoginFromType.talkDetail);
  519. if (isLogin) {
  520. backToSpecificPage(RoutePath.mainTab);
  521. }
  522. return false;
  523. }
  524. return true;
  525. }
  526. void onShareClick() async {
  527. if (!await checkLogin()) {
  528. return;
  529. }
  530. if (talkBean.value?.status.value != TalkStatus.analysisSuccess) {
  531. return;
  532. }
  533. eventReport(EventId.event_101004);
  534. showTalkShareDialog(talkBean.value?.title.value,
  535. (type, shareTo, fileName, tag) {
  536. talkRepository
  537. .talkExport(talkBean.value!.id, fileName, type)
  538. .then((file) async {
  539. if (shareTo == ShareTo.ios) {
  540. await Share.shareXFiles([XFile(file.path)], subject: fileName);
  541. } else if (shareTo == ShareTo.wechat) {
  542. await SystemShareUtil.shareWechatFile(file.path);
  543. } else {
  544. await SystemShareUtil.shareQQFile(file.path);
  545. }
  546. SmartDialog.dismiss(tag: tag);
  547. }).catchError((error) {
  548. if (error is SystemShareException) {
  549. ToastUtil.showToast(error.message);
  550. } else {
  551. ErrorHandler.toastError(error);
  552. }
  553. });
  554. });
  555. }
  556. @override
  557. void onClose() {
  558. super.onClose();
  559. _talkUploadListener?.cancel();
  560. _talkBeanListener?.cancel();
  561. _audioPlayer.dispose();
  562. _agendaContentController?.dispose();
  563. _agendaNameController?.dispose();
  564. }
  565. void seekTo(int? startMs) {
  566. if (startMs == null) {
  567. return;
  568. }
  569. _audioPlayer.seek(Duration(milliseconds: startMs));
  570. }
  571. }