view.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. import 'package:dsbridge_flutter/dsbridge_flutter.dart';
  2. import 'package:electronic_assistant/base/base_page.dart';
  3. import 'package:electronic_assistant/module/talk/controller.dart';
  4. import 'package:electronic_assistant/resource/colors.gen.dart';
  5. import 'package:electronic_assistant/utils/expand.dart';
  6. import 'package:electronic_assistant/utils/fixed_size_tab_indicator.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:flutter_screenutil/flutter_screenutil.dart';
  10. import 'package:get/get.dart';
  11. import '../../data/bean/talks.dart';
  12. import '../../data/consts/event_report_id.dart';
  13. import '../../resource/assets.gen.dart';
  14. import '../../resource/string.gen.dart';
  15. import '../../router/app_pages.dart';
  16. import '../../utils/common_style.dart';
  17. class TalkPage extends BasePage<TalkController> {
  18. final String? talkId;
  19. TalkPage({super.key})
  20. : talkId = Get.arguments[TalkController.argumentTalkId] {
  21. Get.lazyPut<TalkController>(() => TalkController(),
  22. tag: talkId, fenix: true);
  23. }
  24. @override
  25. get controller => Get.find<TalkController>(tag: talkId);
  26. static void start(TalkBean item, {String eventTag = EventId.id_002}) {
  27. if (Get.currentRoute == RoutePath.talkDetail &&
  28. Get.arguments[TalkController.argumentTalkId] == item.id) {
  29. return;
  30. }
  31. Get.toNamed(RoutePath.talkDetail,
  32. arguments: {
  33. TalkController.argumentItem: item,
  34. TalkController.argumentEventTag: eventTag,
  35. TalkController.argumentTalkId: item.id,
  36. },
  37. preventDuplicates: false);
  38. }
  39. static void startById(String talkId, {String eventTag = ''}) {
  40. if (Get.currentRoute == RoutePath.talkDetail &&
  41. Get.arguments[TalkController.argumentTalkId] == talkId) {
  42. return;
  43. }
  44. Get.toNamed(RoutePath.talkDetail,
  45. arguments: {
  46. TalkController.argumentTalkId: talkId,
  47. TalkController.argumentEventTag: eventTag,
  48. },
  49. preventDuplicates: false);
  50. }
  51. @override
  52. Widget buildBody(BuildContext context) {
  53. return WillPopScope(
  54. onWillPop: () async {
  55. if (controller.isEditModel) {
  56. controller.onEditCancel();
  57. return false;
  58. }
  59. if (controller.isSearchModel.value) {
  60. controller.onSearchCancel();
  61. return false;
  62. }
  63. if (controller.isShowMindFullScreen.value) {
  64. controller.onExitMindFullScreen();
  65. return false;
  66. }
  67. return true;
  68. },
  69. child: Stack(
  70. children: [
  71. Obx(() {
  72. return controller.temporaryController != null
  73. ? DWebViewWidget(controller: controller.temporaryController!)
  74. : const SizedBox.shrink();
  75. }),
  76. Obx(() {
  77. return controller.isInitializedView.value
  78. ? DefaultTabController(
  79. initialIndex: controller.defaultIndex,
  80. length: controller.tabBeans.length,
  81. child: _buildTalkContentView())
  82. : const SizedBox.shrink();
  83. }),
  84. buildBottomView()
  85. ],
  86. ),
  87. );
  88. }
  89. Widget _buildTalkContentView() {
  90. return Builder(builder: (context) {
  91. final statusBarHeight = MediaQuery.of(context).padding.top;
  92. return Obx(() {
  93. return Column(
  94. children: [
  95. AnimatedContainer(
  96. decoration: BoxDecoration(
  97. gradient: LinearGradient(
  98. colors: ['#E1E9FF'.toColor(), '#F9FAFE'.toColor()],
  99. begin: Alignment.topCenter,
  100. end: Alignment.bottomCenter,
  101. stops: const [0, 1.0],
  102. ),
  103. ),
  104. height: controller.getChangeHeadHeight(),
  105. duration: controller.mindFullDuration,
  106. child: SingleChildScrollView(
  107. physics: const NeverScrollableScrollPhysics(),
  108. child: Column(key: controller.headGlobalKey, children: [
  109. SizedBox(height: statusBarHeight),
  110. _buildHeadView(),
  111. buildTabBar(context),
  112. ]),
  113. ),
  114. ),
  115. buildTalkContentView()
  116. ],
  117. );
  118. });
  119. });
  120. }
  121. Widget _buildHeadView() {
  122. return Obx(() {
  123. if (controller.isEditModel) {
  124. return _buildEditHeadView();
  125. }
  126. if (controller.isSearchModel.value) {
  127. return _buildSearchHeadView();
  128. }
  129. return _buildHeadNormalView();
  130. });
  131. }
  132. Widget _buildSearchHeadView() {
  133. return Row(
  134. crossAxisAlignment: CrossAxisAlignment.center,
  135. children: [
  136. Align(
  137. alignment: Alignment.centerLeft,
  138. child: IconButton(
  139. icon: Assets.images.iconTalkEditCancel
  140. .image(width: 24.w, height: 24.w),
  141. onPressed: () {
  142. controller.onSearchCancel();
  143. },
  144. ),
  145. ),
  146. // TextField()
  147. Expanded(
  148. child: Container(
  149. height: 38.w,
  150. decoration: BoxDecoration(
  151. color: ColorName.white,
  152. border: Border.all(color: '#E7E9F6'.color, width: 1.w),
  153. borderRadius: BorderRadius.circular(6.w),
  154. ),
  155. child: Row(
  156. crossAxisAlignment: CrossAxisAlignment.center,
  157. children: [
  158. Expanded(
  159. child: TextField(
  160. maxLines: 1,
  161. maxLength: 15,
  162. style: TextStyle(
  163. fontSize: 14.sp, color: ColorName.primaryTextColor),
  164. // controller: controller.searchController,
  165. decoration: InputDecoration(
  166. counterText: '',
  167. hintText: StringName.talkSearchHint.tr,
  168. hintStyle: TextStyle(
  169. fontSize: 14.sp,
  170. color: ColorName.tertiaryTextColor,
  171. ),
  172. contentPadding: EdgeInsets.symmetric(horizontal: 8.w),
  173. border:
  174. const OutlineInputBorder(borderSide: BorderSide.none),
  175. ),
  176. onChanged: (value) {
  177. controller.setSearchChangeTxt(value);
  178. }),
  179. ),
  180. Text(controller.searchResultDesc.value,
  181. style: TextStyle(
  182. fontSize: 14.sp, color: ColorName.secondaryTextColor)),
  183. SizedBox(width: 8.w)
  184. ],
  185. ),
  186. )),
  187. SizedBox(width: 11.w),
  188. GestureDetector(
  189. onTap: () {
  190. controller.onSearchPrevious();
  191. },
  192. child: Assets.images.iconTalkSearchPrevious
  193. .image(width: 24.w, height: 24.w)),
  194. SizedBox(width: 16.w),
  195. GestureDetector(
  196. onTap: () {
  197. controller.onSearchNext();
  198. },
  199. child: Assets.images.iconTalkSearchNext
  200. .image(width: 24.w, height: 24.w)),
  201. SizedBox(width: 12.w),
  202. ],
  203. );
  204. }
  205. Widget _buildEditHeadView() {
  206. return Row(
  207. children: [
  208. Align(
  209. alignment: Alignment.centerLeft,
  210. child: IconButton(
  211. icon: Assets.images.iconTalkEditCancel
  212. .image(width: 24.w, height: 24.w),
  213. onPressed: () {
  214. controller.onEditCancel();
  215. },
  216. ),
  217. ),
  218. const Spacer(),
  219. GestureDetector(
  220. onTap: () {
  221. controller.onEditDoneClick();
  222. },
  223. child: Padding(
  224. padding: EdgeInsets.symmetric(horizontal: 12.w),
  225. child: Text(StringName.done.tr,
  226. style: TextStyle(
  227. fontSize: 17.sp, color: ColorName.primaryTextColor)),
  228. ),
  229. )
  230. ],
  231. );
  232. }
  233. Widget _buildHeadNormalView() {
  234. return Row(
  235. children: [
  236. IconButton(
  237. icon: Assets.images.iconTalkBack.image(width: 24.w, height: 24.w),
  238. onPressed: () {
  239. Get.back();
  240. },
  241. ),
  242. SizedBox(width: 6.w),
  243. Obx(() {
  244. return GestureDetector(
  245. onTap: () {
  246. controller.onEditTitleClick();
  247. },
  248. child: Column(
  249. mainAxisAlignment: MainAxisAlignment.center,
  250. crossAxisAlignment: CrossAxisAlignment.start,
  251. children: [
  252. Row(
  253. children: [
  254. ConstrainedBox(
  255. constraints: BoxConstraints(maxWidth: 0.65.sw),
  256. child: Text(
  257. maxLines: 1,
  258. overflow: TextOverflow.ellipsis,
  259. controller.talkBean.value?.title.value ?? '',
  260. style: TextStyle(
  261. fontWeight: FontWeight.bold,
  262. fontSize: 15.sp,
  263. color: ColorName.primaryTextColor,
  264. ),
  265. ),
  266. ),
  267. SizedBox(width: 2.w),
  268. Assets.images.iconTalkEditTitle
  269. .image(width: 16.w, height: 16.w)
  270. ],
  271. ),
  272. SizedBox(height: 2.h),
  273. Text(
  274. controller.talkBean.value?.createTime ?? '',
  275. style: TextStyle(
  276. fontSize: 11.sp, color: ColorName.secondaryTextColor),
  277. )
  278. ],
  279. ),
  280. );
  281. }),
  282. const Spacer(),
  283. Row(
  284. children: [
  285. IconButton(
  286. icon:
  287. Assets.images.iconTalkShare.image(width: 20.w, height: 20.w),
  288. onPressed: () {
  289. controller.onShareClick();
  290. },
  291. ),
  292. ],
  293. )
  294. ],
  295. );
  296. }
  297. Widget buildTabBar(BuildContext context) {
  298. TabController tabController = DefaultTabController.of(context);
  299. tabController.addListener(() {
  300. controller.updateTabIndex(tabController.index);
  301. });
  302. return Column(
  303. children: [
  304. Obx(() {
  305. return AbsorbPointer(
  306. absorbing: controller.isEditModel || controller.isSearchModel.value,
  307. child: TabBar(
  308. key: controller.tabBarGlobalKey,
  309. labelStyle:
  310. TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
  311. unselectedLabelStyle: TextStyle(fontSize: 14.sp),
  312. labelColor: ColorName.primaryTextColor,
  313. unselectedLabelColor: ColorName.secondaryTextColor,
  314. labelPadding: EdgeInsets.only(top: 4.h),
  315. dividerHeight: 0,
  316. splashFactory: NoSplash.splashFactory,
  317. indicator: FixedSizeTabIndicator(
  318. width: 16.w,
  319. height: 3.w,
  320. radius: 3,
  321. color: ColorName.colorPrimary),
  322. tabs: controller.tabBeans
  323. .map((bean) => Tab(text: bean.title))
  324. .toList()),
  325. );
  326. }),
  327. SizedBox(height: 6.h),
  328. Divider(height: 1, color: '#F2F4F9'.color)
  329. ],
  330. );
  331. }
  332. @override
  333. bool immersive() {
  334. return true;
  335. }
  336. Widget buildTalkContentView() {
  337. return Obx(() {
  338. if (controller.talkBean.value?.status.value == TalkStatus.notAnalysis &&
  339. controller.isUploading.value != true) {
  340. return buildNotAnalysisView();
  341. } else {
  342. return buildTabContentView();
  343. }
  344. });
  345. }
  346. Widget buildTabContentView() {
  347. return Expanded(
  348. child: TabBarView(
  349. physics: controller.isEditModel ||
  350. controller.isSearchModel.value ||
  351. controller.checkTabBean.value?.isDisallowScroll == true
  352. ? const NeverScrollableScrollPhysics()
  353. : null,
  354. children: controller.pages,
  355. ),
  356. );
  357. }
  358. Widget buildNotAnalysisView() {
  359. return SizedBox(
  360. width: double.infinity,
  361. child: Column(
  362. children: [
  363. SizedBox(height: 119.h),
  364. SizedBox(
  365. width: 100.w,
  366. height: 100.w,
  367. child: Assets.images.iconTalkSummaryUnanalyzed.image()),
  368. SizedBox(height: 4.h),
  369. Text(StringName.talkUnAnalyzed.tr,
  370. style: TextStyle(
  371. fontSize: 15.sp, color: ColorName.primaryTextColor)),
  372. SizedBox(height: 2.h),
  373. Text(StringName.talkUnAnalyzedTips.tr,
  374. style: TextStyle(
  375. fontSize: 12.sp, color: ColorName.secondaryTextColor)),
  376. SizedBox(height: 24.h),
  377. GestureDetector(
  378. onTap: () {
  379. controller.checkCanAnalyze();
  380. },
  381. child: Container(
  382. decoration: getPrimaryBtnDecoration(8),
  383. width: 240.w,
  384. height: 48.w,
  385. child: Center(
  386. child: Text(
  387. StringName.talkAnalyzedBtnTxt.tr,
  388. style: TextStyle(fontSize: 16.sp, color: ColorName.white),
  389. ),
  390. ),
  391. ),
  392. )
  393. ],
  394. ),
  395. );
  396. }
  397. Widget buildBottomView() {
  398. return Obx(() {
  399. return AnimatedPositioned(
  400. key: controller.bottomGlobalKey,
  401. duration: controller.mindFullDuration,
  402. bottom: controller.getChangeBottomHeight(),
  403. left: 0,
  404. right: 0,
  405. child: Align(
  406. alignment: Alignment.bottomCenter,
  407. child: Container(
  408. margin: EdgeInsets.only(bottom: 20.h),
  409. child: IntrinsicHeight(
  410. child: Column(
  411. crossAxisAlignment: CrossAxisAlignment.end,
  412. children: [
  413. _buildAIAnalysisView(),
  414. SizedBox(height: 8.h),
  415. _buildOperationBtnView(),
  416. SizedBox(height: 10.h),
  417. buildAudioView()
  418. ],
  419. ),
  420. ),
  421. ),
  422. ),
  423. );
  424. });
  425. }
  426. Widget _buildOperationBtnView() {
  427. return SizedBox(
  428. height: 28.w,
  429. child: Obx(() {
  430. return Visibility(
  431. visible: controller.talkBean.value?.status.value ==
  432. TalkStatus.analysisSuccess &&
  433. controller.isSearchModel.value != true,
  434. child: Row(
  435. children: [
  436. Visibility(
  437. visible: controller.checkTabBean.value?.isShowEdit == true,
  438. child: _buildCommonOperationView(
  439. Assets.images.iconTalkEdit.provider(),
  440. StringName.talkUpdateTxt.tr, onClick: () {
  441. controller.onEditModelClick();
  442. })),
  443. Visibility(
  444. visible: controller.checkTabBean.value?.isShowSearch == true,
  445. child: _buildCommonOperationView(
  446. Assets.images.iconTalkSearch.provider(),
  447. StringName.talkDetailSearchTxt.tr, onClick: () {
  448. controller.onSearchClick();
  449. })),
  450. ],
  451. ),
  452. );
  453. }),
  454. );
  455. }
  456. Widget _buildCommonOperationView(ImageProvider provider, String txt,
  457. {VoidCallback? onClick}) {
  458. return GestureDetector(
  459. onTap: () => onClick?.call(),
  460. child: Container(
  461. margin: EdgeInsets.only(left: 12.w),
  462. height: 28.w,
  463. padding: EdgeInsets.symmetric(horizontal: 8.w),
  464. decoration: BoxDecoration(
  465. color: ColorName.white,
  466. borderRadius: BorderRadius.circular(100),
  467. boxShadow: [
  468. BoxShadow(
  469. color: ColorName.black5.withOpacity(0.08),
  470. spreadRadius: 2,
  471. blurRadius: 12,
  472. offset: const Offset(0, 1),
  473. ),
  474. ]),
  475. child: IntrinsicWidth(
  476. child: Row(
  477. children: [
  478. Image(image: provider, width: 16.w, height: 16.w),
  479. SizedBox(width: 2.w),
  480. Text(txt,
  481. style: TextStyle(
  482. height: 1,
  483. fontSize: 14.sp,
  484. color: ColorName.primaryTextColor))
  485. ],
  486. ),
  487. ),
  488. ),
  489. );
  490. }
  491. Widget _buildAIAnalysisView() {
  492. return Obx(() {
  493. return Visibility(
  494. visible: controller.talkBean.value?.status.value ==
  495. TalkStatus.analysisSuccess &&
  496. controller.isSearchModel.value != true,
  497. child: GestureDetector(
  498. onTap: () {
  499. controller.clickAIAnalysis();
  500. },
  501. child: Container(
  502. margin: EdgeInsets.only(right: 8.w),
  503. width: 64.w,
  504. height: 64.w,
  505. child: Assets.images.iconTalkLogo.image()),
  506. ),
  507. );
  508. });
  509. }
  510. buildAudioView() {
  511. return Container(
  512. decoration: BoxDecoration(
  513. color: Colors.white,
  514. borderRadius: BorderRadius.circular(100),
  515. boxShadow: [
  516. BoxShadow(
  517. color: ColorName.black5.withOpacity(0.1),
  518. spreadRadius: 2,
  519. blurRadius: 12,
  520. offset: const Offset(0, 3),
  521. ),
  522. ],
  523. ),
  524. margin: EdgeInsets.symmetric(horizontal: 12.w),
  525. padding: EdgeInsets.symmetric(vertical: 6.w, horizontal: 9.w),
  526. child: Row(
  527. children: [
  528. GestureDetector(
  529. onTap: () {
  530. controller.clickPlayAudio();
  531. },
  532. child: Obx(() {
  533. return SizedBox(
  534. width: 36.w,
  535. height: 36.w,
  536. child: (controller.isAudioPlaying.value)
  537. ? Assets.images.iconTalkAudioPlaying.image()
  538. : Assets.images.iconTalkAudioPause.image());
  539. }),
  540. ),
  541. SizedBox(width: 15.w),
  542. Builder(builder: (context) {
  543. return Flexible(
  544. child: Obx(() {
  545. return SliderTheme(
  546. data: SliderTheme.of(context).copyWith(
  547. thumbColor: Colors.white,
  548. overlayShape: SliderComponentShape.noOverlay,
  549. trackHeight: 8,
  550. activeTrackColor: "#8A89E9".toColor(),
  551. inactiveTrackColor: "#F6F5F8".toColor(),
  552. trackShape: CustomTrackShape(),
  553. ),
  554. child: Slider(
  555. value: controller.audioProgressValue.value,
  556. min: 0.0,
  557. max: controller.sliderMax,
  558. onChanged: (value) {
  559. controller.updateProgress(value);
  560. },
  561. ),
  562. );
  563. }),
  564. );
  565. }),
  566. SizedBox(width: 11.w),
  567. Obx(() {
  568. return Text(controller.audioDuration.value.toFormattedString(),
  569. style: TextStyle(
  570. fontSize: 10.sp, color: ColorName.secondaryTextColor));
  571. })
  572. ],
  573. ),
  574. );
  575. }
  576. }
  577. class CustomTrackShape extends RoundedRectSliderTrackShape {
  578. @override
  579. Rect getPreferredRect({
  580. required RenderBox parentBox,
  581. Offset offset = Offset.zero,
  582. required SliderThemeData sliderTheme,
  583. bool isEnabled = false,
  584. bool isDiscrete = false,
  585. }) {
  586. final trackHeight = sliderTheme.trackHeight;
  587. final trackLeft = offset.dx;
  588. final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2;
  589. final trackWidth = parentBox.size.width;
  590. return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
  591. }
  592. }