diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 3c367eb2a..e894d1bf1 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -25,7 +25,9 @@ class MeetingDetailsController extends GetxController final PlayerController playerController = PlayerController(); RxBool isTop = false.obs; RxInt durationMax = 0.obs; - RxDouble dragOffset = 210.0.w.obs; + RxDouble dragOffset = 210.w.obs; + + RxInt currentDuration = 0.obs; int id = 0; @@ -55,6 +57,9 @@ class MeetingDetailsController extends GetxController super.onInit(); tabController = TabController(length: 4, vsync: this); tabController.addListener(() { + if (currentIndex.value == 2 && tabController.index != 2) { + FocusScope.of(Get.context!).unfocus(); + } currentIndex.value = tabController.index; }); final Map args = Get.arguments ?? {}; @@ -96,6 +101,9 @@ class MeetingDetailsController extends GetxController ); durationMax.value = await playerController.getDuration(DurationType.max); playerController.setFinishMode(finishMode: FinishMode.pause); + playerController.onCurrentDurationChanged.listen((int position) { + currentDuration.value = position; + }); if (meetingDetails.value.tasktype == 1) { List speakerList = await SqfliteApi.getMeetingSpeaker(id); textList.value = List.from(speakerList); @@ -158,7 +166,7 @@ class MeetingDetailsController extends GetxController String text = ''; for (var element in textList) { if (element['speaker'].isNotEmpty) { - text += '${element['speaker']}:'; + text += '[${element['speaker']}]:'; } text += '${element['content']}\n'; } @@ -311,7 +319,7 @@ class MeetingDetailsController extends GetxController String speaker = ''; if (element['additions'].isNotEmpty) { speaker = 'Speaker ${element['additions']['speaker']}'; - text += '$speaker:'; + text += '[$speaker]:'; if (!personnel.contains(speaker)) { personnel += '[$speaker] '; } diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index 9af120ebb..f6bbd6984 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -92,14 +92,22 @@ class MeetingDetailsView extends GetView { }, ), 4.horizontalSpace, - AudioFileWaveforms( - size: Size(130.w, 30.w), - playerController: controller.playerController, - playerWaveStyle: PlayerWaveStyle( - fixedWaveColor: Colors.grey[400]!, - liveWaveColor: Colors.red, - seekLineColor: Colors.red, - scaleFactor: 100, + GestureDetector( + onTap: () { + if (controller.isSpeakerText.value) return; + controller.dragOffset.value = 210.w; + controller.isTop.value = false; + }, + child: AudioFileWaveforms( + size: Size(130.w, 30.w), + playerController: controller.playerController, + enableSeekGesture: false, + playerWaveStyle: PlayerWaveStyle( + fixedWaveColor: Colors.grey[400]!, + liveWaveColor: Colors.red, + seekLineColor: Colors.red, + scaleFactor: 100, + ), ), ), ], @@ -203,19 +211,23 @@ class MeetingDetailsView extends GetView { child: Column( children: [ _soundWave(isDarkMode), - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w), - margin: EdgeInsets.symmetric(vertical: 10.w), - child: Obx( - () => Row( - children: [ - _iconItem(Icons.summarize, '概述', 0), - _iconItem(Icons.message, '转写', 1), - _iconItem(Icons.article, '总结', 2), - _iconItem(Icons.lan, '思维导图', 3), - ], - ), - ), + Obx( + () => !controller.isSpeakerText.value + ? Container( + padding: EdgeInsets.symmetric(horizontal: 8.w), + margin: EdgeInsets.symmetric(vertical: 10.w), + child: Obx( + () => Row( + children: [ + _iconItem(Icons.summarize, '概述', 0), + _iconItem(Icons.message, '转写', 1), + _iconItem(Icons.article, '总结', 2), + _iconItem(Icons.lan, '思维导图', 3), + ], + ), + ), + ) + : const SizedBox(), ), ], ), @@ -289,25 +301,20 @@ class MeetingDetailsView extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - StreamBuilder( - stream: controller - .playerController.onCurrentDurationChanged, - builder: (context, snapshot) { - final position = snapshot.data ?? 0; - return Text( - DateUtil.formatDateMs( - position, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[500], - fontSize: 12.sp, - ), - ); - }, + Obx( + () => Text( + DateUtil.formatDateMs( + controller.currentDuration.value, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + fontSize: 12.sp, + ), + ), ), Obx( () => Text( @@ -330,10 +337,20 @@ class MeetingDetailsView extends GetView { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Icon( - Icons.replay_10, - color: Colors.black, - size: 30.w, + GestureDetector( + onTap: () { + int progress = + controller.currentDuration.value - 10000; + if (progress < 0) { + progress = 0; + } + controller.playerController.seekTo(progress); + }, + child: Icon( + Icons.replay_10, + color: Colors.black, + size: 30.w, + ), ), StreamBuilder( stream: @@ -356,10 +373,17 @@ class MeetingDetailsView extends GetView { ); }, ), - Icon( - Icons.forward_10, - color: Colors.black, - size: 30.w, + GestureDetector( + onTap: () { + int progress = + controller.currentDuration.value + 10000; + controller.playerController.seekTo(progress); + }, + child: Icon( + Icons.forward_10, + color: Colors.black, + size: 30.w, + ), ), ], ), @@ -382,9 +406,6 @@ class MeetingDetailsView extends GetView { onTap: () { if (isActive) return; controller.tabController.index = index; - controller.isSpeakerText.value = false; - controller.editSpeakerTextId.value = 0; - controller.editSpeakerTextList = []; }, child: Container( height: 45.w, diff --git a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart index 48b331c8f..cd455a945 100644 --- a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart +++ b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart @@ -145,7 +145,9 @@ class _SpeakerAllBottomSheetState extends State { ), child: Focus( onFocusChange: (bool hasFocus) { - item['isShow'] = hasFocus; + setState(() { + item['isShow'] = hasFocus; + }); }, child: TextField( controller: item['speakerText'], @@ -460,12 +462,20 @@ class _SpeakerAllBottomSheetState extends State { List textList = _controller.textList; List newTextList = []; List editSpeakerList = []; + List personnelList = []; String personnel = ''; + bool isLoopEnd = false; for (var element in textList) { final dataMap = Map.from(element); for (var data in _dataList) { String speaker = data['speakerText'].text; if (speaker.isNotEmpty && speaker != data['speaker']) { + if (!isLoopEnd) { + personnelList.add({ + 'original': data['speaker'], + 'speaker': speaker, + }); + } if (dataMap['speaker'] == data['speaker']) { dataMap['speaker'] = speaker; editSpeakerList.add({ @@ -483,13 +493,36 @@ class _SpeakerAllBottomSheetState extends State { personnel += '[${dataMap['speaker']}] '; } newTextList.add(dataMap); + isLoopEnd = true; } - var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList); - if (result != null) { - _controller.textList.value = newTextList; - SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); - _controller.meetingDetails.value.personnel = personnel; - _storage.write("speaker_list", _historySpeakerList); + if (editSpeakerList.isNotEmpty) { + var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList); + if (result != null) { + bool isEditSummary = false; + String summaryText = _controller.meetingDetails.value.summary; + _controller.textList.value = newTextList; + for (var element in personnelList) { + if (summaryText.contains('[${element['original']}]')) { + summaryText = summaryText.replaceAll( + '[${element['original']}]', '[${element['speaker']}]'); + isEditSummary = true; + } + } + if (isEditSummary) { + SqfliteApi.editMeeting(_controller.id, { + 'summary': summaryText, + 'personnel': personnel, + }); + _controller.meetingDetails.value.summary = summaryText; + } else { + SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); + } + _controller.meetingDetails.value.personnel = personnel; + _controller.meetingDetails.refresh(); + _storage.write("speaker_list", _historySpeakerList); + Get.back(); + } + } else { Get.back(); } EasyLoading.dismiss(); diff --git a/lib/modules/meeting/views/speaker_bottom_sheet.dart b/lib/modules/meeting/views/speaker_bottom_sheet.dart index 3bc629643..94c6130cd 100644 --- a/lib/modules/meeting/views/speaker_bottom_sheet.dart +++ b/lib/modules/meeting/views/speaker_bottom_sheet.dart @@ -365,7 +365,19 @@ class _SpeakerBottomSheetState extends State { var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList); if (result != null) { _controller.textList.value = newTextList; - SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); + if (_controller.meetingDetails.value.summary + .contains('[${widget.speakerName}]')) { + String summaryText = _controller.meetingDetails.value.summary + .replaceAll( + '[${widget.speakerName}]', '[${_speakerController.text}]'); + SqfliteApi.editMeeting(_controller.id, { + 'summary': summaryText, + 'personnel': personnel, + }); + _controller.meetingDetails.value.summary = summaryText; + } else { + SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); + } _controller.meetingDetails.value.personnel = personnel; _addHistorySpeaker(); Get.back(); diff --git a/lib/modules/meeting/views/speech_tab.dart b/lib/modules/meeting/views/speech_tab.dart index f7a4bb0f3..1d742a493 100644 --- a/lib/modules/meeting/views/speech_tab.dart +++ b/lib/modules/meeting/views/speech_tab.dart @@ -21,7 +21,7 @@ class SpeechTab extends GetView { itemBuilder: (BuildContext context, int index) { Map item = controller.textList[index]; TextStyle textStyle = TextStyle( - fontSize: 16.sp, + fontSize: 15.sp, color: Colors.black87, letterSpacing: 0, height: 1.4, @@ -35,11 +35,11 @@ class SpeechTab extends GetView { children: [ if (item['speaker'].isNotEmpty) Container( - width: 12.w, - height: 12.w, - margin: EdgeInsets.only(right: 2.w), + width: 11.w, + height: 11.w, + margin: EdgeInsets.only(right: 3.w), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28.r), + borderRadius: BorderRadius.circular(11.r), border: Border.all( color: const Color(0xFFFFC107), width: 3.w, @@ -71,7 +71,7 @@ class SpeechTab extends GetView { item['speaker'], style: TextStyle( color: const Color(0xFFFFC107), - fontSize: 14.sp, + fontSize: 13.sp, ), ), ), @@ -84,7 +84,7 @@ class SpeechTab extends GetView { ), style: TextStyle( color: Colors.grey, - fontSize: 14.sp, + fontSize: 13.sp, ), ), ], @@ -151,16 +151,46 @@ class SpeechTab extends GetView { ), ); } else { - return Container( - padding: EdgeInsets.symmetric( - horizontal: 14.w, - vertical: 5.w, - ), - child: Text( - item['content'], - style: textStyle, - ), - ); + return Obx(() { + bool isActive = false; + int starttime = item['starttime']; + int endtime = 0; + if (index == controller.textList.length - 1) { + endtime = controller.durationMax.value; + } else { + endtime = controller.textList[index + 1]['starttime']; + } + if (controller.currentDuration.value != 0) { + if (controller.currentDuration.value >= starttime && + controller.currentDuration.value < endtime) { + isActive = true; + } + } + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + controller.playerController + .seekTo(item['starttime']); + controller.playerController.startPlayer(); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 5.w, + ), + child: Text( + item['content'], + style: TextStyle( + fontSize: 15.sp, + color: isActive ? Colors.red : Colors.black87, + letterSpacing: 0, + height: 1.4, + ), + ), + ), + ); + }); } }), ], @@ -183,6 +213,8 @@ class SpeechTab extends GetView { '编辑', () { controller.isSpeakerText.value = true; + controller.dragOffset.value = 0; + controller.isTop.value = true; }, ), if (controller.meetingDetails.value.tasktype == 1 && diff --git a/lib/modules/meeting/views/summary_tab.dart b/lib/modules/meeting/views/summary_tab.dart index d663ee748..b4eb594d2 100644 --- a/lib/modules/meeting/views/summary_tab.dart +++ b/lib/modules/meeting/views/summary_tab.dart @@ -4,15 +4,117 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import '../../../data/services/db/sqflite_api.dart'; import '../controllers/meeting_details_controller.dart'; import 'speaker_all_bottom_sheet.dart'; -class SummaryTab extends GetView { +class SummaryTab extends StatefulWidget { const SummaryTab({super.key}); + @override + State createState() => _SummaryTabState(); +} + +class _SummaryTabState extends State { + final _controller = Get.find(); + final String _addressPrefix = '地点:'; + String _addressText = ''; + final String _personnelPrefix = '与会人员:'; + String _personnelText = ''; + final TextEditingController _addressController = TextEditingController(); + final TextEditingController _personnelController = TextEditingController(); + + @override + void initState() { + super.initState(); + _addressController.text = _addressPrefix + + (_controller.meetingDetails.value.address.isNotEmpty + ? _controller.meetingDetails.value.address + : '[输入地点]'); + _personnelController.text = _personnelPrefix + + (_controller.meetingDetails.value.personnel.isNotEmpty + ? _controller.meetingDetails.value.personnel + : '[输入与会人员]'); + _addressController.addListener(_handleAddressChanged); + _personnelController.addListener(_handlePersonnelChanged); + } + + void _handleAddressChanged() { + String currentText = _addressController.text; + TextSelection currentSelection = _addressController.selection; + if (!currentText.startsWith(_addressPrefix)) { + _addressController.value = TextEditingValue( + text: _addressPrefix + _addressText, + selection: TextSelection.collapsed( + offset: _addressPrefix.length + _addressText.length, + ), + ); + return; + } else { + _addressText = currentText.substring(_addressPrefix.length); + } + + if (currentSelection.start < _addressPrefix.length || + currentSelection.end < _addressPrefix.length) { + int newStart = currentSelection.start < _addressPrefix.length + ? _addressPrefix.length + : currentSelection.start; + int newEnd = currentSelection.end < _addressPrefix.length + ? _addressPrefix.length + : currentSelection.end; + _addressController.selection = currentSelection.copyWith( + baseOffset: newStart, + extentOffset: newEnd, + ); + } + } + + void _handlePersonnelChanged() { + String currentText = _personnelController.text; + TextSelection currentSelection = _personnelController.selection; + if (!currentText.startsWith(_personnelPrefix)) { + _personnelController.value = TextEditingValue( + text: _personnelPrefix + _personnelText, + selection: TextSelection.collapsed( + offset: _personnelPrefix.length + _personnelText.length, + ), + ); + return; + } else { + _personnelText = currentText.substring(_personnelPrefix.length); + } + + if (currentSelection.start < _personnelPrefix.length || + currentSelection.end < _personnelPrefix.length) { + int newStart = currentSelection.start < _personnelPrefix.length + ? _personnelPrefix.length + : currentSelection.start; + int newEnd = currentSelection.end < _personnelPrefix.length + ? _personnelPrefix.length + : currentSelection.end; + _personnelController.selection = currentSelection.copyWith( + baseOffset: newStart, + extentOffset: newEnd, + ); + } + } + + @override + void dispose() { + _addressController.dispose(); + _personnelController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final TextStyle textStyle = TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[700], + fontSize: 16.sp, + letterSpacing: 0, + height: 1.5, + ); return Stack( children: [ @@ -25,7 +127,7 @@ class SummaryTab extends GetView { Padding( padding: EdgeInsets.symmetric(vertical: 10.w), child: Text( - controller.meetingData.value.title, + _controller.meetingData.value.title, style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, @@ -43,7 +145,7 @@ class SummaryTab extends GetView { borderRadius: BorderRadius.circular(4.w), ), child: Text( - controller.meetingData.value.type, + _controller.meetingData.value.type, style: TextStyle( color: isDarkMode ? Colors.grey[400] : Colors.grey[500], @@ -59,7 +161,7 @@ class SummaryTab extends GetView { 2.horizontalSpace, Text( DateUtil.formatDateMs( - controller.meetingData.value.creationtime, + _controller.meetingData.value.creationtime, format: DateFormats.y_mo_d_h_m, ), style: TextStyle( @@ -75,7 +177,7 @@ class SummaryTab extends GetView { ), 2.horizontalSpace, Text( - '${controller.meetingData.value.seconds}s', + '${_controller.meetingData.value.seconds}s', style: TextStyle( color: isDarkMode ? Colors.grey[400] : Colors.grey[500], fontSize: 12.sp, @@ -103,43 +205,79 @@ class SummaryTab extends GetView { ), ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '日期和时间:${DateUtil.formatDateMs( - controller.meetingData.value.creationtime, - format: DateFormats.y_mo_d_h_m, - )}', - style: TextStyle( - color: - isDarkMode ? Colors.grey[400] : Colors.grey[700], - fontSize: 16.sp, + child: Focus( + onFocusChange: (bool hasFocus) { + if (!hasFocus) { + bool isEdit = false; + if (_addressText.isNotEmpty && + _addressText != + _controller.meetingDetails.value.address) { + _controller.meetingDetails.value.address = + _addressText; + isEdit = true; + } + if (_personnelText.isNotEmpty && + _personnelText != + _controller.meetingDetails.value.personnel) { + _controller.meetingDetails.value.personnel = + _personnelText; + isEdit = true; + } + if (isEdit) { + SqfliteApi.editMeeting( + _controller.id, + { + 'address': + _controller.meetingDetails.value.address, + 'personnel': + _controller.meetingDetails.value.personnel, + }, + ); + } + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '日期和时间:${DateUtil.formatDateMs( + _controller.meetingData.value.creationtime, + format: DateFormats.y_mo_d_h_m, + )}', + style: textStyle, ), - ), - Text( - '地点:${controller.meetingDetails.value.address.isNotEmpty ? controller.meetingDetails.value.address : '[输入地点]'}', - style: TextStyle( - color: - isDarkMode ? Colors.grey[400] : Colors.grey[700], - fontSize: 16.sp, + TextField( + controller: _addressController, + style: textStyle, + minLines: 1, + maxLines: 100, + decoration: const InputDecoration( + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), ), - ), - Text( - '与会人员:${controller.meetingDetails.value.personnel.isNotEmpty ? controller.meetingDetails.value.personnel : '[输入与会人员]'}', - style: TextStyle( - color: - isDarkMode ? Colors.grey[400] : Colors.grey[700], - fontSize: 16.sp, + TextField( + controller: _personnelController, + style: textStyle, + minLines: 1, + maxLines: 100, + decoration: const InputDecoration( + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), ), - ), - ], + ], + ), ), ), Padding( padding: EdgeInsets.only(top: 10.w), child: MarkdownBody( - data: controller.meetingDetails.value.summary, + data: _controller.meetingDetails.value.summary, ), ), ], @@ -154,7 +292,7 @@ class SummaryTab extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (controller.meetingDetails.value.summary.isNotEmpty) + if (_controller.meetingDetails.value.summary.isNotEmpty) _button( Icons.supervisor_account, '编辑发言者', @@ -174,13 +312,13 @@ class SummaryTab extends GetView { ); }, ), - if (controller.meetingDetails.value.summary.isEmpty) + if (_controller.meetingDetails.value.summary.isEmpty) SizedBox( width: 150.w, child: _button( Icons.auto_awesome, '生成', - controller.summaryCreate, + _controller.summaryCreate, ), ), ],