wolfplus 2 years ago
parent
commit
03e8136415
  1. 572
      android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt
  2. 275
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  3. 24
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  4. 51
      lib/app/theme/app_theme.dart
  5. 5
      lib/core/widgets/common_bottom_nav.dart
  6. 8
      lib/main.dart
  7. 2
      lib/modules/home/controllers/home_controller.dart
  8. 33
      lib/modules/home/views/home_view.dart
  9. 134
      lib/modules/login/views/login_view.dart
  10. 76
      lib/modules/pairing/views/pairing_view.dart

572
android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt

@ -1,572 +0,0 @@
package com.example.deep_voice
import android.content.Context
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.media.AudioManager
import android.os.Build
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import android.app.Application
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.FlutterInjector
/**
* 音频处理插件
*
* 提供回音消除、降噪和自动增益控制等功能
*/
class AudioProcessingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var audioManager: AudioManager
// 音频处理组件
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
// 当前音频会话ID
private var currentAudioSessionId: Int = -1
companion object {
private const val TAG = "AudioProcessingPlugin"
private const val CHANNEL_NAME = "com.deep_voice.audio_processing"
// 保存静态实例,确保在任何地方都能访问
@JvmStatic
private var instance: AudioProcessingPlugin? = null
// 保存应用Context
@JvmStatic
private var applicationContext: Context? = null
// 静态初始化块,尝试在类加载时就获取applicationContext
init {
try {
Log.i(TAG, "AudioProcessingPlugin静态初始化块被执行")
// 尝试通过FlutterInjector获取applicationContext
val flutterLoader = FlutterInjector.instance().flutterLoader()
if (flutterLoader.initialized()) {
Log.i(TAG, "FlutterLoader已初始化")
} else {
Log.i(TAG, "FlutterLoader尚未初始化")
}
} catch (e: Exception) {
Log.e(TAG, "静态初始化块执行失败: ${e.message}")
}
}
// 获取实例的方法
@JvmStatic
fun getInstance(): AudioProcessingPlugin? {
return instance
}
// 获取应用Context的方法
@JvmStatic
fun getApplicationContext(): Context? {
return applicationContext
}
// 设置应用Context的方法,可以从外部调用
@JvmStatic
fun setApplicationContext(ctx: Context) {
if (applicationContext == null) {
applicationContext = ctx.applicationContext
Log.i(TAG, "通过setApplicationContext方法设置了applicationContext")
}
}
}
// 构造函数,尝试获取applicationContext
init {
Log.i(TAG, "AudioProcessingPlugin实例初始化块被执行")
}
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
Log.i(TAG, "onAttachedToEngine被调用")
context = binding.applicationContext
applicationContext = context
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
channel = MethodChannel(binding.binaryMessenger, CHANNEL_NAME)
channel.setMethodCallHandler(this)
instance = this
Log.i(TAG, "音频处理插件已初始化,context: $context, applicationContext: $applicationContext")
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
channel.setMethodCallHandler(null)
releaseAudioEffects()
if (instance == this) {
instance = null
}
Log.i(TAG, "音频处理插件已释放")
}
// 实现ActivityAware接口,获取Activity上下文
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
Log.i(TAG, "onAttachedToActivity被调用")
// 如果context尚未初始化,使用Activity上下文
if (!::context.isInitialized) {
context = binding.activity
Log.i(TAG, "使用Activity上下文初始化context")
// 如果audioManager尚未初始化,初始化它
if (!::audioManager.isInitialized) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "使用Activity上下文初始化audioManager")
}
}
}
override fun onDetachedFromActivity() {
Log.i(TAG, "onDetachedFromActivity被调用")
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"isEchoCancellationAvailable" -> {
result.success(AcousticEchoCanceler.isAvailable())
}
"isNoiseSuppressionAvailable" -> {
result.success(NoiseSuppressor.isAvailable())
}
"enableEchoCancellation" -> {
val audioSessionId = call.argument<Int>("audioSessionId")
if (audioSessionId == null) {
result.error("INVALID_ARGUMENT", "音频会话ID不能为空", null)
return
}
result.success(enableEchoCancellation(audioSessionId))
}
"disableEchoCancellation" -> {
result.success(disableEchoCancellation())
}
"enableNoiseSuppression" -> {
val audioSessionId = call.argument<Int>("audioSessionId")
if (audioSessionId == null) {
result.error("INVALID_ARGUMENT", "音频会话ID不能为空", null)
return
}
result.success(enableNoiseSuppression(audioSessionId))
}
"disableNoiseSuppression" -> {
result.success(disableNoiseSuppression())
}
"getAudioSessionId" -> {
// 尝试获取当前活动的音频会话ID
try {
Log.i(TAG, "getAudioSessionId方法被调用")
// 检查applicationContext是否已初始化
if (applicationContext != null) {
Log.i(TAG, "静态applicationContext已初始化")
// 如果context尚未初始化,使用applicationContext
if (!::context.isInitialized) {
context = applicationContext!!
Log.i(TAG, "使用静态applicationContext初始化context")
}
} else {
Log.w(TAG, "静态applicationContext尚未初始化")
}
// 检查context是否初始化
if (!::context.isInitialized) {
Log.e(TAG, "context尚未初始化,尝试从静态实例获取")
// 尝试从静态实例获取context
val staticInstance = getInstance()
if (staticInstance != null) {
try {
// 尝试访问context,如果未初始化会抛出异常
val testContext = staticInstance.context
context = testContext
Log.i(TAG, "从静态实例获取context成功")
} catch (e: UninitializedPropertyAccessException) {
Log.e(TAG, "静态实例的context也未初始化: ${e.message}")
// 尝试使用应用级别的Context
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
Log.i(TAG, "使用应用级别的Context初始化成功")
} else {
Log.e(TAG, "无法获取任何有效的Context,无法获取音频会话ID")
result.success(-1)
return
}
}
} else {
// 尝试使用应用级别的Context
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
Log.i(TAG, "使用应用级别的Context初始化成功")
} else {
Log.e(TAG, "无法获取context,无法获取音频会话ID")
result.success(-1)
return
}
}
}
// 检查audioManager是否初始化,如果没有则初始化
if (!::audioManager.isInitialized) {
Log.w(TAG, "audioManager尚未初始化,正在初始化")
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "audioManager初始化成功")
} catch (e: Exception) {
Log.e(TAG, "初始化audioManager失败: ${e.message}")
result.success(-1)
return
}
}
// 使用ensureValidAudioSessionId方法获取有效的音频会话ID
val sessionId = ensureValidAudioSessionId()
Log.i(TAG, "返回音频会话ID: $sessionId")
result.success(sessionId)
} catch (e: Exception) {
Log.e(TAG, "获取音频会话ID失败: ${e.message}")
result.success(-1)
}
}
else -> {
result.notImplemented()
}
}
}
/**
* 启用回音消除
*
* @param audioSessionId 音频会话ID
* @return 是否成功启用
*/
private fun enableEchoCancellation(audioSessionId: Int): Boolean {
if (!AcousticEchoCanceler.isAvailable()) {
Log.w(TAG, "设备不支持回音消除")
return false
}
try {
Log.i(TAG, "尝试启用回音消除,音频会话ID: $audioSessionId")
// 确保context已初始化
if (!::context.isInitialized) {
Log.w(TAG, "context尚未初始化,尝试获取")
// 尝试从静态实例或应用级别获取context
val staticInstance = getInstance()
if (staticInstance != null && try { staticInstance.context; true } catch (e: Exception) { false }) {
context = staticInstance.context
} else {
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
} else {
Log.e(TAG, "无法获取context,无法启用回音消除")
return false
}
}
}
// 确保audioManager已初始化
if (!::audioManager.isInitialized) {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "audioManager初始化成功")
} catch (e: Exception) {
Log.e(TAG, "初始化audioManager失败: ${e.message}")
return false
}
}
// 如果已经创建了回音消除器,先释放
if (echoCanceler != null) {
echoCanceler?.release()
echoCanceler = null
}
// 创建新的回音消除器
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler == null) {
Log.e(TAG, "创建回音消除器失败")
return false
}
// 启用回音消除
echoCanceler?.enabled = true
currentAudioSessionId = audioSessionId
Log.i(TAG, "回音消除已启用,音频会话ID: $audioSessionId")
return echoCanceler?.enabled == true
} catch (e: Exception) {
Log.e(TAG, "启用回音消除失败: ${e.message}")
return false
}
}
/**
* 禁用回音消除
*
* @return 是否成功禁用
*/
private fun disableEchoCancellation(): Boolean {
if (echoCanceler == null) {
return true
}
try {
echoCanceler?.enabled = false
echoCanceler?.release()
echoCanceler = null
Log.i(TAG, "回音消除已禁用")
return true
} catch (e: Exception) {
Log.e(TAG, "禁用回音消除失败: ${e.message}")
return false
}
}
/**
* 启用噪声抑制
*
* @param audioSessionId 音频会话ID
* @return 是否成功启用
*/
private fun enableNoiseSuppression(audioSessionId: Int): Boolean {
if (!NoiseSuppressor.isAvailable()) {
Log.w(TAG, "设备不支持噪声抑制")
return false
}
try {
Log.i(TAG, "尝试启用噪声抑制,音频会话ID: $audioSessionId")
// 确保context已初始化
if (!::context.isInitialized) {
Log.w(TAG, "context尚未初始化,尝试获取")
// 尝试从静态实例或应用级别获取context
val staticInstance = getInstance()
if (staticInstance != null && try { staticInstance.context; true } catch (e: Exception) { false }) {
context = staticInstance.context
} else {
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
} else {
Log.e(TAG, "无法获取context,无法启用噪声抑制")
return false
}
}
}
// 确保audioManager已初始化
if (!::audioManager.isInitialized) {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "audioManager初始化成功")
} catch (e: Exception) {
Log.e(TAG, "初始化audioManager失败: ${e.message}")
return false
}
}
// 如果已经创建了噪声抑制器,先释放
if (noiseSuppressor != null) {
noiseSuppressor?.release()
noiseSuppressor = null
}
// 创建新的噪声抑制器
noiseSuppressor = NoiseSuppressor.create(audioSessionId)
if (noiseSuppressor == null) {
Log.e(TAG, "创建噪声抑制器失败")
return false
}
// 启用噪声抑制
noiseSuppressor?.enabled = true
currentAudioSessionId = audioSessionId
Log.i(TAG, "噪声抑制已启用,音频会话ID: $audioSessionId")
return noiseSuppressor?.enabled == true
} catch (e: Exception) {
Log.e(TAG, "启用噪声抑制失败: ${e.message}")
return false
}
}
/**
* 禁用噪声抑制
*
* @return 是否成功禁用
*/
private fun disableNoiseSuppression(): Boolean {
if (noiseSuppressor == null) {
return true
}
try {
noiseSuppressor?.enabled = false
noiseSuppressor?.release()
noiseSuppressor = null
Log.i(TAG, "噪声抑制已禁用")
return true
} catch (e: Exception) {
Log.e(TAG, "禁用噪声抑制失败: ${e.message}")
return false
}
}
/**
* 释放所有音频效果
*/
private fun releaseAudioEffects() {
try {
echoCanceler?.release()
echoCanceler = null
noiseSuppressor?.release()
noiseSuppressor = null
automaticGainControl?.release()
automaticGainControl = null
currentAudioSessionId = -1
Log.i(TAG, "所有音频效果已释放")
} catch (e: Exception) {
Log.e(TAG, "释放音频效果失败: ${e.message}")
}
}
/**
* 检查音频会话ID是否有效,如果无效则创建一个新的
*
* @return 有效的音频会话ID
*/
private fun ensureValidAudioSessionId(): Int {
// 如果已经有有效的音频会话ID,直接返回
if (currentAudioSessionId > 0) {
Log.i(TAG, "使用已存在的音频会话ID: $currentAudioSessionId")
return currentAudioSessionId
}
Log.i(TAG, "尝试创建有效的音频会话ID")
// 尝试从AudioManager获取音频会话ID
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ::audioManager.isInitialized) {
try {
val audioAttributes = audioManager.getActivePlaybackConfigurations()
if (audioAttributes.isNotEmpty()) {
val sessionId = audioAttributes[0].audioAttributes.usage
if (sessionId > 0) {
currentAudioSessionId = sessionId
Log.i(TAG, "从AudioManager获取到音频会话ID: $sessionId")
return sessionId
}
}
} catch (e: Exception) {
Log.e(TAG, "从AudioManager获取音频会话ID失败: ${e.message}")
}
}
// 尝试创建一个临时的AudioTrack来获取会话ID
try {
val audioTrack = android.media.AudioTrack(
android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
android.media.AudioFormat.Builder()
.setSampleRate(16000)
.setChannelMask(android.media.AudioFormat.CHANNEL_OUT_MONO)
.setEncoding(android.media.AudioFormat.ENCODING_PCM_16BIT)
.build(),
1024, // 最小缓冲区大小
android.media.AudioTrack.MODE_STREAM,
android.media.AudioManager.AUDIO_SESSION_ID_GENERATE
)
val generatedSessionId = audioTrack.audioSessionId
if (generatedSessionId > 0) {
currentAudioSessionId = generatedSessionId
Log.i(TAG, "成功创建临时AudioTrack并获取音频会话ID: $generatedSessionId")
// 释放AudioTrack资源
audioTrack.release()
return generatedSessionId
} else {
Log.w(TAG, "创建的AudioTrack未返回有效的音频会话ID")
audioTrack.release()
}
} catch (e: Exception) {
Log.e(TAG, "创建临时AudioTrack失败: ${e.message}")
}
// 尝试创建一个临时的AudioRecord来获取会话ID
try {
val minBufferSize = android.media.AudioRecord.getMinBufferSize(
16000,
android.media.AudioFormat.CHANNEL_IN_MONO,
android.media.AudioFormat.ENCODING_PCM_16BIT
)
if (minBufferSize > 0) {
val audioRecord = android.media.AudioRecord(
android.media.MediaRecorder.AudioSource.MIC,
16000,
android.media.AudioFormat.CHANNEL_IN_MONO,
android.media.AudioFormat.ENCODING_PCM_16BIT,
minBufferSize
)
val recordSessionId = audioRecord.audioSessionId
if (recordSessionId > 0) {
currentAudioSessionId = recordSessionId
Log.i(TAG, "成功创建临时AudioRecord并获取音频会话ID: $recordSessionId")
// 释放AudioRecord资源
audioRecord.release()
return recordSessionId
} else {
Log.w(TAG, "创建的AudioRecord未返回有效的音频会话ID")
audioRecord.release()
}
} else {
Log.w(TAG, "无法获取有效的AudioRecord最小缓冲区大小")
}
} catch (e: Exception) {
Log.e(TAG, "创建临时AudioRecord失败: ${e.message}")
}
// 如果以上方法都失败,使用一个固定的默认值
val defaultSessionId = 1
Log.w(TAG, "使用默认音频会话ID: $defaultSessionId")
currentAudioSessionId = defaultSessionId
return defaultSessionId
}
}

