You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
332 lines
12 KiB
332 lines
12 KiB
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import '../controllers/realtime_controller.dart';
|
|
import 'widgets/shader_mic_flow.dart';
|
|
|
|
class RealtimeView extends GetView<RealtimeController> {
|
|
const RealtimeView({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 设置状态栏为黑色文字
|
|
SystemChrome.setSystemUIOverlayStyle(
|
|
const SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: Brightness.dark,
|
|
statusBarBrightness: Brightness.light,
|
|
),
|
|
);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: SafeArea(
|
|
child: Stack(
|
|
children: [
|
|
// 中间音频可视化
|
|
Positioned.fill(
|
|
child: Obx(() => AnimatedAlign(
|
|
alignment: controller.isSubtitleMode.value
|
|
? const Alignment(0, -0.7) // 字幕模式时移到更上方
|
|
: const Alignment(0, -0.25),
|
|
duration: const Duration(milliseconds: 600),
|
|
curve: Curves.easeInOutCubic,
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final targetSize = controller.isSubtitleMode.value
|
|
? MediaQuery.of(context).size.width * 0.55 // 字幕模式时较小
|
|
: MediaQuery.of(context).size.width * 0.75; // 初始状态更大
|
|
|
|
return TweenAnimationBuilder<double>(
|
|
tween: Tween(end: targetSize),
|
|
duration: const Duration(milliseconds: 600),
|
|
curve: Curves.easeInOutCubic,
|
|
builder: (context, animatedSize, child) {
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// 使用Shader动画
|
|
Obx(() => ShaderMicFlow(
|
|
size: animatedSize,
|
|
amplitude: controller.audioAmplitude.value,
|
|
)),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
)),
|
|
),
|
|
|
|
// 字幕显示区域 - 只有在字幕模式开启时才显示,位于圆形下方
|
|
if (controller.isSubtitleMode.value)
|
|
Positioned(
|
|
left: 20,
|
|
right: 20,
|
|
top: MediaQuery.of(context).size.height * 0.35, // 在圆形下方
|
|
bottom: 160,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Obx(() => AnimatedOpacity(
|
|
opacity: controller.isSubtitleMode.value ? 1.0 : 0.0,
|
|
duration: const Duration(milliseconds: 300),
|
|
child: TweenAnimationBuilder<double>(
|
|
tween: Tween(
|
|
begin: 0.0,
|
|
end: controller.isSubtitleMode.value ? 1.0 : 0.0,
|
|
),
|
|
duration: const Duration(milliseconds: 900), // 延迟显示,让圆圈先移动
|
|
curve: Curves.easeInOut,
|
|
builder: (context, animationValue, child) {
|
|
return Transform.translate(
|
|
offset: Offset(0, 20 * (1 - animationValue)), // 从下方滑入
|
|
child: Opacity(
|
|
opacity: animationValue,
|
|
child: _buildChatSubtitles(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
)),
|
|
),
|
|
),
|
|
|
|
// 底部控制按钮
|
|
Positioned(
|
|
bottom: 80,
|
|
left: 0,
|
|
right: 0,
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
// 麦克风按钮
|
|
Obx(() => _buildMicButton()),
|
|
// 字幕按钮
|
|
Obx(() => _buildMainButton(
|
|
icon: controller.isSubtitleMode.value ? Icons.subtitles : Icons.subtitles_outlined,
|
|
onTap: controller.toggleSubtitleMode,
|
|
enabled: true,
|
|
isActive: controller.isSubtitleMode.value,
|
|
)),
|
|
// 清空聊天按钮
|
|
_buildMainButton(
|
|
icon: Icons.clear_all,
|
|
onTap: controller.clearChat,
|
|
enabled: true,
|
|
isActive: false,
|
|
),
|
|
// 关闭按钮
|
|
_buildMainButton(
|
|
icon: Icons.close,
|
|
onTap: controller.closeAndGoBack,
|
|
enabled: true,
|
|
isActive: false,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Home Indicator
|
|
Positioned(
|
|
bottom: 8,
|
|
left: 0,
|
|
right: 0,
|
|
child: Center(
|
|
child: Container(
|
|
width: 134,
|
|
height: 5,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black,
|
|
borderRadius: BorderRadius.circular(2.5),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建聊天字幕界面 - 上下两行显示用户输入和AI回复
|
|
Widget _buildChatSubtitles() {
|
|
return Obx(() {
|
|
// 组合用户消息:优先显示实时部分转录,否则显示最终消息
|
|
final String displayUserMessage = controller.partialUserMessage.value.isNotEmpty
|
|
? controller.partialUserMessage.value
|
|
: controller.latestUserMessage.value;
|
|
|
|
// 组合AI消息:优先显示流式响应,否则显示最终消息
|
|
final String displayAiMessage = controller.streamingAiMessage.value.isNotEmpty
|
|
? controller.streamingAiMessage.value
|
|
: controller.latestAiMessage.value;
|
|
|
|
final hasUserMessage = displayUserMessage.isNotEmpty;
|
|
final hasAiMessage = displayAiMessage.isNotEmpty;
|
|
|
|
// 如果没有消息,显示空白
|
|
if (!hasUserMessage && !hasAiMessage) {
|
|
return Container();
|
|
}
|
|
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min, // 让Column紧凑排列
|
|
children: [
|
|
// 用户输入行 - 蓝色,动态扩展
|
|
if (hasUserMessage)
|
|
Container(
|
|
width: double.infinity,
|
|
constraints: const BoxConstraints(
|
|
minHeight: 30, // 最小1行高度
|
|
maxHeight: 104, // 最大4行高度 (26*4)
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: SingleChildScrollView(
|
|
reverse: true,
|
|
child: Text(
|
|
displayUserMessage,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.blue[600],
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// 分隔线 - 只有在两个消息都存在时才显示
|
|
if (hasUserMessage && hasAiMessage)
|
|
Container(
|
|
width: 60,
|
|
height: 1,
|
|
margin: const EdgeInsets.symmetric(vertical: 2), // 进一步减少分隔线间距
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(0.5),
|
|
),
|
|
),
|
|
|
|
// AI回复行 - 绿色,动态扩展
|
|
if (hasAiMessage)
|
|
Container(
|
|
width: double.infinity,
|
|
constraints: const BoxConstraints(
|
|
minHeight: 30, // 最小1行高度
|
|
maxHeight: 104, // 最大4行高度 (26*4)
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: SingleChildScrollView(
|
|
reverse: true,
|
|
child: Text(
|
|
displayAiMessage,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.green[600],
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
/// 构建麦克风按钮 - 根据连接状态和录音状态显示不同的状态
|
|
Widget _buildMicButton() {
|
|
// 获取当前状态
|
|
final isConnected = controller.isConnected.value;
|
|
final isConnecting = controller.isConnecting.value;
|
|
final isListening = controller.isListening.value;
|
|
|
|
// 确定按钮状态
|
|
IconData icon;
|
|
Color? backgroundColor;
|
|
Color? iconColor;
|
|
VoidCallback? onTap;
|
|
|
|
if (!isConnected && !isConnecting) {
|
|
// 未连接状态 - 禁用
|
|
icon = Icons.mic_off;
|
|
backgroundColor = Colors.grey[300];
|
|
iconColor = Colors.grey[600];
|
|
onTap = null;
|
|
} else if (isConnecting) {
|
|
// 连接中状态 - 禁用但显示连接中
|
|
icon = Icons.sync;
|
|
backgroundColor = Colors.orange[200];
|
|
iconColor = Colors.orange[700];
|
|
onTap = null;
|
|
} else if (isListening) {
|
|
// 录音中状态 - 激活状态
|
|
icon = Icons.mic;
|
|
backgroundColor = Theme.of(Get.context!).primaryColor;
|
|
iconColor = Colors.white;
|
|
onTap = controller.toggleListening;
|
|
} else {
|
|
// 已连接但未录音状态 - 正常状态
|
|
icon = Icons.mic_none;
|
|
backgroundColor = Colors.grey[200];
|
|
iconColor = Colors.black;
|
|
onTap = controller.toggleListening;
|
|
}
|
|
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
color: backgroundColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: iconColor,
|
|
size: 26,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建主要控制按钮
|
|
Widget _buildMainButton({
|
|
required IconData icon,
|
|
VoidCallback? onTap,
|
|
required bool enabled,
|
|
required bool isActive,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: enabled ? onTap : null,
|
|
child: Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
color: enabled
|
|
? (isActive ? Theme.of(Get.context!).primaryColor : Colors.grey[200])
|
|
: Colors.grey[300],
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: enabled
|
|
? (isActive ? Colors.white : Colors.black)
|
|
: Colors.grey[600],
|
|
size: 26,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
}
|