controller.dart 18 KB

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