Browse Source

feat(语音交互): 新增语言打断功能并同步状态

添加语言打断功能开关,包括:
1. 在三个语言文件中新增翻译键
2. 修复存储键名拼写错误(interrupe->interrupt)
3. 在Home和Agent页面添加开关控件并保持状态同步
4. 通过监听存储实现跨页面状态更新
weicu
lxm 12 months ago
parent
commit
6fbb164940
  1. 1
      lib/core/translations/language/en_us.dart
  2. 3
      lib/core/translations/language/zh_cn.dart
  3. 2
      lib/core/translations/language/zh_hk.dart
  4. 4
      lib/data/services/ble_manager.dart
  5. 18
      lib/modules/agent/controllers/agent_controller.dart
  6. 118
      lib/modules/agent/views/agent_view.dart
  7. 55
      lib/modules/home/controllers/home_controller.dart
  8. 98
      lib/modules/home/views/home_view.dart

1
lib/core/translations/language/en_us.dart

@ -543,6 +543,7 @@ const Map<String, String> enUS = {
"loadFailedClickRetry": "Load failed, click to retry",
"swipeUpForMore": "Swipe up for more",
"callingTool": "I'm helping you, please wait!", // 正在帮您,请稍等!
"language_interrupt": "Language Interrupt", // 语言打断
// 预设提示词
"presetMusic": "Play some music", // Play some music

3
lib/core/translations/language/zh_cn.dart

@ -513,7 +513,8 @@ const Map<String, String> zhCN = {
"loadMoreMessages": "加载更多消息...",
"loadFailedClickRetry": "加载失败,点击重试",
"swipeUpForMore": "向上滑动查看更多",
"callingTool": "正在帮您,请稍等!",
"callingTool": "正在帮您,请稍等!", // 正在帮您,请稍等!
"language_interrupt": "语言打断", // 语言打断
// 预设提示词
"presetMusic": "来点音乐", // 来点音乐

2
lib/core/translations/language/zh_hk.dart

@ -295,6 +295,8 @@ const Map<String, String> zhHK = {
"loadMoreMessages": "載入更多訊息...",
"loadFailedClickRetry": "載入失敗,點擊重試",
"swipeUpForMore": "向上滑動查看更多",
"callingTool": "正在帮您,请稍等!", // 正在帮您,请稍等!
"language_interrupt": "语言打断", // 语言打断
// Agent controller
'welcomeMessage': '你好!我是**小言**,我能幫你:\n\n'

4
lib/data/services/ble_manager.dart

@ -650,7 +650,7 @@ class BleManager extends GetxService {
// 加载TTS偏好设置
Future<bool> _loadTtsInterrupe() async {
try {
if (!_storage.hasData("agent_tts_interrupe")) {
if (!_storage.hasData("agent_tts_interrupt")) {
// 获取设备类型以确定检查策略
String deviceName = getDeviceName();
DBProduct? product = AppConfig.getproduct(deviceName);
@ -662,7 +662,7 @@ class BleManager extends GetxService {
return false;
}
} else {
return _storage.read('agent_tts_interrupe') ?? false;
return _storage.read('agent_tts_interrupt') ?? false;
}
} catch (e) {
return false;

18
lib/modules/agent/controllers/agent_controller.dart

@ -168,6 +168,18 @@ class AgentController extends GetxController with WidgetsBindingObserver {
_initAppDir();
_loadTtsPreference(); //加载TTS偏好设置
_loadTtsInterrupe(); //加载TTS播报打断偏好设置
// 监听存储中的“agent_tts_interrupe”键变化,实现跨页面状态同步
// 中文注释:当 Home 页或 Agent 页任意一处切换时,另一处可以通过监听存储自动刷新 UI
_storage.listenKey('agent_tts_interrupt', (value) {
try {
final bool newValue = value is bool ? value : (value == 'true');
if (isTtsInterruptEnabled.value != newValue) {
isTtsInterruptEnabled.value = newValue;
}
} catch (e) {
// 中文注释:忽略异常,避免因类型不一致导致崩溃
}
});
_setupScrollListener(); //设置滚动监听器
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
@ -266,7 +278,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 加载TTS偏好设置
Future<void> _loadTtsInterrupe() async {
try {
if (!_storage.hasData("agent_tts_interrupe")) {
if (!_storage.hasData("agent_tts_interrupt")) {
// 获取设备类型以确定检查策略
String deviceName = getDeviceName();
DBProduct? product = AppConfig.getproduct(deviceName);
@ -279,7 +291,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
}
} else {
isTtsInterruptEnabled.value =
_storage.read('agent_tts_interrupe') ?? false;
_storage.read('agent_tts_interrupt') ?? false;
}
await AgentService.isInterrupt(isTtsInterruptEnabled.value);
} catch (e) {
@ -293,7 +305,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
/// 当启用播报打断时,用户说话会自动停止当前的TTS播放
Future<void> toggleTtsInterrupt() async {
isTtsInterruptEnabled.toggle();
await _storage.write('agent_tts_interrupe', isTtsInterruptEnabled.value);
await _storage.write('agent_tts_interrupt', isTtsInterruptEnabled.value);
// 保存偏好设置
try {
await AgentService.isInterrupt(isTtsInterruptEnabled.value);

118
lib/modules/agent/views/agent_view.dart

@ -176,7 +176,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
),
color: isDarkMode ? Colors.grey[800] : Colors.white,
itemBuilder: (context) => [
// 播报打断开关选项
// 显示工具开关选项
PopupMenuItem(
child: Obx(() => Row(
children: [
@ -225,7 +225,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
SizedBox(width: 8.w),
Text(
//'ttsInterruptToggle'.tr,
"播报打断",
"language_interrupt".tr, // 语言打断
style: TextStyle(fontSize: 14.sp),
),
const Spacer(),
@ -1286,52 +1286,26 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
// 构建加载更多指示器
Widget _buildLoadMoreIndicator() {
return Obx(() {
if (controller.isLoadingMore.value) {
// 正在加载状态
return Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16.w,
height: 16.h,
child: CircularProgressIndicator(
strokeWidth: 2.w,
),
),
SizedBox(width: 8.w),
Text(
'loadMoreMessages'.tr,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
],
),
),
);
} else if (controller.isLoadingMoreError.value) {
// 加载失败状态
return Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: GestureDetector(
onTap: () => controller.loadMoreMessages(),
return Obx(
() {
if (controller.isLoadingMore.value) {
// 正在加载状态
return Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 16.sp,
color: Colors.grey,
SizedBox(
width: 16.w,
height: 16.h,
child: CircularProgressIndicator(
strokeWidth: 2.w,
),
),
SizedBox(width: 8.w),
Text(
'loadFailedClickRetry'.tr,
'loadMoreMessages'.tr,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
@ -1340,23 +1314,51 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
],
),
),
),
);
} else {
// 默认状态(有更多消息但未加载)
return Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Center(
child: Text(
'swipeUpForMore'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
);
} else if (controller.isLoadingMoreError.value) {
// 加载失败状态
return Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: GestureDetector(
onTap: () => controller.loadMoreMessages(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 16.sp,
color: Colors.grey,
),
SizedBox(width: 8.w),
Text(
'loadFailedClickRetry'.tr,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
],
),
),
),
),
);
}
});
);
} else {
// 默认状态(有更多消息但未加载)
return Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Center(
child: Text(
'swipeUpForMore'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
),
);
}
},
);
}
}

55
lib/modules/home/controllers/home_controller.dart

@ -1,5 +1,6 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:agent_service/agent_service.dart';
// 移除了dart:async导入,因为不再使用Timer
import '../../../data/services/novice_guide_service.dart';
import '../../../routes/app_routes.dart';
@ -32,6 +33,9 @@ class HomeController extends GetxController {
final _isFullyConnected = false.obs;
String? _lastConnectedDeviceName;
// 播报打断开关状态(与 AgentController 对齐)
final isTtsInterruptEnabled = false.obs; // 是否启用播报打断
// 移除了电池信息定时刷新相关变量
// OTA检查服务
@ -101,6 +105,19 @@ class HomeController extends GetxController {
_otaCheckService = Get.find<OtaCheckService>();
_loadLastConnectedDeviceName(); // 加载持久化的设备名称
_initWithCachedInfo();
_loadTtsInterrupt(); // 加载播报打断开关初始状态
// 监听存储中的“agent_tts_interrupe”键变化,实现与 Agent 页同步
// 中文注释:当 Agent 页或 Home 页任意一处切换时,另一处通过此监听自动刷新开关状态
_storage.listenKey('agent_tts_interrupt', (value) {
try {
final bool newValue = value is bool ? value : (value == 'true');
if (isTtsInterruptEnabled.value != newValue) {
isTtsInterruptEnabled.value = newValue;
}
} catch (e) {
// 中文注释:忽略异常,保持现有状态,避免因类型转换失败导致崩溃
}
});
// 移除了电池信息定时刷新的启动
}
@ -144,6 +161,44 @@ class HomeController extends GetxController {
});
}
/// 加载“播报打断”开关状态
/// 逻辑与 AgentController 保持一致:
/// 1)若本地不存在存储,则根据设备产品的 iscontinuouschat 取反作为默认值;
/// 2)若存在存储则直接读取;
/// 3)同步到原生服务 AgentService.isInterrupt。
Future<void> _loadTtsInterrupt() async {
try {
if (!_storage.hasData('agent_tts_interrupt')) {
final deviceName = getDeviceName();
final DBProduct? product = AppConfig.getproduct(deviceName);
if (deviceName.isNotEmpty && product != null) {
isTtsInterruptEnabled.value = !(product.iscontinuouschat ?? true);
} else {
isTtsInterruptEnabled.value = false;
}
} else {
isTtsInterruptEnabled.value =
_storage.read('agent_tts_interrupt') ?? false;
}
await AgentService.isInterrupt(isTtsInterruptEnabled.value);
} catch (e) {
Logger.e(_tag, '加载播报打断开关失败: $e');
isTtsInterruptEnabled.value = false;
}
}
/// 切换“播报打断”开关
/// 同步保存到本地,并通知原生服务。
Future<void> toggleTtsInterrupt() async {
try {
isTtsInterruptEnabled.toggle();
await _storage.write('agent_tts_interrupt', isTtsInterruptEnabled.value);
await AgentService.isInterrupt(isTtsInterruptEnabled.value);
} catch (e) {
Logger.e(_tag, '保存播报打断偏好失败: $e');
}
}
/// 从本地存储加载最后连接的设备名称
void _loadLastConnectedDeviceName() {
_lastConnectedDeviceName = _storage.read(_lastDeviceNameKey);

98
lib/modules/home/views/home_view.dart

@ -170,23 +170,89 @@ class HomeView extends GetView<HomeController> {
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(Icons.settings_outlined,
size: 16.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600]),
padding: EdgeInsets.zero,
onPressed: () {
Get.toNamed(Routes.settings);
},
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withOpacity(0.2),
// blurRadius: 4,
// offset: const Offset(0, 2),
// ),
// ],
),
child: Obx(() => PopupMenuButton(
padding: EdgeInsets.zero,
// 中文注释:缩小弹窗整体宽度,让展开窗口更紧凑
constraints: BoxConstraints(
minWidth: 120.w,
maxWidth: 140.w,
),
icon: Icon(
controller.isTtsInterruptEnabled.value
? Icons.voice_over_off
: Icons.voice_over_off_outlined,
size: 12.sp,
color: controller.isTtsInterruptEnabled.value
? Theme.of(Get.context!).colorScheme.primary // 使用主题主色
: (isDarkMode ? Colors.white70 : Colors.grey[600]),
),
elevation: 0, // 中文注释:去除弹窗阴影,实现扁平化
// 中文注释:将弹窗位置整体上移,使菜单与“Voitrans”更为平齐
offset: Offset(0, -10.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.w), // 中文注释:略微减小圆角
side: BorderSide.none, // 中文注释:移除边框线,边缘不明显
),
color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
itemBuilder: (context) => [
PopupMenuItem(
// 中文注释:降低菜单项高度并收紧默认内边距,减少上下留白
height: 15.h,
padding:
EdgeInsets.symmetric(horizontal: 2.w, vertical: 0.h),
child: Obx(() => Row(
mainAxisSize: MainAxisSize.max, // 中文注释:收紧行宽度
mainAxisAlignment:
MainAxisAlignment.spaceBetween, // 让 Switch 靠右
children: [
Icon(
controller.isTtsInterruptEnabled.value
? Icons.voice_over_off
: Icons.voice_over_off_outlined,
color: controller.isTtsInterruptEnabled.value
? Theme.of(context).colorScheme.primary
: (isDarkMode
? Colors.grey[400]
: Colors.grey),
size: 14.sp, // 中文注释:进一步缩小图标尺寸
),
SizedBox(width: 4.w),
Text(
// 翻译键代表的中文文本内容:语言打断
'language_interrupt'.tr, // 语言打断
style: TextStyle(
fontSize: 11.sp,
height: 1.0, // 中文注释:降低行高,减小垂直占用
),
),
//SizedBox(width: 0.w), // 中文注释:控制文字与开关间距
Transform.scale(
scale: 0.6, // 中文注释:缩放开关控件,减小视觉尺寸
child: Switch(
value: controller.isTtsInterruptEnabled.value,
onChanged: (value) =>
controller.toggleTtsInterrupt(),
activeColor:
Theme.of(context).colorScheme.primary,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
// 中文注释:删除不支持的 visualDensity 参数,保留缩放缩小尺寸
),
),
],
)),
onTap: () => controller.toggleTtsInterrupt(),
),
],
)),
),
],
);

Loading…
Cancel
Save