controller.dart 19 KB

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