From 03e8136415863d81ff8267ecc4b65d2e9945d188 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 11 Mar 2025 16:37:30 +0000 Subject: [PATCH] v006 --- .../deep_voice/AudioProcessingPlugin.kt | 572 ------------------ .../com/example/deep_voice/AzureAsrHelper.kt | 275 ++++++++- .../com/example/deep_voice/MainActivity.kt | 24 +- lib/app/theme/app_theme.dart | 51 +- lib/core/widgets/common_bottom_nav.dart | 5 + lib/main.dart | 8 +- .../home/controllers/home_controller.dart | 2 +- lib/modules/home/views/home_view.dart | 33 +- lib/modules/login/views/login_view.dart | 134 ++-- lib/modules/pairing/views/pairing_view.dart | 76 +-- 10 files changed, 448 insertions(+), 732 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt b/android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt deleted file mode 100644 index 074880e9a..000000000 --- a/android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt +++ /dev/null @@ -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("audioSessionId") - if (audioSessionId == null) { - result.error("INVALID_ARGUMENT", "音频会话ID不能为空", null) - return - } - result.success(enableEchoCancellation(audioSessionId)) - } - "disableEchoCancellation" -> { - result.success(disableEchoCancellation()) - } - "enableNoiseSuppression" -> { - val audioSessionId = call.argument("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 - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt index 185cbf7bd..4ec14fd2e 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt +++ b/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.util.EventHandler 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 { private var recognizer: SpeechRecognizer? = null @@ -15,6 +20,17 @@ class AzureAsrHelper { private var currentLanguage = "zh-CN" private var subscriptionKey = "" 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 fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { @@ -38,10 +54,9 @@ class AzureAsrHelper { speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) speechConfig?.speechRecognitionLanguage = language - // 创建音频配置 - Log.d(TAG, "创建默认麦克风音频配置") + // 创建 PushAudioInputStream 和 AudioConfig try { - audioConfig = AudioConfig.fromDefaultMicrophoneInput() + setupAudioInput() Log.d(TAG, "成功创建音频配置") } catch (e: Exception) { 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) { if (recognizer == null) { @@ -72,14 +129,21 @@ class AzureAsrHelper { } try { + // 开始录音并推送数据 + startRecording() + val result = recognizer?.recognizeOnceAsync()?.get() + // 停止录音 + stopRecording() + if (result != null && result.reason == ResultReason.RecognizedSpeech) { callback.onResult(result.text) } else { callback.onError("未能识别语音") } } catch (e: Exception) { + stopRecording() callback.onError("识别异常: ${e.message}") } } @@ -105,18 +169,13 @@ class AzureAsrHelper { } try { - // 先发送会话开始事件 - // Log.d(TAG, "准备开始连续语音识别") - // callback.onSessionStarted() - // 重新创建识别器 Log.d(TAG, "重新创建识别器") recognizer?.close() try { - // 重新创建音频配置 - audioConfig?.close() - audioConfig = AudioConfig.fromDefaultMicrophoneInput() + // 重新设置音频输入 + setupAudioInput() Log.d(TAG, "重新创建音频配置成功") } catch (e: Exception) { Log.e(TAG, "重新创建音频配置失败: ${e.message}", e) @@ -144,7 +203,7 @@ class AzureAsrHelper { recognizer.recognizing.addEventListener( EventHandler { _, event -> if (event.result.reason == ResultReason.RecognizingSpeech) { - Log.d(TAG, "正在识别: ${event.result.text}") + // Log.d(TAG, "正在识别: ${event.result.text}") callback.onRecognizing(event.result.text) } } @@ -164,6 +223,7 @@ class AzureAsrHelper { EventHandler { _, _ -> Log.d(TAG, "会话结束") isContinuousRecognitionActive = false + stopRecording() callback.onSessionStopped() } ) @@ -175,9 +235,13 @@ class AzureAsrHelper { Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails") callback.onCanceled(event.reason.toString(), errorDetails) isContinuousRecognitionActive = false + stopRecording() } ) + // 开始录音 + startRecording() + // 开始连续识别 Log.d(TAG, "开始连续识别") recognizer.startContinuousRecognitionAsync().get() @@ -190,10 +254,60 @@ class AzureAsrHelper { Log.e(TAG, "开始连续识别失败: ${e.message}", e) callback.onError("开始连续识别失败: ${e.message}") isContinuousRecognitionActive = false + stopRecording() 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 { if (!isContinuousRecognitionActive || recognizer == null) { @@ -203,11 +317,13 @@ class AzureAsrHelper { try { recognizer?.stopContinuousRecognitionAsync()?.get() isContinuousRecognitionActive = false + stopRecording() callback.onSessionStopped() return true } catch (e: Exception) { callback.onError("停止连续识别失败: ${e.message}") isContinuousRecognitionActive = false + stopRecording() return false } } @@ -232,8 +348,7 @@ class AzureAsrHelper { recognizer?.close() recognizer = null - audioConfig?.close() - audioConfig = null + releaseAudioResources() speechConfig?.close() 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 { @@ -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 + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index f9e9ba74e..c58930736 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -156,21 +156,7 @@ class MainActivity: FlutterActivity() { try { events?.success(mapOf("type" to "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) { Log.e(TAG, "发送 channelReady 事件失败: ${e.message}", e) } @@ -221,16 +207,16 @@ class MainActivity: FlutterActivity() { // 为所有方法返回默认值或成功状态 when (call.method) { "isEchoCancellationAvailable" -> { - result.success(false) + result.success(azureAsrHelper.isEchoCancellationAvailable()) } "isNoiseSuppressionAvailable" -> { result.success(false) } "enableEchoCancellation" -> { - result.success(true) + result.success(azureAsrHelper.enableEchoCancellation()) } "disableEchoCancellation" -> { - result.success(true) + result.success(azureAsrHelper.disableEchoCancellation()) } "enableNoiseSuppression" -> { result.success(true) @@ -239,7 +225,7 @@ class MainActivity: FlutterActivity() { result.success(true) } "getAudioSessionId" -> { - result.success(1) + result.success(azureAsrHelper.getAudioSessionId()) } else -> { result.notImplemented() diff --git a/lib/app/theme/app_theme.dart b/lib/app/theme/app_theme.dart index c48b8e25f..9d64fb143 100644 --- a/lib/app/theme/app_theme.dart +++ b/lib/app/theme/app_theme.dart @@ -18,16 +18,55 @@ class AppTheme { static final ThemeData darkTheme = ThemeData( brightness: Brightness.dark, - primaryColor: Colors.blue, - scaffoldBackgroundColor: Colors.grey[900], - appBarTheme: AppBarTheme( - backgroundColor: Colors.grey[900], + primaryColor: const Color(0xFF0066CC), // 深蓝色主色调 + primarySwatch: Colors.blue, + scaffoldBackgroundColor: Colors.black, // 纯黑色背景 + 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, elevation: 0, + iconTheme: IconThemeData(color: Colors.white), ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( - selectedItemColor: Colors.blue, - unselectedItemColor: Colors.grey, + backgroundColor: Color(0xFF1A1A1A), // 深灰色导航栏背景 + 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), + ), ), ); } \ No newline at end of file diff --git a/lib/core/widgets/common_bottom_nav.dart b/lib/core/widgets/common_bottom_nav.dart index 015feadfe..387c40286 100644 --- a/lib/core/widgets/common_bottom_nav.dart +++ b/lib/core/widgets/common_bottom_nav.dart @@ -28,6 +28,11 @@ class CommonBottomNav extends StatelessWidget { break; } }, + backgroundColor: const Color(0xFFF9FAFB), // 浅灰色背景 #F9FAFB + selectedItemColor: const Color(0xFF3B82F6), // 蓝色选中项 #3B82F6 + unselectedItemColor: const Color(0xFF6B7280), // 灰色未选中项 #6B7280 + elevation: 8, // 添加阴影 + type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( icon: const Icon(Icons.home), diff --git a/lib/main.dart b/lib/main.dart index 3ddff4655..32ca30f49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:window_size/window_size.dart'; import 'app/translations/app_translations.dart'; import 'data/services/voice_interaction_service.dart'; +import 'app/theme/app_theme.dart'; void main() async { // 确保Flutter绑定初始化 @@ -71,10 +72,9 @@ class MainApp extends StatelessWidget { builder: (_, child) { return GetMaterialApp( title: 'Deep Voice', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.dark, initialBinding: InitialBinding(), initialRoute: Routes.splash, getPages: AppPages.pages, diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 58b479e3a..c251295dc 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -490,7 +490,7 @@ class HomeController extends GetxController { // 打开AI女友页面 void openAIGirlfriend() { - Get.toNamed(Routes.chat); + Get.toNamed(Routes.chat, arguments: {'agentId': 'cyber_girlfriend'}); } // 打开AI助理(已有方法 exploreAIFeatures) diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 059bb342c..146289c4f 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -118,6 +118,7 @@ class HomeView extends GetView { style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, + color: Colors.grey[800], ), ), SizedBox(width: 8.w), @@ -655,6 +656,13 @@ class HomeView extends GetView { color: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.bold, + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 3.0, + color: Colors.black.withOpacity(0.3), + ), + ], ), ), SizedBox(height: 4.h), @@ -663,8 +671,15 @@ class HomeView extends GetView { child: Text( description, style: TextStyle( - color: Colors.white.withOpacity(0.9), + color: Colors.white, fontSize: 12.sp, + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 2.0, + color: Colors.black.withOpacity(0.2), + ), + ], ), maxLines: 3, overflow: TextOverflow.ellipsis, @@ -703,6 +718,13 @@ class HomeView extends GetView { color: Colors.white, fontSize: 14.sp, fontWeight: FontWeight.bold, + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 3.0, + color: Colors.black.withOpacity(0.3), + ), + ], ), ), SizedBox(height: 2.h), @@ -710,8 +732,15 @@ class HomeView extends GetView { Text( description, style: TextStyle( - color: Colors.white.withOpacity(0.9), + color: Colors.white, fontSize: 10.sp, + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 2.0, + color: Colors.black.withOpacity(0.2), + ), + ], ), maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 931a243dd..7b0528ebe 100644 --- a/lib/modules/login/views/login_view.dart +++ b/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:font_awesome_flutter/font_awesome_flutter.dart'; import '../controllers/login_controller.dart'; +import '../../../app/theme/app_theme.dart'; class LoginView extends GetView { const LoginView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[50], - body: SafeArea( - child: Obx(() => controller.viewState == LoginViewState.welcome - ? _buildWelcomeView() - : _buildVerificationView(context)), + // 使用浅色主题 + return Theme( + data: AppTheme.lightTheme, + child: Scaffold( + body: SafeArea( + child: Obx(() => controller.viewState == LoginViewState.welcome + ? _buildWelcomeView() + : _buildVerificationView(context)), + ), ), ); } @@ -107,16 +111,16 @@ class LoginView extends GetView { '9:41', style: TextStyle( fontSize: 12.sp, - color: Colors.grey[800], + color: Colors.black54, ), ), Row( 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), - Icon(Icons.wifi, size: 12.sp, color: Colors.grey[800]), + Icon(Icons.wifi, size: 12.sp, color: Colors.black54), 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 { gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Colors.blue, Colors.purple], + colors: [Color(0xFF0066CC), Color(0xFF5E5CE6)], ), borderRadius: BorderRadius.circular(24.r), boxShadow: [ BoxShadow( - color: Colors.blue.withOpacity(0.3), + color: const Color(0xFF0066CC).withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), @@ -157,7 +161,7 @@ class LoginView extends GetView { style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold, - color: Colors.grey[800], + color: Colors.black, ), ), SizedBox(height: 8.h), @@ -165,7 +169,7 @@ class LoginView extends GetView { '连接世界,超越声音', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[500], + color: Colors.black54, ), ), ], @@ -181,7 +185,7 @@ class LoginView extends GetView { style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: Colors.black, ), ), SizedBox(height: 4.h), @@ -189,7 +193,7 @@ class LoginView extends GetView { '请使用手机号或邮箱登录/注册', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[500], + color: Colors.black54, ), ), ], @@ -200,15 +204,8 @@ class LoginView extends GetView { Widget _buildInputField() { return Container( decoration: BoxDecoration( - color: Colors.grey[100], + color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h), @@ -216,7 +213,7 @@ class LoginView extends GetView { children: [ Icon( Icons.person_outline, - color: Colors.grey[400], + color: Colors.black54, size: 20.sp, ), SizedBox(width: 12.w), @@ -227,13 +224,13 @@ class LoginView extends GetView { hintText: '手机号码或邮箱', hintStyle: TextStyle( fontSize: 14.sp, - color: Colors.grey[400], + color: Colors.black38, ), border: InputBorder.none, ), style: TextStyle( fontSize: 14.sp, - color: Colors.grey[700], + color: Colors.black, ), ), ), @@ -247,15 +244,8 @@ class LoginView extends GetView { Widget _buildVerificationInputField() { return Container( decoration: BoxDecoration( - color: Colors.grey[100], + color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], ), child: Padding( padding: EdgeInsets.only(left: 16.w, right: 4.w, top: 4.h, bottom: 4.h), @@ -263,7 +253,7 @@ class LoginView extends GetView { children: [ Icon( Icons.lock_outline, - color: Colors.grey[400], + color: Colors.black54, size: 20.sp, ), SizedBox(width: 12.w), @@ -276,14 +266,14 @@ class LoginView extends GetView { hintText: '验证码', hintStyle: TextStyle( fontSize: 14.sp, - color: Colors.grey[400], + color: Colors.black38, ), border: InputBorder.none, counterText: '', ), style: TextStyle( fontSize: 14.sp, - color: Colors.grey[700], + color: Colors.black, ), ), ), @@ -293,9 +283,9 @@ class LoginView extends GetView { : controller.getVerificationCode, style: TextButton.styleFrom( padding: EdgeInsets.symmetric(horizontal: 16.w), - foregroundColor: Colors.blue[600], + foregroundColor: const Color(0xFF0066CC), backgroundColor: Colors.transparent, - disabledForegroundColor: Colors.grey[400], + disabledForegroundColor: Colors.black38, ), child: Text( controller.countdownSeconds > 0 @@ -320,12 +310,12 @@ class LoginView extends GetView { child: Obx(() => ElevatedButton( onPressed: controller.isLoading ? null : controller.loginWithVerificationCode, style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue[600], + backgroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 16.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), ), - elevation: 2, + elevation: 0, ), child: controller.isLoading ? SizedBox( @@ -354,25 +344,25 @@ class LoginView extends GetView { children: [ Row( children: [ - Expanded(child: Divider(color: Colors.grey[300])), + Expanded(child: Divider(color: Colors.black12)), Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: Text( '或使用以下方式登录', style: TextStyle( 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), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildSocialButton(FontAwesomeIcons.google, Colors.grey[700]!), + _buildSocialButton(FontAwesomeIcons.google, Colors.red), SizedBox(width: 24.w), _buildSocialButton(FontAwesomeIcons.weixin, Colors.green), ], @@ -387,15 +377,8 @@ class LoginView extends GetView { width: 48.w, height: 48.w, decoration: BoxDecoration( - color: Colors.grey[100], + color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(24.r), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], ), child: IconButton( icon: Icon(icon, color: color, size: 20.sp), @@ -412,7 +395,7 @@ class LoginView extends GetView { '登录/注册即表示你同意', style: TextStyle( fontSize: 12.sp, - color: Colors.grey[500], + color: Colors.black45, ), ), SizedBox(height: 4.h), @@ -425,7 +408,7 @@ class LoginView extends GetView { '《用户协议》', style: TextStyle( fontSize: 12.sp, - color: Colors.blue[600], + color: const Color(0xFF0066CC), ), ), ), @@ -433,7 +416,7 @@ class LoginView extends GetView { ' 和 ', style: TextStyle( fontSize: 12.sp, - color: Colors.grey[500], + color: Colors.black45, ), ), GestureDetector( @@ -442,7 +425,7 @@ class LoginView extends GetView { '《隐私政策》', style: TextStyle( fontSize: 12.sp, - color: Colors.blue[600], + color: const Color(0xFF0066CC), ), ), ), @@ -458,18 +441,11 @@ class LoginView extends GetView { width: 40.w, height: 40.w, decoration: BoxDecoration( - color: Colors.grey[100], + color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], ), 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, ), ); @@ -485,7 +461,7 @@ class LoginView extends GetView { style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: Colors.black, ), ), SizedBox(height: 8.h), @@ -493,7 +469,7 @@ class LoginView extends GetView { '验证码已发送至 ${controller.displayContact}', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[500], + color: Colors.black54, ), )), ], @@ -514,10 +490,10 @@ class LoginView extends GetView { width: 60.w, height: 60.w, decoration: BoxDecoration( - color: Colors.grey[100]!.withOpacity(0.5), + color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(16.r), border: Border.all( - color: Colors.grey[300]!, + color: Colors.black12, width: 1, ), ), @@ -539,7 +515,7 @@ class LoginView extends GetView { style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: Colors.black, ), ), ), @@ -557,8 +533,8 @@ class LoginView extends GetView { Icons.refresh, size: 14.sp, color: controller.countdownSeconds > 0 - ? Colors.grey[400] - : Colors.blue[600], + ? Colors.black38 + : const Color(0xFF0066CC), ), label: Text( controller.countdownSeconds > 0 @@ -567,8 +543,8 @@ class LoginView extends GetView { style: TextStyle( fontSize: 14.sp, color: controller.countdownSeconds > 0 - ? Colors.grey[400] - : Colors.blue[600], + ? Colors.black38 + : const Color(0xFF0066CC), ), ), )), @@ -582,12 +558,12 @@ class LoginView extends GetView { child: Obx(() => ElevatedButton( onPressed: controller.isLoading ? null : controller.login, style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue[600], + backgroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 16.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), ), - elevation: 2, + elevation: 0, ), child: controller.isLoading ? SizedBox( @@ -619,7 +595,7 @@ class LoginView extends GetView { '使用密码登录', style: TextStyle( fontSize: 14.sp, - color: Colors.blue[600], + color: const Color(0xFF0066CC), ), ), ), diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 78eb730f6..993c12808 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -3,25 +3,29 @@ import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/pairing_controller.dart'; import '../../../core/widgets/common_bottom_nav.dart'; +import '../../../app/theme/app_theme.dart'; class PairingView extends GetView { const PairingView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.grey[50], - body: SafeArea( - child: Stack( - children: [ - // 主要内容 - _buildMainContent(), - ], + // 使用浅色主题 + return Theme( + data: AppTheme.lightTheme, + child: Scaffold( + body: SafeArea( + child: Stack( + 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 { style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold, - color: Colors.grey[800], + color: Colors.black, ), ), SizedBox(height: 8.h), @@ -71,7 +75,7 @@ class PairingView extends GetView { '连接世界,超越声音', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[500], + color: Colors.black54, ), ), ], @@ -116,8 +120,8 @@ class PairingView extends GetView { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - Colors.blue.withOpacity(0.2), - Colors.purple.withOpacity(0.2), + const Color(0xFF0066CC).withOpacity(0.2), + const Color(0xFF5E5CE6).withOpacity(0.2), ], ), shape: BoxShape.circle, @@ -134,12 +138,12 @@ class PairingView extends GetView { width: 140.w, height: 140.w, decoration: BoxDecoration( - gradient: LinearGradient( + gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - Colors.blue, - Colors.purple, + Color(0xFF0066CC), + Color(0xFF5E5CE6), ], ), shape: BoxShape.circle, @@ -159,8 +163,8 @@ class PairingView extends GetView { height: 180.w, child: CircularProgressIndicator( strokeWidth: 4.w, - valueColor: AlwaysStoppedAnimation(Colors.blue), - backgroundColor: Colors.blue.withOpacity(0.1), + valueColor: const AlwaysStoppedAnimation(Color(0xFF0066CC)), + backgroundColor: const Color(0xFF0066CC).withOpacity(0.1), ), ); } @@ -177,7 +181,7 @@ class PairingView extends GetView { style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, - color: Colors.grey[800], + color: Colors.black, ), ), SizedBox(height: 8.h), @@ -185,7 +189,7 @@ class PairingView extends GetView { '请打开耳机充电盒,将耳机放入您的耳朵', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[500], + color: Colors.black54, ), ), ], @@ -213,7 +217,7 @@ class PairingView extends GetView { '正在跳转到主页...', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[500], + color: Colors.black54, ), ), ], @@ -241,7 +245,7 @@ class PairingView extends GetView { controller.errorMessage, style: TextStyle( fontSize: 14.sp, - color: Colors.grey[600], + color: Colors.black54, ), textAlign: TextAlign.center, ), @@ -256,7 +260,7 @@ class PairingView extends GetView { borderRadius: BorderRadius.circular(8.r), ), ), - child: Text('打开设置'), + child: const Text('打开设置'), ), ], ); @@ -284,7 +288,7 @@ class PairingView extends GetView { '请确保耳机已开机并在附近' : controller.errorMessage, style: TextStyle( fontSize: 14.sp, - color: Colors.grey[600], + color: Colors.black54, ), textAlign: TextAlign.center, ), @@ -296,14 +300,14 @@ class PairingView extends GetView { Row( mainAxisAlignment: MainAxisAlignment.center, 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), Text( '需要系统配对', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, - color: Colors.blue, + color: const Color(0xFF0066CC), ), ), ], @@ -314,7 +318,7 @@ class PairingView extends GetView { '请在系统设置中配对您的设备' : controller.errorMessage, style: TextStyle( fontSize: 14.sp, - color: Colors.grey[600], + color: Colors.black54, ), textAlign: TextAlign.center, ), @@ -322,19 +326,19 @@ class PairingView extends GetView { ElevatedButton( onPressed: () => controller.openSystemBluetoothSettings(), style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, + backgroundColor: const Color(0xFF0066CC), foregroundColor: Colors.white, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.r), ), ), - child: Text('打开蓝牙设置'), + child: const Text('打开蓝牙设置'), ), ], ); default: - return SizedBox(); + return const SizedBox(); } }); } @@ -348,8 +352,8 @@ class PairingView extends GetView { child: ElevatedButton( onPressed: controller.refreshSearch, style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[200], - foregroundColor: Colors.grey[800], + backgroundColor: Colors.grey.withOpacity(0.1), + foregroundColor: Colors.black, padding: EdgeInsets.symmetric(vertical: 16.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), @@ -371,7 +375,7 @@ class PairingView extends GetView { child: TextButton( onPressed: controller.skipConnecting, style: TextButton.styleFrom( - foregroundColor: Colors.blue[600], + foregroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), ), child: Text(