275
android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt

@ -5,6 +5,11 @@ import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler import com.microsoft.cognitiveservices.speech.util.EventHandler
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import android.media.audiofx.AcousticEchoCanceler
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.AudioFormat
import java.util.concurrent.Executors
class AzureAsrHelper { class AzureAsrHelper {
private var recognizer: SpeechRecognizer? = null private var recognizer: SpeechRecognizer? = null
@ -15,6 +20,17 @@ class AzureAsrHelper {
private var currentLanguage = "zh-CN" private var currentLanguage = "zh-CN"
private var subscriptionKey = "" private var subscriptionKey = ""
private var serviceRegion = "" private var serviceRegion = ""
// 回音消除相关
private var echoCanceler: AcousticEchoCanceler? = null
private var audioSessionId: Int = -1
private var isEchoCancellationEnabled = false
// 直接管理 AudioRecord 和 PushStream
private var audioRecord: AudioRecord? = null
private var pushStream: PushAudioInputStream? = null
private var isRecording = false
private val executorService = Executors.newSingleThreadExecutor()
// 初始化 SDK // 初始化 SDK
fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean {
@ -38,10 +54,9 @@ class AzureAsrHelper {
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
speechConfig?.speechRecognitionLanguage = language speechConfig?.speechRecognitionLanguage = language
// 创建音频配置 // 创建 PushAudioInputStream 和 AudioConfig
Log.d(TAG, "创建默认麦克风音频配置")
try { try {
audioConfig = AudioConfig.fromDefaultMicrophoneInput() setupAudioInput()
Log.d(TAG, "成功创建音频配置") Log.d(TAG, "成功创建音频配置")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "创建音频配置失败: ${e.message}", e) Log.e(TAG, "创建音频配置失败: ${e.message}", e)
@ -59,6 +74,48 @@ class AzureAsrHelper {
} }
} }
// 设置音频输入
private fun setupAudioInput() {
// 释放之前的资源
releaseAudioResources()
// 创建 PushAudioInputStream
pushStream = AudioInputStream.createPushStream()
// 创建 AudioConfig
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 创建 AudioRecord
val sampleRate = 16000
val channelConfig = AudioFormat.CHANNEL_IN_MONO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
if (bufferSize <= 0) {
Log.e(TAG, "无法获取最小缓冲区大小")
throw RuntimeException("无法获取最小缓冲区大小")
}
audioRecord = AudioRecord(
MediaRecorder.AudioSource.DEFAULT,
sampleRate,
channelConfig,
audioFormat,
bufferSize
)
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "AudioRecord 初始化失败")
audioRecord?.release()
audioRecord = null
throw RuntimeException("AudioRecord 初始化失败")
}
// 获取音频会话 ID 并设置回音消除
audioSessionId = audioRecord?.audioSessionId ?: -1
setupEchoCancellation()
}
// 开始一次性语音识别 // 开始一次性语音识别
fun recognizeOnce(language: String? = null, callback: RecognizeCallback) { fun recognizeOnce(language: String? = null, callback: RecognizeCallback) {
if (recognizer == null) { if (recognizer == null) {
@ -72,14 +129,21 @@ class AzureAsrHelper {
} }
try { try {
// 开始录音并推送数据
startRecording()
val result = recognizer?.recognizeOnceAsync()?.get() val result = recognizer?.recognizeOnceAsync()?.get()
// 停止录音
stopRecording()
if (result != null && result.reason == ResultReason.RecognizedSpeech) { if (result != null && result.reason == ResultReason.RecognizedSpeech) {
callback.onResult(result.text) callback.onResult(result.text)
} else { } else {
callback.onError("未能识别语音") callback.onError("未能识别语音")
} }
} catch (e: Exception) { } catch (e: Exception) {
stopRecording()
callback.onError("识别异常: ${e.message}") callback.onError("识别异常: ${e.message}")
} }
} }
@ -105,18 +169,13 @@ class AzureAsrHelper {
} }
try { try {
// 先发送会话开始事件
// Log.d(TAG, "准备开始连续语音识别")
// callback.onSessionStarted()
// 重新创建识别器 // 重新创建识别器
Log.d(TAG, "重新创建识别器") Log.d(TAG, "重新创建识别器")
recognizer?.close() recognizer?.close()
try { try {
// 重新创建音频配置 // 重新设置音频输入
audioConfig?.close() setupAudioInput()
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
Log.d(TAG, "重新创建音频配置成功") Log.d(TAG, "重新创建音频配置成功")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "重新创建音频配置失败: ${e.message}", e) Log.e(TAG, "重新创建音频配置失败: ${e.message}", e)
@ -144,7 +203,7 @@ class AzureAsrHelper {
recognizer.recognizing.addEventListener( recognizer.recognizing.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) { if (event.result.reason == ResultReason.RecognizingSpeech) {
Log.d(TAG, "正在识别: ${event.result.text}") // Log.d(TAG, "正在识别: ${event.result.text}")
callback.onRecognizing(event.result.text) callback.onRecognizing(event.result.text)
} }
} }
@ -164,6 +223,7 @@ class AzureAsrHelper {
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
Log.d(TAG, "会话结束") Log.d(TAG, "会话结束")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
callback.onSessionStopped() callback.onSessionStopped()
} }
) )
@ -175,9 +235,13 @@ class AzureAsrHelper {
Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails") Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails")
callback.onCanceled(event.reason.toString(), errorDetails) callback.onCanceled(event.reason.toString(), errorDetails)
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
} }
) )
// 开始录音
startRecording()
// 开始连续识别 // 开始连续识别
Log.d(TAG, "开始连续识别") Log.d(TAG, "开始连续识别")
recognizer.startContinuousRecognitionAsync().get() recognizer.startContinuousRecognitionAsync().get()
@ -190,10 +254,60 @@ class AzureAsrHelper {
Log.e(TAG, "开始连续识别失败: ${e.message}", e) Log.e(TAG, "开始连续识别失败: ${e.message}", e)
callback.onError("开始连续识别失败: ${e.message}") callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
return false return false
} }
} }
// 开始录音并推送数据到 PushStream
private fun startRecording() {
if (isRecording || audioRecord == null || pushStream == null) {
return
}
isRecording = true
audioRecord?.startRecording()
executorService.submit {
val bufferSize = AudioRecord.getMinBufferSize(
16000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
val buffer = ByteArray(bufferSize)
while (isRecording && audioRecord != null && pushStream != null) {
try {
val readBytes = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (readBytes > 0) {
// 修复:PushAudioInputStream.write() 只接受一个 ByteArray 参数
// 如果读取的数据长度等于缓冲区大小,直接写入
if (readBytes == buffer.size) {
pushStream?.write(buffer)
} else {
// 否则,创建一个新的数组,只包含实际读取的数据
val audioData = buffer.copyOf(readBytes)
pushStream?.write(audioData)
}
}
} catch (e: Exception) {
Log.e(TAG, "读取音频数据失败: ${e.message}", e)
break
}
}
}
}
// 停止录音
private fun stopRecording() {
isRecording = false
try {
audioRecord?.stop()
} catch (e: Exception) {
Log.e(TAG, "停止录音失败: ${e.message}", e)
}
}
// 停止连续语音识别 // 停止连续语音识别
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (!isContinuousRecognitionActive || recognizer == null) { if (!isContinuousRecognitionActive || recognizer == null) {
@ -203,11 +317,13 @@ class AzureAsrHelper {
try { try {
recognizer?.stopContinuousRecognitionAsync()?.get() recognizer?.stopContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
callback.onSessionStopped() callback.onSessionStopped()
return true return true
} catch (e: Exception) { } catch (e: Exception) {
callback.onError("停止连续识别失败: ${e.message}") callback.onError("停止连续识别失败: ${e.message}")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopRecording()
return false return false
} }
} }
@ -232,8 +348,7 @@ class AzureAsrHelper {
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
audioConfig?.close() releaseAudioResources()
audioConfig = null
speechConfig?.close() speechConfig?.close()
speechConfig = null speechConfig = null
@ -243,6 +358,39 @@ class AzureAsrHelper {
// 忽略异常 // 忽略异常
} }
} }
// 释放音频相关资源
private fun releaseAudioResources() {
// 停止录音
stopRecording()
// 释放 AudioRecord
try {
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
Log.e(TAG, "释放 AudioRecord 失败: ${e.message}", e)
}
// 释放 AudioConfig
try {
audioConfig?.close()
audioConfig = null
} catch (e: Exception) {
Log.e(TAG, "释放 AudioConfig 失败: ${e.message}", e)
}
// 释放 PushStream
try {
pushStream?.close()
pushStream = null
} catch (e: Exception) {
Log.e(TAG, "释放 PushStream 失败: ${e.message}", e)
}
// 释放回音消除资源
releaseEchoCancellation()
}
// 一次性识别回调接口 // 一次性识别回调接口
interface RecognizeCallback { interface RecognizeCallback {
@ -288,4 +436,105 @@ class AzureAsrHelper {
} }
}) })
} }
// 设置回音消除
private fun setupEchoCancellation() {
try {
// 检查设备是否支持回音消除
if (AcousticEchoCanceler.isAvailable()) {
Log.d(TAG, "设备支持回音消除")
// 使用实际的 AudioRecord 会话 ID
if (audioSessionId != -1) {
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler != null) {
echoCanceler?.enabled = true
isEchoCancellationEnabled = echoCanceler?.enabled == true
Log.d(TAG, "回音消除已启用: $isEchoCancellationEnabled")
} else {
Log.e(TAG, "创建回音消除器失败")
}
} else {
Log.e(TAG, "无效的音频会话 ID")
}
} else {
Log.d(TAG, "设备不支持回音消除")
}
} catch (e: Exception) {
Log.e(TAG, "设置回音消除时出错: ${e.message}", e)
}
}
// 释放回音消除资源
private fun releaseEchoCancellation() {
try {
if (echoCanceler != null) {
echoCanceler?.enabled = false
echoCanceler?.release()
echoCanceler = null
isEchoCancellationEnabled = false
audioSessionId = -1
Log.d(TAG, "回音消除资源已释放")
}
} catch (e: Exception) {
Log.e(TAG, "释放回音消除资源时出错: ${e.message}", e)
}
}
// 检查回音消除是否可用
fun isEchoCancellationAvailable(): Boolean {
return AcousticEchoCanceler.isAvailable()
}
// 检查回音消除是否已启用
fun isEchoCancellationEnabled(): Boolean {
return isEchoCancellationEnabled
}
// 启用回音消除
fun enableEchoCancellation(): Boolean {
if (echoCanceler != null) {
try {
echoCanceler?.enabled = true
isEchoCancellationEnabled = echoCanceler?.enabled == true
Log.d(TAG, "回音消除已启用: $isEchoCancellationEnabled")
return isEchoCancellationEnabled
} catch (e: Exception) {
Log.e(TAG, "启用回音消除时出错: ${e.message}", e)
}
} else if (audioSessionId != -1) {
try {
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler != null) {
echoCanceler?.enabled = true
isEchoCancellationEnabled = echoCanceler?.enabled == true
Log.d(TAG, "回音消除已创建并启用: $isEchoCancellationEnabled")
return isEchoCancellationEnabled
}
} catch (e: Exception) {
Log.e(TAG, "创建并启用回音消除时出错: ${e.message}", e)
}
}
return false
}
// 禁用回音消除
fun disableEchoCancellation(): Boolean {
if (echoCanceler != null) {
try {
echoCanceler?.enabled = false
isEchoCancellationEnabled = echoCanceler?.enabled == true
Log.d(TAG, "回音消除已禁用")
return !isEchoCancellationEnabled
} catch (e: Exception) {
Log.e(TAG, "禁用回音消除时出错: ${e.message}", e)
}
}
return true
}
// 获取音频会话ID
fun getAudioSessionId(): Int {
return audioSessionId
}
} }

