diff --git a/assets/lottie/sound_effect.json b/assets/lottie/sound_effect.json new file mode 100644 index 000000000..44c2522c1 --- /dev/null +++ b/assets/lottie/sound_effect.json @@ -0,0 +1 @@ +{"nm":"3line_soundwave","ddd":0,"h":200,"w":80,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"3 Outlines","sr":1,"st":0,"op":24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[16.036,58.68,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[64.215,101.592,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":8},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":12},{"s":[1],"t":16}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.527,-0.046],[1.003,-1.094],[-0.16,-1.436],[-1.184,-1.231],[-3.258,-8.386],[0,-9.388],[3.259,-8.385],[6.107,-6.426],[0.159,-1.435],[-0.957,-1.094],[-1.527,-0.022],[-1.184,1.276],[-3.896,9.684],[0,10.892],[3.897,9.684],[7.11,7.428]],"o":[[-1.527,0.046],[-0.957,1.094],[0.159,1.436],[6.107,6.426],[3.259,8.385],[0,9.388],[-3.258,8.386],[-1.184,1.231],[-0.16,1.436],[1.003,1.14],[1.527,0.023],[7.11,-7.429],[3.897,-9.685],[0,-10.892],[-3.896,-9.685],[-1.184,-1.276]],"v":[[-10.635,-58.385],[-14.429,-56.675],[-15.625,-52.882],[-13.609,-48.882],[0.439,-26.666],[5.327,-0.006],[0.439,26.654],[-13.609,48.871],[-15.625,52.87],[-14.429,56.665],[-10.635,58.408],[-6.568,56.529],[9.941,30.859],[15.786,-0.006],[9.941,-30.87],[-6.568,-56.538]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9569,0.2627,0.2118],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[16.035,58.68],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"2 Outlines","sr":1,"st":0,"op":24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[12.743,42.89,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[44.128,101.592,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.667,"y":1},"s":[0],"t":4},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":8},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":16},{"s":[0],"t":20}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.572,0.046],[1.002,-1.23],[-0.205,-1.413],[-1.185,-1.276],[-2.119,-5.628],[0,-6.289],[2.119,-5.651],[3.965,-4.421],[0.205,-1.39],[-1.049,-1.139],[-1.572,0.023],[-1.23,1.368],[-2.666,6.973],[0,7.748],[2.666,6.973],[4.967,5.377]],"o":[[-1.572,-0.046],[-1.049,1.094],[0.205,1.413],[3.965,4.466],[2.119,5.628],[0,6.289],[-2.119,5.652],[-1.185,1.322],[-0.205,1.39],[1.002,1.185],[1.572,-0.022],[4.967,-5.332],[2.666,-6.972],[0,-7.702],[-2.666,-6.972],[-1.23,-1.322]],"v":[[-7.161,-42.594],[-11.023,-40.816],[-12.288,-37.057],[-10.202,-33.023],[-1.077,-17.882],[2.102,-0.006],[-1.077,17.904],[-10.202,33.012],[-12.288,37.079],[-11.023,40.874],[-7.161,42.617],[-2.956,40.531],[8.493,22.074],[12.493,-0.006],[8.493,-22.018],[-2.956,-40.542]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9569,0.2627,0.2118],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[12.742,42.89],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"1 Outlines","sr":1,"st":0,"op":24,"ip":-5,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[9.137,25.999,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[24.355,101.586,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.641,0.046],[1.048,-1.23],[-0.274,-1.344],[-1.003,-1.413],[0,-6.243],[3.463,-4.466],[0.273,-1.345],[-1.048,-1.139],[-1.641,0.046],[-1.139,1.458],[-1.413,4.011],[0,4.375],[1.412,4.01],[2.598,3.236]],"o":[[-1.641,-0.046],[-1.048,1.14],[0.273,1.344],[3.463,4.466],[0,6.244],[-1.003,1.413],[-0.274,1.344],[1.048,1.231],[1.641,-0.045],[2.598,-3.236],[1.412,-4.01],[0,-4.375],[-1.413,-4.01],[-1.139,-1.458]],"v":[[-3.418,-25.703],[-7.451,-23.926],[-8.613,-20.2],[-6.699,-16.064],[-1.504,0],[-6.699,16.065],[-8.613,20.201],[-7.451,23.926],[-3.418,25.703],[0.752,23.448],[6.768,12.578],[8.887,0],[6.768,-12.578],[0.752,-23.447]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9569,0.2627,0.2118],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[9.137,25.999],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3}],"v":"5.7.4","fr":12,"op":24,"ip":0,"assets":[]} \ No newline at end of file diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 6a65612e2..8200c349c 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -18,15 +18,18 @@ import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; class MeetingDetailsController extends GetxController - with GetSingleTickerProviderStateMixin { + with GetTickerProviderStateMixin { late TabController tabController; final RxInt currentIndex = 0.obs; + late AnimationController lottieController; + final PlayerController playerController = PlayerController(); RxBool isTop = false.obs; RxInt durationMax = 0.obs; RxDouble dragOffset = 210.w.obs; + RxBool isPlay = false.obs; RxInt currentDuration = 0.obs; RxInt speakerAllStarttime = (-1).obs; @@ -65,6 +68,8 @@ class MeetingDetailsController extends GetxController } currentIndex.value = tabController.index; }); + lottieController = AnimationController(vsync: this) + ..duration = const Duration(seconds: 2); final Map args = Get.arguments ?? {}; id = args['id']; _headers = { @@ -80,6 +85,7 @@ class MeetingDetailsController extends GetxController tabController.dispose(); playerController.dispose(); _isDisposed = true; + lottieController.dispose(); super.onClose(); } @@ -114,6 +120,15 @@ class MeetingDetailsController extends GetxController } } }); + playerController.onPlayerStateChanged.listen((PlayerState playerState) { + isPlay.value = playerState.isPlaying; + if (_isDisposed) return; + if (playerState.isPlaying) { + lottieController.repeat(); + } else { + lottieController.stop(); + } + }); if (meetingDetails.value.tasktype == 1) { List speakerList = await SqfliteApi.getMeetingSpeaker(id); textList.value = List.from(speakerList); @@ -161,12 +176,6 @@ class MeetingDetailsController extends GetxController } } - void speakerCreate() async { - EasyLoading.show(); - await _audioSpeaker(); - EasyLoading.dismiss(); - } - void summaryCreate() async { EasyLoading.show(); if (meetingDetails.value.tasktype != 1) { diff --git a/lib/modules/meeting/views/generate_bottom_sheet.dart b/lib/modules/meeting/views/generate_bottom_sheet.dart new file mode 100644 index 000000000..413e81bc2 --- /dev/null +++ b/lib/modules/meeting/views/generate_bottom_sheet.dart @@ -0,0 +1,487 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../controllers/meeting_details_controller.dart'; +import 'record_language_bottom_sheet.dart'; +import 'template_view.dart'; + +class GenerateBottomSheet extends StatefulWidget { + const GenerateBottomSheet({super.key}); + + @override + State createState() => _GenerateBottomSheetState(); +} + +class _GenerateBottomSheetState extends State { + final _controller = Get.find(); + final ScrollController _scrollController = ScrollController(); + + bool _isSpeaker = true; + String _languageText = '中文普通话(简体)'; + String _language = 'zh-CN'; + + final List _originalTemplate = [ + { + 'id': 1, + 'name': '会议纪要', + 'icon': Icons.auto_stories, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + { + 'id': 2, + 'name': 'Autopilot', + 'icon': Icons.auto_graph, + 'tag': ['自适应结构', '全场景适配'], + 'desc': '', + }, + { + 'id': 3, + 'name': '推理Autopilot', + 'icon': Icons.add_to_drive_outlined, + 'tag': ['推理模型', '自动结构化'], + 'desc': '', + }, + { + 'id': 4, + 'name': '通话', + 'icon': Icons.perm_phone_msg, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + ]; + + int _templateId = 0; + int _lastTemplateId = 0; + List _templateList = []; + + @override + void initState() { + super.initState(); + if (_lastTemplateId != 0) { + _templateId = _lastTemplateId; + } else { + _templateId = _originalTemplate[0]['id']; + } + _templateList = List.from(_originalTemplate); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Row( + children: [ + Icon( + Icons.audio_file_outlined, + color: Colors.grey, + size: 32.w, + ), + 5.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _controller.meetingData.value.title, + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + ), + ), + Text( + '${_controller.meetingData.value.seconds}s', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + 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], + ), + ), + ) + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w), + margin: EdgeInsets.only(bottom: 10.w), + child: Divider( + height: 1, + color: Colors.grey[300], + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Row( + children: [ + Expanded( + child: Text( + '选择总结模版', + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + GestureDetector( + onTap: () async { + var result = await Get.to(TemplateView( + _templateId, + _lastTemplateId, + )); + if (result != null) { + _scrollController.jumpTo(0); + List list = List.from(_originalTemplate); + int index = list + .indexWhere((element) => element['id'] == result['id']); + if (index != -1) { + Map itemToMove = list.removeAt(index); + list.insert(0, itemToMove); + } else { + list.insert(0, result); + } + setState(() { + _templateId = result['id']; + _templateList = list; + }); + } + }, + child: Row( + children: [ + Text( + '查看全部', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, + ), + ), + const Icon( + Icons.keyboard_arrow_right, + color: Colors.grey, + ) + ], + ), + ), + ], + ), + ), + Container( + height: 130.w, + margin: EdgeInsets.only(bottom: 16.w), + child: ListView.builder( + controller: _scrollController, + itemCount: _templateList.length, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 4.w), + itemBuilder: (BuildContext context, int index) { + Map data = _templateList[index]; + return _item(data); + }, + ), + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.r), + ), + child: Column( + children: [ + _row( + '区分说话人', + widget: Transform.scale( + scale: 0.8, + alignment: Alignment.centerRight, + child: Switch( + value: _isSpeaker, + onChanged: (bool value) { + setState(() { + _isSpeaker = value; + }); + }, + ), + ), + ), + _row( + '录音语言', + value: _languageText, + isBorder: false, + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (BuildContext context) { + return RecordLanguageBottomSheet(_language); + }, + ).then((value) { + if (value != null) { + setState(() { + _languageText = value['name']; + _language = value['value']; + }); + } + }); + }, + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + child: GestureDetector( + onTap: () { + Get.back(); + _controller.summaryCreate(); + }, + 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 _item(Map data) { + return Container( + width: 200.w, + margin: EdgeInsets.symmetric(horizontal: 8.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14.w), + ), + child: GestureDetector( + onTap: () { + setState(() { + _templateId = data['id']; + }); + }, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + gradient: _templateId == data['id'] + ? const LinearGradient( + colors: [Colors.blue, Colors.pinkAccent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ) + : null, + ), + child: Container( + width: double.infinity, + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.w), + color: Colors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 5.w), + child: Icon( + data['icon'], + color: Colors.orange, + size: 30.w, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Text( + data['name'], + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + ), + data['tag'].isNotEmpty + ? Wrap( + spacing: 5.w, + children: List.generate( + data['tag'].length, + (int idx) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: 5.w, vertical: 2.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(4.r), + ), + child: Text( + data['tag'][idx], + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey[600], + ), + ), + ); + }, + ), + ) + : Text( + data['desc'], + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ), + if (_lastTemplateId == data['id']) + Positioned( + top: 0, + left: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.w), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + bottomRight: Radius.circular(4.w), + ), + ), + child: Text( + '上次使用', + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + if (_templateId == data['id']) + Positioned( + right: -18.w, + bottom: -18.w, + child: Container( + width: 40.w, + height: 40.w, + padding: EdgeInsets.all(2.w), + alignment: Alignment.topLeft, + decoration: BoxDecoration( + color: Colors.pinkAccent, + borderRadius: BorderRadius.circular(40.r), + ), + child: Icon( + Icons.check, + color: Colors.white, + size: 18.w, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _row( + String title, { + String? value, + Widget? widget, + bool isBorder = true, + Function()? onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: EdgeInsets.only(left: 15.w), + child: Container( + height: 46.w, + padding: EdgeInsets.only(right: 10.w), + decoration: BoxDecoration( + border: isBorder + ? Border( + bottom: BorderSide( + color: Colors.grey[300]!, + ), + ) + : null, + ), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + widget ?? + Row( + children: [ + Text( + value ?? '', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, + ), + ), + const Icon( + Icons.keyboard_arrow_right, + color: Colors.grey, + ) + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index f6bbd6984..fa8bb5621 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -71,25 +71,23 @@ class MeetingDetailsView extends GetView { ), child: Row( children: [ - StreamBuilder( - stream: controller.playerController.onPlayerStateChanged, - builder: (context, snapshot) { - bool isPlay = snapshot.data?.isPlaying ?? false; - return GestureDetector( - onTap: () { - if (isPlay) { - controller.playerController.pausePlayer(); - } else { - controller.playerController.startPlayer(); - } - }, - child: Icon( - isPlay ? Icons.pause_circle : Icons.play_circle, - color: Colors.black, - size: 30.w, - ), - ); + GestureDetector( + onTap: () { + if (controller.isPlay.value) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } }, + child: Obx( + () => Icon( + controller.isPlay.value + ? Icons.pause_circle + : Icons.play_circle, + color: Colors.black, + size: 30.w, + ), + ), ), 4.horizontalSpace, GestureDetector( @@ -352,26 +350,23 @@ class MeetingDetailsView extends GetView { size: 30.w, ), ), - StreamBuilder( - stream: - controller.playerController.onPlayerStateChanged, - builder: (context, snapshot) { - bool isPlay = snapshot.data?.isPlaying ?? false; - return GestureDetector( - onTap: () { - if (isPlay) { - controller.playerController.pausePlayer(); - } else { - controller.playerController.startPlayer(); - } - }, - child: Icon( - isPlay ? Icons.pause_circle : Icons.play_circle, - color: Colors.black, - size: 48.w, - ), - ); + GestureDetector( + onTap: () { + if (controller.isPlay.value) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } }, + child: Obx( + () => Icon( + controller.isPlay.value + ? Icons.pause_circle + : Icons.play_circle, + color: Colors.black, + size: 48.w, + ), + ), ), GestureDetector( onTap: () { diff --git a/lib/modules/meeting/views/record_language_bottom_sheet.dart b/lib/modules/meeting/views/record_language_bottom_sheet.dart new file mode 100644 index 000000000..217613373 --- /dev/null +++ b/lib/modules/meeting/views/record_language_bottom_sheet.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class RecordLanguageBottomSheet extends StatefulWidget { + final String language; + + const RecordLanguageBottomSheet(this.language, {super.key}); + + @override + State createState() => + _RecordLanguageBottomSheetState(); +} + +class _RecordLanguageBottomSheetState extends State { + final List _languages = [ + {'name': '中文普通话(简体)', 'value': 'zh-CN'}, + {'name': '粤语', 'value': 'cant'}, + {'name': '四川话', 'value': 'sc'}, + {'name': '上海话', 'value': 'zh_shanghai'}, + {'name': '英文', 'value': 'en-US'}, + {'name': '日语', 'value': 'ja-JP'}, + {'name': '韩语', 'value': 'ko-KR'}, + {'name': '法语', 'value': 'fr-FR'}, + {'name': '西班牙语', 'value': 'es-MX'}, + {'name': '葡萄牙语', 'value': 'pt-BR'}, + {'name': '印尼语', 'value': 'id-ID'}, + ]; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + 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( + margin: EdgeInsets.symmetric(vertical: 20.w), + constraints: BoxConstraints( + maxHeight: 1.sh - 180.w, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.r), + ), + child: ListView.builder( + itemCount: _languages.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + Map data = _languages[index]; + return _item(index, data); + }, + ), + ), + ], + ), + ); + } + + Widget _item(int index, Map data) { + return GestureDetector( + onTap: () { + Get.back(result: data); + }, + child: Padding( + padding: EdgeInsets.only(left: 15.w), + child: Container( + height: 46.w, + padding: EdgeInsets.only(right: 10.w), + decoration: BoxDecoration( + border: index != 0 + ? Border( + top: BorderSide( + color: Colors.grey[300]!, + ), + ) + : null, + ), + child: Row( + children: [ + Expanded( + child: Text( + data['name'], + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + if (widget.language == data['value']) + const Icon( + Icons.check, + color: Colors.black87, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart index ecf12291e..c73e1575d 100644 --- a/lib/modules/meeting/views/speaker_all_bottom_sheet.dart +++ b/lib/modules/meeting/views/speaker_all_bottom_sheet.dart @@ -4,6 +4,7 @@ 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 'package:lottie/lottie.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../controllers/meeting_details_controller.dart'; @@ -361,6 +362,21 @@ class _SpeakerAllBottomSheetState extends State { fontSize: 12.sp, ), ), + if (isActive) + Container( + width: 16.w, + height: 16.w, + margin: EdgeInsets.only(left: 5.w), + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: Transform.scale( + scale: 1.6, + child: Lottie.asset( + 'assets/lottie/sound_effect.json', + ), + ), + ), ], ), Padding( diff --git a/lib/modules/meeting/views/speech_tab.dart b/lib/modules/meeting/views/speech_tab.dart index 1d742a493..0e65a49c8 100644 --- a/lib/modules/meeting/views/speech_tab.dart +++ b/lib/modules/meeting/views/speech_tab.dart @@ -2,8 +2,10 @@ import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:lottie/lottie.dart'; import '../controllers/meeting_details_controller.dart'; +import 'generate_bottom_sheet.dart'; import 'speaker_all_bottom_sheet.dart'; import 'speaker_bottom_sheet.dart'; @@ -87,6 +89,43 @@ class SpeechTab extends GetView { fontSize: 13.sp, ), ), + 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 isActive + ? Container( + width: 16.w, + height: 16.w, + margin: EdgeInsets.only(left: 5.w), + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.red[100], + borderRadius: BorderRadius.circular(4.r), + ), + child: Transform.scale( + scale: 1.6, + child: Lottie.asset( + 'assets/lottie/sound_effect.json', + controller: controller.lottieController, + ), + ), + ) + : const SizedBox(); + }), ], ), ), @@ -169,9 +208,17 @@ class SpeechTab extends GetView { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { - controller.playerController - .seekTo(item['starttime']); - controller.playerController.startPlayer(); + if (isActive) { + if (controller.isPlay.value) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } + } else { + controller.playerController + .seekTo(item['starttime']); + controller.playerController.startPlayer(); + } }, child: Container( width: double.infinity, @@ -244,7 +291,21 @@ class SpeechTab extends GetView { child: _button( Icons.auto_awesome, '生成', - controller.speakerCreate, + () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (BuildContext context) { + return const GenerateBottomSheet(); + }, + ); + }, ), ), ], diff --git a/lib/modules/meeting/views/summary_tab.dart b/lib/modules/meeting/views/summary_tab.dart index b4eb594d2..4da336ef9 100644 --- a/lib/modules/meeting/views/summary_tab.dart +++ b/lib/modules/meeting/views/summary_tab.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../controllers/meeting_details_controller.dart'; +import 'generate_bottom_sheet.dart'; import 'speaker_all_bottom_sheet.dart'; class SummaryTab extends StatefulWidget { @@ -318,7 +319,21 @@ class _SummaryTabState extends State { child: _button( Icons.auto_awesome, '生成', - _controller.summaryCreate, + () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (BuildContext context) { + return const GenerateBottomSheet(); + }, + ); + }, ), ), ], diff --git a/lib/modules/meeting/views/template_view.dart b/lib/modules/meeting/views/template_view.dart new file mode 100644 index 000000000..28e381f5a --- /dev/null +++ b/lib/modules/meeting/views/template_view.dart @@ -0,0 +1,407 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class TemplateView extends StatefulWidget { + final int templateId; + final int lastTemplateId; + + const TemplateView(this.templateId, this.lastTemplateId, {super.key}); + + @override + State createState() => _TemplateViewState(); +} + +class _TemplateViewState extends State { + final List _categoryList = [ + { + 'id': 1, + 'name': '通用', + 'template': [ + { + 'id': 1, + 'name': '会议纪要', + 'icon': Icons.auto_stories, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + { + 'id': 2, + 'name': 'Autopilot', + 'icon': Icons.auto_graph, + 'tag': ['自适应结构', '全场景适配'], + 'desc': '', + }, + { + 'id': 3, + 'name': '推理Autopilot', + 'icon': Icons.add_to_drive_outlined, + 'tag': ['推理模型', '自动结构化'], + 'desc': '', + }, + { + 'id': 4, + 'name': '通话', + 'icon': Icons.perm_phone_msg, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + ], + }, + { + 'id': 2, + 'name': '会议', + 'template': [ + { + 'id': 5, + 'name': '咨询', + 'icon': Icons.auto_stories, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + { + 'id': 6, + 'name': '讨论', + 'icon': Icons.auto_graph, + 'tag': ['自适应结构', '全场景适配'], + 'desc': '', + }, + { + 'id': 7, + 'name': '汇报', + 'icon': Icons.add_to_drive_outlined, + 'tag': ['推理模型', '自动结构化'], + 'desc': '', + }, + { + 'id': 8, + 'name': '任务分配', + 'icon': Icons.perm_phone_msg, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + ], + }, + { + 'id': 3, + 'name': '演讲', + 'template': [ + { + 'id': 9, + 'name': '课堂笔记', + 'icon': Icons.auto_stories, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + { + 'id': 10, + 'name': '讲座', + 'icon': Icons.auto_graph, + 'tag': ['自适应结构', '全场景适配'], + 'desc': '', + }, + { + 'id': 11, + 'name': '培训', + 'icon': Icons.add_to_drive_outlined, + 'tag': ['推理模型', '自动结构化'], + 'desc': '', + }, + { + 'id': 12, + 'name': 'Sermon', + 'icon': Icons.perm_phone_msg, + 'tag': ['主题', '代办事项'], + 'desc': '', + }, + ], + }, + { + 'id': 4, + 'name': '自定义', + 'template': [ + { + 'id': 13, + 'name': '自定义', + 'icon': Icons.auto_fix_high, + 'tag': [], + 'desc': '自定义模版', + }, + ], + }, + ]; + + Map _templateData = {}; + + @override + void initState() { + super.initState(); + outerLoop: + for (var element in _categoryList) { + List template = element['template']; + for (var item in template) { + if (item['id'] == widget.templateId) { + _templateData = item; + break outerLoop; + } + } + } + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(context, isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + '选择总结模版', + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(BuildContext context, bool isDarkMode) { + return Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: _categoryList.length, + itemBuilder: (BuildContext context, int index) { + Map data = _categoryList[index]; + return _item(data); + }, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + child: GestureDetector( + onTap: () { + Get.back(result: _templateData); + }, + 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 _item(Map data) { + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.w), + child: Row( + children: [ + Text( + data['name'], + style: TextStyle( + fontSize: 18.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + const Icon( + Icons.keyboard_arrow_right, + color: Colors.grey, + ) + ], + ), + ), + Container( + height: 150.w, + margin: EdgeInsets.only(bottom: 15.w), + child: ListView.builder( + itemCount: data['template'].length, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 8.w), + itemBuilder: (BuildContext context, int index) { + Map template = data['template'][index]; + return _template(template); + }, + ), + ), + ], + ); + } + + Widget _template(Map template) { + return Container( + width: 200.w, + margin: EdgeInsets.symmetric(horizontal: 4.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14.w), + ), + child: GestureDetector( + onTap: () { + setState(() { + _templateData = template; + }); + }, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: Colors.white, + gradient: _templateData['id'] == template['id'] + ? const LinearGradient( + colors: [Colors.blue, Colors.pinkAccent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ) + : null, + ), + child: Container( + width: double.infinity, + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.w), + color: Colors.white, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 5.w), + child: Icon( + template['icon'], + color: Colors.orange, + size: 30.w, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Text( + template['name'], + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + ), + template['tag'].isNotEmpty + ? Wrap( + spacing: 5.w, + children: List.generate( + template['tag'].length, + (int idx) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: 5.w, vertical: 2.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(4.r), + ), + child: Text( + template['tag'][idx], + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey[600], + ), + ), + ); + }, + ), + ) + : Text( + template['desc'], + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ), + if (widget.lastTemplateId == template['id']) + Positioned( + top: 0, + left: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.w), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + bottomRight: Radius.circular(4.w), + ), + ), + child: Text( + '上次使用', + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + if (_templateData['id'] == template['id']) + Positioned( + right: -18.w, + bottom: -18.w, + child: Container( + width: 40.w, + height: 40.w, + padding: EdgeInsets.all(2.w), + alignment: Alignment.topLeft, + decoration: BoxDecoration( + color: Colors.pinkAccent, + borderRadius: BorderRadius.circular(40.r), + ), + child: Icon( + Icons.check, + color: Colors.white, + size: 18.w, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 94783cbd4..640beb9e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: web_socket_channel: ^3.0.2 flutter_dotenv: ^5.1.0 synchronized: ^3.1.0 - intl: ^0.19.0 + intl: ^0.20.2 crypto: ^3.0.3 flutter_tts: ^4.2.2 logger: ^2.0.2+1