controller.dart 20 KB

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