24
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt

@ -156,21 +156,7 @@ class MainActivity: FlutterActivity() {
try { try {
events?.success(mapOf("type" to "channelReady")) events?.success(mapOf("type" to "channelReady"))
Log.d(TAG, "已发送 channelReady 事件") Log.d(TAG, "已发送 channelReady 事件")
// // 测试发送一个聊天记录事件
// Handler(Looper.getMainLooper()).postDelayed({
// if (voiceInteractionEventSink != null) {
// Log.d(TAG, "发送测试聊天记录事件")
// sendChatHistoryEvent(
// "personal_assistant",
// "这是一条测试用户消息",
// "这是一条测试助手回复",
// System.currentTimeMillis()
// )
// } else {
// Log.e(TAG, "无法发送测试事件:事件通道未准备好")
// }
// }, 5000) // 5秒后发送测试事件
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送 channelReady 事件失败: ${e.message}", e) Log.e(TAG, "发送 channelReady 事件失败: ${e.message}", e)
} }
@ -221,16 +207,16 @@ class MainActivity: FlutterActivity() {
// 为所有方法返回默认值或成功状态 // 为所有方法返回默认值或成功状态
when (call.method) { when (call.method) {
"isEchoCancellationAvailable" -> { "isEchoCancellationAvailable" -> {
result.success(false) result.success(azureAsrHelper.isEchoCancellationAvailable())
} }
"isNoiseSuppressionAvailable" -> { "isNoiseSuppressionAvailable" -> {
result.success(false) result.success(false)
} }
"enableEchoCancellation" -> { "enableEchoCancellation" -> {
result.success(true) result.success(azureAsrHelper.enableEchoCancellation())
} }
"disableEchoCancellation" -> { "disableEchoCancellation" -> {
result.success(true) result.success(azureAsrHelper.disableEchoCancellation())
} }
"enableNoiseSuppression" -> { "enableNoiseSuppression" -> {
result.success(true) result.success(true)
@ -239,7 +225,7 @@ class MainActivity: FlutterActivity() {
result.success(true) result.success(true)
} }
"getAudioSessionId" -> { "getAudioSessionId" -> {
result.success(1) result.success(azureAsrHelper.getAudioSessionId())
} }
else -> { else -> {
result.notImplemented() result.notImplemented()

51
lib/app/theme/app_theme.dart

@ -18,16 +18,55 @@ class AppTheme {
static final ThemeData darkTheme = ThemeData( static final ThemeData darkTheme = ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
primaryColor: Colors.blue, primaryColor: const Color(0xFF0066CC), // 深蓝色主色调
scaffoldBackgroundColor: Colors.grey[900], primarySwatch: Colors.blue,
appBarTheme: AppBarTheme( scaffoldBackgroundColor: Colors.black, // 纯黑色背景
backgroundColor: Colors.grey[900], cardColor: const Color(0xFF1A1A1A), // 深灰色卡片背景
dividerColor: Colors.white12,
colorScheme: const ColorScheme.dark(
primary: Color(0xFF0066CC), // 主色调
secondary: Color(0xFF5E5CE6), // 紫色次要色调
surface: Color(0xFF1A1A1A), // 表面颜色
background: Colors.black, // 背景色
error: Colors.redAccent,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
bodySmall: TextStyle(color: Colors.white70),
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.black,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
iconTheme: IconThemeData(color: Colors.white),
), ),
bottomNavigationBarTheme: const BottomNavigationBarThemeData( bottomNavigationBarTheme: const BottomNavigationBarThemeData(
selectedItemColor: Colors.blue, backgroundColor: Color(0xFF1A1A1A), // 深灰色导航栏背景
unselectedItemColor: Colors.grey, selectedItemColor: Colors.white, // 选中项为白色
unselectedItemColor: Colors.grey, // 未选中项为灰色
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF0066CC)),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
), ),
); );
} }

5
lib/core/widgets/common_bottom_nav.dart

@ -28,6 +28,11 @@ class CommonBottomNav extends StatelessWidget {
break; break;
} }
}, },
backgroundColor: const Color(0xFFF9FAFB), // 浅灰色背景 #F9FAFB
selectedItemColor: const Color(0xFF3B82F6), // 蓝色选中项 #3B82F6
unselectedItemColor: const Color(0xFF6B7280), // 灰色未选中项 #6B7280
elevation: 8, // 添加阴影
type: BottomNavigationBarType.fixed,
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.home), icon: const Icon(Icons.home),

