From a5f4bca2f3ca071af02d8f3367bfe11355c53b08 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Sun, 2 Mar 2025 12:08:37 +0000 Subject: [PATCH] summary --- android/app/build.gradle.kts | 2 +- .../com/example/deep_voice/MainActivity.kt | 376 +------------ .../example/deep_voice/VolcanoAsrHelper.kt | 526 ------------------ .../example/deep_voice/VolcanoTtsHelper.kt | 438 --------------- lib/core/routes/app_pages.dart | 8 +- lib/data/providers/agent_provider.dart | 101 +--- lib/data/services/azure_asr_service.dart | 11 +- .../services/volcano_tts_api_service.dart | 2 +- .../bindings/translation_binding.dart | 14 +- .../bindings/translation_detail_binding.dart | 9 + .../bindings/translation_history_binding.dart | 11 + .../controllers/translation_controller.dart | 231 +++++++- .../translation_detail_controller.dart | 125 +++++ .../translation_history_controller.dart | 129 +++++ .../views/translation_detail_view.dart | 491 ++++++++++++++++ .../views/translation_history_view.dart | 244 ++++++++ .../translation/views/translation_view.dart | 229 +++++++- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 22 files changed, 1495 insertions(+), 1466 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt delete mode 100644 android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt create mode 100644 lib/modules/translation/bindings/translation_detail_binding.dart create mode 100644 lib/modules/translation/bindings/translation_history_binding.dart create mode 100644 lib/modules/translation/controllers/translation_detail_controller.dart create mode 100644 lib/modules/translation/controllers/translation_history_controller.dart create mode 100644 lib/modules/translation/views/translation_detail_view.dart create mode 100644 lib/modules/translation/views/translation_history_view.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7bfee02b7..b7d2b48c7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -59,7 +59,7 @@ dependencies { // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8") // 添加火山语音识别SDK依赖 - implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") + // implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") } flutter { diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index eb93ea0f3..266597ea2 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -13,31 +13,19 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.EventChannel class MainActivity: AudioServiceActivity() { - private val SPEECH_RECOGNITION_CHANNEL = "com.example.deep_voice/volcano_asr" - private val SPEECH_RECOGNITION_EVENT_CHANNEL = "com.example.deep_voice/volcano_events" - private val VOLCANO_TTS_CHANNEL = "com.example.deep_voice/volcano_tts" - private val VOLCANO_TTS_EVENT_CHANNEL = "com.example.deep_voice/volcano_tts_events" + private val AZURE_ASR_CHANNEL = "com.example.deep_voice/azure_asr" private val AZURE_ASR_EVENT_CHANNEL = "com.example.deep_voice/azure_asr_events" private val TAG = "MainActivity" - private lateinit var speechHelper: VolcanoAsrHelper - private lateinit var volcanoTtsHelper: VolcanoTtsHelper private lateinit var azureAsrHelper: AzureAsrHelper - private var eventSink: EventChannel.EventSink? = null private var azureEventSink: EventChannel.EventSink? = null - private var ttsEventSink: EventChannel.EventSink? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Initialize speechHelper with context - speechHelper = VolcanoAsrHelper(applicationContext) - - // Initialize volcanoTtsHelper with context - volcanoTtsHelper = VolcanoTtsHelper(applicationContext) - + // 初始化 Azure ASR 助手 azureAsrHelper = AzureAsrHelper() } @@ -45,327 +33,6 @@ class MainActivity: AudioServiceActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - // 设置语音识别方法通道 - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SPEECH_RECOGNITION_CHANNEL).setMethodCallHandler { call, result -> - when (call.method) { - "initialize" -> { - val subscriptionKey = call.argument("subscriptionKey") - val serviceRegion = call.argument("serviceRegion") - val resourceId = call.argument("resourceId") - val cluster = call.argument("cluster") ?: "cn-beijing" - - if (subscriptionKey == null || serviceRegion == null) { - result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null) - return@setMethodCallHandler - } - - try { - Log.d(TAG, "初始化语音识别服务,APP_ID: ${subscriptionKey.take(3)}***,APP_KEY: ${serviceRegion.take(5)}***") - Log.d(TAG, "APP_ID长度: ${subscriptionKey.length}, APP_KEY长度: ${serviceRegion.length}") - Log.d(TAG, "使用大模型流式识别SDK配置") - Log.d(TAG, "资源ID: ${resourceId ?: subscriptionKey}") - Log.d(TAG, "集群区域: $cluster") - - val success = speechHelper.initialize(subscriptionKey, serviceRegion, cluster, resourceId) - Log.d(TAG, "语音识别服务初始化${if (success) "成功" else "失败"}") - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "初始化语音识别服务失败", e) - result.error("INITIALIZATION_ERROR", e.message, null) - } - } - "recognizeOnce" -> { - try { - speechHelper.recognizeOnce(object : VolcanoAsrHelper.RecognizeCallback { - override fun onResult(text: String) { - result.success(text) - } - - override fun onError(error: String) { - result.error("RECOGNITION_ERROR", error, null) - } - }) - } catch (e: Exception) { - result.error("RECOGNITION_ERROR", e.message, null) - } - } - "startContinuousRecognition" -> { - try { - if (speechHelper.isContinuousRecognitionActive()) { - result.error("ALREADY_ACTIVE", "连续识别已经在进行中", null) - return@setMethodCallHandler - } - - val success = speechHelper.startContinuousRecognition(object : VolcanoAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String) { - sendEvent(mapOf( - "eventType" to "finalResult", - "text" to text - )) - } - - override fun onRecognizing(recognizing: String) { - sendEvent(mapOf( - "eventType" to "intermediateResult", - "text" to recognizing - )) - } - - override fun onSessionStarted() { - sendEvent(mapOf( - "eventType" to "sessionStarted" - )) - } - - override fun onSessionStopped() { - sendEvent(mapOf( - "eventType" to "sessionStopped" - )) - } - - override fun onCanceled(reason: String, errorDetails: String) { - sendEvent(mapOf( - "eventType" to "canceled", - "reason" to reason, - "errorDetails" to errorDetails - )) - } - - override fun onError(error: String) { - sendEvent(mapOf( - "eventType" to "error", - "error" to error - )) - } - }) - result.success(success) - } catch (e: Exception) { - result.error("RECOGNITION_ERROR", e.message, null) - } - } - "stopContinuousRecognition" -> { - try { - if (!speechHelper.isContinuousRecognitionActive()) { - result.error("NOT_ACTIVE", "连续识别未在进行中", null) - return@setMethodCallHandler - } - - val success = speechHelper.stopContinuousRecognition(object : VolcanoAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String) {} - override fun onRecognizing(recognizing: String) {} - override fun onSessionStarted() {} - override fun onSessionStopped() { - sendEvent(mapOf( - "eventType" to "sessionStopped" - )) - } - override fun onCanceled(reason: String, errorDetails: String) {} - override fun onError(error: String) { - result.error("STOP_ERROR", error, null) - } - }) - result.success(success) - } catch (e: Exception) { - result.error("STOP_ERROR", e.message, null) - } - } - "dispose" -> { - try { - speechHelper.dispose() - result.success(true) - } catch (e: Exception) { - result.error("DISPOSE_ERROR", e.message, null) - } - } - else -> { - result.notImplemented() - } - } - } - - - // 设置火山语音合成方法通道 - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOLCANO_TTS_CHANNEL).setMethodCallHandler { call, result -> - when (call.method) { - "initialize" -> { - val appId = call.argument("appId") - val token = call.argument("token") - val cluster = call.argument("cluster") - - if (appId == null || token == null || cluster == null) { - result.error("INVALID_ARGUMENTS", "appId, token and cluster are required", null) - return@setMethodCallHandler - } - - try { - val success = volcanoTtsHelper.initialize(appId, token, cluster) - - // 设置播放状态回调 - volcanoTtsHelper.setPlaybackCallback(object : VolcanoTtsHelper.PlaybackCallback { - override fun onPlaybackStarted() { - sendTtsEvent(mapOf( - "eventType" to "playbackStarted" - )) - } - - override fun onPlaybackCompleted() { - sendTtsEvent(mapOf( - "eventType" to "playbackCompleted" - )) - } - - override fun onPlaybackError(error: String) { - sendTtsEvent(mapOf( - "eventType" to "playbackError", - "error" to error - )) - } - - // 实现合成开始回调 - override fun onSynthesisBegin() { - sendTtsEvent(mapOf( - "eventType" to "synthesisBegin" - )) - } - - // 实现合成结束回调 - override fun onSynthesisEnd() { - sendTtsEvent(mapOf( - "eventType" to "synthesisEnd" - )) - } - }) - - result.success(success) - } catch (e: Exception) { - result.error("INITIALIZATION_ERROR", e.message, null) - } - } - "synthesize" -> { - val text = call.argument("text") - val voiceType = call.argument("voiceType") - - if (text == null || voiceType == null) { - result.error("INVALID_ARGUMENTS", "text and voiceType are required", null) - return@setMethodCallHandler - } - - // Since synthesize method was removed, we'll use speakText instead - // and inform the caller that we no longer support direct audio data return - try { - val success = volcanoTtsHelper.speakText(text, voiceType) - if (success) { - result.success(ByteArray(0)) // Return empty byte array as placeholder - } else { - result.error("TTS_ERROR", "Failed to synthesize text", null) - } - } catch (e: Exception) { - result.error("TTS_ERROR", e.message, null) - } - } - "synthesizeSync" -> { - val text = call.argument("text") - val voiceType = call.argument("voiceType") - - if (text == null || voiceType == null) { - result.error("INVALID_ARGUMENTS", "text and voiceType are required", null) - return@setMethodCallHandler - } - - // Since synthesizeSync method was removed, we'll use speakText instead - // and inform the caller that we no longer support direct audio data return - try { - val success = volcanoTtsHelper.speakText(text, voiceType) - if (success) { - result.success(ByteArray(0)) // Return empty byte array as placeholder - } else { - result.error("TTS_ERROR", "Failed to synthesize text", null) - } - } catch (e: Exception) { - result.error("TTS_ERROR", e.message, null) - } - } - "speakText" -> { - val text = call.argument("text") - val voiceType = call.argument("voiceType") - - if (text == null || voiceType == null) { - result.error("INVALID_ARGUMENTS", "text and voiceType are required", null) - return@setMethodCallHandler - } - - try { - Log.d(TAG, "准备播放文本: $text") - - // 先停止当前播放 - try { - volcanoTtsHelper.stopSpeaking() - // 添加短暂延迟,确保引擎状态已重置 - Thread.sleep(100) - } catch (e: Exception) { - Log.e(TAG, "停止当前播放失败: ${e.message}") - // 继续尝试新的播放 - } - - val success = volcanoTtsHelper.speakText(text, voiceType) - Log.d(TAG, if (success) "成功开始播放" else "播放失败") - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "播放文本失败", e) - result.error("SPEAK_ERROR", e.message, null) - } - } - "stopSpeaking" -> { - try { - val success = volcanoTtsHelper.stopSpeaking() - result.success(success) - } catch (e: Exception) { - result.error("TTS_ERROR", e.message, null) - } - } - "setSpeed" -> { - val speed = call.argument("speed") - - if (speed == null) { - result.error("INVALID_ARGUMENTS", "speed is required", null) - return@setMethodCallHandler - } - - try { - val success = volcanoTtsHelper.setSpeed(speed.toFloat()) - result.success(success) - } catch (e: Exception) { - result.error("TTS_ERROR", e.message, null) - } - } - "setVolume" -> { - val volume = call.argument("volume") - - if (volume == null) { - result.error("INVALID_ARGUMENTS", "volume is required", null) - return@setMethodCallHandler - } - - try { - val success = volcanoTtsHelper.setVolume(volume.toFloat()) - result.success(success) - } catch (e: Exception) { - result.error("TTS_ERROR", e.message, null) - } - } - "dispose" -> { - try { - volcanoTtsHelper.dispose() - result.success(true) - } catch (e: Exception) { - result.error("DISPOSE_ERROR", e.message, null) - } - } - else -> { - result.notImplemented() - } - } - } // 设置 Azure ASR 方法通道 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result -> @@ -542,31 +209,7 @@ class MainActivity: AudioServiceActivity() { } } - // 设置语音识别事件通道 - EventChannel(flutterEngine.dartExecutor.binaryMessenger, SPEECH_RECOGNITION_EVENT_CHANNEL).setStreamHandler( - object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - eventSink = events - } - - override fun onCancel(arguments: Any?) { - eventSink = null - } - } - ) - - // 设置 TTS 事件通道 - EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOLCANO_TTS_EVENT_CHANNEL).setStreamHandler( - object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - ttsEventSink = events - } - - override fun onCancel(arguments: Any?) { - ttsEventSink = null - } - } - ) + // 设置 Azure ASR 事件通道 EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler( @@ -582,17 +225,6 @@ class MainActivity: AudioServiceActivity() { ) } - private fun sendEvent(event: Map) { - runOnUiThread { - eventSink?.success(event) - } - } - - private fun sendTtsEvent(event: Map) { - runOnUiThread { - ttsEventSink?.success(event) - } - } private fun sendAzureEvent(event: Map) { runOnUiThread { @@ -601,8 +233,6 @@ class MainActivity: AudioServiceActivity() { } override fun onDestroy() { - speechHelper.dispose() - volcanoTtsHelper.dispose() azureAsrHelper.dispose() super.onDestroy() } diff --git a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt deleted file mode 100644 index 639ed5c1a..000000000 --- a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt +++ /dev/null @@ -1,526 +0,0 @@ -package com.example.deep_voice - -import android.content.Context -import android.util.Log -import com.bytedance.speech.speechengine.SpeechEngine -import com.bytedance.speech.speechengine.SpeechEngineDefines -import com.bytedance.speech.speechengine.SpeechEngineGenerator -import org.json.JSONException -import org.json.JSONObject -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -/** - * 火山语音识别Helper类 - * - * 该类封装了火山语音SDK的语音识别功能,提供简单的接口供Flutter调用 - */ -class VolcanoAsrHelper(context: Context) : SpeechEngine.SpeechListener { - private val TAG = "VolcanoAsrHelper" - private var mSpeechEngine: SpeechEngine? = null - private var mSpeechEngineHandler: Long = -1 - private var isInitialized = false - private var applicationContext: Context? = context.applicationContext - - // 存储应用ID和密钥,以便在错误处理中使用 - private var appId: String = "" - private var appKey: String = "" - private var cluster: String = "" // 移除默认值 - - // 同步识别相关变量 - private var mRecognitionLatch: CountDownLatch? = null - private var mRecognitionResult: String? = null - private var mRecognitionError: String? = null - - // 当前回调 - private var mCurrentCallback: RecognizeCallback? = null - private var mCurrentContinuousCallback: ContinuousRecognizeCallback? = null - - // 是否正在进行连续识别 - private var mIsContinuousRecognitionActive = false - - /** - * 初始化语音识别引擎 - * @param appId 应用ID - * @param token 应用密钥/令牌 - * @param cluster 集群区域 - * @param resourceId 资源ID,用于大模型流式识别API鉴权 - * @return 初始化是否成功 - */ - fun initialize(appId: String, token: String, cluster: String, resourceId: String? = null): Boolean { - try { - // 设置基本参数 - this.appId = appId - this.appKey = token - this.cluster = cluster - - Log.d(TAG, "初始化参数: APP_ID=${appId}, APP_KEY长度=${token.length}, 集群区域=${cluster}") - Log.d(TAG, "资源ID: ${resourceId ?: appId}") - - // 确保应用上下文已设置 - if (applicationContext == null) { - Log.e(TAG, "应用上下文未设置,请先调用setContext方法") - return false - } - - // 准备环境 - SpeechEngineGenerator.PrepareEnvironment(applicationContext, null) - - // 获取语音引擎实例 - mSpeechEngine = SpeechEngineGenerator.getInstance() - mSpeechEngineHandler = mSpeechEngine?.createEngine() ?: -1 - - if (mSpeechEngineHandler == -1L) { - Log.e(TAG, "创建语音引擎失败") - return false - } - - // 设置上下文 - mSpeechEngine?.setContext(applicationContext) - - // 设置引擎类型为ASR - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, - SpeechEngineDefines.ASR_ENGINE - ) - - // 设置日志级别 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, - SpeechEngineDefines.LOG_LEVEL_DEBUG // 设置为DEBUG级别以获取更多日志信息 - ) - - // 设置用户ID (必需) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_UID_STRING, - "deep_voice_user" - ) - - // 设置设备ID (可选) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, - "deep_voice_device" - ) - - // 设置授权信息 - APP_ID - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, - appId - ) - - - // 使用大模型流式识别SDK (API v3) - Log.d(TAG, "使用大模型流式识别SDK配置 (API v3)") - - // 设置API路径为大模型流式识别API - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, - "/api/v3/sauc/bigmodel" - ) - - // 设置授权信息 - APP_TOKEN (不需要Bearer前缀) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, - token - ) - - // 设置资源ID,用于大模型流式识别API鉴权 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, - resourceId - ) - - // 设置协议类型为SEED - mSpeechEngine?.setOptionInt( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, - SpeechEngineDefines.PROTOCOL_TYPE_SEED - ) - - // 设置ASR请求参数 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING, - "{\"force_to_speech_time\":0, \"end_window_size\":800}" - ) - - - // 设置网络配置 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING, - "wss://openspeech.bytedance.com" - ) - - // 设置集群区域 - 这对于WebSocket连接非常重要 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING, - cluster - ) - - // 设置音频来源为内置录音机 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, - SpeechEngineDefines.RECORDER_TYPE_RECORDER - ) - - // 启用音量获取 - mSpeechEngine?.setOptionBoolean( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ENABLE_GET_VOLUME_BOOL, - true - ) - - // 设置最大录音时长为60秒 - mSpeechEngine?.setOptionInt( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT, - 60000 - ) - - // 控制识别效果 - mSpeechEngine?.setOptionBoolean( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_ENABLE_DDC_BOOL, - true - ) - mSpeechEngine?.setOptionBoolean( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_SHOW_NLU_PUNC_BOOL, - true - ) - - // 设置识别结果形式为全量返回(适合一句话识别) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING, - SpeechEngineDefines.ASR_RESULT_TYPE_SINGLE - ) - - // 记录所有配置参数,便于调试 - Log.d(TAG, "配置参数汇总:") - Log.d(TAG, "APP_ID: $appId") - Log.d(TAG, "APP_TOKEN: ${token.take(5)}***") - Log.d(TAG, "CLUSTER: $cluster") - Log.d(TAG, "RESOURCE_ID: ${resourceId ?: appId}") - Log.d(TAG, "API_PATH: ${if (resourceId != null) "/api/v3/sauc/bigmodel" else "/api/v2/asr"}") - - // 初始化引擎 - val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1 - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "初始化引擎失败: $ret") - return false - } - - // 设置监听器 - mSpeechEngine?.setListener(this) - - isInitialized = true - Log.d(TAG, "火山语音识别引擎初始化成功") - return true - } catch (e: Exception) { - Log.e(TAG, "火山语音识别引擎初始化失败: ${e.message}") - e.printStackTrace() - return false - } - } - - /** - * 设置应用上下文 - */ - fun setContext(context: Context) { - applicationContext = context.applicationContext - } - - /** - * 一次性识别 - * - * @param callback 回调接口,用于返回结果或错误 - */ - fun recognizeOnce(callback: RecognizeCallback) { - if (!isInitialized) { - callback.onError("语音识别引擎尚未初始化") - return - } - - try { - Log.d(TAG, "开始一次性识别") - - // 保存回调以便在onMessage中使用 - mCurrentCallback = callback - - // 设置识别结果形式为全量返回(适合一句话识别) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING, - SpeechEngineDefines.ASR_RESULT_TYPE_FULL - ) - - // 先停止引擎,避免SDK内部异步线程带来的问题 - mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") - - // 启动引擎 - val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - callback.onError("启动引擎失败: $ret") - } - } catch (e: Exception) { - Log.e(TAG, "语音识别异常: ${e.message}") - callback.onError("语音识别异常: ${e.message}") - } - } - - /** - * 开始连续识别 - * - * @param callback 回调接口,用于返回结果或错误 - */ - fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - if (!isInitialized) { - callback.onError("语音识别引擎尚未初始化") - return false - } - - if (mIsContinuousRecognitionActive) { - callback.onError("连续识别已经在进行中") - return false - } - - try { - Log.d(TAG, "开始连续识别") - - // 保存回调以便在onMessage中使用 - mCurrentContinuousCallback = callback - - // 设置识别结果形式为增量返回(适合连续识别) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING, - SpeechEngineDefines.ASR_RESULT_TYPE_SINGLE - ) - - // 先停止引擎,避免SDK内部异步线程带来的问题 - mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") - - // 启动引擎 - val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - callback.onError("启动引擎失败: $ret") - return false - } - - mIsContinuousRecognitionActive = true - return true - } catch (e: Exception) { - Log.e(TAG, "开始连续识别异常: ${e.message}") - callback.onError("开始连续识别异常: ${e.message}") - return false - } - } - - /** - * 停止连续识别 - * - * @param callback 回调接口,用于返回结果或错误 - */ - fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - if (!isInitialized) { - callback.onError("语音识别引擎尚未初始化") - return false - } - - if (!mIsContinuousRecognitionActive) { - callback.onError("连续识别未在进行中") - return false - } - - try { - Log.d(TAG, "停止连续识别") - - // 音频输入完成 - mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "") - - // 停止引擎 - val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - callback.onError("停止引擎失败: $ret") - return false - } - - mIsContinuousRecognitionActive = false - return true - } catch (e: Exception) { - Log.e(TAG, "停止连续识别异常: ${e.message}") - callback.onError("停止连续识别异常: ${e.message}") - return false - } - } - - /** - * 检查连续识别是否活跃 - */ - fun isContinuousRecognitionActive(): Boolean { - return mIsContinuousRecognitionActive - } - - /** - * 释放资源 - */ - fun dispose() { - try { - if (mSpeechEngineHandler != -1L) { - mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - mSpeechEngine?.destroyEngine(mSpeechEngineHandler) - mSpeechEngineHandler = -1 - } - mSpeechEngine = null - isInitialized = false - mIsContinuousRecognitionActive = false - Log.d(TAG, "火山语音识别引擎已释放") - } catch (e: Exception) { - Log.e(TAG, "释放火山语音识别引擎失败: ${e.message}") - } - } - - // 实现SpeechListener接口 - override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) { - val stdData = String(data) - - when (type) { - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { - Log.d(TAG, "引擎启动成功: $stdData") - mCurrentContinuousCallback?.onSessionStarted() - } - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { - Log.d(TAG, "引擎已停止: $stdData") - mIsContinuousRecognitionActive = false - mCurrentContinuousCallback?.onSessionStopped() - } - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> { - Log.e(TAG, "引擎错误: $stdData") - try { - val errorJson = JSONObject(stdData) - val errorCode = errorJson.optInt("err_code", -1) - val errorMsg = errorJson.optString("err_msg", "未知错误") - - // 检查是否是资源授权错误 - if (errorCode == 1001 && (errorMsg.contains("requested resource not granted") || - errorMsg.contains("requested grant not found"))) { - Log.e(TAG, "资源授权错误: 应用可能未开通语音识别服务或密钥无效") - - // 记录更多调试信息,但隐藏敏感信息 - Log.d(TAG, "APP_ID长度: ${appId.length}") - Log.d(TAG, "APP_KEY长度: ${appKey.length}") - - val errorMessage = "资源授权错误: 请确保应用已开通语音识别服务并且密钥有效 (错误码: $errorCode)" - - if (mIsContinuousRecognitionActive) { - mCurrentContinuousCallback?.onError(errorMessage) - } else { - mCurrentCallback?.onError(errorMessage) - } - } else { - // 其他错误 - if (mIsContinuousRecognitionActive) { - mCurrentContinuousCallback?.onError("引擎错误: $errorMsg (错误码: $errorCode)") - } else { - mCurrentCallback?.onError("引擎错误: $errorMsg (错误码: $errorCode)") - } - } - - mRecognitionError = "引擎错误: $stdData" - mRecognitionLatch?.countDown() - } catch (e: JSONException) { - Log.e(TAG, "解析错误信息失败: ${e.message}") - if (mIsContinuousRecognitionActive) { - mCurrentContinuousCallback?.onError("引擎错误: $stdData") - } else { - mCurrentCallback?.onError("引擎错误: $stdData") - } - } - } - SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> { - Log.d(TAG, "中间识别结果: $stdData") - processRecognitionResult(stdData, false) - } - SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> { - Log.d(TAG, "最终识别结果: $stdData") - processRecognitionResult(stdData, true) - } - SpeechEngineDefines.MESSAGE_TYPE_VOLUME_LEVEL -> { - // 音量级别,可用于显示波形 - // Log.d(TAG, "音量级别: $stdData") - } - } - } - - /** - * 处理识别结果 - */ - private fun processRecognitionResult(resultData: String, isFinal: Boolean) { - try { - val resultJson = JSONObject(resultData) - if (!resultJson.has("result")) { - return - } - - val resultArray = resultJson.getJSONArray("result") - if (resultArray.length() == 0) { - return - } - - val resultObj = resultArray.getJSONObject(0) - val text = resultObj.optString("text", "") - - if (text.isEmpty()) { - return - } - - if (mIsContinuousRecognitionActive) { - if (isFinal) { - mCurrentContinuousCallback?.onResult(text) - } else { - mCurrentContinuousCallback?.onRecognizing(text) - } - } else { - if (isFinal) { - mCurrentCallback?.onResult(text) - mRecognitionResult = text - mRecognitionLatch?.countDown() - } - } - } catch (e: JSONException) { - Log.e(TAG, "解析识别结果失败: ${e.message}") - } - } - - /** - * 一次性识别回调接口 - */ - interface RecognizeCallback { - fun onResult(text: String) - fun onError(error: String) - } - - /** - * 连续识别回调接口 - */ - interface ContinuousRecognizeCallback { - fun onResult(text: String) - fun onRecognizing(recognizing: String) - fun onSessionStarted() - fun onSessionStopped() - fun onCanceled(reason: String, errorDetails: String) - fun onError(error: String) - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt deleted file mode 100644 index 15c78d494..000000000 --- a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt +++ /dev/null @@ -1,438 +0,0 @@ -package com.example.deep_voice - -import android.content.Context -import android.util.Log -import com.bytedance.speech.speechengine.SpeechEngine -import com.bytedance.speech.speechengine.SpeechEngineDefines -import com.bytedance.speech.speechengine.SpeechEngineGenerator - -/** - * 火山语音合成Helper类 - * - * 该类封装了火山语音SDK的TTS功能,提供简单的接口供Flutter调用 - * 实现基于官方文档的标准接入流程 - */ -class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener { - private val TAG = "VolcanoTtsHelper" - private var mSpeechEngine: SpeechEngine? = null - private var mSpeechEngineHandler: Long = -1 - private var isInitialized = false - private var applicationContext: Context? = context.applicationContext - - // 播放状态回调 - private var mPlaybackCallback: PlaybackCallback? = null - - /** - * 初始化TTS引擎 - * - * @param appId 火山语音服务AppID - * @param token 火山语音服务Token - * @param cluster 火山语音服务集群 - */ - fun initialize(appId: String, token: String, cluster: String): Boolean { - try { - // 确保应用上下文已设置 - if (applicationContext == null) { - Log.e(TAG, "应用上下文未设置") - return false - } - - Log.d(TAG, "初始化火山语音TTS引擎,APP_ID: ${appId.take(3)}***,Token长度: ${token.length},集群区域: $cluster") - - // 1. 准备环境 - SpeechEngineGenerator.PrepareEnvironment(applicationContext, null) - - // 2. 创建引擎实例 - mSpeechEngine = SpeechEngineGenerator.getInstance() - mSpeechEngineHandler = mSpeechEngine?.createEngine() ?: -1 - - if (mSpeechEngineHandler == -1L) { - Log.e(TAG, "创建语音引擎失败") - return false - } - - // 设置上下文 - mSpeechEngine?.setContext(applicationContext) - - // 3. 参数配置 - // 3.1 引擎类型 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, - SpeechEngineDefines.TTS_ENGINE - ) - - // 3.2 日志级别 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, - SpeechEngineDefines.LOG_LEVEL_WARN - ) - - // 3.3 线上问题定位 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_UID_STRING, - "deep_voice_user" - ) - - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, - "deep_voice_device" - ) - - // 3.4 授权 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, - appId - ) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, - "Bearer;$token" - ) - - // 3.5 合成场景 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, - SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL - ) - - // 3.6 合成策略 - mSpeechEngine?.setOptionInt( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_WORK_MODE_INT, - SpeechEngineDefines.TTS_WORK_MODE_ONLINE - ) - - // 3.7 在线请求资源配置 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_ADDRESS_STRING, - "wss://openspeech.bytedance.com" - ) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_URI_STRING, - "/api/v1/tts/ws_binary" - ) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_CLUSTER_STRING, - cluster - ) - - // 3.8 启用内建播放器 - mSpeechEngine?.setOptionBoolean( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL, - true - ) - - // 3.9 配置内建播放器的音源 - mSpeechEngine?.setOptionInt( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_AUDIO_STREAM_TYPE_INT, - SpeechEngineDefines.AUDIO_STREAM_TYPE_MEDIA - ) - - // 3.10 返回音频数据 - mSpeechEngine?.setOptionInt( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, - 2 - ) - - // 4. 初始化引擎实例 - val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1 - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "初始化引擎失败: $ret") - return false - } - - // 设置回调监听器 - mSpeechEngine?.setListener(this) - - isInitialized = true - Log.d(TAG, "火山语音TTS引擎初始化成功") - return true - } catch (e: Exception) { - Log.e(TAG, "火山语音TTS引擎初始化失败: ${e.message}") - e.printStackTrace() - return false - } - } - - /** - * 直接合成并播放文本 - * - * @param text 要合成的文本 - * @param voiceType 语音类型 - * @return 是否成功开始播放 - */ - fun speakText(text: String, voiceType: String): Boolean { - if (!isInitialized) { - Log.e(TAG, "TTS引擎尚未初始化") - return false - } - - try { - Log.d(TAG, "开始直接播放文本: $text,使用语音类型: $voiceType") - - // 先停止当前播放 - try { - Log.d(TAG, "停止当前播放") - stopSpeaking() - - // 添加短暂延迟,确保引擎状态已重置 - Thread.sleep(100) - } catch (e: Exception) { - Log.e(TAG, "停止当前播放失败: ${e.message}") - // 继续尝试新的播放 - } - - // 设置发音人 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING, - "other" - ) - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING, - voiceType - ) - - // 设置文本 - mSpeechEngine?.setOptionString( - mSpeechEngineHandler, - SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, - text - ) - - // 启动引擎,开始合成并播放 - val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "启动引擎失败: $ret") - return false - } - - Log.d(TAG, "成功开始播放") - return true - } catch (e: Exception) { - Log.e(TAG, "直接播放文本异常: ${e.message}") - return false - } - } - - /** - * 停止播放 - * - * @return 是否成功停止播放 - */ - fun stopSpeaking(): Boolean { - if (!isInitialized) { - Log.e(TAG, "TTS引擎尚未初始化") - return false - } - - try { - Log.d(TAG, "停止播放") - - // 停止引擎 - val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "停止引擎失败: $ret") - - // 尝试再次停止引擎 - try { - Log.d(TAG, "尝试再次停止引擎") - mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - Log.d(TAG, "引擎已停止") - } catch (e: Exception) { - Log.e(TAG, "再次停止引擎失败: ${e.message}") - } - - return false - } - - // 确保引擎完全停止 - try { - Log.d(TAG, "确保引擎完全停止") - val stopAgainRet = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - if (stopAgainRet != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "再次停止引擎失败: $stopAgainRet") - } else { - Log.d(TAG, "引擎已完全停止") - } - } catch (e: Exception) { - Log.e(TAG, "再次停止引擎异常: ${e.message}") - } - - return true - } catch (e: Exception) { - Log.e(TAG, "停止播放异常: ${e.message}") - e.printStackTrace() - - // 尝试再次停止引擎 - try { - Log.d(TAG, "尝试再次停止引擎") - mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - Log.d(TAG, "引擎已停止") - } catch (e: Exception) { - Log.e(TAG, "再次停止引擎失败: ${e.message}") - } - - return false - } - } - - /** - * 设置播放速度 - * - * @param speed 播放速度,范围0.5-2.0 - * @return 是否设置成功 - */ - fun setSpeed(speed: Float): Boolean { - if (!isInitialized) { - Log.e(TAG, "TTS引擎尚未初始化") - return false - } - - try { - Log.d(TAG, "设置播放速度: $speed") - - // 设置播放速度 (使用新版本的API) - mSpeechEngine?.setOptionDouble( - SpeechEngineDefines.PARAMS_KEY_TTS_SPEED_RATIO_DOUBLE, - speed.toDouble() - ) - - return true - } catch (e: Exception) { - Log.e(TAG, "设置播放速度异常: ${e.message}") - return false - } - } - - /** - * 设置播放音量 - * - * @param volume 播放音量,范围0.0-1.0 - * @return 是否设置成功 - */ - fun setVolume(volume: Float): Boolean { - if (!isInitialized) { - Log.e(TAG, "TTS引擎尚未初始化") - return false - } - - try { - Log.d(TAG, "设置播放音量: $volume") - - // 设置播放音量 (使用新版本的API) - mSpeechEngine?.setOptionDouble( - SpeechEngineDefines.PARAMS_KEY_TTS_VOLUME_RATIO_DOUBLE, - volume.toDouble() - ) - - return true - } catch (e: Exception) { - Log.e(TAG, "设置播放音量异常: ${e.message}") - return false - } - } - - /** - * 设置播放状态回调 - * - * @param callback 播放状态回调 - */ - fun setPlaybackCallback(callback: PlaybackCallback) { - mPlaybackCallback = callback - } - - /** - * 释放资源 - */ - fun dispose() { - try { - if (mSpeechEngineHandler != -1L) { - mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - mSpeechEngine?.destroyEngine(mSpeechEngineHandler) - mSpeechEngineHandler = -1 - } - mSpeechEngine = null - isInitialized = false - Log.d(TAG, "火山语音合成引擎已释放") - } catch (e: Exception) { - Log.e(TAG, "释放火山语音合成引擎失败: ${e.message}") - } - } - - // 实现SpeechListener接口 - override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) { - val stdData = String(data) - - when (type) { - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { - Log.d(TAG, "引擎启动成功: $stdData") - } - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { - Log.d(TAG, "引擎已停止: $stdData") - } - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> { - Log.e(TAG, "引擎错误: $stdData") - // 通知播放错误 - mPlaybackCallback?.onPlaybackError("引擎错误: $stdData") - } - SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_BEGIN -> { - Log.d(TAG, "合成开始: $stdData") - // 通知合成开始 - mPlaybackCallback?.onSynthesisBegin() - } - SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_END -> { - Log.d(TAG, "合成结束: $stdData") - // 通知合成结束 - mPlaybackCallback?.onSynthesisEnd() - } - SpeechEngineDefines.MESSAGE_TYPE_TTS_START_PLAYING -> { - Log.d(TAG, "开始播放: $stdData") - // 通知播放开始 - mPlaybackCallback?.onPlaybackStarted() - } - SpeechEngineDefines.MESSAGE_TYPE_TTS_FINISH_PLAYING -> { - Log.d(TAG, "播放结束: $stdData") - // 通知播放结束 - mPlaybackCallback?.onPlaybackCompleted() - } - SpeechEngineDefines.MESSAGE_TYPE_TTS_AUDIO_DATA -> { - Log.d(TAG, "收到音频数据") - } - SpeechEngineDefines.MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS -> { - Log.d(TAG, "播放进度: $stdData") - } - SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_MODE_TOGGLE -> { - Log.d(TAG, "合成模式切换: $stdData") - } - } - } - - /** - * 播放状态回调接口 - */ - interface PlaybackCallback { - fun onPlaybackStarted() - fun onPlaybackCompleted() - fun onPlaybackError(error: String) - - // 新增合成开始回调 - fun onSynthesisBegin() - - // 新增合成结束回调 - fun onSynthesisEnd() - } -} \ No newline at end of file diff --git a/lib/core/routes/app_pages.dart b/lib/core/routes/app_pages.dart index 465c2594e..14d6fbe98 100644 --- a/lib/core/routes/app_pages.dart +++ b/lib/core/routes/app_pages.dart @@ -12,6 +12,8 @@ import '../../modules/test/views/asr_test_view.dart'; import '../../modules/test/bindings/test_binding.dart'; import '../../modules/translation/views/translation_view.dart'; import '../../modules/translation/bindings/translation_binding.dart'; +import '../../modules/translation/views/translation_history_view.dart'; +import '../../modules/translation/bindings/translation_history_binding.dart'; // import '../../modules/speech_demo/speech_demo_page.dart'; // This file doesn't exist import './app_routes.dart'; @@ -57,6 +59,10 @@ abstract class AppPages { page: () => const TranslationView(), binding: TranslationBinding(), ), - + GetPage( + name: Routes.translationHistory, + page: () => const TranslationHistoryView(), + binding: TranslationHistoryBinding(), + ), ]; } \ No newline at end of file diff --git a/lib/data/providers/agent_provider.dart b/lib/data/providers/agent_provider.dart index 1afd8d5d5..bbc875224 100644 --- a/lib/data/providers/agent_provider.dart +++ b/lib/data/providers/agent_provider.dart @@ -61,7 +61,18 @@ class AgentProvider { 请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活。 '''), - +Agent( + id: 'simultaneous_interpreter', + name: '同声传译', + avatarUrl: 'assets/images/agents/interpreter.png', + description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。', + tag: '翻译', + isOnline: true, + voice: 'zh_female_daimengchuanmei_moon_bigtts', + welcomeMessage: + "大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!", + systemPrompt: ""), + Agent( id: 'cyber_girlfriend', name: '赛博女友 芯愿', @@ -199,94 +210,8 @@ class AgentProvider { 请始终以专业、关怀的态度为用户提供健康指导,帮助他们迈向更健康的生活。 '''), - Agent( - id: 'simultaneous_interpreter', - name: 'Lucy (同声翻译官)', - avatarUrl: 'assets/images/agents/interpreter.png', - description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。', - tag: '翻译', - isOnline: true, - voice: 'zh_female_daimengchuanmei_moon_bigtts', - welcomeMessage: - "大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!", - systemPrompt: ''' -任务描述 - -你是一名专业的 同声翻译(Simultaneous Interpreter),你的任务是 即时将源语言文本翻译为目标语言,确保翻译 流畅、自然、精准,适应语境,并符合目标语言的表达习惯。你的翻译必须 迅速 但 不逐字直译,而是以 符合目标语言逻辑的方式 表达原意。 - -输入格式 - -你将接收一个 JSON 格式 的输入,格式如下: - -{ - "source_language": "语言代码", - "target_language": "语言代码", - "text": "待翻译文本" -} - -字段说明: - • "source_language":源语言,使用 ISO 639-1 代码(如 "en" 表示英语,"zh" 表示中文)。 - • "target_language":目标语言,使用 ISO 639-1 代码。 - • "text":待翻译的文本内容。 - -示例输入(英语到中文): - -{ - "source_language": "en", - "target_language": "zh", - "text": "Good evening, ladies and gentlemen. Welcome to the annual conference." -} - -输出格式 - -你的翻译必须严格按照 JSON 结构 输出,格式如下: - -{ - "source_language": "语言代码", - "target_language": "语言代码", - "original_text": "原始文本", - "translated_text": "翻译后的文本", - "confidence_score": 0.0 -} - -字段说明: - • "source_language":回传源语言代码。 - • "target_language":回传目标语言代码。 - • "original_text":回传原始文本内容,以便校对。 - • "translated_text":翻译后的文本,必须符合目标语言的语法和表达习惯。 - • "confidence_score":选填,范围 0.0 ~ 1.0,表示翻译置信度(1.0 表示极高置信度,0.0 表示最低置信度)。 - -示例输出(英语到中文): - -{ - "source_language": "en", - "target_language": "zh", - "original_text": "Good evening, ladies and gentlemen. Welcome to the annual conference.", - "translated_text": "各位女士们,先生们,晚上好!欢迎来到年度大会。", - "confidence_score": 0.98 -} - -翻译要求 - 1. 流畅自然:不能逐字直译,而是要符合目标语言的表达方式。 - 2. 保留语境:准确传达原意,避免误解或遗漏。 - 3. 即时响应:短句应尽快翻译,长句可适当拆分,提高可读性。 - 4. 格式严格:输出必须严格符合 JSON 结构,字段不可缺失。 - -可选高级模式 - -如果需要启用 自适应翻译模式,请在输入 JSON 中添加: - -{ - "mode": "adaptive" -} - - • "mode": "adaptive":允许模型 根据语境调整语气(如正式、非正式风格)。 - -你的任务 + -请按照上述要求进行同声翻译,并以 JSON 结构返回结果。 -''', - ), Agent( id: 'speaking_coach', name: 'Sarah (英语口语教练)', diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/azure_asr_service.dart index 896fa2a33..06f6813b8 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/azure_asr_service.dart @@ -211,7 +211,16 @@ class AzureAsrService extends GetxService { if (_isContinuousRecognitionActive) { Logger.warning('连续识别已经在进行中'); - return false; + // 先停止当前正在进行的识别任务 + Logger.info('尝试停止当前正在进行的识别任务'); + try { + await stopContinuousRecognition(); + // 添加短暂延迟,确保资源被正确释放 + await Future.delayed(Duration(milliseconds: 300)); + } catch (e) { + Logger.error('停止当前识别任务失败', e); + // 继续尝试启动新的识别任务 + } } try { diff --git a/lib/data/services/volcano_tts_api_service.dart b/lib/data/services/volcano_tts_api_service.dart index c2d39595c..5ed89f4b9 100644 --- a/lib/data/services/volcano_tts_api_service.dart +++ b/lib/data/services/volcano_tts_api_service.dart @@ -103,7 +103,7 @@ class VolcanoTtsApiService extends GetxController { bool get isPlaying => _isPlaying.value; // 默认发音人 - 使用预定义的值 - final String defaultSpeaker = 'zh_female_meilinvyou_moon_bigtts'; + final String defaultSpeaker = 'zh_female_yuanqinvyou_moon_bigtts'; // 音频缓冲区 final List _audioBuffer = []; diff --git a/lib/modules/translation/bindings/translation_binding.dart b/lib/modules/translation/bindings/translation_binding.dart index 9483707a5..55f4fbe4d 100644 --- a/lib/modules/translation/bindings/translation_binding.dart +++ b/lib/modules/translation/bindings/translation_binding.dart @@ -7,19 +7,7 @@ import '../../../data/services/volcano_tts_service.dart'; class TranslationBinding implements Bindings { @override void dependencies() { - // Ensure services are registered - if (!Get.isRegistered()) { - Get.put(AzureAsrService()); - } - - if (!Get.isRegistered()) { - Get.put(VolcanoTranslationService()); - } - - if (!Get.isRegistered()) { - Get.put(VolcanoTtsService()); - } - + // Register the controller Get.put(TranslationController()); } diff --git a/lib/modules/translation/bindings/translation_detail_binding.dart b/lib/modules/translation/bindings/translation_detail_binding.dart new file mode 100644 index 000000000..5e779995d --- /dev/null +++ b/lib/modules/translation/bindings/translation_detail_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/translation_detail_controller.dart'; + +class TranslationDetailBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => TranslationDetailController()); + } +} \ No newline at end of file diff --git a/lib/modules/translation/bindings/translation_history_binding.dart b/lib/modules/translation/bindings/translation_history_binding.dart new file mode 100644 index 000000000..ce08077c0 --- /dev/null +++ b/lib/modules/translation/bindings/translation_history_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import '../controllers/translation_history_controller.dart'; + +class TranslationHistoryBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => TranslationHistoryController(), + ); + } +} \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 16ec57a58..5371164bf 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:async'; +import 'dart:convert'; +import 'package:get_storage/get_storage.dart'; import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/volcano_tts_api_service.dart'; @@ -11,6 +13,11 @@ class TranslationController extends GetxController { final AzureAsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = Get.find(); final VolcanoTtsApiService _ttsService = Get.find(); + final GetStorage _storage = GetStorage(); + + // Storage key + static const String _historyKey = 'translation_history'; + static const int _maxHistoryItems = 100; // 最多保存100条历史记录 // State variables final isRecognizing = false.obs; @@ -22,6 +29,39 @@ class TranslationController extends GetxController { final targetLanguage = '英语 (English)'.obs; final translationHistory = [].obs; + // 支持的语言列表 - 改为Map结构,键为显示名称,值为语言代码 + final Map supportedLanguages = { + '中文 (Chinese)': 'zh', + '英语 (English)': 'en', + '日语 (Japanese)': 'ja', + '韩语 (Korean)': 'ko', + '法语 (French)': 'fr', + '德语 (German)': 'de', + '西班牙语 (Spanish)': 'es', + '意大利语 (Italian)': 'it', + '俄语 (Russian)': 'ru', + '泰语 (Thai)': 'th', + '葡萄牙语 (Portuguese)': 'pt', + '阿拉伯语 (Arabic)': 'ar', + '越南语 (Vietnamese)': 'vi', + '印尼语 (Indonesian)': 'id', + '印地语 (Hindi)': 'hi', + '土耳其语 (Turkish)': 'tr', + '波兰语 (Polish)': 'pl', + '荷兰语 (Dutch)': 'nl', + '瑞典语 (Swedish)': 'sv', + '捷克语 (Czech)': 'cs', + '希腊语 (Greek)': 'el', + '罗马尼亚语 (Romanian)': 'ro', + '匈牙利语 (Hungarian)': 'hu', + '乌克兰语 (Ukrainian)': 'uk', + '保加利亚语 (Bulgarian)': 'bg', + '丹麦语 (Danish)': 'da', + '芬兰语 (Finnish)': 'fi', + '挪威语 (Norwegian)': 'no', + '马来语 (Malay)': 'ms', + }; + // Current translation item TranslationItem? currentItem; @@ -34,13 +74,27 @@ class TranslationController extends GetxController { // Timer for debouncing translation requests Timer? _translationDebounceTimer; + // 当前会话ID + String? currentSessionId; + @override void onInit() { super.onInit(); + // 加载历史记录 + loadTranslationHistory(); + // Initialize the translation service _initTranslationService(); + // 添加后处理,确保在历史记录加载后滚动到底部 + WidgetsBinding.instance.addPostFrameCallback((_) { + // 延迟执行滚动操作,确保视图已完全渲染 + Future.delayed(const Duration(milliseconds: 300), () { + _scrollToBottom(); + }); + }); + // Start voice recognition automatically when the page is loaded startRecognition(); } @@ -77,6 +131,9 @@ class TranslationController extends GetxController { if (isRecognizing.value) return; try { + // 生成新的会话ID + currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); + // Initialize a new translation item currentItem = TranslationItem( sourceText: '', @@ -84,6 +141,8 @@ class TranslationController extends GetxController { sourceLanguage: sourceLanguage.value, targetLanguage: targetLanguage.value, timestamp: DateTime.now(), + sessionId: currentSessionId!, + isFirstInSession: true, // 标记为会话中的第一条 ); // Start the voice recognition service @@ -147,6 +206,9 @@ class TranslationController extends GetxController { // 添加当前项到历史(如果有内容) finalizeCurrentItem(); + // 重置当前会话ID + currentSessionId = null; + // 重置当前显示的文本 recognizedText.value = ''; translatedText.value = ''; @@ -165,12 +227,20 @@ class TranslationController extends GetxController { // 如果当前没有活跃的翻译项,创建一个新的 if (currentItem == null) { + // 如果没有当前会话ID,创建一个新的 + if (currentSessionId == null) { + currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); + } + currentItem = TranslationItem( sourceText: text, translatedText: '', sourceLanguage: sourceLanguage.value, targetLanguage: targetLanguage.value, timestamp: DateTime.now(), + sessionId: currentSessionId!, + isFirstInSession: translationHistory.isEmpty || + translationHistory.last.sessionId != currentSessionId, ); } else { // 更新当前翻译项 @@ -196,12 +266,20 @@ class TranslationController extends GetxController { // 更新当前翻译项(如果不存在则创建一个) if (currentItem == null) { + // 如果没有当前会话ID,创建一个新的 + if (currentSessionId == null) { + currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); + } + currentItem = TranslationItem( sourceText: text, translatedText: '', sourceLanguage: sourceLanguage.value, targetLanguage: targetLanguage.value, timestamp: DateTime.now(), + sessionId: currentSessionId!, + isFirstInSession: translationHistory.isEmpty || + translationHistory.last.sessionId != currentSessionId, ); } else { currentItem!.sourceText = text; @@ -228,6 +306,7 @@ class TranslationController extends GetxController { currentItem!.translatedText.isNotEmpty && !translationHistory.contains(currentItem)) { translationHistory.add(currentItem!); + saveTranslationHistory(); currentItem = null; } } @@ -247,10 +326,30 @@ class TranslationController extends GetxController { final TranslationItem? itemToUpdate = currentItem; try { + // 获取源语言和目标语言的代码 + final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; + + // 尝试检测输入文本的语言(这里简化处理,实际项目可能需要更复杂的语言检测) + bool shouldSwapLanguages = false; + + // 简单的语言检测逻辑 - 检查是否包含中文字符 + final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text); + final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text); + + // 如果源语言是中文但文本不包含中文字符,且包含拉丁字符,可能需要交换语言 + if (sourceCode == 'zh' && !containsChinese && containsLatin) { + shouldSwapLanguages = true; + } + // 如果源语言不是中文但文本包含中文字符,可能需要交换语言 + else if (sourceCode != 'zh' && containsChinese) { + shouldSwapLanguages = true; + } + + // 调用翻译服务 final result = await _translationService.translateText( text: text, - sourceLanguage: sourceLanguage.value, - targetLanguage: targetLanguage.value, + sourceLanguage: shouldSwapLanguages ? targetLanguage.value : sourceLanguage.value, + targetLanguage: shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value, ); if (result != null) { @@ -263,7 +362,28 @@ class TranslationController extends GetxController { // 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史 if (isFinal && itemToUpdate == currentItem) { - translationHistory.add(itemToUpdate); + // 检查是否已经存在相同会话ID的项 + bool isFirstInSession = true; + for (var item in translationHistory) { + if (item.sessionId == itemToUpdate.sessionId) { + isFirstInSession = false; + break; + } + } + + // 创建一个新的TranslationItem,包含正确的isFirstInSession值 + final updatedItem = TranslationItem( + sourceText: itemToUpdate.sourceText, + translatedText: itemToUpdate.translatedText, + sourceLanguage: itemToUpdate.sourceLanguage, + targetLanguage: itemToUpdate.targetLanguage, + timestamp: itemToUpdate.timestamp, + sessionId: itemToUpdate.sessionId, + isFirstInSession: isFirstInSession, + ); + + translationHistory.add(updatedItem); + saveTranslationHistory(); currentItem = null; // 重要:将当前项设为null,表示已完成 // 重置当前显示的文本,避免与历史项重复 @@ -311,15 +431,90 @@ class TranslationController extends GetxController { // Scroll to the bottom of the list void _scrollToBottom() { if (scrollController.hasClients) { - Future.delayed(const Duration(milliseconds: 100), () { + try { scrollController.animateTo( scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); + } catch (e) { + // 如果滚动失败,尝试延迟后再次滚动 + Future.delayed(const Duration(milliseconds: 200), () { + if (scrollController.hasClients) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + }); + } + } else { + // 如果scrollController还没有客户端,延迟后再尝试 + Future.delayed(const Duration(milliseconds: 200), _scrollToBottom); + } + } + + // 切换源语言和目标语言 + void swapLanguages() { + final temp = sourceLanguage.value; + sourceLanguage.value = targetLanguage.value; + targetLanguage.value = temp; + + // 如果正在识别,需要停止并重新开始 + if (isRecognizing.value) { + stopRecognition(); + startRecognition(); + } + } + + // 设置源语言 + void setSourceLanguage(String language) { + if (sourceLanguage.value != language) { + sourceLanguage.value = language; + + // 如果正在识别,需要停止并重新开始 + if (isRecognizing.value) { + stopRecognition(); + startRecognition(); + } + } + } + + // 设置目标语言 + void setTargetLanguage(String language) { + if (targetLanguage.value != language) { + targetLanguage.value = language; + } + } + + // Load translation history from storage + void loadTranslationHistory() { + final historyJson = _storage.read(_historyKey); + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + // 确保在历史记录加载后滚动到底部 + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(); }); } } + + // Save translation history to storage + void saveTranslationHistory() { + // 限制历史记录数量,保留最新的记录 + if (translationHistory.length > _maxHistoryItems) { + translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems); + } + + final List> historyJson = + translationHistory.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, jsonEncode(historyJson)); + } + + // Clear translation history + void clearTranslationHistory() { + translationHistory.clear(); + _storage.remove(_historyKey); + } } // Model class for translation items @@ -329,6 +524,8 @@ class TranslationItem { final String sourceLanguage; final String targetLanguage; final DateTime timestamp; + final String sessionId; + final bool isFirstInSession; TranslationItem({ required this.sourceText, @@ -336,5 +533,31 @@ class TranslationItem { required this.sourceLanguage, required this.targetLanguage, required this.timestamp, + required this.sessionId, + this.isFirstInSession = false, }); + + factory TranslationItem.fromJson(Map json) { + return TranslationItem( + sourceText: json['sourceText'], + translatedText: json['translatedText'], + sourceLanguage: json['sourceLanguage'], + targetLanguage: json['targetLanguage'], + timestamp: DateTime.parse(json['timestamp']), + sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), + isFirstInSession: json['isFirstInSession'] ?? false, + ); + } + + Map toJson() { + return { + 'sourceText': sourceText, + 'translatedText': translatedText, + 'sourceLanguage': sourceLanguage, + 'targetLanguage': targetLanguage, + 'timestamp': timestamp.toIso8601String(), + 'sessionId': sessionId, + 'isFirstInSession': isFirstInSession, + }; + } } \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart new file mode 100644 index 000000000..961cb8f6b --- /dev/null +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -0,0 +1,125 @@ +import 'package:get/get.dart'; +import '../../../data/services/volcano_ai_service.dart'; +import '../controllers/translation_controller.dart'; +import '../controllers/translation_history_controller.dart'; +import 'package:get_storage/get_storage.dart'; + +class TranslationDetailController extends GetxController { + // 依赖注入 + final VolcanoAIService _aiService = Get.find(); + final TranslationHistoryController historyController = Get.find(); + final GetStorage _storage = GetStorage(); + + // 存储键前缀 + static const String _summaryKeyPrefix = 'translation_summary_'; + + // 状态变量 + final selectedTabIndex = 0.obs; + final isGeneratingSummary = false.obs; + final summaryResult = ''.obs; + + // 当前会话信息 + late final TranslationItem currentItem; + late final List sessionItems; + + // 初始化方法 + void initialize(TranslationItem item, List items) { + currentItem = item; + sessionItems = items; + + // 尝试从存储中加载摘要 + loadSavedSummary(); + } + + // 加载保存的摘要 + void loadSavedSummary() { + final String? savedSummary = _storage.read(_getSummaryKey()); + if (savedSummary != null && savedSummary.isNotEmpty) { + summaryResult.value = savedSummary; + } + } + + // 保存摘要 + void saveSummary(String summary) { + if (summary.isNotEmpty) { + _storage.write(_getSummaryKey(), summary); + } + } + + // 获取摘要的存储键 + String _getSummaryKey() { + return _summaryKeyPrefix + currentItem.sessionId; + } + + // 切换选项卡 + void switchTab(int index) { + selectedTabIndex.value = index; + } + + // 播放翻译项 + void playTranslationItem(TranslationItem item) { + historyController.playTranslationItem(item); + } + + // 播放文本 + void playText(String text) { + historyController.playText(text); + } + + // 获取合并的源文本 + String getCombinedSourceText() { + return sessionItems.map((e) => e.sourceText).join('\n\n'); + } + + // 获取合并的翻译文本 + String getCombinedTranslatedText() { + return sessionItems.map((e) => e.translatedText).join('\n\n'); + } + + // 提取摘要 + Future extractSummary(String text) async { + isGeneratingSummary.value = true; + + try { + // 构建提示词 + final messages = [ + {'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'}, + ]; + + // 系统提示词 + const systemPrompt = ''' +你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。 +请按照以下格式生成摘要: + +1. 首先用1-2句话对整体内容进行简要概括 +2. 然后使用项目符号列表(•)列出关键点或重要信息 +3. 如果是对话内容,请特别标注出讨论的结论或达成的共识 + +请遵循以下原则: +1. 保留原文的核心信息和关键点 +2. 使用简洁、清晰的语言 +3. 保持客观,不添加个人观点 +4. 摘要总长度应该是原文的1/3或更短 +5. 确保列表中的每个要点都简明扼要,突出重点 +'''; + + // 调用AI服务生成摘要 + final result = await _aiService.sendMessage( + messages: messages, + systemPrompt: systemPrompt, + ); + + summaryResult.value = result; + + // 保存摘要 + saveSummary(result); + + return result; + } catch (e) { + summaryResult.value = '生成摘要失败: ${e.toString()}'; + return '生成摘要失败: ${e.toString()}'; + } finally { + isGeneratingSummary.value = false; + } + } +} \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_history_controller.dart b/lib/modules/translation/controllers/translation_history_controller.dart new file mode 100644 index 000000000..379983afc --- /dev/null +++ b/lib/modules/translation/controllers/translation_history_controller.dart @@ -0,0 +1,129 @@ +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../controllers/translation_controller.dart'; + +class TranslationHistoryController extends GetxController { + // 引用主翻译控制器,以访问历史记录数据 + final TranslationController translationController = Get.find(); + + // 按日期分组的历史记录 + final Rx>> groupedHistory = Rx>>({}); + + // 排序后的日期列表 + final RxList sortedDates = [].obs; + + // 会话映射表,用于存储每个会话的所有项目 + final Rx>> sessionMap = Rx>>({}); + + @override + void onInit() { + super.onInit(); + // 监听翻译历史记录的变化 + ever(translationController.translationHistory, (_) { + _groupHistoryByDate(); + _groupHistoryBySession(); + }); + + // 初始分组 + _groupHistoryByDate(); + _groupHistoryBySession(); + } + + // 按日期对历史记录进行分组 + void _groupHistoryByDate() { + final Map> newGroupedHistory = {}; + + // 筛选出每个会话的第一条记录 + final List firstItemsOfSessions = translationController.translationHistory + .where((item) => item.isFirstInSession) + .toList(); + + for (var item in firstItemsOfSessions) { + final DateTime date = item.timestamp; + String key; + + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + final twoDaysAgo = today.subtract(const Duration(days: 2)); + final itemDate = DateTime(date.year, date.month, date.day); + + if (itemDate == today) { + key = 'TODAY'; + } else if (itemDate == yesterday) { + key = '1 DAY AGO'; + } else if (itemDate == twoDaysAgo) { + key = '2 DAYS AGO'; + } else { + key = DateFormat('yyyy-MM-dd').format(date); + } + + if (!newGroupedHistory.containsKey(key)) { + newGroupedHistory[key] = []; + } + + newGroupedHistory[key]!.add(item); + } + + // 对每个日期组内的项目按时间倒序排序 + newGroupedHistory.forEach((key, items) { + items.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + }); + + // 更新分组后的历史记录 + groupedHistory.value = newGroupedHistory; + + // 按日期排序 + final List dates = newGroupedHistory.keys.toList() + ..sort((a, b) { + if (a == 'TODAY') return -1; + if (b == 'TODAY') return 1; + if (a == '1 DAY AGO') return -1; + if (b == '1 DAY AGO') return 1; + if (a == '2 DAYS AGO') return -1; + if (b == '2 DAYS AGO') return 1; + return b.compareTo(a); // 降序排列其他日期 + }); + + sortedDates.value = dates; + } + + // 按会话ID对历史记录进行分组 + void _groupHistoryBySession() { + final Map> newSessionMap = {}; + + for (var item in translationController.translationHistory) { + if (!newSessionMap.containsKey(item.sessionId)) { + newSessionMap[item.sessionId] = []; + } + + newSessionMap[item.sessionId]!.add(item); + } + + // 对每个会话内的项目按时间升序排序 + newSessionMap.forEach((key, items) { + items.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + }); + + // 更新会话映射表 + sessionMap.value = newSessionMap; + } + + // 获取指定会话的所有翻译项 + List getSessionItems(String sessionId) { + return sessionMap.value[sessionId] ?? []; + } + + // 播放翻译项 + void playTranslationItem(TranslationItem item) { + translationController.playTranslationItem(item); + } + + // 播放指定文本 + void playText(String text) { + translationController.playTranslatedText(text); + } + + // 检查是否有历史记录 + bool get hasHistory => translationController.translationHistory.isNotEmpty; +} \ No newline at end of file diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart new file mode 100644 index 000000000..92151543d --- /dev/null +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -0,0 +1,491 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../controllers/translation_controller.dart'; +import '../controllers/translation_history_controller.dart'; +import '../controllers/translation_detail_controller.dart'; +import 'package:intl/intl.dart'; +import 'package:flutter/services.dart'; +import 'package:share_plus/share_plus.dart'; + +class TranslationDetailView extends GetView { + final TranslationItem item; + final List sessionItems; + final TranslationHistoryController historyController; + + const TranslationDetailView({ + Key? key, + required this.item, + required this.sessionItems, + required this.historyController, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + // 初始化控制器 + controller.initialize(item, sessionItems); + + return Scaffold( + backgroundColor: Color(0xFFF5F5F8), + appBar: AppBar( + backgroundColor: Color(0xFFF5F5F8), + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'Detail', + style: TextStyle( + color: Colors.black, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: Icon( + Icons.share, + color: Colors.black, + size: 22.sp, + ), + onPressed: () { + _shareContent(); + }, + ), + ], + ), + body: Column( + children: [ + // 选项卡按钮 + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + child: Row( + children: [ + Expanded( + child: Obx(() => _buildTabButton( + title: '原始记录', + isSelected: controller.selectedTabIndex.value == 0, + onTap: () => controller.switchTab(0), + )), + ), + SizedBox(width: 12.w), + Expanded( + child: Obx(() => _buildTabButton( + title: '记录摘要', + isSelected: controller.selectedTabIndex.value == 1, + onTap: () => controller.switchTab(1), + )), + ), + ], + ), + ), + + // 内容区域 + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Obx(() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 会话时间 + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), + child: Text( + DateFormat('yyyy-MM-dd HH:mm').format(item.timestamp), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + letterSpacing: 1.0, + ), + ), + ), + SizedBox(height: 8.h), + + // 根据选中的选项卡显示不同内容 + if (controller.selectedTabIndex.value == 0) + // 原始记录 - 显示所有翻译项 + ...sessionItems.map((sessionItem) => _buildSessionItemWidget(sessionItem)).toList() + else + // 记录总结 - 显示合并后的内容 + _buildSummaryWidget(), + + SizedBox(height: 24.h), + ], + ); + }), + ), + ), + ], + ), + ); + } + + // 构建选项卡按钮 + Widget _buildTabButton({ + required String title, + required bool isSelected, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 40.h, + decoration: BoxDecoration( + color: isSelected ? Colors.blue : Colors.white, + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(8), + blurRadius: 4, + offset: Offset(0, 1), + ), + ], + ), + alignment: Alignment.center, + child: Text( + title, + style: TextStyle( + fontSize: 15.sp, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? Colors.white : Colors.black87, + ), + ), + ), + ); + } + + // 构建会话详情中的翻译项 + Widget _buildSessionItemWidget(TranslationItem item) { + return Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 16.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(8), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 源文本部分 + Padding( + padding: EdgeInsets.fromLTRB(20.r, 20.r, 20.r, 10.r), + child: Text( + item.sourceText, + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ), + // 翻译文本部分 + Padding( + padding: EdgeInsets.fromLTRB(20.r, 10.r, 20.r, 16.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Text( + item.translatedText, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[700], + fontWeight: FontWeight.normal, + ), + ), + ), + SizedBox(width: 12.w), + // 播放按钮 + GestureDetector( + onTap: () { + controller.playTranslationItem(item); + }, + child: Container( + width: 28.w, + height: 28.h, + decoration: BoxDecoration( + color: Colors.blue.withAlpha(10), + shape: BoxShape.circle, + ), + child: Icon( + Icons.volume_up, + color: Colors.blue, + size: 16.sp, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + // 构建总结视图 + Widget _buildSummaryWidget() { + final String combinedSourceText = controller.getCombinedSourceText(); + + return Column( + children: [ + // 摘要内容容器 + Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 16.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(8), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Obx(() { + if (controller.summaryResult.value.isEmpty) { + // 如果没有摘要,显示提示信息 + return Padding( + padding: EdgeInsets.all(24.r), + child: Center( + child: Text( + '点击下方按钮生成会话摘要', + style: TextStyle( + fontSize: 15.sp, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.center, + ), + ), + ); + } else { + // 如果有摘要,显示摘要内容 + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(20.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '摘要内容', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + SizedBox(height: 12.h), + Text( + controller.summaryResult.value, + style: TextStyle( + fontSize: 15.sp, + color: Colors.black87, + height: 1.5, + ), + ), + ], + ), + ), + Divider( + height: 1, + thickness: 0.5, + color: Colors.grey[200], + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // 播放按钮 + GestureDetector( + onTap: () { + controller.playText(controller.summaryResult.value); + }, + child: Container( + padding: EdgeInsets.all(8.r), + decoration: BoxDecoration( + color: Colors.blue.withAlpha(10), + shape: BoxShape.circle, + ), + child: Icon( + Icons.volume_up, + color: Colors.blue, + size: 18.sp, + ), + ), + ), + SizedBox(width: 12.w), + // 复制按钮 + GestureDetector( + onTap: () { + // 复制摘要到剪贴板 + Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); + Get.snackbar( + '已复制', + '摘要已复制到剪贴板', + snackPosition: SnackPosition.BOTTOM, + duration: Duration(seconds: 2), + ); + }, + child: Container( + padding: EdgeInsets.all(8.r), + decoration: BoxDecoration( + color: Colors.blue.withAlpha(10), + shape: BoxShape.circle, + ), + child: Icon( + Icons.copy, + color: Colors.blue, + size: 18.sp, + ), + ), + ), + ], + ), + ), + ], + ); + } + }), + ), + + // 提取摘要按钮 + Obx(() => Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 16.h), + child: ElevatedButton( + onPressed: controller.isGeneratingSummary.value || controller.summaryResult.value.isNotEmpty + ? null + : () => _extractSummary(combinedSourceText), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + elevation: 0, + disabledBackgroundColor: controller.isGeneratingSummary.value + ? Colors.blue.withAlpha(128) + : Colors.grey.withAlpha(128), + ), + child: controller.isGeneratingSummary.value + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20.w, + height: 20.h, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.0, + ), + ), + SizedBox(width: 12.w), + Text( + '生成中...', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ) + : Text( + controller.summaryResult.value.isEmpty ? '提取摘要' : '已生成摘要', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + )), + + // 重新生成按钮 + Obx(() { + if (controller.summaryResult.value.isEmpty || controller.isGeneratingSummary.value) { + return SizedBox.shrink(); + } + + return Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 16.h), + child: OutlinedButton( + onPressed: () => _extractSummary(combinedSourceText), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.blue, + side: BorderSide(color: Colors.blue), + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + ), + child: Text( + '重新生成', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + }), + ], + ); + } + + // 提取摘要方法 + void _extractSummary(String text) async { + await controller.extractSummary(text); + } + + // 分享内容方法 + void _shareContent() { + String shareText = ''; + + if (controller.selectedTabIndex.value == 0) { + // 分享原始记录 + final String sourceText = controller.getCombinedSourceText(); + final String translatedText = controller.getCombinedTranslatedText(); + shareText = '原文:\n$sourceText\n\n译文:\n$translatedText'; + } else { + // 分享摘要 + if (controller.summaryResult.value.isNotEmpty) { + shareText = '会话摘要:\n${controller.summaryResult.value}'; + } else { + Get.snackbar( + '无法分享', + '请先生成摘要', + snackPosition: SnackPosition.BOTTOM, + duration: Duration(seconds: 2), + ); + return; + } + } + + // 调用系统分享 + Share.share( + shareText, + subject: '翻译会话记录 - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}', + ); + } +} \ No newline at end of file diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart new file mode 100644 index 000000000..0a0d6c227 --- /dev/null +++ b/lib/modules/translation/views/translation_history_view.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../controllers/translation_controller.dart'; +import '../controllers/translation_history_controller.dart'; +import '../bindings/translation_detail_binding.dart'; +import 'translation_detail_view.dart'; + +class TranslationHistoryView extends GetView { + const TranslationHistoryView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0xFFF5F5F8), + appBar: AppBar( + backgroundColor: Color(0xFFF5F5F8), + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'History', + style: TextStyle( + color: Colors.black, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: Icon( + Icons.delete_outline, + color: Colors.black, + size: 24.sp, + ), + onPressed: () { + _showClearHistoryConfirmDialog(); + }, + ), + ], + ), + body: Obx(() { + if (!controller.hasHistory) { + return Center( + child: Text( + '暂无历史记录', + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey, + ), + ), + ); + } + + return ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + itemCount: controller.sortedDates.length, + itemBuilder: (context, index) { + final date = controller.sortedDates[index]; + final items = controller.groupedHistory.value[date]!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 24.h, bottom: 12.h, left: 12.w), + child: Text( + date, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + letterSpacing: 1.0, + ), + ), + ), + _buildDateGroupContainer(items), + ], + ); + }, + ); + }), + ); + } + + // 构建日期组容器 + Widget _buildDateGroupContainer(List items) { + return Container( + margin: EdgeInsets.only(bottom: 16.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(8), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: items.length, + separatorBuilder: (context, index) => Divider( + height: 1, + thickness: 0.5, + color: Colors.grey[200], + indent: 0, + endIndent: 0, + ), + itemBuilder: (context, index) { + return _buildHistoryItemInGroup(items[index]); + }, + ), + ); + } + + // 构建组内的历史记录项 + Widget _buildHistoryItemInGroup(TranslationItem item) { + return GestureDetector( + onTap: () { + // 点击查看详情 + _showTranslationDetail(item); + }, + child: Stack( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 源文本 + Text( + item.sourceText, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 6.h), + // 翻译文本 + Text( + item.translatedText, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[700], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Positioned( + right: 12.w, + top: 0, + bottom: 0, + child: Icon( + Icons.chevron_right, + color: Colors.grey[350], + size: 20.sp, + ), + ), + ], + ), + ); + } + + void _showTranslationDetail(TranslationItem item) { + // 获取该会话的所有翻译项 + final List sessionItems = controller.getSessionItems(item.sessionId); + + Get.to( + () => TranslationDetailView( + item: item, + sessionItems: sessionItems, + historyController: controller, + ), + binding: TranslationDetailBinding(), + ); + } + + void _showClearHistoryConfirmDialog() { + Get.dialog( + AlertDialog( + title: Text( + '清除历史记录', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + content: Text( + '确定要清除所有翻译历史记录吗?此操作无法撤销。', + style: TextStyle( + fontSize: 16.sp, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey[700], + ), + ), + ), + TextButton( + onPressed: () { + // 清除历史记录 + controller.translationController.clearTranslationHistory(); + Get.back(); + }, + child: Text( + '确定', + style: TextStyle( + fontSize: 16.sp, + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 430126147..4f84f039c 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/translation_controller.dart'; +import '../views/translation_history_view.dart'; +import '../bindings/translation_history_binding.dart'; // import 'package:intl/intl.dart'; class TranslationView extends GetView { @@ -34,29 +36,63 @@ class TranslationView extends GetView { title: Row( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Text( - controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.black, + // 源语言选择 + GestureDetector( + onTap: () => _showLanguageSelectionDialog(true), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Obx(() => Text( + controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + )), ), - )), - SizedBox(width: 4.w), - Icon( - Icons.arrow_forward_ios, - color: Colors.black, - size: 16.sp, ), - SizedBox(width: 4.w), - Obx(() => Text( - controller.targetLanguage.value.split(' ')[0], // Just show the English part - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.black, + + // 语言互换按钮 + GestureDetector( + onTap: controller.swapLanguages, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + padding: EdgeInsets.all(4.r), + decoration: BoxDecoration( + color: Colors.blue[50], + shape: BoxShape.circle, + ), + child: Icon( + Icons.swap_horiz, + color: Colors.blue, + size: 20.sp, + ), + ), + ), + + // 目标语言选择 + GestureDetector( + onTap: () => _showLanguageSelectionDialog(false), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Obx(() => Text( + controller.targetLanguage.value.split(' ')[0], // Just show the English part + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + )), ), - )), + ), ], ), centerTitle: true, @@ -71,6 +107,26 @@ class TranslationView extends GetView { onPressed: controller.toggleTts, tooltip: '自动播放翻译结果', )), + + // 历史记录按钮 + IconButton( + icon: Icon( + Icons.history, + color: Colors.black, + size: 24.sp, + ), + onPressed: () async { + // 停止语音识别,释放资源 + controller.stopRecognition(); + await Get.to( + () => const TranslationHistoryView(), + binding: TranslationHistoryBinding(), + ); + // 返回时重新启动语音识别 + controller.startRecognition(); + }, + tooltip: '查看历史记录', + ), ], ); } @@ -229,4 +285,137 @@ class TranslationView extends GetView { ), ); } + + // 显示语言选择对话框 + void _showLanguageSelectionDialog(bool isSource) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24.r), + ), + insetPadding: EdgeInsets.symmetric(horizontal: 20.w), + backgroundColor: Colors.white, + child: Container( + width: 0.9.sw, + padding: EdgeInsets.only(top: 32.h, bottom: 16.h), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Text( + 'Select Language', + style: TextStyle( + fontSize: 32.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + SizedBox(height: 24.h), + Container( + constraints: BoxConstraints( + maxHeight: 0.6.sh, + ), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: controller.supportedLanguages.keys.map((language) { + final isSelected = isSource + ? controller.sourceLanguage.value == language + : controller.targetLanguage.value == language; + + return _buildLanguageItem( + language: language.split(' ')[0], + isSelected: isSelected, + onTap: () { + if (isSource) { + controller.setSourceLanguage(language); + } else { + controller.setTargetLanguage(language); + } + Get.back(); + }, + ); + }).toList(), + ), + ), + ), + SizedBox(height: 24.h), + GestureDetector( + onTap: () => Get.back(), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 16.w), + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 16.h), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + 'Cancel', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + ), + ), + ], + ), + ), + ), + barrierDismissible: true, + ); + } + + // 构建语言选择项 + Widget _buildLanguageItem({ + required String language, + required bool isSelected, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + language, + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + Container( + width: 28.w, + height: 28.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + border: Border.all( + color: isSelected ? Colors.transparent : Colors.grey[300]!, + width: 1.5, + ), + color: isSelected ? Color(0xFF6C5CE7) : Colors.white, + ), + child: isSelected + ? Icon( + Icons.check, + color: Colors.white, + size: 20.sp, + ) + : null, + ), + ], + ), + ), + ); + } } \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index bc31104c1..ded0e30eb 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,12 +7,16 @@ #include "generated_plugin_registrant.h" #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); g_autoptr(FlPluginRegistrar) window_size_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); window_size_plugin_register_with_registrar(window_size_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1d31f02d1..500c17552 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST record_linux + url_launcher_linux window_size ) diff --git a/pubspec.yaml b/pubspec.yaml index 94502895b..716905c6f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: device_info_plus: ^10.1.2 uuid: ^4.3.3 record: ^5.2.1 + share_plus: ^10.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 6f6d6dca0..1967da96a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,8 @@ #include #include +#include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -15,6 +17,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowSizePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowSizePlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ee295aa03..795d3bc0a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,8 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows record_windows + share_plus + url_launcher_windows window_size )