|
|
@ -3,7 +3,6 @@ import 'dart:math'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:lottie/lottie.dart'; |
|
|
|
|
|
import '../controllers/meeting_record_controller.dart'; |
|
|
import '../controllers/meeting_record_controller.dart'; |
|
|
import '../../../core/theme/app_colors.dart'; |
|
|
import '../../../core/theme/app_colors.dart'; |
|
|
|
|
|
|
|
|
@ -14,17 +13,22 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
Widget build(BuildContext context) { |
|
|
Widget build(BuildContext context) { |
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
|
|
|
|
|
|
|
return WillPopScope( |
|
|
return PopScope( |
|
|
onWillPop: () async { |
|
|
onPopInvokedWithResult: (didPop, result) async { |
|
|
// 在返回时执行保存操作 |
|
|
if (didPop) { |
|
|
controller.saveTitleChange(); |
|
|
// 在返回时执行保存操作 |
|
|
await controller.saveRecording(); |
|
|
controller.saveTitleChange(); |
|
|
return true; // 允许退出 |
|
|
//await controller.saveRecording(); |
|
|
|
|
|
} |
|
|
}, |
|
|
}, |
|
|
child: Scaffold( |
|
|
child: Scaffold( |
|
|
appBar: AppBar( |
|
|
appBar: AppBar( |
|
|
leading: IconButton( |
|
|
leading: IconButton( |
|
|
icon: const Icon(Icons.arrow_back), |
|
|
icon: Icon( |
|
|
|
|
|
Icons.arrow_back_ios, |
|
|
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
|
|
size: 20.sp, |
|
|
|
|
|
), |
|
|
onPressed: () async { |
|
|
onPressed: () async { |
|
|
// 在返回按钮点击时执行保存操作 |
|
|
// 在返回按钮点击时执行保存操作 |
|
|
controller.saveTitleChange(); |
|
|
controller.saveTitleChange(); |
|
|
@ -48,7 +52,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
isDense: true, |
|
|
isDense: true, |
|
|
contentPadding: EdgeInsets.zero, |
|
|
contentPadding: EdgeInsets.zero, |
|
|
border: InputBorder.none, |
|
|
border: InputBorder.none, |
|
|
hintText: '输入文件名', |
|
|
hintText: 'enterFileName'.tr, |
|
|
hintStyle: TextStyle( |
|
|
hintStyle: TextStyle( |
|
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
|
), |
|
|
), |
|
|
@ -88,24 +92,9 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
), |
|
|
), |
|
|
body: Column( |
|
|
body: Column( |
|
|
children: [ |
|
|
children: [ |
|
|
TabBar( |
|
|
// 简化为单一音频界面,移除不必要的TabBar |
|
|
controller: controller.tabController, |
|
|
|
|
|
indicatorColor: Colors.red, |
|
|
|
|
|
labelColor: isDarkMode ? Colors.white : Colors.black, |
|
|
|
|
|
unselectedLabelColor: Colors.grey, |
|
|
|
|
|
tabs: const [ |
|
|
|
|
|
Tab(text: '音频'), |
|
|
|
|
|
// Tab(text: '文本'), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
Expanded( |
|
|
Expanded( |
|
|
child: TabBarView( |
|
|
child: _buildAudioTab(context, isDarkMode), |
|
|
controller: controller.tabController, |
|
|
|
|
|
children: [ |
|
|
|
|
|
_buildAudioTab(context, isDarkMode), |
|
|
|
|
|
// _buildTextTab(context, isDarkMode), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
@ -114,339 +103,574 @@ class MeetingRecordView extends GetView<MeetingRecordController> { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
Widget _buildAudioTab(BuildContext context, bool isDarkMode) { |
|
|
Widget _buildAudioTab(BuildContext context, bool isDarkMode) { |
|
|
return Obx(() { |
|
|
return LayoutBuilder( |
|
|
final screenWidth = MediaQuery.of(context).size.width; |
|
|
builder: (context, constraints) { |
|
|
// 初始化最大点数 |
|
|
// 使用LayoutBuilder获取准确的屏幕宽度 |
|
|
if (controller.maxPoints == 0) { |
|
|
final screenWidth = constraints.maxWidth; |
|
|
controller.setMaxPoints(screenWidth); |
|
|
|
|
|
} |
|
|
// 初始化最大点数 |
|
|
final cursorPos = controller.cursorPositionValue; |
|
|
if (controller.maxPoints == 0) { |
|
|
return Column( |
|
|
controller.setMaxPoints(screenWidth); |
|
|
children: [ |
|
|
} |
|
|
const SizedBox(height: 24), |
|
|
|
|
|
SizedBox( |
|
|
|
|
|
height: 120, |
|
|
|
|
|
child: Stack( |
|
|
|
|
|
alignment: Alignment.center, |
|
|
|
|
|
children: [ |
|
|
|
|
|
// 波形绘制 |
|
|
|
|
|
CustomPaint( |
|
|
|
|
|
painter: WaveformPainter( |
|
|
|
|
|
controller.waveData, |
|
|
|
|
|
cursorPosition: cursorPos, |
|
|
|
|
|
// color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], |
|
|
|
|
|
), |
|
|
|
|
|
size: Size(screenWidth, 120), |
|
|
|
|
|
), |
|
|
|
|
|
// 光标指示器 |
|
|
|
|
|
Positioned( |
|
|
|
|
|
left: MediaQuery.of(context).size.width * cursorPos, |
|
|
|
|
|
child: Container( |
|
|
|
|
|
width: 2, |
|
|
|
|
|
height: 120, |
|
|
|
|
|
color: Colors.red, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
const SizedBox(height: 16), |
|
|
|
|
|
Text( |
|
|
|
|
|
controller.formatTime(controller.seconds.value), |
|
|
|
|
|
style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold), |
|
|
|
|
|
), |
|
|
|
|
|
const Spacer(), |
|
|
|
|
|
_buildBottomBar(context, isDarkMode), |
|
|
|
|
|
const SizedBox(height: 32), |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Widget _buildTextTab(BuildContext context, bool isDarkMode) { |
|
|
return Column( |
|
|
return Obx(() => Column( |
|
|
|
|
|
children: [ |
|
|
children: [ |
|
|
const SizedBox(height: 48), |
|
|
SizedBox(height: 24.h), |
|
|
Center( |
|
|
// 专业级实时流动波形显示区域 |
|
|
child: Column( |
|
|
Container( |
|
|
children: [ |
|
|
height: 120.h, |
|
|
const Icon(Icons.graphic_eq, color: Colors.red, size: 32), |
|
|
margin: EdgeInsets.symmetric(horizontal: 16.w), |
|
|
const SizedBox(height: 8), |
|
|
decoration: BoxDecoration( |
|
|
Text( |
|
|
gradient: LinearGradient( |
|
|
controller.formatTime(controller.seconds.value), |
|
|
begin: Alignment.topCenter, |
|
|
style: const TextStyle( |
|
|
end: Alignment.bottomCenter, |
|
|
fontSize: 36, fontWeight: FontWeight.bold), |
|
|
colors: isDarkMode |
|
|
|
|
|
? [ |
|
|
|
|
|
const Color(0xFF1A1A1A), |
|
|
|
|
|
const Color(0xFF2A2A2A), |
|
|
|
|
|
const Color(0xFF1A1A1A), |
|
|
|
|
|
] |
|
|
|
|
|
: [ |
|
|
|
|
|
const Color(0xFFF8F9FA), |
|
|
|
|
|
const Color(0xFFFFFFFF), |
|
|
|
|
|
const Color(0xFFF8F9FA), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
|
|
border: Border.all( |
|
|
|
|
|
color: isDarkMode |
|
|
|
|
|
? Colors.white.withValues(alpha: 0.1) |
|
|
|
|
|
: Colors.black.withValues(alpha: 0.05), |
|
|
|
|
|
width: 1, |
|
|
|
|
|
), |
|
|
|
|
|
boxShadow: [ |
|
|
|
|
|
BoxShadow( |
|
|
|
|
|
color: isDarkMode |
|
|
|
|
|
? Colors.black.withValues(alpha: 0.3) |
|
|
|
|
|
: Colors.black.withValues(alpha: 0.08), |
|
|
|
|
|
blurRadius: 8, |
|
|
|
|
|
offset: const Offset(0, 2), |
|
|
), |
|
|
), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
), |
|
|
child: ClipRRect( |
|
|
const SizedBox(height: 24), |
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
// 新增:转文本提示区域 |
|
|
child: Stack( |
|
|
if (!controller.showTextContent.value) ...[ |
|
|
children: [ |
|
|
const Text('无文本', |
|
|
// 实时流动波形绘制组件 |
|
|
style: TextStyle( |
|
|
_WaveformWidget( |
|
|
color: Colors.grey, |
|
|
controller: controller, |
|
|
fontSize: 16, |
|
|
isDarkMode: isDarkMode, |
|
|
)), |
|
|
size: Size(screenWidth - 32.w, 120.h), |
|
|
const SizedBox(height: 16), |
|
|
|
|
|
ElevatedButton( |
|
|
|
|
|
onPressed: controller.toggleTextDisplay, |
|
|
|
|
|
style: ElevatedButton.styleFrom( |
|
|
|
|
|
backgroundColor: Colors.red, |
|
|
|
|
|
foregroundColor: Colors.white, |
|
|
|
|
|
shape: RoundedRectangleBorder( |
|
|
|
|
|
borderRadius: BorderRadius.circular(20), |
|
|
|
|
|
), |
|
|
|
|
|
padding: const EdgeInsets.symmetric( |
|
|
|
|
|
horizontal: 24, |
|
|
|
|
|
vertical: 8, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
child: const Text('转文本'), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
// 文本内容区域(转文本后显示) |
|
|
|
|
|
if (controller.showTextContent.value) |
|
|
|
|
|
Expanded( |
|
|
|
|
|
child: Padding( |
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 24), |
|
|
|
|
|
child: SingleChildScrollView( |
|
|
|
|
|
child: Text( |
|
|
|
|
|
'${controller.finalContent.value}${controller.intermediateContent.value}', |
|
|
|
|
|
style: const TextStyle(fontSize: 16), |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
|
|
|
// 录音状态指示器 |
|
|
|
|
|
Obx(() { |
|
|
|
|
|
if (controller.timerStatus.value == TimerStatus.running) { |
|
|
|
|
|
return Positioned( |
|
|
|
|
|
top: 8.h, |
|
|
|
|
|
right: 8.w, |
|
|
|
|
|
child: Container( |
|
|
|
|
|
padding: EdgeInsets.symmetric( |
|
|
|
|
|
horizontal: 6.w, |
|
|
|
|
|
vertical: 2.h, |
|
|
|
|
|
), |
|
|
|
|
|
decoration: BoxDecoration( |
|
|
|
|
|
color: Colors.red.withValues(alpha: 0.9), |
|
|
|
|
|
borderRadius: BorderRadius.circular(8.r), |
|
|
|
|
|
), |
|
|
|
|
|
child: Row( |
|
|
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
|
|
children: [ |
|
|
|
|
|
Container( |
|
|
|
|
|
width: 4.w, |
|
|
|
|
|
height: 4.h, |
|
|
|
|
|
decoration: const BoxDecoration( |
|
|
|
|
|
color: Colors.white, |
|
|
|
|
|
shape: BoxShape.circle, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
SizedBox(width: 3.w), |
|
|
|
|
|
Text( |
|
|
|
|
|
'REC', |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
color: Colors.white, |
|
|
|
|
|
fontSize: 8.sp, |
|
|
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
return const SizedBox.shrink(); |
|
|
|
|
|
}), |
|
|
|
|
|
], |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
SizedBox(height: 24.h), |
|
|
|
|
|
// 时间显示 - 只包装需要响应式更新的Text widget |
|
|
|
|
|
Obx(() => Text( |
|
|
|
|
|
controller.formatTime(controller.seconds.value), |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
fontSize: 48.sp, |
|
|
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
|
|
), |
|
|
|
|
|
)), |
|
|
const Spacer(), |
|
|
const Spacer(), |
|
|
_buildBottomBar(context, isDarkMode), |
|
|
_buildBottomBar(context, isDarkMode), |
|
|
const SizedBox(height: 32), |
|
|
SizedBox(height: 32.h), |
|
|
], |
|
|
], |
|
|
)); |
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 底部操作栏 |
|
|
// 底部操作栏 |
|
|
Widget _buildBottomBar(BuildContext context, bool isDarkMode) { |
|
|
Widget _buildBottomBar(BuildContext context, bool isDarkMode) { |
|
|
return Row( |
|
|
return Padding( |
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
|
padding: EdgeInsets.symmetric(horizontal: 20.w), |
|
|
children: [ |
|
|
child: Row( |
|
|
// 收藏按钮 |
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
|
GestureDetector( |
|
|
children: [ |
|
|
onTap: controller.toggleMarked, |
|
|
// 收藏按钮 |
|
|
child: Obx(() => CircleAvatar( |
|
|
_buildActionButton( |
|
|
radius: 32, |
|
|
onTap: controller.toggleMarked, |
|
|
backgroundColor: Colors.white, |
|
|
child: Obx(() => Icon( |
|
|
child: Icon( |
|
|
|
|
|
controller.isMarked.value ? Icons.star : Icons.star_border, |
|
|
controller.isMarked.value ? Icons.star : Icons.star_border, |
|
|
size: 36, |
|
|
size: 36.sp, |
|
|
color: |
|
|
color: |
|
|
controller.isMarked.value ? Colors.orange : Colors.grey, |
|
|
controller.isMarked.value ? Colors.orange : Colors.grey, |
|
|
), |
|
|
)), |
|
|
)), |
|
|
radius: 32.w, |
|
|
), |
|
|
tooltip: controller.isMarked.value |
|
|
// 录音按钮 |
|
|
? 'unmarkFavorite'.tr |
|
|
GestureDetector( |
|
|
: 'markFavorite'.tr, |
|
|
onTap: controller.toggleRecording, |
|
|
), |
|
|
child: Obx(() => CircleAvatar( |
|
|
// 录音按钮 |
|
|
radius: 40, |
|
|
_buildActionButton( |
|
|
backgroundColor: Colors.white, |
|
|
onTap: controller.toggleRecording, |
|
|
child: Icon( |
|
|
child: Obx(() => Icon( |
|
|
controller.timerStatus.value == TimerStatus.running |
|
|
controller.timerStatus.value == TimerStatus.running |
|
|
? Icons.pause |
|
|
? Icons.pause |
|
|
: Icons.play_arrow, |
|
|
: Icons.play_arrow, |
|
|
size: 44, |
|
|
size: 44.sp, |
|
|
color: Colors.red, |
|
|
color: Colors.red, |
|
|
), |
|
|
)), |
|
|
)), |
|
|
radius: 40.w, |
|
|
), |
|
|
tooltip: controller.timerStatus.value == TimerStatus.running |
|
|
// 完成按钮 |
|
|
? 'pauseRecording'.tr |
|
|
GestureDetector( |
|
|
: 'startRecording'.tr, |
|
|
onTap: () => _bottomSheet(context, isDarkMode), // 改为匿名函数 |
|
|
), |
|
|
child: const CircleAvatar( |
|
|
// 完成按钮 |
|
|
radius: 32, |
|
|
_buildActionButton( |
|
|
backgroundColor: Colors.white, |
|
|
onTap: () => _bottomSheet(context, isDarkMode), |
|
|
child: Icon( |
|
|
child: Icon( |
|
|
Icons.check, |
|
|
Icons.check, |
|
|
size: 36, |
|
|
size: 36.sp, |
|
|
color: Colors.green, |
|
|
color: Colors.green, |
|
|
), |
|
|
), |
|
|
|
|
|
radius: 32.w, |
|
|
|
|
|
tooltip: 'finishRecording'.tr, |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 统一的操作按钮组件 |
|
|
|
|
|
Widget _buildActionButton({ |
|
|
|
|
|
required VoidCallback onTap, |
|
|
|
|
|
required Widget child, |
|
|
|
|
|
required double radius, |
|
|
|
|
|
required String tooltip, |
|
|
|
|
|
}) { |
|
|
|
|
|
return Tooltip( |
|
|
|
|
|
message: tooltip, |
|
|
|
|
|
child: Material( |
|
|
|
|
|
color: Colors.white, |
|
|
|
|
|
shape: const CircleBorder(), |
|
|
|
|
|
elevation: 4, |
|
|
|
|
|
child: InkWell( |
|
|
|
|
|
onTap: onTap, |
|
|
|
|
|
customBorder: const CircleBorder(), |
|
|
|
|
|
child: CircleAvatar( |
|
|
|
|
|
radius: radius, |
|
|
|
|
|
backgroundColor: Colors.white, |
|
|
|
|
|
child: child, |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
], |
|
|
), |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 底部编辑弹窗 |
|
|
// 底部编辑弹窗 |
|
|
void _bottomSheet(BuildContext context, bool isDarkMode) { |
|
|
void _bottomSheet(BuildContext context, bool isDarkMode) { |
|
|
Get.bottomSheet( |
|
|
Get.bottomSheet( |
|
|
Padding( |
|
|
Container( |
|
|
padding: MediaQuery.of(context).viewInsets, |
|
|
padding: EdgeInsets.only( |
|
|
child: Container( |
|
|
left: 20.w, |
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), |
|
|
right: 20.w, |
|
|
decoration: BoxDecoration( |
|
|
top: 20.w, |
|
|
color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], |
|
|
bottom: MediaQuery.of(context).viewInsets.bottom + 20.w, |
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)), |
|
|
), |
|
|
), |
|
|
decoration: BoxDecoration( |
|
|
child: Column( |
|
|
color: isDarkMode ? AppColors.darkBackground : Colors.white, |
|
|
mainAxisSize: MainAxisSize.min, |
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), |
|
|
children: [ |
|
|
boxShadow: [ |
|
|
// 标题 |
|
|
BoxShadow( |
|
|
Padding( |
|
|
color: Colors.black.withValues(alpha: 0.1), |
|
|
padding: EdgeInsets.only(bottom: 20.w), |
|
|
blurRadius: 10, |
|
|
child: Text( |
|
|
offset: const Offset(0, -2), |
|
|
'editMeetingTitle'.tr, |
|
|
), |
|
|
style: TextStyle( |
|
|
], |
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
), |
|
|
fontSize: 18.sp, |
|
|
child: Column( |
|
|
fontWeight: FontWeight.bold, |
|
|
mainAxisSize: MainAxisSize.min, |
|
|
), |
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
|
|
children: [ |
|
|
|
|
|
// 拖拽指示器 |
|
|
|
|
|
Center( |
|
|
|
|
|
child: Container( |
|
|
|
|
|
width: 40.w, |
|
|
|
|
|
height: 4.h, |
|
|
|
|
|
margin: EdgeInsets.only(bottom: 20.h), |
|
|
|
|
|
decoration: BoxDecoration( |
|
|
|
|
|
color: Colors.grey[400], |
|
|
|
|
|
borderRadius: BorderRadius.circular(2.r), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
|
|
|
// 标题 |
|
|
|
|
|
Text( |
|
|
|
|
|
'editMeetingTitle'.tr, |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
|
|
fontSize: 20.sp, |
|
|
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
// 输入框 |
|
|
SizedBox(height: 16.h), |
|
|
Container( |
|
|
|
|
|
decoration: BoxDecoration( |
|
|
// 输入框 |
|
|
color: isDarkMode ? Colors.white10 : Colors.white, |
|
|
Container( |
|
|
borderRadius: BorderRadius.circular(12.w), |
|
|
decoration: BoxDecoration( |
|
|
|
|
|
color: isDarkMode ? Colors.white10 : Colors.grey[100], |
|
|
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
|
|
border: Border.all( |
|
|
|
|
|
color: isDarkMode ? Colors.white24 : Colors.grey[300]!, |
|
|
|
|
|
width: 1, |
|
|
), |
|
|
), |
|
|
child: TextField( |
|
|
), |
|
|
controller: controller.titleEditingController, |
|
|
child: TextField( |
|
|
autofocus: true, // 自动弹出键盘 |
|
|
controller: controller.titleEditingController, |
|
|
maxLength: 50, // 添加字符限制 |
|
|
autofocus: true, |
|
|
style: TextStyle( |
|
|
maxLength: 50, |
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
style: TextStyle( |
|
|
fontSize: 16.sp, |
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
), |
|
|
fontSize: 16.sp, |
|
|
decoration: InputDecoration( |
|
|
), |
|
|
hintText: 'maxCharacters'.tr, |
|
|
decoration: InputDecoration( |
|
|
hintStyle: TextStyle( |
|
|
hintText: 'enterFileName'.tr, |
|
|
color: isDarkMode ? Colors.white60 : Colors.black45, |
|
|
hintStyle: TextStyle( |
|
|
), |
|
|
color: isDarkMode ? Colors.white60 : Colors.grey[600], |
|
|
border: InputBorder.none, |
|
|
|
|
|
contentPadding: EdgeInsets.all(16.w), |
|
|
|
|
|
counterText: '', // 隐藏默认计数器 |
|
|
|
|
|
), |
|
|
), |
|
|
|
|
|
border: InputBorder.none, |
|
|
|
|
|
contentPadding: EdgeInsets.all(16.w), |
|
|
|
|
|
counterText: '', |
|
|
), |
|
|
), |
|
|
|
|
|
textInputAction: TextInputAction.done, |
|
|
|
|
|
onSubmitted: (_) async { |
|
|
|
|
|
await controller.saveTitleChange(); |
|
|
|
|
|
await controller.saveRecording(); |
|
|
|
|
|
Get.back(); |
|
|
|
|
|
}, |
|
|
), |
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
Padding( |
|
|
SizedBox(height: 24.h), |
|
|
padding: EdgeInsets.only(top: 30.w), |
|
|
|
|
|
child: Row( |
|
|
// 按钮组 |
|
|
children: [ |
|
|
Row( |
|
|
// 取消按钮 |
|
|
children: [ |
|
|
Expanded( |
|
|
Expanded( |
|
|
child: TextButton( |
|
|
child: OutlinedButton( |
|
|
onPressed: Get.back, // |
|
|
onPressed: Get.back, |
|
|
style: TextButton.styleFrom( |
|
|
style: OutlinedButton.styleFrom( |
|
|
foregroundColor: |
|
|
foregroundColor: |
|
|
isDarkMode ? AppColors.primary : Colors.blue, |
|
|
isDarkMode ? Colors.white70 : Colors.grey[700], |
|
|
padding: EdgeInsets.symmetric(vertical: 12.w), |
|
|
side: BorderSide( |
|
|
shape: RoundedRectangleBorder( |
|
|
color: isDarkMode ? Colors.white30 : Colors.grey[400]!, |
|
|
borderRadius: BorderRadius.circular(12.w), |
|
|
), |
|
|
side: BorderSide( |
|
|
padding: EdgeInsets.symmetric(vertical: 14.h), |
|
|
color: |
|
|
shape: RoundedRectangleBorder( |
|
|
isDarkMode ? AppColors.primary : Colors.blue, |
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
width: 2.w, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
child: Text('cancel'.tr), |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
child: Text('cancel'.tr), |
|
|
SizedBox(width: 20.w), |
|
|
), |
|
|
|
|
|
), |
|
|
// 确认按钮 |
|
|
SizedBox(width: 16.w), |
|
|
Expanded( |
|
|
Expanded( |
|
|
child: TextButton( |
|
|
child: ElevatedButton( |
|
|
onPressed: () async { |
|
|
onPressed: () async { |
|
|
await controller.saveTitleChange(); |
|
|
await controller.saveTitleChange(); |
|
|
await controller.saveRecording(); |
|
|
await controller.saveRecording(); |
|
|
Get.back(); |
|
|
Get.back(); |
|
|
}, |
|
|
}, |
|
|
style: TextButton.styleFrom( |
|
|
style: ElevatedButton.styleFrom( |
|
|
backgroundColor: |
|
|
backgroundColor: |
|
|
isDarkMode ? AppColors.primary : Colors.blue, |
|
|
isDarkMode ? AppColors.primary : Colors.blue, |
|
|
foregroundColor: Colors.white, |
|
|
foregroundColor: Colors.white, |
|
|
padding: EdgeInsets.symmetric(vertical: 12.w), |
|
|
padding: EdgeInsets.symmetric(vertical: 14.h), |
|
|
shape: RoundedRectangleBorder( |
|
|
shape: RoundedRectangleBorder( |
|
|
borderRadius: BorderRadius.circular(12.w), |
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
child: Text('confirm'.tr), |
|
|
|
|
|
), |
|
|
), |
|
|
|
|
|
elevation: 2, |
|
|
), |
|
|
), |
|
|
], |
|
|
child: Text('confirm'.tr), |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
], |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
isScrollControlled: true, // 防止键盘遮挡 |
|
|
isScrollControlled: true, |
|
|
backgroundColor: Colors.transparent, // 移除默认背景 |
|
|
backgroundColor: Colors.transparent, |
|
|
|
|
|
isDismissible: true, |
|
|
|
|
|
enableDrag: true, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class WaveformPainter extends CustomPainter { |
|
|
/// 实时流动波形绘制器 |
|
|
final List<double> data; |
|
|
class RealTimeWaveformPainter extends CustomPainter { |
|
|
final double cursorPosition; |
|
|
final List<double> waveData; |
|
|
|
|
|
final bool isDarkMode; |
|
|
|
|
|
final bool isRecording; |
|
|
|
|
|
final double animationValue; // 动画值,用于流动效果 |
|
|
|
|
|
|
|
|
WaveformPainter(this.data, {required this.cursorPosition}); |
|
|
RealTimeWaveformPainter({ |
|
|
|
|
|
required this.waveData, |
|
|
|
|
|
required this.isDarkMode, |
|
|
|
|
|
required this.isRecording, |
|
|
|
|
|
this.animationValue = 0.0, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
void paint(Canvas canvas, Size size) { |
|
|
void paint(Canvas canvas, Size size) { |
|
|
final paint = Paint() |
|
|
final centerY = size.height / 2; |
|
|
..color = Colors.blue[300]! |
|
|
final centerX = size.width / 2; |
|
|
..strokeWidth = 2 |
|
|
|
|
|
..strokeCap = StrokeCap.round; |
|
|
// 绘制中央基准线 |
|
|
|
|
|
_drawCenterLine(canvas, size, centerX, centerY); |
|
|
|
|
|
|
|
|
|
|
|
// 只在录音状态下且有波形数据时才绘制波形 |
|
|
|
|
|
if (isRecording && waveData.isNotEmpty) { |
|
|
|
|
|
_drawWaveform(canvas, size, centerX, centerY); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
final cursorPaint = Paint() |
|
|
/// 绘制中央时间参考线 |
|
|
|
|
|
void _drawCenterLine( |
|
|
|
|
|
Canvas canvas, Size size, double centerX, double centerY) { |
|
|
|
|
|
// 绘制水平中线 |
|
|
|
|
|
final horizontalLinePaint = Paint() |
|
|
|
|
|
..color = |
|
|
|
|
|
(isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15) |
|
|
|
|
|
..strokeWidth = 1.0; |
|
|
|
|
|
|
|
|
|
|
|
canvas.drawLine( |
|
|
|
|
|
Offset(0, centerY), |
|
|
|
|
|
Offset(size.width, centerY), |
|
|
|
|
|
horizontalLinePaint, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 绘制垂直时间参考线(红色,更醒目) |
|
|
|
|
|
final centerLinePaint = Paint() |
|
|
|
|
|
..color = Colors.red.withValues(alpha: 0.8) |
|
|
|
|
|
..strokeWidth = 2.0 |
|
|
|
|
|
..style = PaintingStyle.stroke; |
|
|
|
|
|
|
|
|
|
|
|
canvas.drawLine( |
|
|
|
|
|
Offset(centerX, centerY - size.height * 0.4), |
|
|
|
|
|
Offset(centerX, centerY + size.height * 0.4), |
|
|
|
|
|
centerLinePaint, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 在参考线顶部绘制小三角形指示器 |
|
|
|
|
|
final trianglePath = Path(); |
|
|
|
|
|
final triangleSize = 4.0; |
|
|
|
|
|
trianglePath.moveTo(centerX, centerY - size.height * 0.4); |
|
|
|
|
|
trianglePath.lineTo( |
|
|
|
|
|
centerX - triangleSize, centerY - size.height * 0.4 - triangleSize); |
|
|
|
|
|
trianglePath.lineTo( |
|
|
|
|
|
centerX + triangleSize, centerY - size.height * 0.4 - triangleSize); |
|
|
|
|
|
trianglePath.close(); |
|
|
|
|
|
|
|
|
|
|
|
final trianglePaint = Paint() |
|
|
..color = Colors.red |
|
|
..color = Colors.red |
|
|
..strokeWidth = 2; |
|
|
..style = PaintingStyle.fill; |
|
|
|
|
|
|
|
|
final double barWidth = size.width / (data.length * 1.2); // 增加条宽 |
|
|
canvas.drawPath(trianglePath, trianglePaint); |
|
|
final centerY = size.height / 2; |
|
|
} |
|
|
|
|
|
|
|
|
// 改为绘制矩形条,增加视觉冲击力 |
|
|
/// 绘制实时流动波形 |
|
|
for (int i = 0; i < data.length; i++) { |
|
|
void _drawWaveform(Canvas canvas, Size size, double centerX, double centerY) { |
|
|
final x = i * barWidth; |
|
|
final barWidth = 2.5; // 波形条宽度 |
|
|
final height = min(data[i], 50); // 限制最大高度 |
|
|
final barSpacing = 0.5; // 波形条间距 |
|
|
final rect = Rect.fromLTWH( |
|
|
final totalBarWidth = barWidth + barSpacing; |
|
|
x, |
|
|
|
|
|
centerY - height / 2, |
|
|
// 计算中央参考线左侧可以显示多少个波形条 |
|
|
barWidth * 0.8, // 条宽稍小于间隔 |
|
|
final maxBarsOnLeft = (centerX / totalBarWidth).floor(); |
|
|
height.toDouble(), |
|
|
|
|
|
); |
|
|
// 绘制左侧流动波形(历史数据,从右向左流动) |
|
|
|
|
|
_drawLeftWaveform( |
|
|
|
|
|
canvas, centerX, centerY, barWidth, totalBarWidth, maxBarsOnLeft); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 绘制左侧历史波形(从右向左流动) |
|
|
|
|
|
void _drawLeftWaveform(Canvas canvas, double centerX, double centerY, |
|
|
|
|
|
double barWidth, double totalBarWidth, int maxBars) { |
|
|
|
|
|
final dataLength = waveData.length; |
|
|
|
|
|
if (dataLength == 0) return; |
|
|
|
|
|
|
|
|
|
|
|
// 计算流动偏移量 |
|
|
|
|
|
final flowOffset = animationValue * totalBarWidth; |
|
|
|
|
|
|
|
|
|
|
|
// 绘制最近的波形条(最多显示maxBars个) |
|
|
|
|
|
final displayCount = min(dataLength, maxBars); |
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < displayCount; i++) { |
|
|
|
|
|
// 计算波形条的x位置:从参考线开始,向左排列,加上流动偏移 |
|
|
|
|
|
final x = centerX - (i + 1) * totalBarWidth + flowOffset; |
|
|
|
|
|
|
|
|
|
|
|
// 如果波形条完全移出屏幕左侧,跳过绘制 |
|
|
|
|
|
if (x + barWidth < 0) continue; |
|
|
|
|
|
|
|
|
|
|
|
// 获取对应的音频数据(最新的数据在数组末尾) |
|
|
|
|
|
final dataIndex = dataLength - 1 - i; |
|
|
|
|
|
if (dataIndex < 0) continue; |
|
|
|
|
|
|
|
|
// 添加渐变效果增强视觉 |
|
|
final amplitude = waveData[dataIndex]; |
|
|
final gradient = LinearGradient( |
|
|
|
|
|
colors: [Colors.blue[400]!, Colors.blue[200]!], |
|
|
// 计算透明度:越远的波形条越透明 |
|
|
begin: Alignment.topCenter, |
|
|
final opacity = (1.0 - (i / displayCount) * 0.6).clamp(0.2, 1.0); |
|
|
end: Alignment.bottomCenter, |
|
|
|
|
|
|
|
|
_drawWaveBar(canvas, x, centerY, barWidth, amplitude, opacity); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 绘制单个上下对称的波形条 |
|
|
|
|
|
void _drawWaveBar(Canvas canvas, double x, double centerY, double barWidth, |
|
|
|
|
|
double amplitude, double opacity) { |
|
|
|
|
|
// 将振幅转换为显示高度(上下对称) |
|
|
|
|
|
final maxHeight = 50.0; // 最大高度 |
|
|
|
|
|
final barHeight = (amplitude / 100.0) * maxHeight; |
|
|
|
|
|
|
|
|
|
|
|
// 创建上下对称的圆角矩形 |
|
|
|
|
|
final rect = RRect.fromRectAndRadius( |
|
|
|
|
|
Rect.fromCenter( |
|
|
|
|
|
center: Offset(x + barWidth / 2, centerY), |
|
|
|
|
|
width: barWidth, |
|
|
|
|
|
height: barHeight * 2, // 上下对称,总高度是barHeight的2倍 |
|
|
|
|
|
), |
|
|
|
|
|
Radius.circular(barWidth / 2), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 创建渐变效果 |
|
|
|
|
|
final gradient = LinearGradient( |
|
|
|
|
|
colors: _getGradientColors(opacity), |
|
|
|
|
|
begin: Alignment.topCenter, |
|
|
|
|
|
end: Alignment.bottomCenter, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
final paint = Paint() |
|
|
|
|
|
..shader = gradient.createShader(rect.outerRect) |
|
|
|
|
|
..style = PaintingStyle.fill; |
|
|
|
|
|
|
|
|
|
|
|
canvas.drawRRect(rect, paint); |
|
|
|
|
|
|
|
|
|
|
|
// 可选:添加高光效果(仅对较新的波形条) |
|
|
|
|
|
if (opacity > 0.7) { |
|
|
|
|
|
final highlightRect = RRect.fromRectAndRadius( |
|
|
|
|
|
Rect.fromCenter( |
|
|
|
|
|
center: Offset(x + barWidth / 2, centerY), |
|
|
|
|
|
width: barWidth * 0.4, |
|
|
|
|
|
height: barHeight * 2 * 0.8, |
|
|
|
|
|
), |
|
|
|
|
|
Radius.circular(barWidth / 4), |
|
|
); |
|
|
); |
|
|
final rectPaint = Paint() |
|
|
|
|
|
..shader = gradient.createShader(rect) |
|
|
final highlightPaint = Paint() |
|
|
|
|
|
..color = Colors.white.withValues(alpha: 0.2 * opacity) |
|
|
..style = PaintingStyle.fill; |
|
|
..style = PaintingStyle.fill; |
|
|
|
|
|
|
|
|
canvas.drawRRect( |
|
|
canvas.drawRRect(highlightRect, highlightPaint); |
|
|
RRect.fromRectAndRadius(rect, const Radius.circular(4)), |
|
|
|
|
|
rectPaint, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
final cursorX = size.width * cursorPosition; |
|
|
/// 获取渐变颜色 |
|
|
canvas.drawLine( |
|
|
List<Color> _getGradientColors(double opacity) { |
|
|
Offset(cursorX, 0), |
|
|
return isDarkMode |
|
|
Offset(cursorX, size.height), |
|
|
? [ |
|
|
cursorPaint, |
|
|
Colors.blue[400]!.withValues(alpha: opacity), |
|
|
); |
|
|
Colors.blue[200]!.withValues(alpha: opacity * 0.7), |
|
|
|
|
|
Colors.blue[100]!.withValues(alpha: opacity * 0.4), |
|
|
|
|
|
] |
|
|
|
|
|
: [ |
|
|
|
|
|
Colors.blue[500]!.withValues(alpha: opacity), |
|
|
|
|
|
Colors.blue[300]!.withValues(alpha: opacity * 0.7), |
|
|
|
|
|
Colors.blue[100]!.withValues(alpha: opacity * 0.4), |
|
|
|
|
|
]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
bool shouldRepaint(covariant WaveformPainter oldDelegate) { |
|
|
bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) { |
|
|
return oldDelegate.data != data || |
|
|
return oldDelegate.waveData != waveData || |
|
|
oldDelegate.cursorPosition != cursorPosition; |
|
|
oldDelegate.isDarkMode != isDarkMode || |
|
|
|
|
|
oldDelegate.isRecording != isRecording || |
|
|
|
|
|
oldDelegate.animationValue != animationValue; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 专门的波形Widget,正确管理GetX响应式状态 |
|
|
|
|
|
class _WaveformWidget extends StatelessWidget { |
|
|
|
|
|
final MeetingRecordController controller; |
|
|
|
|
|
final bool isDarkMode; |
|
|
|
|
|
final Size size; |
|
|
|
|
|
|
|
|
|
|
|
const _WaveformWidget({ |
|
|
|
|
|
required this.controller, |
|
|
|
|
|
required this.isDarkMode, |
|
|
|
|
|
required this.size, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
// 使用AnimatedBuilder监听动画变化 |
|
|
|
|
|
return AnimatedBuilder( |
|
|
|
|
|
animation: controller.waveformAnimation, |
|
|
|
|
|
builder: (context, child) { |
|
|
|
|
|
// 使用Obx只监听需要响应式更新的数据 |
|
|
|
|
|
return Obx(() { |
|
|
|
|
|
return CustomPaint( |
|
|
|
|
|
painter: RealTimeWaveformPainter( |
|
|
|
|
|
waveData: controller.waveData, |
|
|
|
|
|
isDarkMode: isDarkMode, |
|
|
|
|
|
isRecording: controller.isRecording.value, |
|
|
|
|
|
animationValue: controller.waveformAnimation.value, |
|
|
|
|
|
), |
|
|
|
|
|
size: size, |
|
|
|
|
|
); |
|
|
|
|
|
}); |
|
|
|
|
|
}, |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|