8
lib/main.dart

@ -10,6 +10,7 @@ import 'dart:io';
import 'package:window_size/window_size.dart'; import 'package:window_size/window_size.dart';
import 'app/translations/app_translations.dart'; import 'app/translations/app_translations.dart';
import 'data/services/voice_interaction_service.dart'; import 'data/services/voice_interaction_service.dart';
import 'app/theme/app_theme.dart';
void main() async { void main() async {
// 确保Flutter绑定初始化 // 确保Flutter绑定初始化
@ -71,10 +72,9 @@ class MainApp extends StatelessWidget {
builder: (_, child) { builder: (_, child) {
return GetMaterialApp( return GetMaterialApp(
title: 'Deep Voice', title: 'Deep Voice',
theme: ThemeData( theme: AppTheme.lightTheme,
primarySwatch: Colors.blue, darkTheme: AppTheme.darkTheme,
visualDensity: VisualDensity.adaptivePlatformDensity, themeMode: ThemeMode.dark,
),
initialBinding: InitialBinding(), initialBinding: InitialBinding(),
initialRoute: Routes.splash, initialRoute: Routes.splash,
getPages: AppPages.pages, getPages: AppPages.pages,

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

@ -490,7 +490,7 @@ class HomeController extends GetxController {
// 打开AI女友页面 // 打开AI女友页面
void openAIGirlfriend() { void openAIGirlfriend() {
Get.toNamed(Routes.chat); Get.toNamed(Routes.chat, arguments: {'agentId': 'cyber_girlfriend'});
} }
// 打开AI助理(已有方法 exploreAIFeatures) // 打开AI助理(已有方法 exploreAIFeatures)

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

@ -118,6 +118,7 @@ class HomeView extends GetView<HomeController> {
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[800],
), ),
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
@ -655,6 +656,13 @@ class HomeView extends GetView<HomeController> {
color: Colors.white, color: Colors.white,
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
shadows: [
Shadow(
offset: Offset(0, 1),
blurRadius: 3.0,
color: Colors.black.withOpacity(0.3),
),
],
), ),
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
@ -663,8 +671,15 @@ class HomeView extends GetView<HomeController> {
child: Text( child: Text(
description, description,
style: TextStyle( style: TextStyle(
color: Colors.white.withOpacity(0.9), color: Colors.white,
fontSize: 12.sp, fontSize: 12.sp,
shadows: [
Shadow(
offset: Offset(0, 1),
blurRadius: 2.0,
color: Colors.black.withOpacity(0.2),
),
],
), ),
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -703,6 +718,13 @@ class HomeView extends GetView<HomeController> {
color: Colors.white, color: Colors.white,
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
shadows: [
Shadow(
offset: Offset(0, 1),
blurRadius: 3.0,
color: Colors.black.withOpacity(0.3),
),
],
), ),
), ),
SizedBox(height: 2.h), SizedBox(height: 2.h),
@ -710,8 +732,15 @@ class HomeView extends GetView<HomeController> {
Text( Text(
description, description,
style: TextStyle( style: TextStyle(
color: Colors.white.withOpacity(0.9), color: Colors.white,
fontSize: 10.sp, fontSize: 10.sp,
shadows: [
Shadow(
offset: Offset(0, 1),
blurRadius: 2.0,
color: Colors.black.withOpacity(0.2),
),
],
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

134
lib/modules/login/views/login_view.dart

@ -3,18 +3,22 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../controllers/login_controller.dart'; import '../controllers/login_controller.dart';
import '../../../app/theme/app_theme.dart';
class LoginView extends GetView<LoginController> { class LoginView extends GetView<LoginController> {
const LoginView({Key? key}) : super(key: key); const LoginView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( // 使用浅色主题
backgroundColor: Colors.grey[50], return Theme(
body: SafeArea( data: AppTheme.lightTheme,
child: Obx(() => controller.viewState == LoginViewState.welcome child: Scaffold(
? _buildWelcomeView() body: SafeArea(
: _buildVerificationView(context)), child: Obx(() => controller.viewState == LoginViewState.welcome
? _buildWelcomeView()
: _buildVerificationView(context)),
),
), ),
); );
} }
@ -107,16 +111,16 @@ class LoginView extends GetView<LoginController> {
'9:41', '9:41',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Colors.grey[800], color: Colors.black54,
), ),
), ),
Row( Row(
children: [ children: [
Icon(Icons.signal_cellular_4_bar, size: 12.sp, color: Colors.grey[800]), Icon(Icons.signal_cellular_4_bar, size: 12.sp, color: Colors.black54),
SizedBox(width: 4.w), SizedBox(width: 4.w),
Icon(Icons.wifi, size: 12.sp, color: Colors.grey[800]), Icon(Icons.wifi, size: 12.sp, color: Colors.black54),
SizedBox(width: 4.w), SizedBox(width: 4.w),
Icon(Icons.battery_full, size: 12.sp, color: Colors.grey[800]), Icon(Icons.battery_full, size: 12.sp, color: Colors.black54),
], ],
), ),
], ],
@ -134,12 +138,12 @@ class LoginView extends GetView<LoginController> {
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Colors.blue, Colors.purple], colors: [Color(0xFF0066CC), Color(0xFF5E5CE6)],
), ),
borderRadius: BorderRadius.circular(24.r), borderRadius: BorderRadius.circular(24.r),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.blue.withOpacity(0.3), color: const Color(0xFF0066CC).withOpacity(0.3),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),
@ -157,7 +161,7 @@ class LoginView extends GetView<LoginController> {
style: TextStyle( style: TextStyle(
fontSize: 24.sp, fontSize: 24.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[800], color: Colors.black,
), ),
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
@ -165,7 +169,7 @@ class LoginView extends GetView<LoginController> {
'连接世界,超越声音', '连接世界,超越声音',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[500], color: Colors.black54,
), ),
), ),
], ],
@ -181,7 +185,7 @@ class LoginView extends GetView<LoginController> {
style: TextStyle( style: TextStyle(
fontSize: 20.sp, fontSize: 20.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[800], color: Colors.black,
), ),
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
@ -189,7 +193,7 @@ class LoginView extends GetView<LoginController> {
'请使用手机号或邮箱登录/注册', '请使用手机号或邮箱登录/注册',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[500], color: Colors.black54,
), ),
), ),
], ],
@ -200,15 +204,8 @@ class LoginView extends GetView<LoginController> {
Widget _buildInputField() { Widget _buildInputField() {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
@ -216,7 +213,7 @@ class LoginView extends GetView<LoginController> {
children: [ children: [
Icon( Icon(
Icons.person_outline, Icons.person_outline,
color: Colors.grey[400], color: Colors.black54,
size: 20.sp, size: 20.sp,
), ),
SizedBox(width: 12.w), SizedBox(width: 12.w),
@ -227,13 +224,13 @@ class LoginView extends GetView<LoginController> {
hintText: '手机号码或邮箱', hintText: '手机号码或邮箱',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[400], color: Colors.black38,
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[700], color: Colors.black,
), ),
), ),
), ),
@ -247,15 +244,8 @@ class LoginView extends GetView<LoginController> {
Widget _buildVerificationInputField() { Widget _buildVerificationInputField() {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only(left: 16.w, right: 4.w, top: 4.h, bottom: 4.h), padding: EdgeInsets.only(left: 16.w, right: 4.w, top: 4.h, bottom: 4.h),
@ -263,7 +253,7 @@ class LoginView extends GetView<LoginController> {
children: [ children: [
Icon( Icon(
Icons.lock_outline, Icons.lock_outline,
color: Colors.grey[400], color: Colors.black54,
size: 20.sp, size: 20.sp,
), ),
SizedBox(width: 12.w), SizedBox(width: 12.w),
@ -276,14 +266,14 @@ class LoginView extends GetView<LoginController> {
hintText: '验证码', hintText: '验证码',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[400], color: Colors.black38,
), ),
border: InputBorder.none, border: InputBorder.none,
counterText: '', counterText: '',
), ),
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[700], color: Colors.black,
), ),
), ),
), ),
@ -293,9 +283,9 @@ class LoginView extends GetView<LoginController> {
: controller.getVerificationCode, : controller.getVerificationCode,
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 16.w), padding: EdgeInsets.symmetric(horizontal: 16.w),
foregroundColor: Colors.blue[600], foregroundColor: const Color(0xFF0066CC),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
disabledForegroundColor: Colors.grey[400], disabledForegroundColor: Colors.black38,
), ),
child: Text( child: Text(
controller.countdownSeconds > 0 controller.countdownSeconds > 0
@ -320,12 +310,12 @@ class LoginView extends GetView<LoginController> {
child: Obx(() => ElevatedButton( child: Obx(() => ElevatedButton(
onPressed: controller.isLoading ? null : controller.loginWithVerificationCode, onPressed: controller.isLoading ? null : controller.loginWithVerificationCode,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600], backgroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 16.h), padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
), ),
elevation: 2, elevation: 0,
), ),
child: controller.isLoading child: controller.isLoading
? SizedBox( ? SizedBox(
@ -354,25 +344,25 @@ class LoginView extends GetView<LoginController> {
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: Divider(color: Colors.grey[300])), Expanded(child: Divider(color: Colors.black12)),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w), padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text( child: Text(
'或使用以下方式登录', '或使用以下方式登录',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Colors.grey[500], color: Colors.black45,
), ),
), ),
), ),
Expanded(child: Divider(color: Colors.grey[300])), Expanded(child: Divider(color: Colors.black12)),
], ],
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildSocialButton(FontAwesomeIcons.google, Colors.grey[700]!), _buildSocialButton(FontAwesomeIcons.google, Colors.red),
SizedBox(width: 24.w), SizedBox(width: 24.w),
_buildSocialButton(FontAwesomeIcons.weixin, Colors.green), _buildSocialButton(FontAwesomeIcons.weixin, Colors.green),
], ],
@ -387,15 +377,8 @@ class LoginView extends GetView<LoginController> {
width: 48.w, width: 48.w,
height: 48.w, height: 48.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(24.r), borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: IconButton( child: IconButton(
icon: Icon(icon, color: color, size: 20.sp), icon: Icon(icon, color: color, size: 20.sp),
@ -412,7 +395,7 @@ class LoginView extends GetView<LoginController> {
'登录/注册即表示你同意', '登录/注册即表示你同意',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Colors.grey[500], color: Colors.black45,
), ),
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
@ -425,7 +408,7 @@ class LoginView extends GetView<LoginController> {
'《用户协议》', '《用户协议》',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Colors.blue[600], color: const Color(0xFF0066CC),
), ),
), ),
), ),
@ -433,7 +416,7 @@ class LoginView extends GetView<LoginController> {
' 和 ', ' 和 ',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Colors.grey[500], color: Colors.black45,
), ),
), ),
GestureDetector( GestureDetector(
@ -442,7 +425,7 @@ class LoginView extends GetView<LoginController> {
'《隐私政策》', '《隐私政策》',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Colors.blue[600], color: const Color(0xFF0066CC),
), ),
), ),
), ),
@ -458,18 +441,11 @@ class LoginView extends GetView<LoginController> {
width: 40.w, width: 40.w,
height: 40.w, height: 40.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(20.r), borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.grey[500], size: 20.sp), icon: Icon(Icons.arrow_back, color: Colors.black54, size: 20.sp),
onPressed: controller.showWelcomeView, onPressed: controller.showWelcomeView,
), ),
); );
@ -485,7 +461,7 @@ class LoginView extends GetView<LoginController> {
style: TextStyle( style: TextStyle(
fontSize: 20.sp, fontSize: 20.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[800], color: Colors.black,
), ),
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
@ -493,7 +469,7 @@ class LoginView extends GetView<LoginController> {
'验证码已发送至 ${controller.displayContact}', '验证码已发送至 ${controller.displayContact}',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[500], color: Colors.black54,
), ),
)), )),
], ],
@ -514,10 +490,10 @@ class LoginView extends GetView<LoginController> {
width: 60.w, width: 60.w,
height: 60.w, height: 60.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100]!.withOpacity(0.5), color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
border: Border.all( border: Border.all(
color: Colors.grey[300]!, color: Colors.black12,
width: 1, width: 1,
), ),
), ),
@ -539,7 +515,7 @@ class LoginView extends GetView<LoginController> {
style: TextStyle( style: TextStyle(
fontSize: 24.sp, fontSize: 24.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[800], color: Colors.black,
), ),
), ),
), ),
@ -557,8 +533,8 @@ class LoginView extends GetView<LoginController> {
Icons.refresh, Icons.refresh,
size: 14.sp, size: 14.sp,
color: controller.countdownSeconds > 0 color: controller.countdownSeconds > 0
? Colors.grey[400] ? Colors.black38
: Colors.blue[600], : const Color(0xFF0066CC),
), ),
label: Text( label: Text(
controller.countdownSeconds > 0 controller.countdownSeconds > 0
@ -567,8 +543,8 @@ class LoginView extends GetView<LoginController> {
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: controller.countdownSeconds > 0 color: controller.countdownSeconds > 0
? Colors.grey[400] ? Colors.black38
: Colors.blue[600], : const Color(0xFF0066CC),
), ),
), ),
)), )),
@ -582,12 +558,12 @@ class LoginView extends GetView<LoginController> {
child: Obx(() => ElevatedButton( child: Obx(() => ElevatedButton(
onPressed: controller.isLoading ? null : controller.login, onPressed: controller.isLoading ? null : controller.login,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600], backgroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 16.h), padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
), ),
elevation: 2, elevation: 0,
), ),
child: controller.isLoading child: controller.isLoading
? SizedBox( ? SizedBox(
@ -619,7 +595,7 @@ class LoginView extends GetView<LoginController> {
'使用密码登录', '使用密码登录',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.blue[600], color: const Color(0xFF0066CC),
), ),
), ),
), ),

