22 changed files with 1495 additions and 1466 deletions
@ -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) |
|||
} |
|||
} |
|||
@ -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() |
|||
} |
|||
} |
|||
@ -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>(() => TranslationDetailController()); |
|||
} |
|||
} |
|||
@ -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>( |
|||
() => TranslationHistoryController(), |
|||
); |
|||
} |
|||
} |
|||
@ -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<VolcanoAIService>(); |
|||
final TranslationHistoryController historyController = Get.find<TranslationHistoryController>(); |
|||
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<TranslationItem> sessionItems; |
|||
|
|||
// 初始化方法 |
|||
void initialize(TranslationItem item, List<TranslationItem> items) { |
|||
currentItem = item; |
|||
sessionItems = items; |
|||
|
|||
// 尝试从存储中加载摘要 |
|||
loadSavedSummary(); |
|||
} |
|||
|
|||
// 加载保存的摘要 |
|||
void loadSavedSummary() { |
|||
final String? savedSummary = _storage.read<String>(_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<String> 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; |
|||
} |
|||
} |
|||
} |
|||
@ -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<TranslationController>(); |
|||
|
|||
// 按日期分组的历史记录 |
|||
final Rx<Map<String, List<TranslationItem>>> groupedHistory = Rx<Map<String, List<TranslationItem>>>({}); |
|||
|
|||
// 排序后的日期列表 |
|||
final RxList<String> sortedDates = <String>[].obs; |
|||
|
|||
// 会话映射表,用于存储每个会话的所有项目 |
|||
final Rx<Map<String, List<TranslationItem>>> sessionMap = Rx<Map<String, List<TranslationItem>>>({}); |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
// 监听翻译历史记录的变化 |
|||
ever(translationController.translationHistory, (_) { |
|||
_groupHistoryByDate(); |
|||
_groupHistoryBySession(); |
|||
}); |
|||
|
|||
// 初始分组 |
|||
_groupHistoryByDate(); |
|||
_groupHistoryBySession(); |
|||
} |
|||
|
|||
// 按日期对历史记录进行分组 |
|||
void _groupHistoryByDate() { |
|||
final Map<String, List<TranslationItem>> newGroupedHistory = {}; |
|||
|
|||
// 筛选出每个会话的第一条记录 |
|||
final List<TranslationItem> 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<String> 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<String, List<TranslationItem>> 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<TranslationItem> 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; |
|||
} |
|||
@ -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<TranslationDetailController> { |
|||
final TranslationItem item; |
|||
final List<TranslationItem> 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)}', |
|||
); |
|||
} |
|||
} |
|||
@ -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<TranslationHistoryController> { |
|||
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<TranslationItem> 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<TranslationItem> 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), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue