diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index f4bed8cc6..9d9aec1d7 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -290,12 +290,16 @@ class AgentView extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: Colors.grey[200], + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22), ), child: Icon( isTextMode ? Icons.mic_none : Icons.keyboard, - color: Colors.grey[700], + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], size: 22, ), ), @@ -315,10 +319,15 @@ class AgentView extends StatelessWidget { //height: 44, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: Colors.grey[200], + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22), border: Border.all( - color: Colors.grey.withOpacity(0.1), + color: isDarkMode + ? Colors.grey[ + 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] + : Colors.grey.withOpacity(0.1), width: 1.0, ), ), @@ -338,7 +347,7 @@ class AgentView extends StatelessWidget { fontSize: 15, color: isDarkMode ? Colors.white - : Colors.black87, + : Colors.grey[800], ), decoration: InputDecoration( hintText: isImageActive @@ -386,10 +395,14 @@ class AgentView extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: Colors.grey[200], + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22), border: Border.all( - color: Colors.grey.withOpacity(0.1), + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey.withOpacity(0.1), width: 1.0, ), ), @@ -450,13 +463,14 @@ class AgentView extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: Colors.grey[200], + color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22), ), child: PopupMenuButton( icon: Icon( Icons.add_photo_alternate_outlined, - color: Colors.grey[700], + color: + isDarkMode ? Colors.grey[300] : Colors.grey[700], size: 22, ), padding: EdgeInsets.zero, diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index f7205a1a8..589b18b4a 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -88,7 +88,9 @@ class MessageBubble extends GetView { timeFormat.format(timestamp), style: TextStyle( fontSize: 10, - color: Colors.grey[500], + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[400] + : Colors.grey[500], fontWeight: FontWeight.w400, ), ), @@ -104,12 +106,16 @@ class MessageBubble extends GetView { /// 获取气泡颜色 Color _getBubbleColor(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + if (isUser) { return isRecognizing ? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色 : Color(0xFF3B82F6); // 使用更明亮的蓝色 } else { - return Color(0xFFF1F5F9); // 更淡的灰色背景 + return isDarkMode + ? Color(0xFF2A2A2A) + : Color(0xFFF1F5F9); // 根据主题调整AI消息背景色 } } @@ -297,7 +303,11 @@ class MessageBubble extends GetView { return Text( message, style: TextStyle( - color: isUser ? Colors.white : Colors.black87, + color: isUser + ? Colors.white + : (Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.9) + : Colors.black87), fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, @@ -329,13 +339,15 @@ class MessageBubble extends GetView { /// 构建音乐内容卡片 Widget _buildMusicContent( BuildContext context, List> playlist) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( '以下是一些推荐的音乐:', style: TextStyle( - color: Colors.black87, + color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, @@ -373,7 +385,9 @@ class MessageBubble extends GetView { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: Colors.grey[700], + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[300] + : Colors.grey[700], ), ), ], @@ -393,7 +407,13 @@ class MessageBubble extends GetView { children: [ _buildMusicItem(context, id, name, url, singer, image), if (index != playlist.length - 1) - Divider(height: 1, thickness: 1, color: Colors.grey[100]), + Divider( + height: 1, + thickness: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[800] + : Colors.grey[100], + ), ], ); }).toList(), @@ -615,6 +635,8 @@ class MessageBubble extends GetView { Widget _buildMusicItem(BuildContext context, String id, String name, String url, String singer, String image) { final controller = Get.find(); // 获取控制器 + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + Map song = { 'id': id, 'url': url, @@ -626,11 +648,11 @@ class MessageBubble extends GetView { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, + color: isDarkMode ? Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), + color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.03), blurRadius: 3, spreadRadius: 1, offset: const Offset(0, 1), @@ -726,6 +748,8 @@ class MessageBubble extends GetView { /// 构建地点项目 Widget _buildLocationItem( BuildContext context, String name, String description) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( @@ -734,7 +758,7 @@ class MessageBubble extends GetView { width: 38, height: 38, decoration: BoxDecoration( - color: Colors.blue[50], + color: isDarkMode ? Color(0xFF1A365D) : Colors.blue[50], borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -753,7 +777,9 @@ class MessageBubble extends GetView { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: Colors.grey[800], + color: isDarkMode + ? Colors.white.withOpacity(0.9) + : Colors.grey[800], ), overflow: TextOverflow.ellipsis, ), @@ -762,7 +788,7 @@ class MessageBubble extends GetView { description, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), @@ -784,13 +810,15 @@ class MessageBubble extends GetView { /// 构建天气内容卡片 Widget _buildWeatherContent(BuildContext context, String city, Map now, List> days) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '查询天气情况:', style: TextStyle( - color: Colors.black87, + color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, @@ -800,15 +828,21 @@ class MessageBubble extends GetView { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)], - ), + gradient: isDarkMode + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF1E40AF), Color(0xFF1E3A8A)], + ) + : LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)], + ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1), blurRadius: 8, offset: const Offset(0, 2), ), @@ -885,7 +919,7 @@ class MessageBubble extends GetView { SizedBox(height: 16), Container( height: 1, - color: Colors.white.withOpacity(0.2), + color: Colors.white.withOpacity(isDarkMode ? 0.15 : 0.2), ), SizedBox(height: 16), const Text( @@ -931,7 +965,9 @@ class MessageBubble extends GetView { Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: isDarkMode + ? Colors.white.withOpacity(0.15) + : Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: const Row( diff --git a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart new file mode 100644 index 000000000..48b331c8f --- /dev/null +++ b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart @@ -0,0 +1,497 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../../../data/services/db/sqflite_api.dart'; +import '../controllers/meeting_details_controller.dart'; + +class SpeakerAllBottomSheet extends StatefulWidget { + const SpeakerAllBottomSheet({super.key}); + + @override + State createState() => _SpeakerAllBottomSheetState(); +} + +class _SpeakerAllBottomSheetState extends State { + final _controller = Get.find(); + final GetStorage _storage = GetStorage(); + + List _dataList = []; + + List _historySpeakerList = []; + List _speakerList = []; + + @override + void initState() { + super.initState(); + _getSpeakerList(); + } + + void _getSpeakerList() { + _historySpeakerList = _storage.read("speaker_list") ?? []; + _speakerList = List.from(_historySpeakerList); + List textList = _controller.textList; + List dataList = []; + for (var item in textList) { + String speaker = item['speaker']; + Map? foundElement = dataList.firstWhere( + (element) => element['speaker'] == speaker, + orElse: () => null, + ); + if (foundElement != null) { + int time = item['endtime'] - item['starttime']; + foundElement['totaltime'] = foundElement['totaltime'] + time; + if (!foundElement['isOmit']) { + if (foundElement['list'].length < 2) { + foundElement['list'].add(Map.from(item)); + } else { + foundElement['isOmit'] = true; + } + } + } else { + dataList.add({ + 'speaker': speaker, + 'speakerText': TextEditingController(), + 'isShow': false, + 'totaltime': item['endtime'] - item['starttime'], + 'isOmit': false, + 'list': [Map.from(item)], + }); + } + if (speaker.isNotEmpty && + !RegExp(r'^Speaker \d+$').hasMatch(speaker) && + !_speakerList.contains(speaker)) { + _speakerList.add(speaker); + } + } + setState(() { + _dataList = dataList; + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + left: 12.w, + right: 12.w, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '命名发言者', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], + ), + ), + ) + ], + ), + ), + Divider( + height: 1, + color: Colors.grey[300], + ), + Container( + constraints: BoxConstraints( + maxHeight: + 1.sh - 240.w - MediaQuery.of(context).viewInsets.bottom, + ), + child: ListView.builder( + itemCount: _dataList.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + Map item = _dataList[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(top: 15.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: item['isShow'] && _speakerList.isNotEmpty + ? BorderRadius.vertical( + top: Radius.circular(12.r), + ) + : BorderRadius.circular(12.r), + ), + child: Focus( + onFocusChange: (bool hasFocus) { + item['isShow'] = hasFocus; + }, + child: TextField( + controller: item['speakerText'], + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + letterSpacing: 0, + height: 1.4, + ), + decoration: InputDecoration( + hintText: item['speaker'], + hintStyle: TextStyle( + fontSize: 16.sp, + color: Colors.grey[400], + letterSpacing: 0, + height: 1.4, + ), + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 12.w, + ), + ), + ), + ), + ), + if (item['isShow'] && _speakerList.isNotEmpty) + Container( + padding: EdgeInsets.only(left: 10.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12.r), + ), + border: Border( + top: BorderSide( + width: 1, + color: Colors.grey[300]!, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 10.w, bottom: 5.w), + child: Text( + '最近使用过的名称', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600]!, + ), + ), + ), + Container( + constraints: BoxConstraints( + maxHeight: 150.h, + ), + child: ListView.builder( + itemCount: _speakerList.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int idx) { + return GestureDetector( + onTap: () { + item['speakerText'].text = + _speakerList[idx]; + FocusScope.of(context).unfocus(); + }, + behavior: HitTestBehavior.translucent, + child: SizedBox( + height: 42.w, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + _speakerList[idx], + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 8.w, + ), + child: Row( + children: [ + Text( + '总计', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, + ), + ), + 5.horizontalSpace, + Text( + '${item['totaltime'] / 1000}s', + style: TextStyle( + fontSize: 12.sp, + color: Colors.black87, + ), + ), + ], + ), + ), + ...List.generate( + item['list'].length, + (int idx) { + Map data = item['list'][idx]; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.play_circle, + color: Colors.red[400], + size: 15.w, + ), + 2.horizontalSpace, + Text( + DateUtil.formatDateMs( + data['starttime'], + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 2.w), + child: Text( + '-', + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + ), + Text( + DateUtil.formatDateMs( + data['endtime'], + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + 5.horizontalSpace, + Text( + '${(data['endtime'] - data['starttime']) / 1000}s', + style: TextStyle( + color: Colors.black54, + fontSize: 12.sp, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8.w), + child: Text( + data['content'], + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: TextStyle( + color: Colors.grey[700], + fontSize: 12.sp, + ), + ), + ), + ], + ), + ); + }, + ), + if (item['isOmit']) + Padding( + padding: + EdgeInsets.only(left: 10.w, top: 5.w, bottom: 8.w), + child: Row( + children: List.generate( + 6, + (int idx) => Container( + width: 3.w, + height: 3.w, + margin: EdgeInsets.only(right: 3.w), + decoration: BoxDecoration( + color: Colors.grey[700], + borderRadius: BorderRadius.circular(3.r), + ), + ), + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 7.w), + child: _separator(), + ), + ], + ); + }, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Text( + '总结的发言者将自动更新', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 20.w), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '取消', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), + ), + ), + ), + ), + 10.horizontalSpace, + Expanded( + child: GestureDetector( + onTap: _save, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '确认', + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _separator() { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final boxWidth = constraints.constrainWidth(); + const dashWidth = 2.0; + const dashHeight = 1.0; + final dashCount = (boxWidth / (2 * dashWidth)).floor(); + return Flex( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + direction: Axis.horizontal, + children: List.generate(dashCount, (_) { + return SizedBox( + width: dashWidth, + height: dashHeight, + child: DecoratedBox( + decoration: BoxDecoration(color: Colors.grey[300]), + ), + ); + }), + ); + }, + ); + } + + void _save() async { + EasyLoading.show(); + List textList = _controller.textList; + List newTextList = []; + List editSpeakerList = []; + String personnel = ''; + 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 (dataMap['speaker'] == data['speaker']) { + dataMap['speaker'] = speaker; + editSpeakerList.add({ + 'id': dataMap['id'], + 'speaker': dataMap['speaker'], + }); + } + if (!RegExp(r'^Speaker \d+$').hasMatch(speaker) && + !_historySpeakerList.contains(speaker)) { + _historySpeakerList.insert(0, speaker); + } + } + } + if (!personnel.contains(dataMap['speaker'])) { + personnel += '[${dataMap['speaker']}] '; + } + newTextList.add(dataMap); + } + 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); + 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 4d51927f6..3bc629643 100644 --- a/lib/modules/meeting/views/speaker_bottom_sheet.dart +++ b/lib/modules/meeting/views/speaker_bottom_sheet.dart @@ -171,7 +171,6 @@ class _SpeakerBottomSheetState extends State { }, behavior: HitTestBehavior.translucent, child: SizedBox( - width: 200.w, height: 42.w, child: Align( alignment: Alignment.centerLeft, diff --git a/lib/modules/meeting/views/speech_tab.dart b/lib/modules/meeting/views/speech_tab.dart index 8a586f27d..f7a4bb0f3 100644 --- a/lib/modules/meeting/views/speech_tab.dart +++ b/lib/modules/meeting/views/speech_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/meeting_details_controller.dart'; +import 'speaker_all_bottom_sheet.dart'; import 'speaker_bottom_sheet.dart'; class SpeechTab extends GetView { @@ -200,7 +201,7 @@ class SpeechTab extends GetView { ), ), builder: (BuildContext context) { - return Container(); + return const SpeakerAllBottomSheet(); }, ); }, diff --git a/lib/modules/meeting/views/summary_tab.dart b/lib/modules/meeting/views/summary_tab.dart index ed3a49437..d663ee748 100644 --- a/lib/modules/meeting/views/summary_tab.dart +++ b/lib/modules/meeting/views/summary_tab.dart @@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/meeting_details_controller.dart'; +import 'speaker_all_bottom_sheet.dart'; class SummaryTab extends GetView { const SummaryTab({super.key}); @@ -168,7 +169,7 @@ class SummaryTab extends GetView { ), ), builder: (BuildContext context) { - return Container(); + return const SpeakerAllBottomSheet(); }, ); },