76
lib/modules/pairing/views/pairing_view.dart

@ -3,25 +3,29 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart'; import '../controllers/pairing_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart'; import '../../../core/widgets/common_bottom_nav.dart';
import '../../../app/theme/app_theme.dart';
class PairingView extends GetView<PairingController> { class PairingView extends GetView<PairingController> {
const PairingView({Key? key}) : super(key: key); const PairingView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( // 使用浅色主题
backgroundColor: Colors.grey[50], return Theme(
body: SafeArea( data: AppTheme.lightTheme,
child: Stack( child: Scaffold(
children: [ body: SafeArea(
// 主要内容 child: Stack(
_buildMainContent(), children: [
], // 主要内容
_buildMainContent(),
],
),
),
bottomNavigationBar: Opacity(
opacity: 0.5, // 保持半透明效果
child: const CommonBottomNav(currentIndex: 0),
), ),
),
bottomNavigationBar: Opacity(
opacity: 0.5, // 保持半透明效果
child: const CommonBottomNav(currentIndex: 0),
), ),
); );
} }
@ -63,7 +67,7 @@ class PairingView extends GetView<PairingController> {
style: TextStyle( style: TextStyle(
fontSize: 24.sp, fontSize: 24.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[800], color: Colors.black,
), ),
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
@ -71,7 +75,7 @@ class PairingView extends GetView<PairingController> {
'连接世界,超越声音', '连接世界,超越声音',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[500], color: Colors.black54,
), ),
), ),
], ],
@ -116,8 +120,8 @@ class PairingView extends GetView<PairingController> {
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
Colors.blue.withOpacity(0.2), const Color(0xFF0066CC).withOpacity(0.2),
Colors.purple.withOpacity(0.2), const Color(0xFF5E5CE6).withOpacity(0.2),
], ],
), ),
shape: BoxShape.circle, shape: BoxShape.circle,
@ -134,12 +138,12 @@ class PairingView extends GetView<PairingController> {
width: 140.w, width: 140.w,
height: 140.w, height: 140.w,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
Colors.blue, Color(0xFF0066CC),
Colors.purple, Color(0xFF5E5CE6),
], ],
), ),
shape: BoxShape.circle, shape: BoxShape.circle,
@ -159,8 +163,8 @@ class PairingView extends GetView<PairingController> {
height: 180.w, height: 180.w,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 4.w, strokeWidth: 4.w,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF0066CC)),
backgroundColor: Colors.blue.withOpacity(0.1), backgroundColor: const Color(0xFF0066CC).withOpacity(0.1),
), ),
); );
} }
@ -177,7 +181,7 @@ class PairingView extends GetView<PairingController> {
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[800], color: Colors.black,
), ),
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
@ -185,7 +189,7 @@ class PairingView extends GetView<PairingController> {
'请打开耳机充电盒,将耳机放入您的耳朵', '请打开耳机充电盒,将耳机放入您的耳朵',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[500], color: Colors.black54,
), ),
), ),
], ],
@ -213,7 +217,7 @@ class PairingView extends GetView<PairingController> {
'正在跳转到主页...', '正在跳转到主页...',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[500], color: Colors.black54,
), ),
), ),
], ],
@ -241,7 +245,7 @@ class PairingView extends GetView<PairingController> {
controller.errorMessage, controller.errorMessage,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[600], color: Colors.black54,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -256,7 +260,7 @@ class PairingView extends GetView<PairingController> {
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
), ),
), ),
child: Text('打开设置'), child: const Text('打开设置'),
), ),
], ],
); );
@ -284,7 +288,7 @@ class PairingView extends GetView<PairingController> {
'请确保耳机已开机并在附近' : controller.errorMessage, '请确保耳机已开机并在附近' : controller.errorMessage,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[600], color: Colors.black54,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -296,14 +300,14 @@ class PairingView extends GetView<PairingController> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.bluetooth_searching, color: Colors.blue, size: 20.sp), Icon(Icons.bluetooth_searching, color: const Color(0xFF0066CC), size: 20.sp),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( Text(
'需要系统配对', '需要系统配对',
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.blue, color: const Color(0xFF0066CC),
), ),
), ),
], ],
@ -314,7 +318,7 @@ class PairingView extends GetView<PairingController> {
'请在系统设置中配对您的设备' : controller.errorMessage, '请在系统设置中配对您的设备' : controller.errorMessage,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[600], color: Colors.black54,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -322,19 +326,19 @@ class PairingView extends GetView<PairingController> {
ElevatedButton( ElevatedButton(
onPressed: () => controller.openSystemBluetoothSettings(), onPressed: () => controller.openSystemBluetoothSettings(),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, backgroundColor: const Color(0xFF0066CC),
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
), ),
), ),
child: Text('打开蓝牙设置'), child: const Text('打开蓝牙设置'),
), ),
], ],
); );
default: default:
return SizedBox(); return const SizedBox();
} }
}); });
} }
@ -348,8 +352,8 @@ class PairingView extends GetView<PairingController> {
child: ElevatedButton( child: ElevatedButton(
onPressed: controller.refreshSearch, onPressed: controller.refreshSearch,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200], backgroundColor: Colors.grey.withOpacity(0.1),
foregroundColor: Colors.grey[800], foregroundColor: Colors.black,
padding: EdgeInsets.symmetric(vertical: 16.h), padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
@ -371,7 +375,7 @@ class PairingView extends GetView<PairingController> {
child: TextButton( child: TextButton(
onPressed: controller.skipConnecting, onPressed: controller.skipConnecting,
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Colors.blue[600], foregroundColor: const Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h), padding: EdgeInsets.symmetric(vertical: 12.h),
), ),
child: Text( child: Text(

Loading…
Cancel
Save