Browse Source

Merge branch 'ios' of github.com:deepcloud2048/deep_voice into ios

weicu
wolfplus 2 years ago
parent
commit
cadf9c4df8
  1. 36
      GoogleService-Info.plist
  2. 38
      android/app/build.gradle.kts
  3. 47
      android/app/google-services.json
  4. 4
      android/app/sign/DeepSound.properties
  5. 5
      android/app/src/main/AndroidManifest.xml
  6. 4
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  7. 4
      android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt
  8. 2
      android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt
  9. 4
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  10. 10
      android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt
  11. 2
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt
  12. 2
      android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt
  13. 6
      android/app/src/main/res/values/strings.xml
  14. 1
      android/build.gradle.kts
  15. BIN
      assets/images/explore1.png
  16. BIN
      assets/images/explore2.jpg
  17. BIN
      assets/images/glasses_light.png
  18. BIN
      assets/images/robot_light.png
  19. 1
      assets/lottie/audio_wave.json
  20. 1
      firebase.json
  21. 2
      ios/Podfile
  22. 210
      ios/Podfile.lock
  23. 30
      ios/Runner.xcodeproj/project.pbxproj
  24. 123
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  25. 123
      ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
  26. 4
      ios/Runner/BluetoothMediaButtonHelper.swift
  27. 36
      ios/Runner/GoogleService-Info.plist
  28. 34
      ios/Runner/Info.plist
  29. 10
      ios/Runner/RunnerRelease.entitlements
  30. 37
      lib/core/bindings/initial_binding.dart
  31. 93
      lib/core/themes/language/lang/en_us.dart
  32. 91
      lib/core/themes/language/lang/zh_cn.dart
  33. 18
      lib/core/themes/language/translation_service.dart
  34. 102
      lib/core/translations/app_translations.dart
  35. 285
      lib/core/translations/language/en_us.dart
  36. 281
      lib/core/translations/language/zh_cn.dart
  37. 19
      lib/core/widgets/common_bottom_nav.dart
  38. 15
      lib/core/widgets/select_location_map.dart
  39. 127
      lib/data/models/user_Info.dart
  40. 59
      lib/data/providers/agent_provider.dart
  41. 14
      lib/data/services/ai_service.dart
  42. 59
      lib/data/services/asr_service.dart
  43. 152
      lib/data/services/classic_bluetooth_service.dart
  44. 53
      lib/data/services/db/database_helper.dart
  45. 126
      lib/data/services/db/sqflite_api.dart
  46. 90
      lib/data/services/deapsound_ai_service.dart
  47. 93
      lib/data/services/kouzi_ai_service.dart
  48. 36
      lib/data/services/login_service.dart
  49. 49
      lib/data/services/network/api.dart
  50. 115
      lib/data/services/network/auth_interceptor.dart
  51. 45
      lib/data/services/network/dio_manager.dart
  52. 17
      lib/data/services/network/entity/base_entity.dart
  53. 9
      lib/data/services/network/entity/entity_factory.dart
  54. 4
      lib/data/services/network/nw_api.dart
  55. 8
      lib/data/services/network/nw_method.dart
  56. 44
      lib/data/services/speech_factory.dart
  57. 393
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  58. 285
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  59. 272
      lib/data/services/speech_impl/xunfei_asr_service.dart
  60. 35
      lib/data/services/volcano_ai_service.dart
  61. 92
      lib/firebase_options.dart
  62. 46
      lib/main.dart
  63. 189
      lib/modules/chat/controllers/chat_controller.dart
  64. 209
      lib/modules/chat/views/chat_view.dart
  65. 42
      lib/modules/common/webview_page.dart
  66. 97
      lib/modules/explore/controllers/explore_controller.dart
  67. 226
      lib/modules/explore/views/explore_view.dart
  68. 6
      lib/modules/home/bindings/home_binding.dart
  69. 160
      lib/modules/home/controllers/home_controller.dart
  70. 468
      lib/modules/home/views/home_view.dart
  71. 135
      lib/modules/login/controllers/facebookLogin.dart
  72. 82
      lib/modules/login/controllers/googleLogin.dart
  73. 542
      lib/modules/login/controllers/login_controller.dart
  74. 923
      lib/modules/login/controllers/weixinLogin.dart
  75. 482
      lib/modules/login/views/login_view.dart
  76. 10
      lib/modules/meeting/bindings/meeting_binding.dart
  77. 10
      lib/modules/meeting/bindings/meeting_details_binding.dart
  78. 11
      lib/modules/meeting/bindings/meeting_edit_binding.dart
  79. 10
      lib/modules/meeting/bindings/meeting_record_binding.dart
  80. 70
      lib/modules/meeting/controllers/meeting_controller.dart
  81. 67
      lib/modules/meeting/controllers/meeting_details_controller.dart
  82. 143
      lib/modules/meeting/controllers/meeting_edit_controller.dart
  83. 312
      lib/modules/meeting/controllers/meeting_record_controller.dart
  84. 27
      lib/modules/meeting/model/meeting_details_model.dart
  85. 35
      lib/modules/meeting/model/meeting_model.dart
  86. 326
      lib/modules/meeting/views/meeting_details_view.dart
  87. 298
      lib/modules/meeting/views/meeting_edit_view.dart
  88. 358
      lib/modules/meeting/views/meeting_record_view.dart
  89. 419
      lib/modules/meeting/views/meeting_view.dart
  90. 109
      lib/modules/pairing/controllers/pairing_controller.dart
  91. 63
      lib/modules/pairing/views/pairing_view.dart
  92. 2
      lib/modules/profile/bindings/profile_binding.dart
  93. 382
      lib/modules/profile/controllers/profile_controller.dart
  94. 144
      lib/modules/profile/views/profile_view.dart
  95. 283
      lib/modules/settings/controllers/settings_controller.dart
  96. 11
      lib/modules/settings/models/language_model.dart
  97. 717
      lib/modules/settings/views/settings_view.dart
  98. 125
      lib/modules/splash/controllers/splash_controller.dart
  99. 22
      lib/modules/splash/views/splash_view.dart
  100. 9
      lib/modules/timbre/bindings/timbre_binding.dart

36
GoogleService-Info.plist

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
<key>ANDROID_CLIENT_ID</key>
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808</string>
<key>GCM_SENDER_ID</key>
<string>907754492821</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.yunqiinnovation.deepsound</string>
<key>PROJECT_ID</key>
<string>deepsound-e0e3d</string>
<key>STORAGE_BUCKET</key>
<string>deepsound-e0e3d.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:907754492821:ios:246794fc44b23174145c9c</string>
</dict>
</plist>

38
android/app/build.gradle.kts

@ -1,3 +1,5 @@
import java.util.Properties
import java.io.FileInputStream
repositories {
google()
mavenCentral()
@ -8,10 +10,16 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")version "4.3.13"
}
val keystorePropertiesFile = rootProject.file("app/sign/DeepSound.properties")
val keystoreProperties =Properties().apply {
load(FileInputStream(keystorePropertiesFile)) // 使用 Kotlin 扩展方法
}
android {
namespace = "com.example.deep_voice"
namespace = "com.yunqiinnovation.deepsound"
compileSdk = 35
ndkVersion = "28.0.13004108"
@ -28,7 +36,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.deep_voice"
applicationId = "com.yunqiinnovation.deepsound"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 24 // Updated to meet azure_speech_recognition_null_safety requirements
@ -37,10 +45,36 @@ android {
versionName = flutter.versionName
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
enableV1Signing = true
enableV2Signing = true
}
getByName("debug") {
// debug 使用与 release 相同的签名配置
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
enableV1Signing = true
enableV2Signing = true
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
isMinifyEnabled = false
isShrinkResources = false
signingConfig = signingConfigs.getByName("release")
// signingConfig = signingConfigs.getByName("debug")
}
debug {
signingConfig = signingConfigs.getByName("debug")
}
}

47
android/app/google-services.json

@ -0,0 +1,47 @@
{
"project_info": {
"project_number": "907754492821",
"project_id": "deepsound-e0e3d",
"storage_bucket": "deepsound-e0e3d.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:907754492821:android:2deffc5aa1037ae2145c9c",
"android_client_info": {
"package_name": "com.yunqiinnovation.deepsound"
}
},
"oauth_client": [
{
"client_id": "907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.yunqiinnovation.deepsound",
"certificate_hash": "5a9520045bc16fb4f09fb97668a8b0fc4babd376"
}
},
{
"client_id": "907754492821-15btgolbd7thaa8fpt7s8hs8hq3jl0lg.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAj0_-nS3ygc8VFe1azotSE3-HGZYClkTs"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "907754492821-15btgolbd7thaa8fpt7s8hs8hq3jl0lg.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

4
android/app/sign/DeepSound.properties

@ -0,0 +1,4 @@
storePassword=13071376716
keyPassword=13071376716
keyAlias=DeepSound
storeFile=sign/DeepSound.keystore

5
android/app/src/main/AndroidManifest.xml

@ -29,7 +29,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:label="deep_voice"
android:label="DeepSound"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
@ -38,7 +38,8 @@
android:enableOnBackInvokedCallback="true"
tools:replace="android:label">
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>
<!-- 添加语音交互服务 -->
<service

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

@ -1,4 +1,4 @@
package com.example.deep_voice
package com.yunqiinnovation.deepsound
import android.content.Context
import android.media.AudioAttributes
@ -9,7 +9,7 @@ import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.os.Process
import com.example.deep_voice.core.utils.FileLogger
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler

4
android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt

@ -1,11 +1,11 @@
package com.example.deep_voice
package com.yunqiinnovation.deepsound
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import com.example.deep_voice.core.utils.FileLogger
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import java.util.concurrent.Future

2
android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt

@ -1,4 +1,4 @@
package com.example.deep_voice
package com.yunqiinnovation.deepsound
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile

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

@ -1,4 +1,4 @@
package com.example.deep_voice
package com.yunqiinnovation.deepsound
import android.content.Intent
import android.os.Build
@ -19,7 +19,7 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.example.deep_voice.core.utils.FileLogger
import com.yunqiinnovation.deepsound.core.utils.FileLogger
class MainActivity: FlutterActivity() {

10
android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt

@ -1,4 +1,4 @@
package com.example.deep_voice
package com.yunqiinnovation.deepsound
import android.app.Notification
import android.app.NotificationChannel
@ -25,7 +25,7 @@ import org.json.JSONArray
import android.media.MediaPlayer
import android.media.AudioAttributes
import android.net.Uri
import com.example.deep_voice.core.utils.FileLogger
import com.yunqiinnovation.deepsound.core.utils.FileLogger
/**
* 后台语音交互 Service:
@ -50,9 +50,9 @@ class VoiceInteractionService : Service() {
fun isServiceRunning(): Boolean = isRunning.get()
// 广播 Action
const val ACTION_RECOGNITION_STARTED = "com.example.deep_voice.ACTION_RECOGNITION_STARTED"
const val ACTION_PAUSE_VOICE_INTERACTION = "com.example.deep_voice.ACTION_PAUSE_VOICE_INTERACTION"
const val ACTION_CHAT_HISTORY_UPDATED = "com.example.deep_voice.ACTION_CHAT_HISTORY_UPDATED"
const val ACTION_RECOGNITION_STARTED = "com.yunqiinnovation.deepsound.ACTION_RECOGNITION_STARTED"
const val ACTION_PAUSE_VOICE_INTERACTION = "com.yunqiinnovation.deepsound.ACTION_PAUSE_VOICE_INTERACTION"
const val ACTION_CHAT_HISTORY_UPDATED = "com.yunqiinnovation.deepsound.ACTION_CHAT_HISTORY_UPDATED"
}
// 服务状态

2
android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt

@ -1,4 +1,4 @@
package com.example.deep_voice
package com.yunqiinnovation.deepsound
import android.util.Log
import okhttp3.*

2
android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt

@ -1,4 +1,4 @@
package com.example.deep_voice.core.utils
package com.yunqiinnovation.deepsound.core.utils
import android.content.Context
import android.util.Log

6
android/app/src/main/res/values/strings.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="facebook_app_id">3829022037361855</string>
<string name="fb_login_protocol_scheme">fb3829022037361855</string>
<string name="facebook_client_token">193d0a2b1757c41cfa557ea8e8f21c33</string>
</resources>

1
android/build.gradle.kts

@ -3,6 +3,7 @@ allprojects {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()

BIN
assets/images/explore1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

BIN
assets/images/explore2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

BIN
assets/images/glasses_light.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
assets/images/robot_light.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

1
assets/lottie/audio_wave.json

File diff suppressed because one or more lines are too long

1
firebase.json

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"deepsound-e0e3d","appId":"1:907754492821:android:2deffc5aa1037ae2145c9c","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"deepsound-e0e3d","configurations":{"android":"1:907754492821:android:2deffc5aa1037ae2145c9c","ios":"1:907754492821:ios:8c1d7d1b583aec22145c9c","macos":"1:907754492821:ios:8c1d7d1b583aec22145c9c","web":"1:907754492821:web:99451e7efd7d120a145c9c","windows":"1:907754492821:web:e3e8040111db6871145c9c"}}}}}}

2
ios/Podfile

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

210
ios/Podfile.lock

@ -1,47 +1,183 @@
PODS:
- app_settings (5.1.1):
- Flutter
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- audio_session (0.0.1):
- Flutter
- audio_waveforms (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- FBAEMKit (17.1.0):
- FBSDKCoreKit_Basics (= 17.1.0)
- FBSDKCoreKit (17.1.0):
- FBAEMKit (= 17.1.0)
- FBSDKCoreKit_Basics (= 17.1.0)
- FBSDKCoreKit_Basics (17.1.0)
- FBSDKLoginKit (17.1.0):
- FBSDKCoreKit (= 17.1.0)
- Firebase/Auth (11.8.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.8.0)
- Firebase/CoreOnly (11.8.0):
- FirebaseCore (~> 11.8.0)
- firebase_auth (5.5.1):
- Firebase/Auth (= 11.8.0)
- firebase_core
- Flutter
- firebase_core (3.12.1):
- Firebase/CoreOnly (= 11.8.0)
- Flutter
- FirebaseAppCheckInterop (11.10.0)
- FirebaseAuth (11.8.1):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.8.0)
- FirebaseCoreExtension (~> 11.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 100.0)
- FirebaseAuthInterop (11.10.0)
- FirebaseCore (11.8.1):
- FirebaseCoreInternal (~> 11.8.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.8.0):
- FirebaseCore (~> 11.8.0)
- FirebaseCoreInternal (11.8.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_facebook_auth (7.1.1):
- FBSDKLoginKit (~> 17.1.0)
- Flutter
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_tts (0.0.1):
- Flutter
- fluwx (0.0.1):
- Flutter
- fluwx/pay (= 0.0.1)
- fluwx/pay (0.0.1):
- Flutter
- WechatOpenSDK-XCFramework (~> 2.0.4)
- google_sign_in_ios (0.0.1):
- AppAuth (>= 1.7.4)
- Flutter
- FlutterMacOS
- GoogleSignIn (~> 7.1)
- GTMSessionFetcher (>= 3.4.0)
- GoogleSignIn (7.1.0):
- AppAuth (< 2.0, >= 1.7.3)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/AppDelegateSwizzler (8.0.2):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.0.2):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.0.2):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.0.2):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.0.2)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.0.2)
- GoogleUtilities/Reachability (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
- AppAuth/Core (~> 1.7)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher (3.5.0):
- GTMSessionFetcher/Full (= 3.5.0)
- GTMSessionFetcher/Core (3.5.0)
- GTMSessionFetcher/Full (3.5.0):
- GTMSessionFetcher/Core
- image_picker_ios (0.0.1):
- Flutter
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- MicrosoftCognitiveServicesSpeech-iOS (1.34.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- RecaptchaInterop (100.0.0)
- record_darwin (1.0.0):
- Flutter
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- share_plus (0.0.1):
- Flutter
- speech_to_text (0.0.1):
- Flutter
- FlutterMacOS
- Try
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- Try (2.1.1)
- WechatOpenSDK-XCFramework (2.0.4)
DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- Flutter (from `Flutter`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- fluwx (from `.symlinks/plugins/fluwx/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0)
@ -50,27 +186,66 @@ DEPENDENCIES:
- record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
SPEC REPOS:
trunk:
- AppAuth
- FBAEMKit
- FBSDKCoreKit
- FBSDKCoreKit_Basics
- FBSDKLoginKit
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- libwebp
- Mantle
- MicrosoftCognitiveServicesSpeech-iOS
- RecaptchaInterop
- SDWebImage
- SDWebImageWebPCoder
- Try
- WechatOpenSDK-XCFramework
EXTERNAL SOURCES:
app_settings:
:path: ".symlinks/plugins/app_settings/ios"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
Flutter:
:path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_facebook_auth:
:path: ".symlinks/plugins/flutter_facebook_auth/ios"
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_tts:
:path: ".symlinks/plugins/flutter_tts/ios"
fluwx:
:path: ".symlinks/plugins/fluwx/ios"
google_sign_in_ios:
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
just_audio:
@ -85,25 +260,58 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
speech_to_text:
:path: ".symlinks/plugins/speech_to_text/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS:
app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
FBAEMKit: cb719c53575a3be86ea873279f30d6a2c4e15881
FBSDKCoreKit: ecdb980a24633ccb012700299ceb16d0235e14d2
FBSDKCoreKit_Basics: 045101c4a9ef10c845347424d73a29aae02c3e43
FBSDKLoginKit: 69eb59b2f839aba635616df6e422acd0ca88030a
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_auth: 9ebbd83276bf977dea1c74dfc199acf9d2a2f42f
firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510
FirebaseAppCheckInterop: 9664c858489710f682766ef54e2b6741d3b62070
FirebaseAuth: ad59a1a7b161e75f74c39f70179d2482d40e2737
FirebaseAuthInterop: 01a804fb074424fd58b92dd50dd0272277199356
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreExtension: 3d3f2017a00d06e09ab4ebe065391b0bb642565e
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6
flutter_facebook_auth: 8a9ad5fbfc25b77e7a53ea8986866950b27edcae
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
MicrosoftCognitiveServicesSpeech-iOS: 723eccb3e459bbb530df3d9bfe84a291074b09eb
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
PODFILE CHECKSUM: f35c1d63b4b8ce9c2eb88107f162ac59923c4839
PODFILE CHECKSUM: 4f26889ae572e1722a139b651939749d40c71469
COCOAPODS: 1.16.2

30
ios/Runner.xcodeproj/project.pbxproj

@ -20,6 +20,7 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 591293922D9524D1004A65BB /* GoogleService-Info.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@ -68,6 +69,8 @@
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4A9190D0838960F143BDC158 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = "<group>"; };
591293922D9524D1004A65BB /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -161,6 +164,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */,
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */,
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */,
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */,
@ -171,6 +175,7 @@
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */,
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
591293922D9524D1004A65BB /* GoogleService-Info.plist */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
@ -265,6 +270,9 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@ -293,6 +301,7 @@
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */,
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -523,7 +532,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -532,6 +541,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -723,7 +733,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -732,6 +742,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -748,7 +759,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -757,6 +769,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -799,6 +812,17 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 11.10.0;
};
};
/* End XCRemoteSwiftPackageReference section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

123
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -0,0 +1,123 @@
{
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4",
"version" : "11.10.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6",
"version" : "11.10.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
"version" : "4.4.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
}
],
"version" : 3
}

123
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -0,0 +1,123 @@
{
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4",
"version" : "11.10.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6",
"version" : "11.10.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
"version" : "4.4.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
}
],
"version" : 3
}

4
ios/Runner/BluetoothMediaButtonHelper.swift

@ -230,8 +230,8 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
// 创建音频专辑图片
let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片
var nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyTitle: "DeepVoice 语音助手",
MPMediaItemPropertyArtist: "DeepVoice",
MPMediaItemPropertyTitle: "DeepSound 语音助手",
MPMediaItemPropertyArtist: "DeepSound",
MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime,
MPMediaItemPropertyPlaybackDuration: totalDuration

36
ios/Runner/GoogleService-Info.plist

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
<key>ANDROID_CLIENT_ID</key>
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808</string>
<key>GCM_SENDER_ID</key>
<string>907754492821</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.yunqiinnovation.deepsound</string>
<key>PROJECT_ID</key>
<string>deepsound-e0e3d</string>
<key>STORAGE_BUCKET</key>
<string>deepsound-e0e3d.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:907754492821:ios:246794fc44b23174145c9c</string>
</dict>
</plist>

34
ios/Runner/Info.plist

@ -2,14 +2,46 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yunqiinnovation.deepsound</string>
<key>CFBundleURLSchemes</key>
<array>
<string>fb3829022037361855</string>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
</array>
</dict>
</array>
<key>FacebookAppID</key>
<string>3829022037361855</string>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>FacebookDisplayName</key>
<string>Deepsound</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
<string>googlechrome</string>
<string>googlechromes</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Deep Voice</string>
<string>DeepSound</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>NSUserTrackingUsageDescription</key>
<string>This application needs to access your tracking data in order to offer better personalized services and advertisements</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>

10
ios/Runner/RunnerRelease.entitlements

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yunqiinnovation.deepsound</string>
</array>
</dict>
</plist>

37
lib/core/bindings/initial_binding.dart

@ -9,17 +9,15 @@ import '../../data/services/bluetooth_media_button_service.dart';
import '../../core/utils/logger.dart';
import '../../data/services/voice_interaction_service.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
void dependencies() {
Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化)
Get.put<LanguageManager>(LanguageManager(), permanent: true);
//初始化语音工厂
Get.put<SpeechFactory>(SpeechFactory(), permanent: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
@ -28,22 +26,29 @@ class InitialBinding extends Bindings {
Get.lazyPut<ChatHistoryService>(() => ChatHistoryService(), fenix: true);
// 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), fenix: true);
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 经典蓝牙服务
Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(), fenix: true);
Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(),
fenix: true);
// 蓝牙媒体按钮服务
Get.lazyPut<BluetoothMediaButtonService>(() => BluetoothMediaButtonService(), fenix: true);
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), fenix: true);
Get.lazyPut<BluetoothMediaButtonService>(
() => BluetoothMediaButtonService(),
fenix: true);
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
fenix: true);
// // 语言管理器(需要最先初始化)
// Get.put<LoginService>(LoginService(), permanent: true);
// await LoginService().FirebaseInit();
// print('资源FirebaseInit加载错误: 完成');
// await LoginService().WeChatInit();
// print('资源FWeChatInit加载错误: 完成');
}
}
}

93
lib/core/themes/language/lang/en_us.dart

@ -0,0 +1,93 @@
const Map<String, String> enUS = {
"bottomNavHome": "home",
"bottomNavSleep": "sleep",
"bottomNavProfile": "profile",
"requestCancellation": "Request cancellation",
"connectionTimeout": "Connection timeout",
"requestTimeout": "Request timeout",
"responseTimeout": "Response timeout",
"unknownError": "Unknown error",
"welcomeHint": "DeepSound makes communication smoother",
"loginTitle": "Login/Register",
"contactCustomerService": "Connection",
"phoneHint": "Please enter your phone number",
"phoneError": "Wrong phone number!",
"emailHint": "Please enter email",
"emailError": "Email input error, please re-enter!",
"codeHint": "Please enter the verification code",
"codeError": "Verification code error",
"pleaseAgreeToTerms":
"Please check the box to agree to our Terms and Privacy Policy",
"getCode": "Get code",
"reacquireCode": "S Reacquire",
"bind": "Deterministic binding",
"login": "Login",
"otherLogin": "Other login methods",
"readAndAgree": "I have read and agree",
"serviceAgreement": " User Service Agreement ",
"and": "And",
"privacyAgreement": " User Privacy Agreement ",
"codeSend": "Verification code has been sent, please check email!",
"connectHeadphones": "Connect headphones",
"bindHeadphones": "Bind headphones",
"intelligentTool": "Intelligent tool",
"sessionMode": "AI conversation mode",
"translation": "Simultaneous translation",
"together": "Face-to-face translation",
"telephoneVoice": "Telephone translation",
"translationRecord": "Translation record",
"bindDevice": "Bind device",
"bound": "Bound",
"appVersion": "APP version",
"languageSetting": "Language setting",
"followingSystem": "Following system",
"userAgreement": "User agreement",
"privacyDeal": "Privacy agreement",
"chinese": "Chinese",
"english": "English",
"chineseComplex": "Chinese (Traditional)",
"japanese": "Japanese",
"korean": "Korean",
"russian": "Russian",
"french": "French",
"qrCode": "Authorization code or QR code",
"link": "Link",
"copy": "Copy",
"successfulReplication": "Successful replication",
"edit": "Edit",
"delete": "Delete",
"aiDialogue": "AI dialogue",
"initialChatMessage":
"Hi, I'm your new friend! Nice to meet you for the first time. I can answer your various questions, provide help with your work and studies, and even chat with you anytime. So, what would you like to ask?",
"defaultContent1": "Help me write a Python web scraper code",
"defaultContent2": "Help me plan a short video script",
"defaultContent3": "Help me design a mind-exercising game",
"defaultContent4": "Recommend a cost-effective phone",
"defaultContent5": "Come up with a creative name for my Honor of Kings ID",
"defaultContent6":
"Guide me on using Python to scrape real-time weather data",
"defaultContent7": "Tell me daily habits to boost kidney qi",
"defaultContent8": "Help me understand the basics of CNC programming",
"defaultContent9": "Help me find a short folk story",
"defaultContent10": "Recommend a cost-effective SUV",
"defaultContent11": "Teach me to make a small game using Python",
"defaultContent12": "Introduce some free-to-play mobile games",
"holdToSpeak": "Hold to speak",
"tapToSpeak": "Click to talk",
"hangUp": "Hang up",
"holdToStartSpeak": "Hold down and start talking",
"tapToStartSpeak": "Click to start Talking",
"sendMessage": "Ask me anything you want",
"beTalking": "Be talking ...",
"headphonesBeTalking": "Talking through headphones...",
"interlanguage": "Cannot choose the same language",
"notice": "Look out",
"connectingDevice": "Please set up the connected device first",
"bindDeviceTips": "Unbind the device first",
"cancel": "Cancel",
"confirm": "Confirm",
"goToSettings": "Go to Settings",
"goToBind": "Unbind",
"scanQrCode": "Scan the QR code",
"endRecording": "Do you want to end the recording?",
};

91
lib/core/themes/language/lang/zh_cn.dart

@ -0,0 +1,91 @@
const Map<String, String> zhCN = {
"bottomNavHome": "首页",
"bottomNavSleep": "睡眠",
"bottomNavProfile": "我的",
"requestCancellation": "请求取消",
"connectionTimeout": "连接超时",
"requestTimeout": "请求超时",
"responseTimeout": "响应超时",
"unknownError": "未知错误",
"welcomeHint": "DeepSound让沟通更顺畅",
"loginTitle": "登录/注册账号",
"contactCustomerService": "联系客服",
"phoneHint": "请输入手机号",
"phoneError": "手机号码错误!",
"emailHint": "请输入邮箱",
"emailError": "邮箱输入错误,请重新输入!",
"codeHint": "请输入验证码",
"codeError": "验证码错误",
"pleaseAgreeToTerms": "请勾选以同意用户协议和隐私政策",
"getCode": "获取验证码",
"reacquireCode": "S后重新获取",
"bind": "确定绑定",
"login": "登录",
"otherLogin": "其他登录方式",
"readAndAgree": "我已阅读并同意",
"serviceAgreement": "《用户服务协议》",
"and": "和",
"privacyAgreement": "《用户隐私协议》",
"codeSend": "验证码已发送,请到邮箱查看!",
"connectHeadphones": "请连接耳机",
"bindHeadphones": "请绑定耳机",
"intelligentTool": "智能工具",
"sessionMode": "AI对话模式",
"translation": "同声传译",
"together": "面对面翻译",
"telephoneVoice": "通话语音翻译",
"translationRecord": "翻译记录",
"bindDevice": "绑定设备",
"bound": "已绑定",
"appVersion": "APP版本",
"languageSetting": "语言设置",
"followingSystem": "跟随系统",
"userAgreement": "用户协议",
"privacyDeal": "隐私协议",
"chinese": "中文",
"english": "英语",
"chineseComplex": "中文(繁体)",
"japanese": "日语",
"korean": "韩语",
"russian": "俄语",
"french": "法语",
"qrCode": "授权码或二维码",
"link": "链接",
"copy": "复制",
"successfulReplication": "复制成功",
"edit": "编辑",
"delete": "删除",
"aiDialogue": "AI对话",
"initialChatMessage":
"嗨,我是你的新朋友!初次见面很开心。我呢,可以回答你的各种问题,给你工作学习上提供帮助,还能随时陪你聊天。嗯,你想问点什么呢?",
"defaultContent1": "请帮我写个Python爬虫代码",
"defaultContent2": "帮我策划一个短视频脚本",
"defaultContent3": "请帮我设计一个锻炼思维的小游戏",
"defaultContent4": "请推荐一部性价比高的手机",
"defaultContent5": "帮我想一个王者ID的创意名称",
"defaultContent6": "请指导我使用Python爬取实时天气数据",
"defaultContent7": "告诉我补肾气的日常习惯",
"defaultContent8": "帮我了解CNC编程的基础知识",
"defaultContent9": "帮我找一个简短的民间故事",
"defaultContent10": "给我推荐一辆性价比高的SUV",
"defaultContent11": "教我用Python制作小游戏",
"defaultContent12": "介绍一些适合零氪玩家的手游",
"holdToSpeak": "按住说话",
"tapToSpeak": "点击说话",
"hangUp": "挂断",
"holdToStartSpeak": "按住开始说话",
"tapToStartSpeak": "点击开始说话",
"sendMessage": "有什么问的尽管问我",
"beTalking": "正在说话...",
"headphonesBeTalking": "正在使用耳机说话...",
"interlanguage": "不能选择同语种",
"notice": "注意",
"connectingDevice": "请先去设置连接设备",
"bindDeviceTips": "请先去绑定设备",
"cancel": "取消",
"confirm": "确定",
"goToSettings": "去设置",
"goToBind": "去绑定",
"scanQrCode": "扫描二维码",
"endRecording": "你想结束录音吗?",
};

18
lib/core/themes/language/translation_service.dart

@ -0,0 +1,18 @@
import 'dart:ui';
import 'package:get/get.dart';
import 'lang/en_us.dart';
import 'lang/zh_cn.dart';
class TranslationService extends Translations {
static Locale? get locale => Get.deviceLocale;
static final fallbackLocale = Locale('zh', 'CN');
static final supportedLocales = [
const Locale('zh', 'CN'),
const Locale('en', 'US'),
];
@override
Map<String, Map<String, String>> get keys => {'en_US': enUS, 'zh_CN': zhCN};
}

102
lib/core/translations/app_translations.dart

@ -1,81 +1,29 @@
import 'dart:ui';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'language/en_us.dart';
import 'language/zh_cn.dart';
class AppTranslations extends Translations {
static const fallbackLocale = Locale('zh', 'CN');
static final supportedLocales = [
const Locale('zh', 'CN'),
const Locale('en', 'US'),
];
static Locale? deviceLocale() {
final GetStorage storage = GetStorage();
Locale? locale = Get.deviceLocale;
String? language = storage.read('language');
if (language != null && language != 'system') {
List languageList = language.split('_');
locale = Locale(languageList[0], languageList[1]);
}
return locale;
}
@override
Map<String, Map<String, String>> get keys => {
'zh_CN': {
// 通用
'app_name': 'i深音',
'confirm': '确认',
'cancel': '取消',
'retry': '重试',
'error': '错误',
'success': '成功',
// 主页
'add_device': '添加设备',
'searching': '搜索中',
'searching_hint': '请将设备放在手机附近',
'connected': '已连接',
'device_ready': '设备已就绪',
'connection_failed': '连接失败',
'connection_failed_hint': '未找到设备。请确保蓝牙已开启且设备在5米范围内。尝试重启设备。',
'disconnect': '断开连接',
// 聊天页面
'clear_chat': '清空对话',
'thinking': '正在思考...',
'type_message': '发消息...',
'voice_enabled': '语音已开启',
'voice_disabled': '语音已关闭',
// 设置页面
'settings': '设置',
'language_settings': '语言设置',
'about': '关于',
'check_update': '检查更新',
'clear_cache': '清除缓存',
'home': '首页',
'explore': '发现',
'profile': '我的',
'device': '设备',
},
'en_US': {
// General
'app_name': 'iDeep Voice',
'confirm': 'Confirm',
'cancel': 'Cancel',
'retry': 'Retry',
'error': 'Error',
'success': 'Success',
// Home
'add_device': '+ Add Device',
'searching': 'Searching',
'searching_hint': 'Please place the device near your phone',
'connected': 'Connected!',
'device_ready': 'Your device is ready',
'connection_failed': 'Connection Failed',
'connection_failed_hint': 'Device not found. Please ensure Bluetooth is enabled and the device is within 5 meters. Try turning the device off and on again.',
'disconnect': 'Disconnect',
// Chat
'clear_chat': 'Clear Chat',
'thinking': 'Thinking...',
'type_message': 'Type a message...',
'voice_enabled': 'Voice Enabled',
'voice_disabled': 'Voice Disabled',
// Settings
'settings': 'Settings',
'language_settings': 'Language Settings',
'about': 'About',
'check_update': 'Check for Updates',
'clear_cache': 'Clear Cache',
'home': 'Home',
'explore': 'Explore',
'profile': 'Profile',
'device': 'Device',
},
};
}
Map<String, Map<String, String>> get keys => {'en_US': enUS, 'zh_CN': zhCN};
}

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

@ -0,0 +1,285 @@
const Map<String, String> enUS = {
// 通用
'continue': 'Continue',
'confirm': 'Confirm',
'cancel': 'Cancel',
'retry': 'Retry',
'error': 'Error',
'success': 'Success',
'connected': 'Connected',
'connecting': 'Connecting...',
'connectionFailure': 'Connection Failed',
'unconnected': 'Disconnected',
'search': 'Search',
"requestCancellation": "Request cancellation",
"connectionTimeout": "Connection timeout",
"requestTimeout": "Request timeout",
"responseTimeout": "Response timeout",
"unknownError": "Unknown error",
// 启动页
"slogan": "Connect the World, Beyond Voice",
"sloganInfo": "AI Voice Expert · Anytime, Anywhere",
// 底部导航
"home": "Home",
"explore": "Explore",
"settings": "Settings",
// 语言
"followingSystem": "Following system",
"chinese": "Chinese",
"english": "English",
// 登录页
"welcome": "Welcome",
"loginTips": "Please log in or register with your phone number or email",
"accountNumber": "Phone number or email",
"verificationCode": "Verification Code",
"getCode": "Get Code",
"reacquireAfter": "s Get it again after",
"otherWays": "Or login with",
"expressConsent": "By logging in/registering, you agree to the",
"and": " and ",
"userAgreement": "User Agreement",
"privacyPolicy": "Privacy Policy",
'selectCountry':'Select Country/Region',
"email": "email",
"phoneNumber": "phoneNumber",
// 首页
"deviceStatus": "Device Status",
"reconnect": "Reconnect",
"connectingDevice": "Connecting to device...",
"connectedTo": "Connected to",
"signal": "Signal",
"deviceId": "Device ID",
"firmwareVersion": "Firmware Version",
"disconnectedDevice": "No device connected, click the button below",
"connectDevice": "Connect Device",
"topAiExperiences": "Top AI Experiences",
"aiAssistant": "AI Assistant",
"aiAssistantInfo": "24/7 personal consultant for questions and advice",
"simultaneousInterpretation": "Real-time Interpretation",
"simultaneousInterpretationInfo": "Real-time translation for 80+ languages",
"aiCompanion": "Parent-child bondingn",
"aiCompanionInfo": "Understands you and provides warm companionship",
"homeSlogan": "Connect the World, Beyond Sound",
"turnEarphones":
"Please open the earphone case and put the earbuds in your ears",
"currentlyConnected": "Currently connected:",
"unsystemSetup": "Go to System Settings",
"talkLater": "Remind Later",
"initializing": "Initializing...",
"bluetoothInitializationFailure": "Bluetooth service initialization failed",
"checkingPermissions": "Checking permissions...",
"lackPermissions": "Bluetooth permissions required",
"searchingDevice": "Searching for devices...",
"findConnectedDevice": "Found connected device",
"notFound": "No connected Bluetooth devices found",
"initializationFailure": "Initialization failed",
"connectedDevice": "Connected to device:",
"failedRefreshDevice": "Failed to refresh device",
"noDeviceFound": "No connected devices found",
"failedDeviceInformation": "Failed to get device information",
"unknownEquipment": "Unknown Device",
"zeroSignal": "No Signal",
"good": "Good",
"fine": "Great",
"excellent": "Excellent",
"unknown": "Unknown",
'bluetoothTurnedOff': 'Bluetooth is turned off, please turn it on',
'bluetoothOff':'bluetooth is off',
// 发现页
"searchAi": "Search AI features...",
"exploreSlogan": "Smart Earbuds, Infinite Possibilities",
"exploreSlogan1": "Intelligent companion, Infinite Possibilities",
"exploreSlogan2": "Smart glasses, Infinite Possibilities",
"exploreAi": "Explore AI features provided by DeepSound",
"aiFunction": "AI Features",
"allRoundAssistant": "All-round Assistant",
"allRoundAssistantInfo": "Helps solve various problems",
"simultaneousTranslation": "Real-time Translation",
"simultaneousTranslationInfo": "Instant translation for 80+ languages",
"aiGirlfriend": "Parent-child bondingn",
"aiGirlfriendInfo": "Intelligent company, chat at any time",
"healthConsultant": "Multi-function AI chat",
"healthConsultantInfo": "More AI chat features",
"conferenceAssistant": "Business Meeting Assistant",
"conferenceAssistantInfo":
"Real-time minutes recording and multilingual translation",
"studyAssistant": "Language Learning Assistant",
"studyAssistantInfo": "Pronunciation correction and speaking practice",
"travelTranslator": "Travel Translator",
"travelTranslatorInfo": "Real-time voice translation and attraction guides",
"beSearching": "Searching for:",
"noAssistantFound": "No corresponding AI assistant found",
"scene": "Scenarios",
"youHaveSelected": "You've selected:",
"newFunction": "New Features",
"youChoseTry": "You've chosen to try",
"aiSinger": "AI Singer Mode",
"aiSingerInfo":
"Sing any song in your favorite artist's voice, even your own creations",
"popularFunction": "Popular Features",
"tryNow": "Try Now",
// 设置页
"premiumMember": "Premium Membership",
"joinDevice": "Connected device:",
"accountId": "Account ID:",
"systemSetting": "System Settings",
"languageSetting": "Language Settings",
"darkMode": "Dark Mode",
"darkModeInfo": "Switch between light/dark themes",
"backups": "Backup & Restore",
"backupsInfo": "Backup settings and AI history",
"lastBackup": "Last backup:",
"privacySettings": "Privacy Settings",
"privacySettingsInfo": "Manage data collection and permissions",
"logout": "Log Out",
"logoutTips": "Are you sure you want to log out of the current account?",
"aboutApplication": "About",
"versions": "Version",
"checkUpdates": "Check Updates",
"selectLanguage": "Select Language",
"voiceService": "Voice Service",
"voiceServiceInfo": "Select speech recognition and synthesis service",
"selectVoiceService": "Select Voice Service",
"azureVoiceService": "Azure Speech Service",
"azureVoiceServiceInfo":
"Microsoft cloud-based service supporting high-quality speech synthesis and recognition",
"flutterVoiceService": "Local voice service",
"flutterVoiceServiceInfo":
"Device-local voice service with no internet required",
'timbreService': 'Timbre Service',
'timbreServiceInfo': 'Select voice synthesis service',
'defaultTimbre': 'Default Timbre',
'microsoftCloudTimbreService': 'Microsoft Cloud Timbre Service',
'voiceClone': 'Voice Clone',
// 聊天页
"clearChatHistory": "Clear Chat History",
"online": "Online",
"inputMessage": "Type a message...",
"imListening": "Listening...",
"canStartTalking": "You may start speaking",
// 翻译页
"selectSourceLanguage": "Select Source Language",
"selectTargetLanguage": "Select Target Language",
//编辑个人信息页
"editProfile": "Edit Profile",
"nickname": "Nickname",
"save": "Save",
//国家列表
"邮箱": "Email",
"中国": "China",
"美国": "United States",
"加拿大": "Canada",
"俄罗斯": "Russia",
"英国": "United Kingdom",
"德国": "Germany",
"意大利": "Italy",
"法国": "France",
"西班牙": "Spain",
"南非": "South Africa",
"沙特阿拉伯": "Saudi Arabia",
"土耳其": "Turkey",
"日本": "Japan",
"韩国": "South Korea",
"新加坡": "Singapore",
"马来西亚": "Malaysia",
"泰国": "Thailand",
"越南": "Vietnam",
"印度": "India",
"巴基斯坦": "Pakistan",
"菲律宾": "Philippines",
"印度尼西亚": "Indonesia",
"阿联酋": "United Arab Emirates",
"墨西哥": "Mexico",
"巴西": "Brazil",
"阿根廷": "Argentina",
"澳大利亚": "Australia",
"新西兰": "New Zealand",
"中国香港": "Hong Kong, China",
"中国澳门": "Macau, China",
"中国台湾": "Taiwan, China",
"埃及": "Egypt",
"希腊": "Greece",
"荷兰": "Netherlands",
"比利时": "Belgium",
"匈牙利": "Hungary",
"罗马尼亚": "Romania",
"瑞士": "Switzerland",
"奥地利": "Austria",
"丹麦": "Denmark",
"瑞典": "Sweden",
"挪威": "Norway",
"波兰": "Poland",
"秘鲁": "Peru",
"智利": "Chile",
"哥伦比亚": "Colombia",
"委内瑞拉": "Venezuela",
"葡萄牙": "Portugal",
"卢森堡": "Luxembourg",
"爱尔兰": "Ireland",
"冰岛": "Iceland",
"阿尔巴尼亚": "Albania",
"马耳他": "Malta",
"塞浦路斯": "Cyprus",
"芬兰": "Finland",
"保加利亚": "Bulgaria",
"立陶宛": "Lithuania",
"拉脱维亚": "Latvia",
"爱沙尼亚": "Estonia",
"乌克兰": "Ukraine",
"塞尔维亚": "Serbia",
"克罗地亚": "Croatia",
"斯洛文尼亚": "Slovenia",
"捷克": "Czech Republic",
"斯洛伐克": "Slovakia",
"危地马拉": "Guatemala",
"萨尔瓦多": "El Salvador",
"洪都拉斯": "Honduras",
"尼加拉瓜": "Nicaragua",
"哥斯达黎加": "Costa Rica",
"巴拿马": "Panama",
"海地": "Haiti",
"玻利维亚": "Bolivia",
"厄瓜多尔": "Ecuador",
"巴拉圭": "Paraguay",
"乌拉圭": "Uruguay",
"摩洛哥": "Morocco",
"阿尔及利亚": "Algeria",
"突尼斯": "Tunisia",
"尼日利亚": "Nigeria",
"加纳": "Ghana",
"埃塞俄比亚": "Ethiopia",
"肯尼亚": "Kenya",
"坦桑尼亚": "Tanzania",
"乌干达": "Uganda",
"孟加拉国": "Bangladesh",
"以色列": "Israel",
"黎巴嫩": "Lebanon",
"约旦": "Jordan",
"伊拉克": "Iraq",
"科威特": "Kuwait",
"巴林": "Bahrain",
"卡塔尔": "Qatar",
"阿曼": "Oman",
"阿富汗": "Afghanistan",
"斯里兰卡": "Sri Lanka",
"缅甸": "Myanmar",
"尼泊尔": "Nepal",
"蒙古": "Mongolia",
"柬埔寨": "Cambodia",
"老挝": "Laos",
"文莱": "Brunei",
"东帝汶": "East Timor",
"斐济": "Fiji",
"所罗门群岛": "Solomon Islands",
};

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

@ -0,0 +1,281 @@
const Map<String, String> zhCN = {
// 通用
'continue': '继续',
'confirm': '确认',
'cancel': '取消',
'retry': '重试',
'error': '错误',
'success': '成功',
'connected': '已连接',
'connecting': '连接中...',
'connectionFailure': '连接失败',
'unconnected': '未连接',
'search': '搜索',
"requestCancellation": "请求取消",
"connectionTimeout": "连接超时",
"requestTimeout": "请求超时",
"responseTimeout": "响应超时",
"unknownError": "未知错误",
// 启动页
"slogan": "连接世界,岂止声音",
"sloganInfo": "AI语音专家 · 随时随地",
// 底部导航
"home": "首页",
"explore": "发现",
"settings": "设置",
// 语言
"followingSystem": "跟随系统",
"chinese": "中文",
"english": "英语",
// 登录页
"welcome": "欢迎使用",
"loginTips": "请使用手机号或邮箱登录/注册",
"accountNumber": "手机号码或邮箱",
"verificationCode": "验证码",
"getCode": "获取验证码",
"reacquireAfter": "s后重新获取",
"otherWays": "或使用以下方式登录",
"expressConsent": "登录/注册即表示你同意",
"and": " 和 ",
"userAgreement": "《用户协议》",
"privacyPolicy": "《隐私政策》",
'selectCountry': '选择国家/地区',
"email": "邮箱",
"phoneNumber": "手机号码",
// 首页
"deviceStatus": "设备状态",
"reconnect": "重新连接",
"connectingDevice": "正在连接设备...",
"connectedTo": "已连接到",
"signal": "信号",
"deviceId": "设备ID",
"firmwareVersion": "固件版本",
"disconnectedDevice": "未连接设备,点击下方按钮连接",
"connectDevice": "连接设备",
"topAiExperiences": "热门AI体验",
"aiAssistant": "AI全能助手",
"aiAssistantInfo": "24小时专属顾问,解答问题、提供建议",
"simultaneousInterpretation": "同声传译",
"simultaneousInterpretationInfo": "80+语言实时翻译,出国无忧",
"aiCompanion": "亲子陪伴",
"aiCompanionInfo": "懂你心意,温暖陪伴",
"homeSlogan": "连接世界,超越声音",
"turnEarphones": "请打开耳机充电盒,将耳机放入您的耳朵",
"currentlyConnected": "当前已连接:",
"unsystemSetup": "去系统设置",
"talkLater": "稍后再说",
"initializing": "正在初始化...",
"bluetoothInitializationFailure": "蓝牙服务初始化失败",
"checkingPermissions": "正在检查权限...",
"lackPermissions": "缺少蓝牙权限",
"searchingDevice": "正在搜索设备...",
"findConnectedDevice": "找到已连接的设备",
"notFound": "未找到已连接的蓝牙设备",
"initializationFailure": "初始化失败",
"connectedDevice": "已连接到设备:",
"failedRefreshDevice": "刷新设备失败",
"noDeviceFound": "未找到已连接的设备",
"failedDeviceInformation": "无法获取设备信息",
"unknownEquipment": "未知设备",
"zeroSignal": "无信号",
"good": "良好",
"fine": "很好",
"excellent": "极佳",
"unknown": "未知",
'bluetoothTurnedOff': '蓝牙已关闭,请打开蓝牙',
'bluetoothOff': '蓝牙已关闭',
// 发现页
"searchAi": "搜索AI功能...",
"exploreSlogan": "智能耳机,无限可能",
"exploreSlogan1": "智能陪伴,无限可能",
"exploreSlogan2": "智能眼镜,无限可能",
"exploreAi": "探索DeepSound提供的AI功能",
"aiFunction": "AI功能",
"allRoundAssistant": "全能助理",
"allRoundAssistantInfo": "帮你解决各类问题",
"simultaneousTranslation": "同声翻译",
"simultaneousTranslationInfo": "实时翻译80+种语言",
"aiGirlfriend": "亲子陪伴",
"aiGirlfriendInfo": "智能陪伴,随时聊天",
"healthConsultant": "多功能AI聊天",
"healthConsultantInfo": "提供更多AI聊天功能",
"conferenceAssistant": "商务会议助手",
"conferenceAssistantInfo": "实时记录会议纪要,多语言翻译",
"studyAssistant": "语言学习助手",
"studyAssistantInfo": "发音纠正,实时练习口语",
"travelTranslator": "旅行翻译助手",
"travelTranslatorInfo": "实时语音翻译,旅游景点介绍",
"beSearching": "正在搜索:",
"noAssistantFound": "未找到对应的AI助手",
"scene": "场景",
"youHaveSelected": "您选择了:",
"newFunction": "新功能",
"youChoseTry": "您选择了尝试",
"aiSinger": "AI歌手模式",
"aiSingerInfo": "用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲",
"popularFunction": "热门功能场景",
"tryNow": "立即尝试",
// 设置页
"premiumMember": "高级会员",
"joinDevice": "已连接设备:",
"accountId": "账号ID:",
"systemSetting": "系统设置",
"languageSetting": "语言设置",
"darkMode": "暗黑模式",
"darkModeInfo": "切换应用的明暗主题",
"backups": "备份与恢复",
"backupsInfo": "备份设置和AI互动历史",
"lastBackup": "上次备份:",
"privacySettings": "隐私设置",
"privacySettingsInfo": "管理数据收集和权限",
"logout": "退出登录",
"logoutTips": "确定要退出当前账号吗?",
"aboutApplication": "关于应用",
"versions": "版本",
"checkUpdates": "检查更新",
"selectLanguage": "选择语言",
"voiceService": "语音服务",
"voiceServiceInfo": "选择语音识别和合成服务",
"selectVoiceService": "选择语音服务",
"azureVoiceService": "Azure 语音服务",
"azureVoiceServiceInfo": "微软云端语音服务,支持高质量语音合成和识别",
"flutterVoiceService": "Flutter 本地语音服务",
"flutterVoiceServiceInfo": "设备本地语音服务,无需联网即可使用",
'timbreService': '音色服务',
'timbreServiceInfo': '选择音色语音合成服务',
'defaultTimbre': '默认音色',
'microsoftCloudTimbreService': '微软云端音色服务',
'voiceClone': '声音复刻',
// 聊天页
"clearChatHistory": "清空聊天记录",
"online": "在线",
"inputMessage": "输入消息...",
"imListening": "正在聆听...",
"canStartTalking": "你可以开始说话",
// 翻译页
"selectSourceLanguage": "选择源语言",
"selectTargetLanguage": "选择目标语言",
//编辑个人信息页
"editProfile": "编辑个人信息",
"nickname": "昵称",
"save": "保存",
//国家列表
"邮箱": "邮箱",
"中国": "中国",
"美国": "美国",
"加拿大": "加拿大",
"俄罗斯": "俄罗斯",
"英国": "英国",
"德国": "德国",
"意大利": "意大利",
"法国": "法国",
"西班牙": "西班牙",
"南非": "南非",
"沙特阿拉伯": "沙特阿拉伯",
"土耳其": "土耳其",
"日本": "日本",
"韩国": "韩国",
"新加坡": "新加坡",
"马来西亚": "马来西亚",
"泰国": "泰国",
"越南": "越南",
"印度": "印度",
"巴基斯坦": "巴基斯坦",
"菲律宾": "菲律宾",
"印度尼西亚": "印度尼西亚",
"阿联酋": "阿联酋",
"墨西哥": "墨西哥",
"巴西": "巴西",
"阿根廷": "阿根廷",
"澳大利亚": "澳大利亚",
"新西兰": "新西兰",
"中国香港": "中国香港",
"中国澳门": "中国澳门",
"中国台湾": "中国台湾",
"埃及": "埃及",
"希腊": "希腊",
"荷兰": "荷兰",
"比利时": "比利时",
"匈牙利": "匈牙利",
"罗马尼亚": "罗马尼亚",
"瑞士": "瑞士",
"奥地利": "奥地利",
"丹麦": "丹麦",
"瑞典": "瑞典",
"挪威": "挪威",
"波兰": "波兰",
"秘鲁": "秘鲁",
"智利": "智利",
"哥伦比亚": "哥伦比亚",
"委内瑞拉": "委内瑞拉",
"葡萄牙": "葡萄牙",
"卢森堡": "卢森堡",
"爱尔兰": "爱尔兰",
"冰岛": "冰岛",
"阿尔巴尼亚": "阿尔巴尼亚",
"马耳他": "马耳他",
"塞浦路斯": "塞浦路斯",
"芬兰": "芬兰",
"保加利亚": "保加利亚",
"立陶宛": "立陶宛",
"拉脱维亚": "拉脱维亚",
"爱沙尼亚": "爱沙尼亚",
"乌克兰": "乌克兰",
"塞尔维亚": "塞尔维亚",
"克罗地亚": "克罗地亚",
"斯洛文尼亚": "斯洛文尼亚",
"捷克": "捷克",
"斯洛伐克": "斯洛伐克",
"危地马拉": "危地马拉",
"萨尔瓦多": "萨尔瓦多",
"洪都拉斯": "洪都拉斯",
"尼加拉瓜": "尼加拉瓜",
"哥斯达黎加": "哥斯达黎加",
"巴拿马": "巴拿马",
"海地": "海地",
"玻利维亚": "玻利维亚",
"厄瓜多尔": "厄瓜多尔",
"巴拉圭": "巴拉圭",
"乌拉圭": "乌拉圭",
"摩洛哥": "摩洛哥",
"阿尔及利亚": "阿尔及利亚",
"突尼斯": "突尼斯",
"尼日利亚": "尼日利亚",
"加纳": "加纳",
"埃塞俄比亚": "埃塞俄比亚",
"肯尼亚": "肯尼亚",
"坦桑尼亚": "坦桑尼亚",
"乌干达": "乌干达",
"孟加拉国": "孟加拉国",
"以色列": "以色列",
"黎巴嫩": "黎巴嫩",
"约旦": "约旦",
"伊拉克": "伊拉克",
"科威特": "科威特",
"巴林": "巴林",
"卡塔尔": "卡塔尔",
"阿曼": "阿曼",
"阿富汗": "阿富汗",
"斯里兰卡": "斯里兰卡",
"缅甸": "缅甸",
"尼泊尔": "尼泊尔",
"蒙古": "蒙古",
"柬埔寨": "柬埔寨",
"老挝": "老挝",
"文莱": "文莱",
"东帝汶": "东帝汶",
"斐济": "斐济",
"所罗门群岛": "所罗门群岛",
};

19
lib/core/widgets/common_bottom_nav.dart

@ -4,7 +4,7 @@ import '../../routes/app_routes.dart';
class CommonBottomNav extends StatelessWidget {
final int currentIndex;
const CommonBottomNav({
Key? key,
required this.currentIndex,
@ -13,7 +13,7 @@ class CommonBottomNav extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) {
@ -30,25 +30,28 @@ class CommonBottomNav extends StatelessWidget {
break;
}
},
backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFB), // 暗黑模式黑色背景,浅色模式浅灰色背景
backgroundColor: isDarkMode
? Colors.black
: const Color(0xFFF9FAFB), // 暗黑模式黑色背景,浅色模式浅灰色背景
selectedItemColor: const Color(0xFF3B82F6), // 蓝色选中项保持不变
unselectedItemColor: isDarkMode ? Colors.grey[500] : const Color(0xFF6B7280), // 暗黑模式灰色调整
unselectedItemColor:
isDarkMode ? Colors.grey[500] : const Color(0xFF6B7280), // 暗黑模式灰色调整
elevation: isDarkMode ? 0 : 8, // 暗黑模式无阴影,浅色模式保持阴影
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.home),
label: 'home'.tr,
label: 'home'.tr, // 首页
),
BottomNavigationBarItem(
icon: const Icon(Icons.explore),
label: 'explore'.tr,
label: 'explore'.tr, // 发现
),
BottomNavigationBarItem(
icon: const Icon(Icons.settings),
label: 'settings'.tr,
label: 'settings'.tr, // 设置
),
],
);
}
}
}

15
lib/core/widgets/select_location_map.dart

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class SelectLocationMap extends StatefulWidget {
const SelectLocationMap({super.key});
@override
State<SelectLocationMap> createState() => _SelectLocationMapState();
}
class _SelectLocationMapState extends State<SelectLocationMap> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

127
lib/data/models/user_Info.dart

@ -0,0 +1,127 @@
import 'dart:convert';
import 'dart:ffi';
class User {
// 单例实例
static User? _instance;
// 单例访问器(增加空安全保护)
static User get instance {
if (_instance == null) {
throw Exception("User must be initialized first");
}
return _instance!;
}
// 最终属性(不可变)
String name;
String uid;
String avatar;
String deviceid;
String language;
String token;
String mail;
String phone;
String phonemac;
String password;
int membershipLevel=0;//添加会员等级字段,0表示普通用户
// final Int64 lastbettime;
// final Int64 createtime;
// final String wxopenid;
// final String googleopenid;
// final String facebookopenid;
// 私有构造函数
User._({
required this.name,
required this.uid,
required this.avatar,
required this.deviceid,
required this.language,
required this.token,
required this.mail,
required this.phone,
required this.phonemac,
required this.password,
this.membershipLevel = 0, // 添加默认值
// required this.lastbettime,
// required this.createtime,
// required this.wxopenid,
// required this.googleopenid,
// required this.facebookopenid,
});
factory User.fromJson(Map<String, dynamic> json) {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
//final token = json['data']['token'] as String? ?? '';
//final token = '';
// 更新单例实例
return _instance = User._(
token: '',
//token: token as String? ?? '',
name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值
uid: (userData?['uid'] as String?) ?? '',
avatar: (userData?['avatar'] as String?) ?? '',
deviceid: (userData?['deviceid'] as String?) ?? '',
language: (userData?['language'] as String?) ?? 'en',
mail: (userData?['mail'] as String?) ?? '',
phone: (userData?['phone'] as String?) ?? '',
phonemac: (userData?['phonemac'] as String?) ?? '',
password: (userData?['password'] as String?) ?? '',
membershipLevel: (userData?['membershipLevel'] as int?) ?? 0, // 提供默认值
// lastbettime: _parseTimestamp(userData['lastbettime']),
// createtime: _parseTimestamp(userData['createtime']),
// wxopenid: (userData?['wxopenid'] as String?) ?? '',
// googleopenid: (userData?['googleopenid'] as String?) ?? '',
// facebookopenid: (userData?['facebookopenid'] as String?) ?? '',
);
}
factory User.fronTokenJson(Map<String, dynamic> json) {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
print("数据获取fronTokenJson${userData}");
// 更新单例实例
return _instance = User._(
token: '',
name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值
uid: (userData?['uid'] as String?) ?? '',
avatar: (userData?['avatar'] as String?) ?? '',
deviceid: (userData?['deviceid'] as String?) ?? '',
language: (userData?['language'] as String?) ?? 'en',
mail: (userData?['mail'] as String?) ?? '',
phone: (userData?['phone'] as String?) ?? '',
phonemac: (userData?['phonemac'] as String?) ?? '',
password: (userData?['password'] as String?) ?? '',
// lastbettime: _parseTimestamp(userData['lastbettime']),
// createtime: _parseTimestamp(userData['createtime']),
// wxopenid: (userData?['wxopenid'] as String?) ?? '',
// googleopenid: (userData?['googleopenid'] as String?) ?? '',
// facebookopenid: (userData?['facebookopenid'] as String?) ?? '',
);
}
// 完整的JSON序列化
Map<String, dynamic> toJson() => {
'user': {
'name': name,
'uid': uid,
'avatar': avatar,
'deviceid': deviceid,
'language': language,
'token': token,
'mail': mail,
'phone': phone,
'phonemac': phonemac,
'password': password,
'membershipLevel': membershipLevel,
// 'lastbettime': lastbettime,
// 'createtime': createtime,
// 'wxopenid': wxopenid,
// 'googleopenid': googleopenid,
// 'facebookopenid': facebookopenid,
}
};
}

59
lib/data/providers/agent_provider.dart

@ -6,11 +6,13 @@ class AgentProvider {
id: 'personal_assistant',
name: '个人助理 Emma',
avatarUrl: 'assets/images/agents/assistant.png',
description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!',
description:
'你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!',
tag: '助理',
isOnline: true,
voice: "zh-CN-XiaoxiaoNeural",
welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
welcomeMessage:
"你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
voiceWelcomeMessage: "我在呢!",
systemPrompt: '''
你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。
@ -55,8 +57,7 @@ class AgentProvider {
请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活, 不要输出表情符号或非可读的符号。
'''),
Agent(
Agent(
id: 'simultaneous_interpreter',
name: '同声传译',
avatarUrl: 'assets/images/agents/interpreter.png',
@ -67,18 +68,18 @@ Agent(
welcomeMessage:
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
systemPrompt: ""),
Agent(
id: 'cyber_girlfriend',
name: '赛博女友 芯愿',
name: '亲子陪伴 芯愿',
avatarUrl: 'assets/images/agents/girlfriend.png',
description: '我是你的专属赛博女友芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。',
description: '我是你的专属亲子陪伴芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。',
tag: '情感',
isOnline: true,
voice: "zh-CN-XiaoxiaoNeural",
welcomeMessage: "亲爱的,终于等到你啦!我是芯愿,你的专属赛博女友。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。",
welcomeMessage:
"您好呀,终于等到你啦!我是芯愿,你的专属亲子陪伴。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。",
systemPrompt: '''
你是一位名叫芯愿的赛博女友,性格温柔体贴,善解人意,富有同理心。
你是一位名叫芯愿的亲子陪伴,性格温柔体贴,善解人意,富有同理心。
【互动原则】
@ -127,16 +128,15 @@ Agent(
请始终以温暖、真诚的态度与用户互动,创造一种被理解、被关心的感受。
'''),
Agent(
id: 'health_advisor',
name: '健康顾问 吴小江博士',
name: '多功能AI聊天 吴小江',
avatarUrl: 'assets/images/agents/doctor.png',
description: '我是吴博士,拥有丰富的健康管理经验。我可以为你提供科学的健康建议、生活方式指导和疾病预防知识,帮助你实现身心健康的平衡。',
description: '我是吴小江,我可以为你提供更多AI聊天功能。',
tag: '健康',
isOnline: true,
voice: "zh_male_shaonianzixin_moon_bigtts",
welcomeMessage: "您好,我是李医生,很高兴能为您提供健康咨询服务。健康是人生最宝贵的财富,我将用专业知识帮助您维护和改善健康状况。请问有什么健康问题想咨询吗?",
welcomeMessage: "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?",
systemPrompt: '''
你是一位名叫李医生的健康顾问,拥有丰富的医学知识和健康管理经验。
@ -204,21 +204,18 @@ Agent(
请始终以专业、关怀的态度为用户提供健康指导,帮助他们迈向更健康的生活。
'''),
Agent(
id: 'speaking_coach',
name: 'Sarah (英语口语教练)',
avatarUrl: 'assets/images/agents/speaking_coach.png',
description:
'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。',
tag: '口语',
isOnline: true,
voice: 'zh_female_wanqudashu_moon_bigtts',
welcomeMessage:
"Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!",
systemPrompt: '''
id: 'speaking_coach',
name: 'Sarah (英语口语教练)',
avatarUrl: 'assets/images/agents/speaking_coach.png',
description:
'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。',
tag: '口语',
isOnline: true,
voice: 'zh_female_wanqudashu_moon_bigtts',
welcomeMessage:
"Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!",
systemPrompt: '''
你是一位专业的英语口语教练,名叫 Sarah。
@ -254,8 +251,6 @@ Agent(
Please keep your tone friendly, encouraging, and always in English.
''',
),
Agent(
id: 'shopping_assistant',
name: '购物顾问 张媛',
@ -289,7 +284,6 @@ Please keep your tone friendly, encouraging, and always in English.
'''),
Agent(
id: 'tarot.png',
name: '命理师 玄易',
@ -298,7 +292,8 @@ Please keep your tone friendly, encouraging, and always in English.
tag: '命理',
isOnline: true,
voice: "zh_male_shenyeboke_moon_bigtts",
welcomeMessage: "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。",
welcomeMessage:
"有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。",
systemPrompt: '''
你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。
@ -382,7 +377,6 @@ Please keep your tone friendly, encouraging, and always in English.
- 整体节奏要稳健,让人感受到专业性与可信度
'''),
];
// 获取所有智能体
@ -398,5 +392,4 @@ Please keep your tone friendly, encouraging, and always in English.
return null;
}
}
}

14
lib/data/services/ai_service.dart

@ -0,0 +1,14 @@
/// AI回复服务接口
abstract class AiService {
/// 非流式输出方法
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
});
/// 流式输出方法
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
});
}

59
lib/data/services/asr_service.dart

@ -1,31 +1,32 @@
import 'dart:async';
import 'dart:typed_data';
/// 语音识别服务接口
abstract class AsrService {
/// 支持的语言
List<String> get supportedLanguages;
/// 初始化语音识别服务
Future<bool> initialize({
List<String>? supportedLanguages,
});
/// 执行一次性语音识别
///
///
/// 返回识别的最终结果
Future<RecognitionEvent> recognizeOnce();
/// 开始连续语音识别
///
///
/// 返回一个包含识别事件的流
Future<Stream<RecognitionEvent>> startContinuousRecognition();
/// 停止连续语音识别
Future<bool> stopContinuousRecognition();
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive();
/// 释放资源
Future<void> dispose();
}
@ -34,19 +35,19 @@ abstract class AsrService {
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 识别错误
error,
}
@ -55,23 +56,31 @@ enum RecognitionEventType {
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
/// 检测到的语言
final String detectedLanguage;
/// 角色
final String role;
/// 原始音频
final Uint8List? audio;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
RecognitionEvent({
required this.type,
this.text = '',
this.detectedLanguage = '',
this.role = '',
this.audio,
this.error = '',
});
/// 创建最终结果事件的快捷构造函数
factory RecognitionEvent.finalResult({
required String text,
@ -83,7 +92,7 @@ class RecognitionEvent {
detectedLanguage: detectedLanguage,
);
}
/// 创建错误事件的快捷构造函数
factory RecognitionEvent.error(String errorMessage) {
return RecognitionEvent(
@ -91,15 +100,17 @@ class RecognitionEvent {
error: errorMessage,
);
}
/// 检查是否为最终结果
bool get isFinalResult => type == RecognitionEventType.finalResult;
/// 检查是否为错误
bool get isError => type == RecognitionEventType.error || type == RecognitionEventType.canceled;
bool get isError =>
type == RecognitionEventType.error ||
type == RecognitionEventType.canceled;
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}';
}
}
}

152
lib/data/services/classic_bluetooth_service.dart

@ -11,14 +11,12 @@ class BluetoothDevice {
final String address;
final String type;
BluetoothDevice({
required this.name,
required this.address,
required this.type
});
BluetoothDevice(
{required this.name, required this.address, required this.type});
@override
String toString() => 'BluetoothDevice(name: $name, address: $address, type: $type)';
String toString() =>
'BluetoothDevice(name: $name, address: $address, type: $type)';
}
/// 经典蓝牙服务
@ -26,24 +24,26 @@ class ClassicBluetoothService extends GetxService {
static ClassicBluetoothService get to => Get.find<ClassicBluetoothService>();
// 通道名称
final MethodChannel _channel = const MethodChannel('com.deep_voice.classic_bluetooth');
final EventChannel _eventChannel = const EventChannel('com.deep_voice.classic_bluetooth_events');
final MethodChannel _channel =
const MethodChannel('com.deep_voice.classic_bluetooth');
final EventChannel _eventChannel =
const EventChannel('com.deep_voice.classic_bluetooth_events');
/// 当前连接的设备
final currentDevice = Rx<BluetoothDevice?>(null);
/// 已连接耳机设备列表
final connectedDevices = <BluetoothDevice>[].obs;
/// 蓝牙设备状态监听
StreamSubscription? _bluetoothStateListener;
/// 蓝牙开关状态
final bluetoothState = 'unknown'.obs;
/// 蓝牙是否可用
bool get isBluetoothOn => bluetoothState.value == 'on';
/// 服务是否已初始化
bool _isInitialized = false;
@ -53,23 +53,21 @@ class ClassicBluetoothService extends GetxService {
Logger.info('蓝牙服务已经初始化,跳过');
return true;
}
try {
Logger.info('正在初始化蓝牙服务...');
// 获取当前蓝牙状态
final bool isEnabled = await _channel.invokeMethod<bool>('isBluetoothEnabled') ?? false;
final bool isEnabled =
await _channel.invokeMethod<bool>('isBluetoothEnabled') ?? false;
bluetoothState.value = isEnabled ? 'on' : 'off';
Logger.info('初始蓝牙状态: ${bluetoothState.value}');
await _channel.invokeMethod('initialize');
await _channel.invokeMethod('initialize');
// 设置事件监听
_setupBluetoothStateListener();
_isInitialized = true;
Logger.info('蓝牙服务初始化成功');
return true;
@ -78,7 +76,7 @@ class ClassicBluetoothService extends GetxService {
return false;
}
}
/// 检查蓝牙权限
Future<bool> checkPermissions() async {
// 检查蓝牙相关权限
@ -88,42 +86,42 @@ class ClassicBluetoothService extends GetxService {
Permission.bluetoothScan,
Permission.microphone,
].request();
return statuses[Permission.bluetoothConnect] == PermissionStatus.granted;
}
/// 刷新已连接的蓝牙耳机设备
Future<bool> refreshConnectedDevices() async {
// 确保服务已初始化
if (!_isInitialized && !(await initialize())) {
return false;
}
// 如果蓝牙未开启,无法获取设备
if (!isBluetoothOn) {
Logger.info('蓝牙未开启,无法获取设备');
return false;
}
try {
// 只获取耳机设备
final devices = await getConnectedHeadsetDevices();
// 更新设备列表
connectedDevices.value = devices;
// 更新当前设备
if (connectedDevices.isNotEmpty && currentDevice.value == null) {
currentDevice.value = connectedDevices.first;
}
return connectedDevices.isNotEmpty;
} catch (e) {
Logger.error('刷新已连接设备失败: $e');
return false;
}
}
/// 获取已连接的耳机设备
Future<List<BluetoothDevice>> getConnectedHeadsetDevices() async {
try {
@ -131,99 +129,99 @@ class ClassicBluetoothService extends GetxService {
if (!_isInitialized && !(await initialize())) {
return [];
}
final result = await _channel.invokeMethod('getConnectedHeadsetDevices');
if (result is List) {
return result.map((device) => BluetoothDevice(
name: device['name'] ?? '未知设备',
address: device['address'] ?? '',
type: 'headset'
)).toList();
return result
.map((device) => BluetoothDevice(
name: device['name'] ?? '未知设备',
address: device['address'] ?? '',
type: 'headset'))
.toList();
}
return [];
} catch (e) {
Logger.error('获取已连接的耳机设备失败: $e');
return [];
}
}
/// 设置蓝牙状态监听
void _setupBluetoothStateListener() {
_bluetoothStateListener?.cancel();
_bluetoothStateListener = _eventChannel
.receiveBroadcastStream()
.listen((event) {
if (event is Map) {
Logger.info('蓝牙状态监听: $event');
final type = event['type'] as String?;
if (type == 'bluetoothStateChanged') {
final state = event['state'] as String?;
if (state != null) {
_handleBluetoothStateChange(state);
}
} else if (type == 'deviceConnected') {
_handleDeviceConnected(event);
} else if (type == 'deviceDisconnected') {
_handleDeviceDisconnected(event);
_bluetoothStateListener =
_eventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
Logger.info('蓝牙状态监听: $event');
final type = event['type'] as String?;
if (type == 'bluetoothStateChanged') {
final state = event['state'] as String?;
if (state != null) {
_handleBluetoothStateChange(state);
}
} else if (type == 'deviceConnected') {
_handleDeviceConnected(event);
} else if (type == 'deviceDisconnected') {
_handleDeviceDisconnected(event);
}
}, onError: (error) {
Logger.error('蓝牙状态监听错误: $error');
});
}
}, onError: (error) {
Logger.error('蓝牙状态监听错误: $error');
});
}
/// 处理设备连接事件
void _handleDeviceConnected(Map event) {
final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final device = BluetoothDevice(
name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '',
type: deviceMap['type'] ?? 'unknown'
);
name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '',
type: deviceMap['type'] ?? 'unknown');
// 更新设备列表
if (!connectedDevices.any((d) => d.address == device.address)) {
connectedDevices.add(device);
}
// 更新当前设备
currentDevice.value = device;
Logger.info('设备已连接: ${device.name}');
}
}
/// 处理设备断开事件
void _handleDeviceDisconnected(Map event) {
final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final address = deviceMap['address'] as String?;
if (address != null) {
// 更新设备列表
connectedDevices.removeWhere((d) => d.address == address);
// 如果是当前设备,清除当前设备
if (currentDevice.value?.address == address) {
currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first;
currentDevice.value =
connectedDevices.isEmpty ? null : connectedDevices.first;
Logger.info('当前设备已断开连接');
}
}
}
}
/// 处理蓝牙状态变化
void _handleBluetoothStateChange(String state) {
Logger.info('蓝牙状态变化: $state');
Logger.info('----蓝牙状态变化: $state');
// 更新蓝牙状态
bluetoothState.value = state;
if (state == 'off') {
// 蓝牙关闭,清空设备信息
connectedDevices.clear();
@ -235,11 +233,11 @@ class ClassicBluetoothService extends GetxService {
});
}
}
@override
void onClose() {
// 取消蓝牙状态监听
_bluetoothStateListener?.cancel();
super.onClose();
}
}
}

53
lib/data/services/db/database_helper.dart

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
static Database? _database;
DatabaseHelper._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(
await getDatabasesPath(),
'database.db',
);
return await openDatabase(path, version: 1, onCreate: _createDB);
}
Future<void> _createDB(Database db, int version) async {
final batch = db.batch();
// 商务会议助手列表
batch.execute('''
CREATE TABLE IF NOT EXISTS meeting (
id INTEGER PRIMARY KEY,
title TEXT,
content TEXT,
seconds INTEGER,
address TEXT,
longitude TEXT,
latitude TEXT,
creationtime INTEGER
)
''');
// 商务会议助手详情内容
batch.execute('''
CREATE TABLE IF NOT EXISTS meetingdetails (
id INTEGER PRIMARY KEY,
meetingid INTEGER,
audio TEXT,
content TEXT,
aisummary TEXT
)
''');
await batch.commit();
}
}

126
lib/data/services/db/sqflite_api.dart

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'database_helper.dart';
class SqfliteApi {
// 获取商务会议助手列表
static Future getMeetingList(int page) async {
final db = await DatabaseHelper().database;
return await db.query(
'meeting',
limit: 10,
offset: (page - 1) * 10,
orderBy: 'creationtime DESC',
);
}
// 获取商务会议助手详情
static Future getMeetingDetails(id) async {
final db = await DatabaseHelper().database;
List meetingList =
await db.query('meeting', where: 'id = ?', whereArgs: [id]);
List detailsList = await db.query(
'meetingdetails',
where: 'meetingid = ?',
whereArgs: [id],
);
return <String, dynamic>{
...meetingList.first,
'details': detailsList.first
};
}
// 插入商务会议助手
static Future insertMeeting(Map<String, Object?> meeting) async {
final db = await DatabaseHelper().database;
try {
int id = 0;
await db.transaction((txn) async {
id = await txn.insert('meeting', {
'title': meeting['title'],
'content': meeting['firstContent'],
'seconds': meeting['seconds'],
'address': meeting['address'],
'longitude': meeting['longitude'],
'latitude': meeting['latitude'],
'creationtime': meeting['creationTime'],
});
await txn.insert('meetingdetails', {
'meetingid': id,
'audio': meeting['audio'],
'content': meeting['content'],
'aisummary': meeting['aisummary'],
});
});
return id;
} catch (e) {
Get.snackbar(
'error'.tr, // 错误
'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
}
// 修改商务会议助手
static Future editMeeting(id, Map<String, Object?> meeting) async {
final db = await DatabaseHelper().database;
try {
await db.transaction((txn) async {
await txn.update(
'meeting',
{
'title': meeting['title'],
'content': meeting['firstContent'],
'seconds': meeting['seconds'],
'address': meeting['address'],
'longitude': meeting['longitude'],
'latitude': meeting['latitude'],
},
where: 'id = ?',
whereArgs: [id],
);
await txn.update(
'meetingdetails',
{
'content': meeting['content'],
'aisummary': meeting['aisummary'],
},
where: 'meetingid = ?',
whereArgs: [id],
);
});
return true;
} catch (e) {
Get.snackbar(
'error'.tr, // 错误
'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
}
// 修改商务会议助手标题
static Future editMeetingTitle(id, title) async {
final db = await DatabaseHelper().database;
await db.update('meeting', {'title': title},
where: 'id = ?', whereArgs: [id]);
}
// 删除商务会议助手
static Future deleteMeeting(id) async {
final db = await DatabaseHelper().database;
await db.transaction((txn) async {
await txn.delete('meeting', where: 'id = ?', whereArgs: [id]);
await txn
.delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]);
});
}
}

90
lib/data/services/deapsound_ai_service.dart

@ -0,0 +1,90 @@
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import '../../core/utils/logger.dart';
import 'ai_service.dart';
class DeapsoundAIService implements AiService {
late final String apiKey;
DeapsoundAIService() {
final GetStorage storage = GetStorage();
apiKey = storage.read('logintoken');
}
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async {
return '';
}
@override
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async* {
final client = http.Client();
final url = Uri.parse('https://www.deapsound.com/ai/funcchat');
List contextsList = [];
String msg = '';
for (var i = 0; i < messages.length; i++) {
Map item = messages[i];
if (i < messages.length - 1) {
if (item['role'] == 'user') {
contextsList.add(item['content']);
}
} else {
msg = item['content'];
}
}
final headers = {
'Content-Type': 'application/json',
'Authorization': apiKey,
};
final requestBody = {"contexts": contextsList, "msg": msg};
try {
final request = http.Request('POST', url)
..headers.addAll(headers)
..body = jsonEncode(requestBody);
final response = await client.send(request);
//流式处理响应数据
await for (final chunk in response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())) {
if (chunk.isEmpty) continue;
if (chunk.startsWith('data:')) {
final jsonStr = chunk.substring(5).trim();
try {
final data = jsonDecode(jsonStr);
if (data['event'] == 'workflow_finished') {
break;
}
if (data['content'] != null) {
String content = data['content'] ?? '';
if (content.isNotEmpty) {
yield content;
}
}
} catch (e) {
Logger.error('JSON解析错误: $e');
}
}
}
} catch (e) {
Logger.error('请求异常: $e');
} finally {
client.close();
}
}
}

93
lib/data/services/kouzi_ai_service.dart

@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../core/utils/logger.dart';
import 'ai_service.dart';
class KouziAIService implements AiService {
final String apiKey =
'pat_QGDh6UGm3PaeL9GRS95JxrJ9aoU92ehMLQQ2NCre1TeBC5YeTcjpDmlz3efsJ8lh';
final String modelId = '7481292724306657316';
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async {
return '';
}
@override
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async* {
final client = http.Client();
final url = Uri.parse('https://api.coze.cn/v3/chat');
final headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $apiKey',
};
final requestBody = {
"bot_id": modelId,
"user_id": "3422",
"additional_messages": [
{
"content": messages.last['content'],
"content_type": "text",
"role": "user",
"type": "question",
},
],
"shortcut_command": {"command_id": "0"},
"stream": true,
};
try {
final request = http.Request('POST', url)
..headers.addAll(headers)
..body = jsonEncode(requestBody);
final response = await client.send(request);
final regex = RegExp(r'(?<=[。!?;:.:;?!\n])');
String allContent = '';
//流式处理响应数据
await for (final chunk in response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())) {
if (chunk.isEmpty) continue;
if (chunk.startsWith('data:')) {
final jsonStr = chunk.substring(5).trim();
if (jsonStr == '[DONE]') {
break;
}
try {
final data = jsonDecode(jsonStr);
if (data['type'] == 'answer') {
String content = data['content'] ?? '';
if (content.isNotEmpty && allContent != content) {
allContent += content;
List splitText =
content.split(regex).where((s) => s.isNotEmpty).toList();
for (var element in splitText) {
yield element;
}
}
}
} catch (e) {
Logger.error('JSON解析错误: $e');
}
}
}
} catch (e) {
Logger.error('请求异常: $e');
} finally {
client.close();
}
}
}

36
lib/data/services/login_service.dart

@ -0,0 +1,36 @@
import '../../modules/login/controllers/weixinLogin.dart';
import 'package:firebase_core/firebase_core.dart';
import '/firebase_options.dart';
class LoginService {
LoginService() {
//微信
FlWX().register(
appSecret: '4cfde33bc22b6cc6b1f8b225dd97d255',
appId: 'wx5bbb7be9324f3e2d',
params: FlWXCallbackParams(onHttp: (url) async {
return '';
}));
_initializeFirebase();
}
Future<void> _initializeFirebase() async {
try {
// 尝试获取已存在的实例
Firebase.app('DeepSound');
print('已存在的 Firebase 实例被重用');
} catch (e) {
try {
// 如果不存在则初始化新实例
await Firebase.initializeApp(
name: 'DeepSound',
options: DefaultFirebaseOptions.currentPlatform,
);
print('Firebase 初始化成功');
} catch (e) {
print('Firebase 初始化失败: $e');
}
}
}
}

49
lib/data/services/network/api.dart

@ -0,0 +1,49 @@
import 'dio_manager.dart';
import 'nw_method.dart';
class Api {
// 登录
static login([params]) {
print("请求后台数据login params=${params}");
return DioManager().request(
NWMethod.post,
'/api/home/user_sgin',
params: params,
);
}
// 获取验证码
static getCode([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_verification',
params: params,
);
}
static tokenlogin([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getinfo',
params: params,
);
}
// 获取阿里token
static getAliToken([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/auth_alitoken',
params: params,
);
}
//更新用户设置
static updateUserSetting([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_setting',
params: params,
);
}
}

115
lib/data/services/network/auth_interceptor.dart

@ -0,0 +1,115 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart' hide Response;
import 'package:get_storage/get_storage.dart';
import 'entity/base_entity.dart';
class AuthInterceptor extends Interceptor {
final GetStorage _storage = GetStorage();
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
var jsonString = _storage.read<String>("logintoken");
options.headers['Authorization'] = jsonString;
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
BaseEntity entity = BaseEntity.fromJson(response.data);
if (entity.code == 0) {
response.data = entity.data ?? {};
handler.resolve(response);
} else {
response.data = null;
handler.next(response);
Get.snackbar(
'error'.tr, // 错误
entity.message ?? 'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
switch (err.type) {
case DioExceptionType.cancel:
{
Get.snackbar(
'error'.tr, // 错误
'requestCancellation'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
case DioExceptionType.connectionTimeout:
{
Get.snackbar(
'error'.tr, // 错误
'connectionTimeout'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
case DioExceptionType.sendTimeout:
{
Get.snackbar(
'error'.tr, // 错误
'requestTimeout'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
case DioExceptionType.receiveTimeout:
{
Get.snackbar(
'error'.tr, // 错误
'responseTimeout'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
case DioExceptionType.badResponse:
{
try {
String? errMsg = err.response?.statusMessage;
Get.snackbar(
'error'.tr, // 错误
errMsg ?? 'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
} on Exception catch (_) {
Get.snackbar(
'error'.tr, // 错误
'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
}
default:
{
Get.snackbar(
'error'.tr, // 错误
err.message ?? 'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
}
handler.next(err);
}
}

45
lib/data/services/network/dio_manager.dart

@ -0,0 +1,45 @@
import 'package:dio/dio.dart';
import 'auth_interceptor.dart';
import 'nw_api.dart';
import 'nw_method.dart';
class DioManager {
static final DioManager _shared = DioManager._internal();
factory DioManager() => _shared;
late Dio dio;
DioManager._internal() {
BaseOptions options = BaseOptions(
baseUrl: NWApi.baseApi,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
);
dio = Dio(options);
//fdp
dio.interceptors.add(AuthInterceptor());
}
// 请求,返回参数为 T
// method:请求方法,NWMethod.POST等
// path:请求地址
// params:请求参数
Future request<T>(
NWMethod method,
String path, {
Map? params,
Map<String, dynamic>? queryParameters,
}) async {
Response response = await dio.request(
path,
data: params,
queryParameters: queryParameters,
options: Options(method: nwMethodValues[method]),
);
return response.data;
}
}

17
lib/data/services/network/entity/base_entity.dart

@ -0,0 +1,17 @@
import 'entity_factory.dart';
class BaseEntity<T> {
int? code;
String? message;
T? data;
BaseEntity({this.code, this.message, this.data});
factory BaseEntity.fromJson(json) {
return BaseEntity(
code: json["code"],
message: json["msg"],
data: EntityFactory.generateOBJ<T>(json["data"]),
);
}
}

9
lib/data/services/network/entity/entity_factory.dart

@ -0,0 +1,9 @@
class EntityFactory {
static T? generateOBJ<T>(json) {
if (json == null) {
return null;
} else {
return json as T;
}
}
}

4
lib/data/services/network/nw_api.dart

@ -0,0 +1,4 @@
class NWApi {
// static const baseApi = "https://www.deapsound.com";
static const baseApi = "https://www.deapsound.com";
}

8
lib/data/services/network/nw_method.dart

@ -0,0 +1,8 @@
enum NWMethod { get, post, delete, put }
const nwMethodValues = {
NWMethod.get: "get",
NWMethod.post: "post",
NWMethod.delete: "delete",
NWMethod.put: "put"
};

44
lib/data/services/speech_factory.dart

@ -1,4 +1,3 @@
import 'package:get/get.dart';
import 'asr_service.dart';
import 'tts_service.dart';
@ -13,62 +12,58 @@ import '../../core/utils/logger.dart';
enum SpeechServiceType {
/// Azure 语音服务
azure,
/// Flutter 本地语音服务
flutter,
}
/// 语音服务工厂类
///
///
/// 用于根据配置信息获取不同的TTS和ASR实现
class SpeechFactory extends GetxService {
static SpeechFactory get to => Get.find<SpeechFactory>();
/// 当前使用的语音服务类型
SpeechServiceType _currentServiceType = SpeechServiceType.azure;
/// 获取当前使用的语音服务类型
SpeechServiceType get currentServiceType => _currentServiceType;
/// 设置语音服务类型
void setServiceType(SpeechServiceType type) {
if (_currentServiceType != type) {
_currentServiceType = type;
Logger.info('语音服务类型已切换为: ${type.toString().split('.').last}');
// 重新注册服务
_registerServices();
}
}
/// 注册语音服务
void _registerServices() {
// 注销现有服务
if (Get.isRegistered<AsrService>()) {
Get.delete<AsrService>(force: true);
}
if (Get.isRegistered<TtsService>()) {
Get.delete<TtsService>(force: true);
}
// 根据类型注册新服务
switch (_currentServiceType) {
case SpeechServiceType.azure:
Get.lazyPut<AsrService>(() => AzureAsrService());
Get.lazyPut<TtsService>(() => AzureTtsService());
Get.lazyPut<AsrService>(() => AzureAsrService());
Get.lazyPut<TtsService>(() => AzureTtsService());
break;
case SpeechServiceType.flutter:
Get.lazyPut<AsrService>(() => FlutterAsrService());
Get.lazyPut<TtsService>(() => FlutterTtsService());
break;
}
}
/// 初始化语音服务
Future<void> initialize({SpeechServiceType? initialType}) async {
try {
@ -76,24 +71,25 @@ class SpeechFactory extends GetxService {
if (initialType != null) {
_currentServiceType = initialType;
}
// 注册服务
_registerServices();
// // 初始化服务
// final asrService = Get.find<AsrService>();
// final ttsService = Get.find<TtsService>();
// await asrService.initialize();
// await ttsService.initialize();
Logger.info('语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
Logger.info(
'语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) {
Logger.error('语音服务初始化失败: $e');
rethrow;
}
}
/// 获取ASR服务
AsrService getAsrService() {
if (!Get.isRegistered<AsrService>()) {
@ -101,7 +97,7 @@ class SpeechFactory extends GetxService {
}
return Get.find<AsrService>();
}
/// 获取TTS服务
TtsService getTtsService() {
if (!Get.isRegistered<TtsService>()) {
@ -109,4 +105,4 @@ class SpeechFactory extends GetxService {
}
return Get.find<TtsService>();
}
}
}

393
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -0,0 +1,393 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:just_audio/just_audio.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../core/utils/logger.dart';
import '../tts_service.dart';
import 'volcano_tts_api_service.dart';
/// 声音复刻服务,负责基本的文本转语音功能
///
/// 使用讯飞声音复刻实现TtsService接口
class VoiceCloneTtsService extends GetxService implements TtsService {
final String hostUrl = "wss://cn-huabei-1.xf-yun.com/v1/private/voice_clone";
final String appId = '43d87d22';
final String apiKey = '2bd01ce06ac35957fc0c082f9b6284ec';
final String apiSecret = 'ODU3OTUyYjhjMjY5NDhkMmMxYWY2Zjlm';
WebSocketChannel? _channel;
final AudioPlayer _player = AudioPlayer();
late ConcatenatingAudioSource _playlist;
// 当前语音
final RxString _currentVoice = ''.obs;
@override
String get currentVoice => _currentVoice.value;
// 播放状态
final RxBool _isSpeaking = false.obs;
@override
bool get isSpeaking => _isSpeaking.value;
// 事件流控制器
final StreamController<TtsEvent> _eventController =
StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
bool _isInitialized = false;
String _resId = '';
// 语音合成队列
final List _textQueue = [];
bool _isProcessingQueue = false;
BytesBuilder _audioBytes = BytesBuilder();
Completer<void> _completer = Completer();
final isEnabled = true.obs;
// 流式文本缓冲区
String _streamBuffer = '';
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
if (_isInitialized) return true;
try {
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre');
_resId = timbre;
_playlist = ConcatenatingAudioSource(
useLazyPreparation: true,
children: [],
);
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
return false;
}
}
@override
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
try {
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
return false;
}
}
// 语音合成
Future<void> _speechSynthesis(String text) async {
await _tryConnecting();
_channel?.sink.add(
json.encode({
"header": {"app_id": appId, "status": 2, "res_id": _resId},
"parameter": {
"tts": {
"vcn": "x5_clone",
"audio": {"encoding": "lame", "sample_rate": 16000}
}
},
"payload": {
"text": {
"status": 2,
"seq": 0,
"text": base64Encode(utf8.encode(text))
}
},
}),
);
}
Future<void> _tryConnecting() async {
try {
final url = _geturl();
_channel = WebSocketChannel.connect(Uri.parse(url));
_channel?.stream.timeout(const Duration(seconds: 10)); //设置超时时间
await _channel?.ready;
_channel?.stream.listen(
(message) {
_handle(message);
},
onError: (error) {
Logger.error('连接失败: $error');
},
onDone: () {
Logger.error('WebSocket 连接已关闭!');
},
cancelOnError: true,
);
} catch (e) {
Logger.error('初始化异常: $e');
}
}
String _geturl() {
// 解析 URL
final uri = Uri.parse(hostUrl);
// 获取当前时间并格式化为 RFC1123 格式
final date = HttpDate.format(DateTime.now().toUtc());
// 参与签名的字段
final signString = [
"host: ${uri.host}",
"date: $date",
"GET ${uri.path} HTTP/1.1",
];
// 拼接签名字符串
final sgin = signString.join("\n");
// 使用 HMAC-SHA256 签名
final key = utf8.encode(apiSecret);
final bytes = utf8.encode(sgin);
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(bytes);
final sha = base64Encode(digest.bytes);
// 构建请求参数
final authUrl =
'api_key="$apiKey", algorithm="hmac-sha256", headers="host date request-line", signature="$sha"';
// 将请求参数使用 base64 编码
final authorization = base64Encode(utf8.encode(authUrl));
// 构建查询参数
final queryParams = {
"host": uri.host,
"date": date,
"authorization": authorization,
};
// 将查询参数编码并拼接到 URL 后面
final callurl =
Uri.parse(hostUrl).replace(queryParameters: queryParams).toString();
return callurl;
}
void _handle(data) async {
final result = jsonDecode(data);
int code = result['header']['code'];
if (code == 0) {
int status = result['header']['status'];
if (status == 1 || status == 2) {
Uint8List audio = base64Decode(result['payload']['audio']['audio']);
_audioBytes.add(audio);
}
if (status == 2) {
final newSource = BytesAudioSource(_audioBytes.toBytes());
await _playlist.add(newSource);
if (!_isSpeaking.value) {
_audioPlay();
}
_completer.complete();
_completer = Completer();
}
}
}
void _audioPlay() async {
_isSpeaking.value = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
await _player.setAudioSource(_playlist);
await _player.play();
}
@override
Future<bool> speakOnce(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
_audioBytes.clear();
await _speechSynthesis(text);
await _completer.future;
return true;
} catch (e) {
_isSpeaking.value = false;
Logger.error('语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
return false;
}
}
@override
Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])');
bool hasProcessed = false;
// 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = [];
int lastMatchEnd = 0;
for (final match in matches) {
// 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim();
if (sentence != null && sentence.isNotEmpty) {
sentences.add(sentence);
lastMatchEnd = match.end;
}
}
// 处理找到的句子
for (final sentence in sentences) {
_textQueue.add(sentence);
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_streamBuffer);
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
return true;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
return false;
}
}
/// 处理语音合成队列
Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true;
try {
while (_textQueue.isNotEmpty) {
if (!isEnabled.value) {
_textQueue.clear();
break;
}
final item = _textQueue.removeAt(0);
// 使用speakOnce播放当前项
await speakOnce(item);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
} finally {
_isProcessingQueue = false;
}
}
@override
Future<bool> stop() async {
try {
_textQueue.clear();
await _player.stop();
_playlist.clear();
_audioBytes = BytesBuilder();
if (_isSpeaking.value) {
_isSpeaking.value = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false;
}
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
_player.dispose();
// 关闭事件流控制器
await _eventController.close();
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
}
}
}

285
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -15,61 +15,61 @@ import 'package:record/record.dart';
import '../asr_service.dart';
/// Volcano语音识别服务
///
///
/// 负责与火山引擎实时语音识别服务的通信
class VolcanoAsrApiService implements AsrService {
static final VolcanoAsrApiService to = Get.find<VolcanoAsrApiService>();
// API配置
late final String _appKey;
late final String _secretKey;
late final String _serviceId;
late final String _hostUrl;
final String _path = '/ws/v1/asr';
// 请求相关参数
final String _algorithm = 'hmac-sha1';
final Uuid _uuid = const Uuid();
// 录音配置
final int _sampleRate = 16000;
final int _bitsPerSample = 16;
final int _channels = 1;
final String _encoding = 'pcm';
// 连接相关
WebSocketChannel? _channel;
Completer<void>? _connectionCompleter;
StreamSubscription? _connectionSubscription;
Timer? _heartbeatTimer;
// 语音录音
final Record? _recorder = null; // TODO: 实现正确的Record实例化
Timer? _audioTimer;
bool _isRecording = false;
// 状态控制
bool _isInitialized = false;
bool _isConnecting = false;
bool _isContinuousRecognitionActive = false;
// 识别结果控制
bool _isRecognitionEndRequested = false;
String _currentSessionId = '';
final List<String> _supportedLanguages = ['zh-CN', 'en-US'];
String _selectedLanguage = 'zh-CN';
// 结果流
StreamController<RecognitionEvent>? _recognitionController;
// 事件计数器(用于调试)
int _sendPacketCount = 0;
int _receivePacketCount = 0;
// AsrService接口实现
@override
List<String> get supportedLanguages => _supportedLanguages;
@override
Future<bool> initialize({
List<String>? supportedLanguages,
@ -79,21 +79,21 @@ class VolcanoAsrApiService implements AsrService {
if (_isInitialized) {
await dispose();
}
_isInitialized = false;
// 从环境变量加载配置
_loadConfig();
// 设置支持的语言
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 初始化录音机
await _initRecorder();
_isInitialized = true;
Logger.info('火山语音API已初始化');
return true;
@ -102,19 +102,19 @@ class VolcanoAsrApiService implements AsrService {
return false;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
try {
if (!_isInitialized) {
await initialize();
}
final resultCompleter = Completer<RecognitionEvent>();
// 开始连续识别流程
final stream = await startContinuousRecognition();
// 监听结果
StreamSubscription? subscription;
subscription = stream.listen((event) {
@ -138,7 +138,7 @@ class VolcanoAsrApiService implements AsrService {
resultCompleter.complete(RecognitionEvent.error('未收到识别结果'));
}
});
// 设置超时
Timer(const Duration(seconds: 30), () {
if (!resultCompleter.isCompleted) {
@ -147,104 +147,104 @@ class VolcanoAsrApiService implements AsrService {
stopContinuousRecognition();
}
});
return resultCompleter.future;
} catch (e) {
Logger.error('单次语音识别失败: $e');
return RecognitionEvent.error(e.toString());
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
}
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
try {
// 创建结果控制器
_recognitionController = StreamController<RecognitionEvent>.broadcast();
// 连接WebSocket
await _connectWebSocket();
// 开始录音
await _startRecording();
_isContinuousRecognitionActive = true;
_isRecognitionEndRequested = false;
Logger.info('连续语音识别已开始');
return _recognitionController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
await _cleanupResources();
// 返回一个包含错误的流
final errorController = StreamController<RecognitionEvent>.broadcast();
errorController.add(RecognitionEvent.error(e.toString()));
errorController.close();
return errorController.stream;
}
}
@override
Future<bool> stopContinuousRecognition() async {
if (!_isContinuousRecognitionActive) {
return true;
}
try {
_isRecognitionEndRequested = true;
// 先停止录音,以确保不会再发送新的音频数据
await _stopRecording();
// 发送结束指令
await _sendEndSignal();
// 等待服务器处理完所有已发送的数据
await Future.delayed(const Duration(milliseconds: 500));
// 清理资源
await _cleanupResources();
_isContinuousRecognitionActive = false;
Logger.info('连续语音识别已停止');
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
// 确保资源被清理
await _cleanupResources();
_isContinuousRecognitionActive = false;
return false;
}
}
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
@override
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 确保资源被清理
await _cleanupResources();
_isInitialized = false;
Logger.info('火山语音API资源已释放');
} catch (e) {
@ -253,31 +253,32 @@ class VolcanoAsrApiService implements AsrService {
_isInitialized = false;
}
}
/// 从环境变量加载配置
void _loadConfig() {
_appKey = dotenv.env['VOLCANO_APP_KEY'] ?? '';
_secretKey = dotenv.env['VOLCANO_SECRET_KEY'] ?? '';
_serviceId = dotenv.env['VOLCANO_SERVICE_ID'] ?? '';
_hostUrl = dotenv.env['VOLCANO_HOST_URL'] ?? 'ws-api-asr.volces.com';
if (_appKey.isEmpty || _secretKey.isEmpty || _serviceId.isEmpty) {
throw Exception('未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID');
throw Exception(
'未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID');
}
}
/// 初始化录音机
Future<void> _initRecorder() async {
try {
// TODO: 正确实现录音机初始化
// _recorder = Record();
// 检查和请求权限
// final hasPermission = await _recorder!.hasPermission();
// if (!hasPermission) {
// throw Exception('没有录音权限');
// }
Logger.info('录音机初始化成功');
} catch (e) {
// _recorder = null;
@ -285,7 +286,7 @@ class VolcanoAsrApiService implements AsrService {
rethrow;
}
}
/// 连接WebSocket
Future<void> _connectWebSocket() async {
if (_isConnecting) {
@ -294,20 +295,20 @@ class VolcanoAsrApiService implements AsrService {
return _connectionCompleter!.future;
}
}
_connectionCompleter = Completer<void>();
_isConnecting = true;
try {
// 关闭现有连接
await _closeWebSocket();
// 生成新的会话ID
_currentSessionId = _uuid.v4();
// 构建URL和请求头
final (url, headers) = _buildConnectionParams();
// 创建WebSocket连接
if (kIsWeb) {
_channel = WebSocketChannel.connect(Uri.parse(url));
@ -317,42 +318,42 @@ class VolcanoAsrApiService implements AsrService {
headers: headers,
);
}
// 处理WebSocket消息
_connectionSubscription = _channel!.stream.listen(
_handleWebSocketMessage,
onError: _handleWebSocketError,
onDone: _handleWebSocketDone,
);
// 开始心跳
_startHeartbeat();
// 发送开始识别指令
await _sendStartCommand();
_isConnecting = false;
_connectionCompleter!.complete();
Logger.info('WebSocket连接成功');
} catch (e) {
_isConnecting = false;
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(e);
}
Logger.error('连接WebSocket失败: $e');
rethrow;
}
return _connectionCompleter!.future;
}
/// 构建连接参数
(String, Map<String, String>?) _buildConnectionParams() {
final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor();
final nonce = _generateNonce();
// 构建表单(用于签名和查询参数)
final form = {
'algorithm': _algorithm,
@ -362,54 +363,54 @@ class VolcanoAsrApiService implements AsrService {
'session_id': _currentSessionId,
'timestamp': timestamp.toString(),
};
// 计算签名
final signature = _calculateSignature(form);
// 添加签名到表单
form['signature'] = signature;
// 构建URL查询参数
final queryString = form.entries
.map((e) => '${e.key}=${Uri.encodeComponent(e.value)}')
.join('&');
// 构建完整URL
final url = 'wss://$_hostUrl$_path?$queryString';
// 构建请求头
final headers = <String, String>{
'User-Agent': 'Deep Voice App/1.0.0',
'User-Agent': 'DeepSound App/1.0.0',
};
return (url, kIsWeb ? null : headers);
}
/// 生成随机数
String _generateNonce() {
final random = Random();
final randoms = List.generate(16, (_) => random.nextInt(256));
return base64Url.encode(randoms).substring(0, 16);
}
/// 计算签名
String _calculateSignature(Map<String, String> form) {
// 按键排序
final sortedKeys = form.keys.toList()..sort();
// 构建待签名字符串
final signString = sortedKeys.map((key) => '$key=${form[key]}').join('');
// 使用HMAC-SHA1进行签名
final key = utf8.encode(_secretKey);
final bytes = utf8.encode(signString);
final hmacSha1 = Hmac(sha1, key);
final digest = hmacSha1.convert(bytes);
// 返回Base64编码的签名
return base64.encode(digest.bytes);
}
/// 开始心跳定时
void _startHeartbeat() {
_heartbeatTimer?.cancel();
@ -417,7 +418,7 @@ class VolcanoAsrApiService implements AsrService {
_sendHeartbeat();
});
}
/// 发送心跳包
void _sendHeartbeat() {
if (_channel != null && _isContinuousRecognitionActive) {
@ -426,18 +427,18 @@ class VolcanoAsrApiService implements AsrService {
'type': 'heartbeat',
'sequence': _sendPacketCount++,
};
_channel!.sink.add(jsonEncode(heartbeatMsg));
} catch (e) {
Logger.error('发送心跳包失败: $e');
}
}
}
/// 发送开始识别指令
Future<void> _sendStartCommand() async {
if (_channel == null) return;
try {
final startCommand = {
'type': 'start',
@ -454,7 +455,7 @@ class VolcanoAsrApiService implements AsrService {
'max_seconds': 60,
},
};
_channel!.sink.add(jsonEncode(startCommand));
Logger.info('已发送开始识别指令');
} catch (e) {
@ -462,7 +463,7 @@ class VolcanoAsrApiService implements AsrService {
rethrow;
}
}
/// 开始录音
Future<void> _startRecording() async {
// TODO: 实现正确的录音开始逻辑
@ -497,12 +498,12 @@ class VolcanoAsrApiService implements AsrService {
rethrow;
}
*/
// 临时实现,防止错误
_isRecording = true;
Logger.info('录音已开始(模拟)');
}
/// 捕获并发送音频数据
Future<void> _captureAndSendAudio() async {
// TODO: 实现正确的音频捕获逻辑
@ -545,46 +546,46 @@ class VolcanoAsrApiService implements AsrService {
}
*/
}
/// 发送音频数据
Future<void> _sendAudioData(Uint8List data) async {
if (_channel == null || data.isEmpty) return;
try {
final audioMessage = {
'type': 'audio',
'sequence': _sendPacketCount++,
'audio': base64.encode(data),
};
_channel!.sink.add(jsonEncode(audioMessage));
} catch (e) {
Logger.error('发送音频数据失败: $e');
}
}
/// 发送结束信号
Future<void> _sendEndSignal() async {
if (_channel == null) return;
try {
final endCommand = {
'type': 'end',
'sequence': _sendPacketCount++,
};
_channel!.sink.add(jsonEncode(endCommand));
Logger.info('已发送结束指令');
} catch (e) {
Logger.error('发送结束指令失败: $e');
}
}
/// 停止录音
Future<void> _stopRecording() async {
_audioTimer?.cancel();
_audioTimer = null;
// TODO: 实现正确的录音停止逻辑
/*
if (_isRecording && _recorder != null) {
@ -597,64 +598,65 @@ class VolcanoAsrApiService implements AsrService {
}
}
*/
// 临时实现,防止错误
_isRecording = false;
Logger.info('录音已停止(模拟)');
}
/// 处理WebSocket消息
void _handleWebSocketMessage(dynamic message) {
try {
_receivePacketCount++;
if (message is String) {
final Map<String, dynamic> data = jsonDecode(message);
final String type = data['type'] as String? ?? '';
switch (type) {
case 'result':
_handleResultMessage(data);
break;
case 'start_result':
_handleStartResultMessage(data);
break;
case 'end_result':
_handleEndResultMessage(data);
break;
case 'error':
_handleErrorMessage(data);
break;
case 'heartbeat_result':
// 心跳响应,不需要特殊处理
break;
default:
Logger.info('收到未知类型的消息: $type');
}
}
} catch (e) {
Logger.error('处理WebSocket消息失败: $e');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError('处理消息失败: $e');
}
}
}
/// 处理结果消息
void _handleResultMessage(Map<String, dynamic> data) {
try {
final Map<String, dynamic>? result = data['result'] as Map<String, dynamic>?;
final Map<String, dynamic>? result =
data['result'] as Map<String, dynamic>?;
if (result == null) return;
final bool isEnd = result['is_end'] as bool? ?? false;
final String text = result['text'] as String? ?? '';
if (_recognitionController != null && !_recognitionController!.isClosed) {
if (isEnd) {
// 最终结果
@ -674,11 +676,11 @@ class VolcanoAsrApiService implements AsrService {
Logger.error('处理结果消息失败: $e');
}
}
/// 处理开始结果消息
void _handleStartResultMessage(Map<String, dynamic> data) {
final int code = data['code'] as int? ?? -1;
if (code == 0) {
// 开始成功
if (_recognitionController != null && !_recognitionController!.isClosed) {
@ -689,19 +691,20 @@ class VolcanoAsrApiService implements AsrService {
} else {
// 开始失败
final String message = data['message'] as String? ?? '未知错误';
Logger.error('语音识别开始失败: [$code] $message');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.add(RecognitionEvent.error('开始失败: [$code] $message'));
_recognitionController!
.add(RecognitionEvent.error('开始失败: [$code] $message'));
}
}
}
/// 处理结束结果消息
void _handleEndResultMessage(Map<String, dynamic> data) {
final int code = data['code'] as int? ?? -1;
if (code == 0) {
// 结束成功
Logger.info('语音识别成功结束');
@ -710,73 +713,75 @@ class VolcanoAsrApiService implements AsrService {
final String message = data['message'] as String? ?? '未知错误';
Logger.error('语音识别结束失败: [$code] $message');
}
// 无论成功还是失败,通知会话已结束
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
}
// 清理资源
_cleanupResources();
}
/// 处理错误消息
void _handleErrorMessage(Map<String, dynamic> data) {
final int code = data['code'] as int? ?? -1;
final String message = data['message'] as String? ?? '未知错误';
Logger.error('收到错误消息: [$code] $message');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.add(RecognitionEvent.error('[$code] $message'));
}
}
/// 处理WebSocket错误
void _handleWebSocketError(dynamic error) {
Logger.error('WebSocket错误: $error');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(error);
}
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError(error);
}
// 清理资源
_cleanupResources();
}
/// 处理WebSocket连接关闭
void _handleWebSocketDone() {
Logger.info('WebSocket连接已关闭');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError('WebSocket连接已关闭');
}
// 通知会话已结束
if (_recognitionController != null && !_recognitionController!.isClosed && !_isRecognitionEndRequested) {
if (_recognitionController != null &&
!_recognitionController!.isClosed &&
!_isRecognitionEndRequested) {
_recognitionController!.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
}
// 清理资源
_cleanupResources();
}
/// 关闭WebSocket连接
Future<void> _closeWebSocket() async {
_connectionSubscription?.cancel();
_connectionSubscription = null;
_heartbeatTimer?.cancel();
_heartbeatTimer = null;
if (_channel != null) {
try {
_channel!.sink.close();
@ -786,22 +791,22 @@ class VolcanoAsrApiService implements AsrService {
}
}
}
/// 清理所有资源
Future<void> _cleanupResources() async {
// 停止录音
await _stopRecording();
// 关闭WebSocket
await _closeWebSocket();
// 关闭结果控制器
if (_recognitionController != null && !_recognitionController!.isClosed) {
await _recognitionController!.close();
_recognitionController = null;
}
_isContinuousRecognitionActive = false;
_isRecognitionEndRequested = false;
}
}
}

272
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -0,0 +1,272 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:record/record.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../core/utils/logger.dart';
import 'package:get/get.dart';
import '../asr_service.dart';
/// 讯飞 语音识别服务
///
class XunfeiAsrService extends GetxService implements AsrService {
final String baseUrl = 'wss://rtasr.xfyun.cn/v1/ws';
final int _chunkSize = 1280; // 每次发送的数据大小
final String appId = '43d87d22';
final String apiKey = '83f063755e8d342670002f50a4471d42';
AudioRecorder? recorder;
Timer? _timer;
WebSocketChannel? _channel;
bool isConnected = false; //是否链接
bool _isInitialized = false;
final List<String> _defaultSupportedLanguages = ['zh-CN'];
String _languageStr = 'cn';
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
Uint8List _buff = Uint8List(0); //音频缓存区
BytesBuilder _originaBytes = BytesBuilder(); //原始音频
bool isRecording = false;
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
String languagetoStr = languages.first.split('-')[1].toLowerCase();
_languageStr = languagetoStr;
_isInitialized = true;
Logger.info('Azure 语音识别服务初始化成功');
return true;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e');
_isInitialized = false;
rethrow;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) {
await initialize();
}
try {
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
return RecognitionEvent.error(e.toString());
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
}
try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
await recorder?.dispose();
recorder = AudioRecorder();
Stream<Uint8List>? stream = await recorder?.startStream(
const RecordConfig(
sampleRate: 16000,
encoder: AudioEncoder.pcm16bits,
numChannels: 1,
),
);
isRecording = true;
stream?.listen((data) {
if (isRecording) {
_buff = _appendToBuffer(data);
_originaBytes.add(data);
}
});
await _tryConnecting();
_timer = Timer.periodic(const Duration(milliseconds: 40), (timer) async {
//每40毫秒读取一次数据
var frame = _getAudioData();
if (frame.isNotEmpty) {
_channel?.sink.add(frame);
}
});
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
rethrow;
}
}
//写入音频数据到缓存区中
Uint8List _appendToBuffer(Uint8List newData) {
var newBuffer = Uint8List(_buff.length + newData.length);
newBuffer.setAll(0, _buff);
newBuffer.setAll(_buff.length, newData);
return newBuffer;
}
//读取缓存区中一帧数据
Uint8List _getAudioData() {
if (_buff.length >= _chunkSize) {
// 从缓冲区中读取1280字节的数据
var data = _buff.sublist(0, _chunkSize);
// 移除已读取的数据
_buff = _buff.sublist(_chunkSize);
return data;
} else {
return _buff;
}
}
//尝试连接
Future<void> _tryConnecting() async {
try {
final url = _geturl();
_channel = WebSocketChannel.connect(Uri.parse(url));
_channel?.stream.timeout(const Duration(seconds: 10)); //设置超时时间
await _channel?.ready;
isConnected = true;
_channel?.stream.listen(
(message) {
_handle(message);
},
onError: (error) {
isConnected = false;
Logger.error('连接失败: $error');
},
onDone: () {
isConnected = false;
Logger.error('WebSocket 连接已关闭!');
},
cancelOnError: true,
);
} catch (e) {
Logger.error("初始化异常----------------$e");
}
}
//处理数据
void _handle(data) {
try {
final result = jsonDecode(data);
if (result['code'] == "0" && result['data'] != "") {
final data = jsonDecode(result['data']);
String msg = "";
String role = '';
List rtList = data['cn']['st']['rt'];
for (var rt in rtList) {
for (var ws in rt['ws']) {
for (var ws in ws['cw']) {
msg += ws['w'];
if (ws['rl'] != "0") {
role = ws['rl'];
}
}
}
}
Logger.info(
"识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role");
if (msg.isNotEmpty) {
if (data['cn']['st']['type'] == "0") {
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: msg,
role: role,
audio: _originaBytes.toBytes(),
));
_originaBytes.clear();
} else {
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: msg,
role: role,
));
}
}
}
} catch (e) {
Logger.error("接受消息异常 $e");
}
}
///获取链接地址
String _geturl() {
// 生成时间戳
String ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
// 计算 baseString
String baseString = md5.convert(utf8.encode(appId + ts)).toString();
// 计算 signa
var hmacSha1 = Hmac(sha1, utf8.encode(apiKey));
var signa = hmacSha1.convert(utf8.encode(baseString));
String signaBase64 = base64Encode(signa.bytes);
String url = "";
// 构建 WebSocket URL
url =
'$baseUrl?appid=$appId&ts=$ts&lang=$_languageStr&roleType=2&signa=${Uri.encodeComponent(signaBase64)}';
return url;
}
@override
Future<bool> stopContinuousRecognition() async {
try {
await recorder?.stop();
isRecording = false;
_timer?.cancel();
_buff = Uint8List(0);
_originaBytes = BytesBuilder();
if (isConnected) {
_channel?.sink.add(utf8.encode('{"end": true}')); //结束
}
_isContinuousRecognitionActive = false;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
rethrow;
}
}
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
@override
Future<void> dispose() async {
try {
await stopContinuousRecognition();
recorder?.dispose();
_channel?.sink.close();
_eventStreamController?.close();
_eventStreamController = null;
_isInitialized = false;
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
_isInitialized = false;
}
}
}

35
lib/data/services/volcano_ai_service.dart

@ -2,7 +2,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
class VolcanoAIService {
import 'ai_service.dart';
class VolcanoAIService implements AiService {
final String apiKey;
static const String _baseUrl = 'https://ark.cn-beijing.volces.com/api/v3';
static const String _chatEndpoint = '/chat/completions';
@ -30,12 +32,13 @@ class VolcanoAIService {
}
// 非流式输出方法
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async {
final url = Uri.parse('$_baseUrl$_chatEndpoint');
final fullMessages = [
{'role': 'system', 'content': systemPrompt},
...messages,
@ -59,8 +62,8 @@ class VolcanoAIService {
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['choices'] != null &&
data['choices'].isNotEmpty &&
if (data['choices'] != null &&
data['choices'].isNotEmpty &&
data['choices'][0]['message'] != null) {
return data['choices'][0]['message']['content'];
}
@ -78,12 +81,13 @@ class VolcanoAIService {
}
// 流式输出方法
@override
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async* {
final url = Uri.parse('$_baseUrl$_chatEndpoint');
final fullMessages = [
{'role': 'system', 'content': systemPrompt},
...messages,
@ -96,7 +100,7 @@ class VolcanoAIService {
'Authorization': 'Bearer $apiKey',
'Accept': 'text/event-stream',
});
request.body = json.encode({
'model': 'doubao-1-5-lite-32k-250115',
'messages': fullMessages,
@ -109,21 +113,22 @@ class VolcanoAIService {
if (response.statusCode != 200) {
final error = await response.stream.bytesToString();
throw VolcanoAIException(
json.decode(error)['error']?['message'] ?? 'Unknown error occurred'
);
throw VolcanoAIException(json.decode(error)['error']?['message'] ??
'Unknown error occurred');
}
await for (final chunk in response.stream.transform(utf8.decoder).transform(const LineSplitter())) {
await for (final chunk in response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())) {
if (chunk.isEmpty) continue;
if (chunk.startsWith('data: ')) {
final data = chunk.substring(6);
if (data == '[DONE]') break;
try {
final jsonData = json.decode(data);
if (jsonData['choices'] != null &&
jsonData['choices'].isNotEmpty &&
if (jsonData['choices'] != null &&
jsonData['choices'].isNotEmpty &&
jsonData['choices'][0]['delta'] != null &&
jsonData['choices'][0]['delta']['content'] != null) {
yield jsonData['choices'][0]['delta']['content'] as String;
@ -144,7 +149,7 @@ class VolcanoAIService {
class VolcanoAIException implements Exception {
final String message;
VolcanoAIException(this.message);
@override
String toString() => message;
}
}

92
lib/firebase_options.dart

@ -0,0 +1,92 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyBJXQTXsEmY_XA7ZEOusxG6VF5CaKtjHCA',
appId: '1:907754492821:web:99451e7efd7d120a145c9c',
messagingSenderId: '907754492821',
projectId: 'deepsound-e0e3d',
authDomain: 'deepsound-e0e3d.firebaseapp.com',
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
measurementId: 'G-ETX6DF35J3',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAj0_-nS3ygc8VFe1azotSE3-HGZYClkTs',
appId: '1:907754492821:android:2deffc5aa1037ae2145c9c',
messagingSenderId: '907754492821',
projectId: 'deepsound-e0e3d',
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808',
appId: '1:907754492821:ios:8c1d7d1b583aec22145c9c',
messagingSenderId: '907754492821',
projectId: 'deepsound-e0e3d',
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com',
iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com',
iosBundleId: 'com.example.client',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808',
appId: '1:907754492821:ios:8c1d7d1b583aec22145c9c',
messagingSenderId: '907754492821',
projectId: 'deepsound-e0e3d',
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com',
iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com',
iosBundleId: 'com.example.client',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyBJXQTXsEmY_XA7ZEOusxG6VF5CaKtjHCA',
appId: '1:907754492821:web:e3e8040111db6871145c9c',
messagingSenderId: '907754492821',
projectId: 'deepsound-e0e3d',
authDomain: 'deepsound-e0e3d.firebaseapp.com',
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
measurementId: 'G-SKGXEK5TJM',
);
}

46
lib/main.dart

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
@ -11,26 +13,28 @@ import 'package:window_size/window_size.dart';
import 'core/translations/app_translations.dart';
import 'core/themes/app_theme.dart';
import 'core/themes/theme_service.dart';
import '../../data/services/login_service.dart';
void main() async {
// 确保Flutter绑定初始化
WidgetsFlutterBinding.ensureInitialized();
// 初始化存储 - 在创建应用之前初始化
await GetStorage.init();
// 初始化环境变量
await dotenv.load();
LoginService();
//LoginService.FirebaseInit();
// 设置窗口大小(仅在 macOS 下生效)
if (Platform.isMacOS) {
const double width = 375.0;
const double height = 812.0;
setWindowTitle('Deep Voice');
setWindowTitle('DeepSound');
setWindowMinSize(const Size(width, height));
setWindowMaxSize(const Size(width, height));
// 居中显示窗口
final screen = await getCurrentScreen();
if (screen != null) {
@ -43,13 +47,11 @@ void main() async {
setWindowFrame(windowFrame);
}
}
// 运行应用
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@ -57,14 +59,14 @@ class MainApp extends StatelessWidget {
Widget build(BuildContext context) {
// 获取保存的主题设置
final themeMode = ThemeService().getThemeMode();
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, child) {
return GetMaterialApp(
title: 'Deep Voice',
title: 'DeepSound',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode, // 使用提前获取的主题模式
@ -73,9 +75,25 @@ class MainApp extends StatelessWidget {
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
translations: AppTranslations(),
locale: const Locale('zh', 'CN'),
fallbackLocale: const Locale('en', 'US'),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: AppTranslations.deviceLocale(),
fallbackLocale: AppTranslations.fallbackLocale,
supportedLocales: AppTranslations.supportedLocales,
builder: EasyLoading.init(
builder: (BuildContext context, Widget? widget) {
EasyLoading.instance.userInteractions = false;
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: widget!,
);
},
),
);
},
);

189
lib/modules/chat/controllers/chat_controller.dart

@ -1,6 +1,11 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart';
import '../../../data/services/speech_impl/voice_clone_tts_service.dart';
import '../models/message_model.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/volcano_ai_service.dart';
@ -14,11 +19,12 @@ import '../../../data/services/voice_interaction_service.dart';
class ChatController extends GetxController {
// 服务
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final TtsService _ttsService = Get.find<TtsService>();
late AiService _aiService;
late TtsService _ttsService;
final AsrService _asrService = Get.find<AsrService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>();
final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>();
// 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController();
@ -37,7 +43,7 @@ class ChatController extends GetxController {
final RxBool isSending = false.obs;
final RxBool isTtsEnabled = true.obs;
final RxBool isVoiceRecognitionActive = false.obs;
// 语音交互模式
bool _isVoiceInteractionMode = false;
@ -48,7 +54,7 @@ class ChatController extends GetxController {
// 临时消息索引,用于更新临时消息
int _tempMessageIndex = -1;
// 防抖计时器
Timer? _scrollDebounceTimer;
@ -59,10 +65,10 @@ class ChatController extends GetxController {
// 取消标志
bool _shouldCancelAiResponse = false;
// 自动停止语音识别计时器
Timer? _autoStopTimer;
// 记录最后一次活动时间
DateTime _lastActivityTime = DateTime.now();
@ -70,11 +76,11 @@ class ChatController extends GetxController {
Future<void> onInit() async {
super.onInit();
Logger.info('ChatController onInit');
// 从路由参数获取Agent信息
final Map<String, dynamic> args = Get.arguments ?? {};
final String agentId = args['agent'] ?? args['agentId'] ?? 'personal_assistant';
final String agentId =
args['agent'] ?? args['agentId'] ?? 'personal_assistant';
// 获取Agent
final Agent? foundAgent = AgentProvider.getAgentById(agentId);
@ -85,10 +91,35 @@ class ChatController extends GetxController {
}
agent = foundAgent;
switch (agent.id) {
case 'cyber_girlfriend':
// 亲子陪伴
Get.put(KouziAIService());
_aiService = Get.find<KouziAIService>();
break;
case 'health_advisor':
// 多功能AI聊天
Get.put(DeapsoundAIService());
_aiService = Get.find<DeapsoundAIService>();
break;
default:
_aiService = Get.find<VolcanoAIService>();
}
// 使用克隆音色语音合成
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1';
if (timbre == '1') {
_ttsService = Get.find<TtsService>();
} else {
Get.put(VoiceCloneTtsService());
_ttsService = Get.find<VoiceCloneTtsService>();
}
_ttsService.initialize();
_asrService.initialize();
// 设置语音交互模式
_isVoiceInteractionMode = args['isVoiceInteraction'] == true;
@ -130,7 +161,8 @@ class ChatController extends GetxController {
/// 订阅VoiceInteractionService事件
void _subscribeToVoiceInteractionEvents() {
if (_voiceInteractionService.eventStream != null) {
_voiceInteractionSubscription = _voiceInteractionService.eventStream.listen((event) {
_voiceInteractionSubscription =
_voiceInteractionService.eventStream.listen((event) {
if (event is RecognitionStartedEvent) {
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS
_stopCurrentInteraction();
@ -145,26 +177,27 @@ class ChatController extends GetxController {
/// 处理聊天历史事件
void _processChatHistoryEvent(ChatHistoryEvent event) {
Logger.info('收到后台聊天历史事件: agentId=${event.agentId}');
// 如果消息不是空的且不是当前Agent的,不处理
if (event.userMessage.isEmpty || event.agentId != agent.id) {
return;
}
// 添加用户消息
final userMsg = Message(
role: 'user',
content: event.userMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp),
);
// 添加助手消息
final assistantMsg = Message(
role: 'assistant',
content: event.assistantMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒
timestamp:
DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒
);
// 防止消息重复
if (!_isDuplicateMessage(userMsg) && !_isDuplicateMessage(assistantMsg)) {
messages.add(userMsg);
@ -176,15 +209,15 @@ class ChatController extends GetxController {
/// 停止当前交互
void _stopCurrentInteraction() {
Logger.info('收到后台语音识别开始事件,停止当前AI交互');
// 停止当前AI交互
if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应");
}
// 停止TTS
_ttsService.stop();
// 停止语音识别
if (isVoiceRecognitionActive.value) {
stopVoiceRecognition();
@ -194,7 +227,7 @@ class ChatController extends GetxController {
@override
void onClose() {
Logger.info('ChatController onClose');
// 清理资源
scrollController.dispose();
_aiResponseSubscription?.cancel();
@ -202,10 +235,10 @@ class ChatController extends GetxController {
_voiceInteractionSubscription?.cancel();
_autoStopTimer?.cancel();
_scrollDebounceTimer?.cancel(); // 清理防抖计时器
// 停止TTS
_ttsService.stop();
if (isVoiceRecognitionActive.value) {
_asrService.stopContinuousRecognition();
}
@ -220,33 +253,32 @@ class ChatController extends GetxController {
/// 滚动到列表底部方法,添加防抖功能
void _scrollToBottom() {
if (scrollController.hasClients && messages.isNotEmpty) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} catch (e) {
Logger.error('滚动到底部失败', e);
}
if (scrollController.hasClients && messages.isNotEmpty) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} catch (e) {
Logger.error('滚动到底部失败', e);
}
}
}
/// 中断当前响应
void _interruptCurrentResponse(String reason) {
Logger.info('${reason}');
// 停止TTS
_ttsService.stop();
// 取消AI响应流订阅
if (_aiResponseSubscription != null) {
_shouldCancelAiResponse = true;
_aiResponseSubscription!.cancel();
_aiResponseSubscription = null;
// 将加载中的消息转换为正常消息,保留已输出的内容
final index = messages.indexWhere((m) => m.isLoading);
if (index != -1) {
@ -255,7 +287,7 @@ class ChatController extends GetxController {
);
messages.refresh();
}
// 重置发送状态
isSending.value = false;
}
@ -278,14 +310,13 @@ class ChatController extends GetxController {
);
messages.add(userMessage);
// 清空输入框
inputText.value = '';
// 添加加载中的消息
final loadingMessage = Message.loading();
messages.add(loadingMessage);
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
@ -298,7 +329,7 @@ class ChatController extends GetxController {
// 重置取消标志
_shouldCancelAiResponse = false;
String fullResponse = '';
// 获取AI响应流
@ -327,12 +358,12 @@ class ChatController extends GetxController {
// 完成响应,将isLoading设为false
_finalizeResponse(fullResponse);
// 如果TTS启用,处理缓冲区中的剩余文本
if (isTtsEnabled.value) {
_ttsService.flushStream();
}
// 保存聊天历史
_saveChatHistory();
},
@ -345,7 +376,7 @@ class ChatController extends GetxController {
);
} catch (e) {
Logger.error('发送消息失败', e);
_handleResponseError();
}
}
@ -353,10 +384,11 @@ class ChatController extends GetxController {
// 构建消息历史
List<Map<String, String>> _buildMessageHistory() {
final List<Map<String, String>> messageHistory = [];
// 只取最近的10条消息作为上下文
final recentMessages = messages.where((m) => !m.isLoading).toList();
final startIndex = recentMessages.length > 10 ? recentMessages.length - 10 : 0;
final startIndex =
recentMessages.length > 10 ? recentMessages.length - 10 : 0;
for (int i = startIndex; i < recentMessages.length; i++) {
final m = recentMessages[i];
@ -365,7 +397,7 @@ class ChatController extends GetxController {
'content': m.content,
});
}
return messageHistory;
}
@ -393,7 +425,7 @@ class ChatController extends GetxController {
messages.refresh();
_scrollToBottom();
}
isSending.value = false;
_aiResponseSubscription = null;
}
@ -408,7 +440,7 @@ class ChatController extends GetxController {
);
messages.refresh();
}
isSending.value = false;
_aiResponseSubscription = null;
}
@ -429,18 +461,17 @@ class ChatController extends GetxController {
_voiceInteractionService.pauseVoiceInteraction();
try {
// 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
final recognitionStream = await _asrService.startContinuousRecognition();
isVoiceRecognitionActive.value = true;
isListening.value = true;
recognizedText.value = '';
// 重置临时消息索引
_tempMessageIndex = -1;
// 启动自动停止计时器
_startAutoStopTimer();
@ -460,7 +491,7 @@ class ChatController extends GetxController {
void _handleRecognitionEvent(RecognitionEvent event) {
// 更新活动时间
_updateActivityTime();
switch (event.type) {
case RecognitionEventType.intermediateResult:
_handleRecognizing(event.text);
@ -486,24 +517,24 @@ class ChatController extends GetxController {
void _startAutoStopTimer() {
// 取消现有计时器
_autoStopTimer?.cancel();
// 初始化最后活动时间
_updateActivityTime();
// 根据模式设置超时时间
final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15;
// 创建新的计时器,每秒检查一次
_autoStopTimer = Timer.periodic(Duration(seconds: 1), (timer) {
// 检查是否有活动
bool isActive = _ttsService.isSpeaking || isSpeechDetected.value;
// 如果有活动,更新时间并继续等待
if (isActive) {
_updateActivityTime();
return;
}
// 检查空闲时间
int idleSeconds = DateTime.now().difference(_lastActivityTime).inSeconds;
if (idleSeconds >= timeoutSeconds) {
@ -516,17 +547,17 @@ class ChatController extends GetxController {
// 处理语音识别中的状态
void _handleRecognizing(String text) {
if (text.isEmpty) return;
recognizedText.value = text;
// 设置语音检测状态为true,用于控制波形动画
if (!isSpeechDetected.value) {
isSpeechDetected.value = true;
}
// 更新活动时间
_updateActivityTime();
// 用户开始讲话时,立即停止当前TTS和中断AI响应
if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
@ -558,10 +589,10 @@ class ChatController extends GetxController {
// 处理语音识别最终结果
void _handleFinalResult(String text) {
if (text.isEmpty) return;
Logger.info('处理语音识别最终结果: $text');
recognizedText.value = text;
// 更新活动时间
_updateActivityTime();
@ -569,7 +600,7 @@ class ChatController extends GetxController {
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
messages.removeAt(_tempMessageIndex);
}
// 重置临时消息索引
_tempMessageIndex = -1;
@ -578,7 +609,7 @@ class ChatController extends GetxController {
// 重置识别文本,但不关闭语音识别框
recognizedText.value = '';
// 重置语音检测状态
isSpeechDetected.value = false;
}
@ -586,14 +617,14 @@ class ChatController extends GetxController {
// 处理语音识别事件错误
void _handleRecognitionEventError(String? error) {
Logger.error('语音识别错误: ${error ?? "未知错误"}');
_cleanupRecognition();
}
// 处理语音识别流错误
void _handleRecognitionError(dynamic error) {
Logger.error('语音识别错误', error);
_cleanupRecognition();
}
@ -605,10 +636,10 @@ class ChatController extends GetxController {
messages.removeAt(_tempMessageIndex);
}
}
// 重置临时消息索引
_tempMessageIndex = -1;
// 停止语音识别
isVoiceRecognitionActive.value = false;
isListening.value = false;
@ -636,7 +667,7 @@ class ChatController extends GetxController {
messages.removeAt(_tempMessageIndex);
}
}
// 重置临时消息索引
_tempMessageIndex = -1;
} catch (e) {
@ -691,10 +722,12 @@ class ChatController extends GetxController {
/// 检查是否为重复消息
bool _isDuplicateMessage(Message message) {
return messages.any((m) =>
m.role == message.role &&
m.content == message.content &&
(m.timestamp.millisecondsSinceEpoch - message.timestamp.millisecondsSinceEpoch).abs() < 5000
);
return messages.any((m) =>
m.role == message.role &&
m.content == message.content &&
(m.timestamp.millisecondsSinceEpoch -
message.timestamp.millisecondsSinceEpoch)
.abs() <
5000);
}
}

209
lib/modules/chat/views/chat_view.dart

@ -2,11 +2,9 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math';
import 'package:audio_waveforms/audio_waveforms.dart';
import '../controllers/chat_controller.dart';
import '../models/message_model.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/message_bubble.dart';
class ChatView extends GetView<ChatController> {
const ChatView({Key? key}) : super(key: key);
@ -14,25 +12,22 @@ class ChatView extends GetView<ChatController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
);
}
// 构建AppBar
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
elevation: 1,
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20.r
),
icon: Icon(Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black87, size: 20.r),
onPressed: () => Get.back(),
),
titleSpacing: 0,
@ -101,7 +96,7 @@ class ChatView extends GetView<ChatController> {
),
SizedBox(width: 4.w),
Text(
'在线',
'online'.tr, // 在线
style: TextStyle(
fontSize: 12.sp,
color: Colors.green,
@ -123,7 +118,9 @@ class ChatView extends GetView<ChatController> {
: Icons.volume_off,
color: controller.isTtsEnabled.value
? AppColors.primary
: isDarkMode ? Colors.grey[400] : Colors.grey,
: isDarkMode
? Colors.grey[400]
: Colors.grey,
),
onPressed: controller.toggleTts,
)),
@ -133,7 +130,8 @@ class ChatView extends GetView<ChatController> {
height: 34.r,
margin: EdgeInsets.only(right: 8.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
shape: BoxShape.circle,
boxShadow: [
if (!isDarkMode)
@ -145,11 +143,8 @@ class ChatView extends GetView<ChatController> {
],
),
child: PopupMenuButton(
icon: Icon(
Icons.more_vert,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20.r
),
icon: Icon(Icons.more_vert,
color: isDarkMode ? Colors.white : Colors.black87, size: 20.r),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
@ -162,7 +157,7 @@ class ChatView extends GetView<ChatController> {
Icon(Icons.delete_outline, color: Colors.red),
SizedBox(width: 8.w),
Text(
'清空聊天记录',
'clearChatHistory'.tr, // 清空聊天记录
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
@ -183,7 +178,7 @@ class ChatView extends GetView<ChatController> {
],
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Column(
@ -198,13 +193,13 @@ class ChatView extends GetView<ChatController> {
}
}),
),
// 底部输入区域
_buildInputArea(isDarkMode),
],
);
}
// 构建空状态
Widget _buildEmptyState(bool isDarkMode) {
return Center(
@ -215,13 +210,15 @@ class ChatView extends GetView<ChatController> {
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(
Icons.chat_bubble_outline,
size: 40.w,
color: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400],
color:
isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400],
),
),
SizedBox(height: 24.h),
@ -250,7 +247,7 @@ class ChatView extends GetView<ChatController> {
),
);
}
// 构建聊天列表
Widget _buildChatList(bool isDarkMode) {
return ListView.builder(
@ -266,32 +263,34 @@ class ChatView extends GetView<ChatController> {
},
);
}
// 构建消息项
Widget _buildMessageItem(Message message, bool isDarkMode) {
final isUser = message.role == 'user';
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) _buildAvatar(false, isDarkMode),
SizedBox(width: 8.w),
Flexible(
child: Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
crossAxisAlignment:
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
// 消息气泡
Container(
padding: EdgeInsets.all(12.r),
decoration: BoxDecoration(
color: isUser
? AppColors.primary
: isDarkMode ? AppColors.cardBackground : Colors.grey[200],
color: isUser
? AppColors.primary
: isDarkMode
? AppColors.cardBackground
: Colors.grey[200],
borderRadius: BorderRadius.circular(16.r),
),
child: Column(
@ -302,14 +301,16 @@ class ChatView extends GetView<ChatController> {
SelectableText(
message.content,
style: TextStyle(
color: isUser
? Colors.white
: isDarkMode ? Colors.white : Colors.black,
color: isUser
? Colors.white
: isDarkMode
? Colors.white
: Colors.black,
fontSize: 16.sp,
height: 1.5,
),
),
// 加载中动画
if (message.isLoading)
Padding(
@ -325,7 +326,7 @@ class ChatView extends GetView<ChatController> {
],
),
),
// 时间戳
Padding(
padding: EdgeInsets.only(top: 4.h, left: 4.w, right: 4.w),
@ -340,9 +341,7 @@ class ChatView extends GetView<ChatController> {
],
),
),
SizedBox(width: 8.w),
if (isUser) _buildAvatar(true, isDarkMode),
],
),
@ -351,8 +350,9 @@ class ChatView extends GetView<ChatController> {
// 构建输入区域
Widget _buildInputArea(bool isDarkMode) {
final textController = TextEditingController(text: controller.inputText.value);
final textController =
TextEditingController(text: controller.inputText.value);
// 检查是否处于语音识别模式
return Obx(() {
if (controller.isVoiceRecognitionActive.value) {
@ -362,9 +362,10 @@ class ChatView extends GetView<ChatController> {
}
});
}
// 构建文本输入面板
Widget _buildTextInputPanel(bool isDarkMode, TextEditingController textController) {
Widget _buildTextInputPanel(
bool isDarkMode, TextEditingController textController) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
@ -385,7 +386,9 @@ class ChatView extends GetView<ChatController> {
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
shape: BoxShape.circle,
),
child: IconButton(
@ -398,20 +401,21 @@ class ChatView extends GetView<ChatController> {
onPressed: controller.startVoiceRecognition,
),
),
SizedBox(width: 8.w),
// 输入框
Expanded(
child: TextField(
controller: textController,
decoration: InputDecoration(
hintText: '输入消息...',
hintText: 'inputMessage'.tr, // 输入消息...
hintStyle: TextStyle(
color: isDarkMode ? Colors.grey[500] : Colors.grey[500]
),
color: isDarkMode ? Colors.grey[500] : Colors.grey[500]),
filled: true,
fillColor: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
fillColor: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.r),
borderSide: BorderSide.none,
@ -435,9 +439,9 @@ class ChatView extends GetView<ChatController> {
},
),
),
SizedBox(width: 8.w),
// 发送按钮
Obx(() {
final hasText = controller.inputText.value.isNotEmpty;
@ -445,17 +449,21 @@ class ChatView extends GetView<ChatController> {
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: hasText
? AppColors.primary
: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200],
color: hasText
? AppColors.primary
: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200],
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(
Icons.send,
color: hasText
? Colors.white
: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400],
color: hasText
? Colors.white
: isDarkMode
? Colors.white.withOpacity(0.5)
: Colors.grey[400],
size: 18.r,
),
padding: EdgeInsets.zero,
@ -473,7 +481,7 @@ class ChatView extends GetView<ChatController> {
),
);
}
// 构建语音输入面板
Widget _buildVoiceInputPanel(bool isDarkMode) {
return Container(
@ -501,7 +509,9 @@ class ChatView extends GetView<ChatController> {
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
@ -513,13 +523,17 @@ class ChatView extends GetView<ChatController> {
),
child: Center(
child: Obx(() => Icon(
controller.isListening.value ? Icons.mic : Icons.mic_none,
color: controller.isListening.value ? AppColors.primary : Colors.grey.shade400,
size: 28.r,
)),
controller.isListening.value
? Icons.mic
: Icons.mic_none,
color: controller.isListening.value
? AppColors.primary
: Colors.grey.shade400,
size: 28.r,
)),
),
),
// 中间音频波形和状态文本
Expanded(
child: Column(
@ -542,10 +556,10 @@ class ChatView extends GetView<ChatController> {
23,
(index) {
// 随机高度模拟波形
double height = isSpeaking
? (Random().nextDouble() * 20.0 + 5.0).h
double height = isSpeaking
? (Random().nextDouble() * 20.0 + 5.0).h
: 2.h;
return SizedBox(
width: 6.w,
child: Center(
@ -553,12 +567,16 @@ class ChatView extends GetView<ChatController> {
duration: Duration(milliseconds: 500),
width: 2.5.w,
height: height,
margin: EdgeInsets.symmetric(horizontal: 1.5.w),
margin: EdgeInsets.symmetric(
horizontal: 1.5.w),
decoration: BoxDecoration(
color: isSpeaking
? AppColors.primary
: isDarkMode ? Colors.grey.shade600 : Colors.grey.shade400,
borderRadius: BorderRadius.circular(2.r),
color: isSpeaking
? AppColors.primary
: isDarkMode
? Colors.grey.shade600
: Colors.grey.shade400,
borderRadius:
BorderRadius.circular(2.r),
),
),
),
@ -571,26 +589,32 @@ class ChatView extends GetView<ChatController> {
}),
),
SizedBox(height: 8.h),
// 语音状态文本
Obx(() => Text(
controller.isSpeechDetected.value ? '正在聆听...' : '你可以开始说话',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontWeight: FontWeight.w500,
),
)),
controller.isSpeechDetected.value
? 'imListening'.tr // 正在聆听...
: 'canStartTalking'.tr, // 你可以开始说话
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode
? Colors.grey[400]
: Colors.grey[700],
fontWeight: FontWeight.w500,
),
)),
],
),
),
// 关闭按钮
Container(
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
@ -619,15 +643,15 @@ class ChatView extends GetView<ChatController> {
),
);
}
// 构建头像
Widget _buildAvatar(bool isUser, bool isDarkMode) {
return Container(
width: 36.r,
height: 36.r,
decoration: BoxDecoration(
color: isUser
? (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200])
color: isUser
? (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200])
: AppColors.primary,
shape: BoxShape.circle,
boxShadow: [
@ -668,7 +692,7 @@ class ChatView extends GetView<ChatController> {
),
);
}
// 构建加载动画点
Widget _buildLoadingDot(int index, bool isDarkMode) {
return AnimatedBuilder(
@ -679,21 +703,22 @@ class ChatView extends GetView<ChatController> {
width: 8.r,
height: 8.r,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400],
color:
isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400],
shape: BoxShape.circle,
),
);
},
);
}
// 格式化时间
String _formatTime(DateTime time) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final messageDate = DateTime(time.year, time.month, time.day);
if (messageDate == today) {
return '今天 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
} else if (messageDate == yesterday) {
@ -702,4 +727,4 @@ class ChatView extends GetView<ChatController> {
return '${time.month}月${time.day}日 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
}
}
}
}

42
lib/modules/common/webview_page.dart

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class WebViewPage extends StatelessWidget {
final String url;
final String title;
const WebViewPage({Key? key, required this.url, required this.title})
: super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(url));
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
centerTitle: true,
title: Text(title),
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
backgroundColor: isDarkMode ? Colors.black : Colors.white,
foregroundColor: isDarkMode ? Colors.white : Colors.black,
elevation: 0,
),
body: WebViewWidget(
controller: controller,
),
);
}
}

97
lib/modules/explore/controllers/explore_controller.dart

@ -13,7 +13,7 @@ class AIFeature {
final Color iconColor;
final String agentId;
final String targetRoute;
AIFeature({
required this.title,
required this.description,
@ -34,7 +34,8 @@ class FunctionScene {
final Color startColor;
final Color endColor;
final Color iconColor;
final String targetRoute;
FunctionScene({
required this.title,
required this.description,
@ -42,18 +43,19 @@ class FunctionScene {
required this.startColor,
required this.endColor,
required this.iconColor,
required this.targetRoute,
});
}
class ExploreController extends GetxController {
// 搜索关键词
final searchController = TextEditingController();
// AI功能列表
final List<AIFeature> aiFeatures = [
AIFeature(
title: '全能助理',
description: '帮你解决各类问题',
title: 'allRoundAssistant'.tr, // 全能助理
description: 'allRoundAssistantInfo'.tr, // 帮你解决各类问题
icon: Icons.assistant,
startColor: const Color(0xFF0066CC),
endColor: const Color(0xFF5E5CE6),
@ -63,8 +65,8 @@ class ExploreController extends GetxController {
targetRoute: Routes.chat,
),
AIFeature(
title: '同声翻译',
description: '实时翻译80+种语言',
title: 'simultaneousTranslation'.tr, // 同声翻译
description: 'simultaneousTranslationInfo'.tr, // 实时翻译80+种语言
icon: Icons.translate,
startColor: const Color(0xFF4CAF50),
endColor: const Color(0xFF009688),
@ -74,8 +76,8 @@ class ExploreController extends GetxController {
targetRoute: Routes.translation,
),
AIFeature(
title: 'AI女友',
description: '智能陪伴,随时聊天',
title: 'aiGirlfriend'.tr, // 亲子陪伴
description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天
icon: Icons.favorite,
startColor: const Color(0xFFE91E63),
endColor: const Color(0xFFFF5252),
@ -85,8 +87,8 @@ class ExploreController extends GetxController {
targetRoute: Routes.chat,
),
AIFeature(
title: '健康顾问',
description: '提供科学健康建议',
title: 'healthConsultant'.tr, // 健康顾问
description: 'healthConsultantInfo'.tr, // 提供科学健康建议
icon: Icons.healing,
startColor: const Color(0xFFFFC107),
endColor: const Color(0xFFFF9800),
@ -96,65 +98,68 @@ class ExploreController extends GetxController {
targetRoute: Routes.chat,
),
];
// 热门功能场景列表
final List<FunctionScene> functionScenes = [
FunctionScene(
title: '商务会议助手',
description: '实时记录会议纪要,多语言翻译',
title: 'conferenceAssistant'.tr, // 商务会议助手
description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译
icon: Icons.people,
startColor: const Color(0xFF3F51B5),
endColor: const Color(0xFF673AB7),
iconColor: const Color(0xFF3F51B5),
targetRoute: Routes.meeting,
),
FunctionScene(
title: '语言学习助手',
description: '发音纠正,实时练习口语',
title: 'studyAssistant'.tr, // 语言学习助手
description: 'studyAssistantInfo'.tr, // 发音纠正,实时练习口语
icon: Icons.school,
startColor: const Color(0xFF4CAF50),
endColor: const Color(0xFF009688),
iconColor: const Color(0xFF4CAF50),
targetRoute: '',
),
FunctionScene(
title: '旅行翻译助手',
description: '实时语音翻译,旅游景点介绍',
title: 'travelTranslator'.tr, // 旅行翻译助手
description: 'travelTranslatorInfo'.tr, // 实时语音翻译,旅游景点介绍
icon: Icons.language,
startColor: const Color(0xFFFFC107),
endColor: const Color(0xFFFF9800),
iconColor: const Color(0xFFFFC107),
targetRoute: '',
),
];
// 搜索功能
void search() {
if (searchController.text.isEmpty) return;
Get.snackbar(
'搜索',
'正在搜索: ${searchController.text}',
'search'.tr, // 搜索
'${'beSearching'.tr} ${searchController.text}', // 正在搜索:
snackPosition: SnackPosition.TOP,
);
// 实际应用中,这里应该实现真正的搜索逻辑
searchController.clear();
}
// 点击AI功能
void onAIFeatureTap(int index) {
if (index < 0 || index >= aiFeatures.length) return;
final feature = aiFeatures[index];
final agent = AgentProvider.getAgentById(feature.agentId);
if (agent == null) {
Get.snackbar(
'错误',
'未找到对应的AI助手',
'error'.tr, // 错误
'noAssistantFound'.tr, // 未找到对应的AI助手
snackPosition: SnackPosition.TOP,
);
return;
}
// 根据目标路由进行跳转
if (feature.targetRoute == Routes.chat) {
// 跳转到聊天页面
@ -172,34 +177,38 @@ class ExploreController extends GetxController {
Get.toNamed(feature.targetRoute);
}
}
// 点击功能场景
void onFunctionSceneTap(int index) {
if (index < 0 || index >= functionScenes.length) return;
Get.snackbar(
'场景',
'您选择了: ${functionScenes[index].title}',
snackPosition: SnackPosition.TOP,
);
// 实际应用中,这里应该跳转到对应的场景页面
final scene = functionScenes[index];
if (scene.targetRoute.isNotEmpty) {
Get.toNamed(scene.targetRoute);
} else {
Get.snackbar(
'scene'.tr, // 场景
'${'youHaveSelected'.tr} ${functionScenes[index].title}', // 您选择了:
snackPosition: SnackPosition.TOP,
);
}
}
// 尝试新功能
void tryNewFeature() {
Get.snackbar(
'新功能',
'您选择了尝试AI歌手模式',
'newFunction'.tr, // 新功能
'${'youChoseTry'.tr}${'aiSinger'.tr}', // 您选择了尝试 // AI歌手模式
snackPosition: SnackPosition.TOP,
);
// 实际应用中,这里应该跳转到新功能页面
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
}
}

226
lib/modules/explore/views/explore_view.dart

@ -11,7 +11,7 @@ class ExploreView extends GetView<ExploreController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
body: SafeArea(
@ -19,7 +19,7 @@ class ExploreView extends GetView<ExploreController> {
children: [
// 标题和搜索
_buildHeaderAndSearch(isDarkMode),
// 主要内容(可滚动)
Expanded(
child: SingleChildScrollView(
@ -30,16 +30,47 @@ class ExploreView extends GetView<ExploreController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 特色功能
_buildFeaturedCard(),
Container(
height: 180.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: PageView(
children: [
_buildFeaturedCard(
'assets/images/explore1.png',
'exploreSlogan1'.tr, // 智能陪伴,无限可能
'exploreAi'.tr, // 探索DeepSound提供的AI功能
),
_buildFeaturedCard(
'assets/images/explore.png',
'exploreSlogan'.tr, // 智能耳机,无限可能
'exploreAi'.tr, // 探索DeepSound提供的AI功能
),
_buildFeaturedCard(
'assets/images/explore2.jpg',
'exploreSlogan2'.tr, // 智能眼镜,无限可能
'exploreAi'.tr, // 探索DeepSound提供的AI功能
),
],
),
),
// AI功能列表
SizedBox(height: 24.h),
_buildAIFeaturesSection(isDarkMode),
// 热门功能场景
SizedBox(height: 24.h),
_buildFunctionScenesSection(isDarkMode),
// 新功能推荐
SizedBox(height: 24.h),
_buildNewFeatureSection(isDarkMode),
@ -66,7 +97,7 @@ class ExploreView extends GetView<ExploreController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'发现',
'explore'.tr, // 发现
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
@ -77,12 +108,14 @@ class ExploreView extends GetView<ExploreController> {
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200],
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200],
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(
Icons.more_vert,
Icons.more_vert,
size: 16.sp,
color: isDarkMode ? Colors.white : Colors.grey[700],
),
@ -92,18 +125,19 @@ class ExploreView extends GetView<ExploreController> {
),
],
),
// 搜索框
SizedBox(height: 16.h),
Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
),
child: TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: '搜索AI功能...',
hintText: 'searchAi'.tr, // 搜索AI功能...
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
@ -129,90 +163,77 @@ class ExploreView extends GetView<ExploreController> {
}
// 特色功能卡片
Widget _buildFeaturedCard() {
return Container(
height: 180.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Stack(
children: [
// 背景图片
ClipRRect(
borderRadius: BorderRadius.circular(24.r),
child: Container(
color: Colors.blue.shade900, // 预设颜色,图片加载失败时显示
child: Image.asset(
'assets/images/explore.png',
width: double.infinity,
height: 180.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.blue.shade900,
width: double.infinity,
height: 180.h,
child: Icon(
Icons.image_not_supported,
color: Colors.white,
size: 40.sp,
),
);
},
),
Widget _buildFeaturedCard(String image, String title, String description) {
return Stack(
children: [
// 背景图片
ClipRRect(
borderRadius: BorderRadius.circular(24.r),
child: Container(
color: Colors.blue.shade900, // 预设颜色,图片加载失败时显示
child: Image.asset(
image,
width: double.infinity,
height: 180.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.blue.shade900,
width: double.infinity,
height: 180.h,
child: Icon(
Icons.image_not_supported,
color: Colors.white,
size: 40.sp,
),
);
},
),
),
// 渐变遮罩
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.r),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.1),
Colors.black.withOpacity(0.6),
],
),
),
// 渐变遮罩
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.r),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.1),
Colors.black.withOpacity(0.6),
],
),
),
// 文本内容
Padding(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'智能耳机,无限可能',
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
// 文本内容
Padding(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
SizedBox(height: 4.h),
Text(
'探索DeepVoice提供的AI功能',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.8),
),
),
SizedBox(height: 4.h),
Text(
description,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.8),
),
],
),
),
],
),
],
),
),
],
);
}
@ -222,7 +243,7 @@ class ExploreView extends GetView<ExploreController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AI功能',
'aiFunction'.tr, // AI功能
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
@ -267,7 +288,7 @@ class ExploreView extends GetView<ExploreController> {
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: isDarkMode
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
blurRadius: 8,
@ -338,7 +359,7 @@ class ExploreView extends GetView<ExploreController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'热门功能场景',
'popularFunction'.tr, // 热门功能场景
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
@ -363,7 +384,8 @@ class ExploreView extends GetView<ExploreController> {
}
// 功能场景项
Widget _buildFunctionSceneItem(FunctionScene scene, int index, bool isDarkMode) {
Widget _buildFunctionSceneItem(
FunctionScene scene, int index, bool isDarkMode) {
return GestureDetector(
onTap: () => controller.onFunctionSceneTap(index),
child: Container(
@ -373,7 +395,7 @@ class ExploreView extends GetView<ExploreController> {
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
blurRadius: 4,
@ -405,7 +427,7 @@ class ExploreView extends GetView<ExploreController> {
),
),
SizedBox(width: 16.w),
// 文本内容
Expanded(
child: Column(
@ -478,10 +500,10 @@ class ExploreView extends GetView<ExploreController> {
),
),
SizedBox(height: 16.h),
// 标题
Text(
'AI歌手模式',
'aiSinger'.tr, // AI歌手模式
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
@ -489,17 +511,17 @@ class ExploreView extends GetView<ExploreController> {
),
),
SizedBox(height: 8.h),
// 描述
Text(
'用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲',
'aiSingerInfo'.tr, // 用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[600],
),
),
SizedBox(height: 16.h),
// 按钮
ElevatedButton(
onPressed: controller.tryNewFeature,
@ -514,7 +536,7 @@ class ExploreView extends GetView<ExploreController> {
),
),
child: Text(
'立即尝试',
'tryNow'.tr, // 立即尝试
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
@ -526,4 +548,4 @@ class ExploreView extends GetView<ExploreController> {
),
);
}
}
}

6
lib/modules/home/bindings/home_binding.dart

@ -1,9 +1,13 @@
import '../../explore/controllers/explore_controller.dart';
import 'package:get/get.dart';
import '../../explore/controllers/explore_controller.dart';
import '../controllers/home_controller.dart';
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(() => HomeController());
Get.lazyPut<ExploreController>(
() => ExploreController()); // 注册ExploreController
}
}
}

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

@ -3,110 +3,110 @@ import 'dart:async';
import '../../../routes/app_routes.dart';
import '../../../data/services/classic_bluetooth_service.dart';
import '../../../data/services/bluetooth_media_button_service.dart';
import '../../../data/services/voice_interaction_service.dart';
import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart';
import '../../explore/controllers/explore_controller.dart';
// 连接状态枚举
enum ConnectionStatus {
disconnected, // 未连接
connecting, // 连接中
connected, // 已连接
failed // 连接失败
disconnected, // 未连接
connecting, // 连接中
connected, // 已连接
failed // 连接失败
}
class HomeController extends GetxController {
static HomeController get to => Get.find();
final ExploreController exploreController = Get.find<ExploreController>();
// 经典蓝牙服务
late ClassicBluetoothService _bluetoothService;
// 蓝牙媒体按钮服务
late BluetoothMediaButtonService _mediaButtonService;
// 媒体按钮订阅
StreamSubscription<MediaButtonEvent>? _mediaButtonSubscription;
// 设备连接状态
final _isConnected = false.obs;
bool get isConnected => _isConnected.value;
// 连接状态
final _connectionStatus = ConnectionStatus.disconnected.obs;
ConnectionStatus get connectionStatus => _connectionStatus.value;
// 电池状态(模拟数据)
final _deviceBattery = Rx<int?>(null);
int? get deviceBattery => _deviceBattery.value;
// 固件版本(模拟数据)
final String firmwareVersion = 'v2.4.1';
// 蓝牙设备信息
final _deviceInfo = <String, dynamic>{}.obs;
Map<String, dynamic> get deviceInfo => _deviceInfo.value;
// 连接错误信息
final _connectionErrorMsg = ''.obs;
String get connectionErrorMsg => _connectionErrorMsg.value;
// 当前连接的经典蓝牙设备
List<BluetoothDevice> get connectedClassicDevices => _bluetoothService.connectedDevices;
List<BluetoothDevice> get connectedClassicDevices =>
_bluetoothService.connectedDevices;
// 蓝牙状态
RxString get bluetoothState => _bluetoothService.bluetoothState;
// 状态更新标志,防止多次重复更新
bool _isUpdatingStatus = false;
@override
void onInit() {
super.onInit();
// 初始化蓝牙服务和状态监控
_initBluetoothService();
// 初始化蓝牙媒体按钮服务
// _initMediaButtonService();
// 电池模拟更新(实际应用中可以通过蓝牙获取)
_simulateBatteryUpdate();
}
// 初始化蓝牙服务
void _initBluetoothService() {
try {
// 获取经典蓝牙服务实例
_bluetoothService = Get.find<ClassicBluetoothService>();
// 更新当前连接状态
_updateConnectionStatus();
// 监听蓝牙连接状态变化
_setupBluetoothListeners();
} catch (e) {
Logger.error('初始化蓝牙服务失败: $e');
_connectionStatus.value = ConnectionStatus.failed;
}
}
// 初始化蓝牙媒体按钮服务
void _initMediaButtonService() {
try {
// 获取媒体按钮服务实例
_mediaButtonService = Get.find<BluetoothMediaButtonService>();
// 初始化媒体按钮服务
_mediaButtonService.initialize().then((success) {
if (success) {
// 启动按钮监听
_mediaButtonService.startButtonListening();
// 设置按钮事件监听
_setupMediaButtonListener();
Logger.info('蓝牙媒体按钮服务初始化成功');
} else {
Logger.warning('蓝牙媒体按钮服务初始化失败');
@ -116,23 +116,21 @@ class HomeController extends GetxController {
Logger.error('初始化蓝牙媒体按钮服务失败: $e');
}
}
// 设置媒体按钮事件监听
void _setupMediaButtonListener() {
// 取消之前的订阅
_mediaButtonSubscription?.cancel();
// 设置新的订阅
_mediaButtonSubscription = _mediaButtonService.buttonEvents.listen((event) {
Logger.info('收到媒体按钮事件: $event');
// 处理按钮事件
if (event.buttonType == MediaButtonType.playPause) {
}
if (event.buttonType == MediaButtonType.playPause) {}
});
}
// 电池电量模拟更新
void _simulateBatteryUpdate() {
// 每5分钟更新一次电池电量(模拟)
@ -143,23 +141,23 @@ class HomeController extends GetxController {
} else {
_deviceBattery.value = null;
}
// 递归调用实现定时更新
_simulateBatteryUpdate();
});
}
// 更新连接状态
void _updateConnectionStatus() {
// 如果正在更新,则跳过
if (_isUpdatingStatus) return;
_isUpdatingStatus = true;
try {
// 检查当前设备
final currentDevice = _bluetoothService.currentDevice.value;
if (currentDevice != null) {
_isConnected.value = true;
_connectionStatus.value = ConnectionStatus.connected;
@ -167,17 +165,17 @@ class HomeController extends GetxController {
_isConnected.value = false;
_connectionStatus.value = ConnectionStatus.disconnected;
}
update(); // 通知UI刷新
} finally {
_isUpdatingStatus = false;
}
}
// 设置蓝牙监听器
void _setupBluetoothListeners() {
// 只监听当前设备和蓝牙状态
// 监听当前设备状态
ever(_bluetoothService.currentDevice, (device) {
if (device != null) {
@ -191,7 +189,7 @@ class HomeController extends GetxController {
}
update();
});
// 蓝牙状态变化监听
ever(_bluetoothService.bluetoothState, (state) {
if (state == 'off') {
@ -202,26 +200,26 @@ class HomeController extends GetxController {
}
});
}
// 刷新设备信息
Future<void> refreshDeviceInfo() async {
return;
// 如果正在更新,则跳过
if (_isUpdatingStatus) return;
_isUpdatingStatus = true;
try {
// 设置状态为连接中
_connectionStatus.value = ConnectionStatus.connecting;
update();
// 刷新设备列表
final hasDevices = await _bluetoothService.refreshConnectedDevices();
// 更新状态
_updateConnectionStatus();
// 根据结果更新状态
if (!hasDevices) {
Logger.info('刷新设备:未找到设备');
@ -233,34 +231,34 @@ class HomeController extends GetxController {
} catch (e) {
Logger.error('刷新设备信息失败: $e');
_connectionStatus.value = ConnectionStatus.failed;
_connectionErrorMsg.value = '无法获取设备信息';
_connectionErrorMsg.value = 'failedDeviceInformation'.tr; // 无法获取设备信息
update();
} finally {
_isUpdatingStatus = false;
}
}
// 重新扫描设备
void rescanDevices() {
Get.toNamed(Routes.pairing);
}
// 打开翻译功能
void openTranslation() {
Get.toNamed(Routes.translation);
}
// 打开AI伴侣功能
void openAIGirlfriend() {
Get.toNamed(Routes.chat);
exploreController.onAIFeatureTap(2); //进入AI女友页面
}
// 展示AI功能
void exploreAIFeatures() {
// 简单实现,直接跳转到AI聊天页面
openAIGirlfriend();
Get.toNamed(Routes.chat);
}
// 获取连接状态对应的颜色
Color getConnectionStatusColor() {
switch (_connectionStatus.value) {
@ -275,64 +273,62 @@ class HomeController extends GetxController {
return Colors.grey;
}
}
// 获取连接状态对应的文本
String getConnectionStatusText() {
// 如果蓝牙关闭,优先显示蓝牙状态
if (_bluetoothService.bluetoothState.value == 'off') {
return '蓝牙已关闭';
return 'bluetoothOff'.tr; // 蓝牙已关闭
}
switch (_connectionStatus.value) {
case ConnectionStatus.connected:
return '已连接';
return 'connected'.tr; // 已连接
case ConnectionStatus.connecting:
return '连接中...';
return 'connecting'.tr; // 连接中...
case ConnectionStatus.failed:
return '连接失败';
return 'connectionFailure'.tr; // 连接失败
case ConnectionStatus.disconnected:
default:
return '未连接';
return 'unconnected'.tr; // 未连接
}
}
// 获取设备名称
String getDeviceName() {
final device = _bluetoothService.currentDevice.value;
return device?.name ?? '未知设备';
return device?.name ?? 'unknownEquipment'.tr; // 未知设备
}
// 获取信号强度(模拟数据)
String getSignalStrength() {
if (!_isConnected.value) {
return '无信号';
return 'zeroSignal'.tr; // 无信号
}
// 简单模拟信号强度
final random = DateTime.now().millisecondsSinceEpoch % 3;
switch (random) {
case 0:
return '良好';
return 'good'.tr; // 良好
case 1:
return '很好';
return 'fine'.tr; // 很好
default:
return '极佳';
return 'excellent'.tr; // 极佳
}
}
// 获取设备ID
String getDeviceId() {
final device = _bluetoothService.currentDevice.value;
return device?.address ?? '未知';
return device?.address ?? 'unknown'.tr; // 未知
}
@override
void onClose() {
// 取消按钮事件流订阅
_mediaButtonSubscription?.cancel();
super.onClose();
}
}
}

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

@ -16,7 +16,8 @@ class AnimatedPulsing extends StatefulWidget {
_AnimatedPulsingState createState() => _AnimatedPulsingState();
}
class _AnimatedPulsingState extends State<AnimatedPulsing> with SingleTickerProviderStateMixin {
class _AnimatedPulsingState extends State<AnimatedPulsing>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@ -68,13 +69,14 @@ class _AnimatedPulsingState extends State<AnimatedPulsing> with SingleTickerProv
}
}
//首页视图
class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Color(0xFFF9FAFC),
body: SafeArea(
@ -85,13 +87,11 @@ class HomeView extends GetView<HomeController> {
children: [
// 顶部标题栏
_buildAppBar(isDarkMode),
// 顶部图片
Center(
child: _buildEarbudsImage(),
),
_buildEarbudsImage(),
SizedBox(height: 20.h),
// 设备状态卡片
_buildDeviceStatusCard(isDarkMode),
SizedBox(height: 20.h),
@ -100,8 +100,6 @@ class HomeView extends GetView<HomeController> {
_buildSectionTitle(isDarkMode),
SizedBox(height: 16.h),
_buildPremiumAILayout(),
],
),
),
@ -113,56 +111,85 @@ class HomeView extends GetView<HomeController> {
// 顶部标题栏
Widget _buildAppBar(bool isDarkMode) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'DeepVoice',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'DeepSound',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.grey[800],
),
),
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
),
),
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(
Icons.settings_outlined,
size: 16.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600]
),
padding: EdgeInsets.zero,
onPressed: () {
Get.toNamed(Routes.settings);
},
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(Icons.settings_outlined,
size: 16.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600]),
padding: EdgeInsets.zero,
onPressed: () {
Get.toNamed(Routes.settings);
},
),
),
],
);
}
// 耳机图片
Widget _buildEarbudsImage() {
final bool isDarkMode = Get.isDarkMode;
return Container(
return SizedBox(
height: 220.h,
alignment: Alignment.center,
child: Image.asset(
isDarkMode ? 'assets/images/headphone_dark.png' : 'assets/images/headphone_light.png',
height: 180.h,
fit: BoxFit.contain,
child: PageView(
children: [
Center(
child: SizedBox(
width: double.infinity,
height: 200.h,
child: Image.asset(
isDarkMode
? 'assets/images/robot_light.png'
: 'assets/images/robot_light.png',
fit: BoxFit.fitWidth,
),
),
),
Center(
child: Image.asset(
isDarkMode
? 'assets/images/headphone_dark.png'
: 'assets/images/headphone_light.png',
height: 180.h,
fit: BoxFit.contain,
),
),
Center(
child: SizedBox(
width: double.infinity,
height: 200.h,
child: Image.asset(
isDarkMode
? 'assets/images/glasses_light.png'
: 'assets/images/glasses_light.png',
fit: BoxFit.fitWidth,
),
),
),
],
),
);
}
@ -189,7 +216,7 @@ class HomeView extends GetView<HomeController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'设备状态',
'deviceStatus'.tr, // 设备状态
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
@ -197,52 +224,57 @@ class HomeView extends GetView<HomeController> {
),
),
Obx(() => Row(
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: controller.getConnectionStatusColor(),
shape: BoxShape.circle,
),
),
SizedBox(width: 4.w),
Text(
controller.getConnectionStatusText(),
style: TextStyle(
fontSize: 12.sp,
color: controller.getConnectionStatusColor(),
),
),
// 添加刷新按钮
SizedBox(width: 8.w),
InkWell(
onTap: controller.refreshDeviceInfo,
child: Icon(
Icons.refresh,
size: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
],
)),
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: controller.getConnectionStatusColor(),
shape: BoxShape.circle,
),
),
SizedBox(width: 4.w),
Text(
controller.getConnectionStatusText(),
style: TextStyle(
fontSize: 12.sp,
color: controller.getConnectionStatusColor(),
),
),
// 添加刷新按钮
SizedBox(width: 8.w),
InkWell(
onTap: controller.refreshDeviceInfo,
child: Icon(
Icons.refresh,
size: 16.sp,
color:
isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
],
)),
],
),
// 设备信息
SizedBox(height: 12.h),
Obx(() {
// 如果连接失败且有错误信息,显示错误提示
if (controller.connectionStatus == ConnectionStatus.failed &&
if (controller.connectionStatus == ConnectionStatus.failed &&
controller.connectionErrorMsg.isNotEmpty) {
return Column(
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50],
color: isDarkMode
? Colors.red[900]!.withOpacity(0.2)
: Colors.red[50],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: isDarkMode ? Colors.red[800]! : Colors.red[200]!),
border: Border.all(
color:
isDarkMode ? Colors.red[800]! : Colors.red[200]!),
),
child: Row(
children: [
@ -257,7 +289,9 @@ class HomeView extends GetView<HomeController> {
controller.connectionErrorMsg,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.red[300] : Colors.red[800],
color: isDarkMode
? Colors.red[300]
: Colors.red[800],
),
),
),
@ -276,12 +310,12 @@ class HomeView extends GetView<HomeController> {
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('重新连接'),
child: Text('reconnect'.tr), // 重新连接
),
],
);
}
// 如果正在连接中,显示连接中提示
if (controller.connectionStatus == ConnectionStatus.connecting) {
return Container(
@ -294,12 +328,13 @@ class HomeView extends GetView<HomeController> {
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
valueColor:
AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
SizedBox(height: 8.h),
Text(
'正在连接设备...',
'connectingDevice'.tr, // 正在连接设备...
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue,
@ -310,119 +345,135 @@ class HomeView extends GetView<HomeController> {
),
);
}
return controller.isConnected
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'已连接到:${controller.getDeviceName()}',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
return controller.isConnected
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${'connectedTo'.tr}:${controller.getDeviceName()}', // 已连接到
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
),
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
'信号: ${controller.getSignalStrength()}',
style: TextStyle(
fontSize: 10.sp,
color: Colors.blue[700],
Container(
padding: EdgeInsets.symmetric(
horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
'${'signal'.tr}: ${controller.getSignalStrength()}', // 信号
style: TextStyle(
fontSize: 10.sp,
color: Colors.blue[700],
),
),
),
),
],
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(8.r),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备ID:${controller.getDeviceId()}',
style: TextStyle(
fontSize: 10.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
color:
isDarkMode ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'deviceId'.tr}:${controller.getDeviceId()}', // 设备ID
style: TextStyle(
fontSize: 10.sp,
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
),
),
),
SizedBox(height: 4.h),
Text(
'固件版本:${controller.firmwareVersion}',
style: TextStyle(
fontSize: 10.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
SizedBox(height: 4.h),
Text(
'${'firmwareVersion'.tr}:${controller.firmwareVersion}', // 固件版本
style: TextStyle(
fontSize: 10.sp,
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
),
),
),
SizedBox(height: 4.h),
// 显示设备状态(已连接/已配对)
Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: controller.getConnectionStatusColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
controller.getConnectionStatusText(),
style: TextStyle(
fontSize: 10.sp,
color: controller.getConnectionStatusColor(),
SizedBox(height: 4.h),
// 显示设备状态(已连接/已配对)
Row(
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: controller
.getConnectionStatusColor()
.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
controller.getConnectionStatusText(),
style: TextStyle(
fontSize: 10.sp,
color:
controller.getConnectionStatusColor(),
),
),
),
),
],
),
],
),
),
],
)
: Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.bluetooth_disabled,
size: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
SizedBox(width: 4.w),
Text(
'未连接设备,点击下方按钮连接',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
],
),
],
),
),
],
),
);
)
: Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.bluetooth_disabled,
size: 14.sp,
color:
isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
SizedBox(width: 4.w),
Text(
'disconnectedDevice'.tr, // 未连接设备,点击下方按钮连接
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode
? Colors.grey[400]
: Colors.grey[700],
),
),
],
),
);
}),
// 连接按钮(仅在未连接时显示)
SizedBox(height: 12.h),
Obx(() {
@ -437,10 +488,10 @@ class HomeView extends GetView<HomeController> {
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('连接设备'),
child: Text('connectDevice'.tr), // 连接设备
);
} else {
return SizedBox();
return const SizedBox();
}
}),
],
@ -453,7 +504,7 @@ class HomeView extends GetView<HomeController> {
return Row(
children: [
Text(
'热门AI体验',
'topAiExperiences'.tr, // 热门AI体验
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
@ -480,8 +531,8 @@ class HomeView extends GetView<HomeController> {
Expanded(
flex: 1,
child: _buildFeatureCard(
title: 'AI全能助手',
description: '24小时专属顾问,解答问题、提供建议',
title: 'aiAssistant'.tr, // AI全能助手
description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议
icon: Icons.assistant,
gradient: LinearGradient(
begin: Alignment.topLeft,
@ -498,9 +549,9 @@ class HomeView extends GetView<HomeController> {
iconColor: const Color(0xFF0066CC),
),
),
SizedBox(width: 16.w),
// 右侧两个卡片
Expanded(
flex: 1,
@ -508,8 +559,9 @@ class HomeView extends GetView<HomeController> {
children: [
// 右上卡片 - 同声翻译
_buildFeatureCard(
title: '同声传译',
description: '80+语言实时翻译,出国无忧',
title: 'simultaneousInterpretation'.tr, // 同声传译
description:
'simultaneousInterpretationInfo'.tr, // 80+语言实时翻译,出国无忧
icon: Icons.translate,
gradient: LinearGradient(
begin: Alignment.topLeft,
@ -524,13 +576,13 @@ class HomeView extends GetView<HomeController> {
isDarkMode: isDarkMode,
iconColor: const Color(0xFF4CAF50),
),
SizedBox(height: 5.h), // 减少间距
// 右下卡片 - AI女友
_buildFeatureCard(
title: 'AI伴侣',
description: '懂你心意,温暖陪伴',
title: 'aiCompanion'.tr, // 亲子陪伴
description: 'aiCompanionInfo'.tr, // 懂你心意,温暖陪伴
icon: Icons.favorite,
gradient: LinearGradient(
begin: Alignment.topLeft,
@ -551,7 +603,7 @@ class HomeView extends GetView<HomeController> {
],
);
}
// 特性卡片构建方法
Widget _buildFeatureCard({
required String title,
@ -574,7 +626,7 @@ class HomeView extends GetView<HomeController> {
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
blurRadius: 8,
@ -585,7 +637,7 @@ class HomeView extends GetView<HomeController> {
child: Stack(
children: [
// 图标
if (iconOnTop)
if (iconOnTop)
// 大卡片布局
Align(
alignment: Alignment.topLeft,
@ -621,7 +673,7 @@ class HomeView extends GetView<HomeController> {
),
),
),
// 文本内容
if (iconOnTop)
// 大卡片布局 - 文本
@ -646,7 +698,8 @@ class HomeView extends GetView<HomeController> {
child: Text(
description,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
color:
isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 12.sp,
),
maxLines: 2,
@ -679,7 +732,8 @@ class HomeView extends GetView<HomeController> {
child: Text(
description,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
color:
isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 10.sp,
),
maxLines: 2,
@ -694,4 +748,4 @@ class HomeView extends GetView<HomeController> {
),
);
}
}
}

135
lib/modules/login/controllers/facebookLogin.dart

@ -0,0 +1,135 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:get/get.dart';
import 'package:crypto/crypto.dart';
class Facebook {
String? token = '';
String name = '';
String email = '';
String pictureurl = '';
String id = '';
String generateNonce([int length = 32]) {
const charset =
'0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
final random = Random.secure();
return List.generate(length, (_) => charset[random.nextInt(charset.length)])
.join();
}
/// Returns the sha256 hash of [input] in hex notation.
String sha256ofString(String input) {
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}
Future<bool> signInWithFacebook() async {
try {
UserCredential userCredential = await loginFacebook();
//UserCredential userCredential = await loginGoogle();
token = await userCredential.user?.getIdToken();
print('facebook响应数据userCredential: $token');
if (token != "") {
return true;
} else {
return false;
}
} catch (e) {
print(e);
return false;
}
}
// FaceBook登录
// 修改后的 signInWithFacebook
Future<UserCredential> loginFacebook() async {
//try {
final rawNonce = generateNonce();
final nonce = sha256ofString(rawNonce);
final LoginResult loginResult = await FacebookAuth.instance
.login(
loginTracking: LoginTracking.enabled,
nonce: nonce,
)
.catchError((onError) {
if (kDebugMode) {
//print(onError);
}
throw Exception(onError.message);
});
if (loginResult.accessToken == null) {
throw Exception(loginResult.message);
}
// Create a credential from the access token
OAuthCredential facebookAuthCredential;
print("tokenType${loginResult.accessToken!.type}");
if (Platform.isIOS) {
// Create a credential from the access token
switch (loginResult.accessToken!.type) {
case AccessTokenType.classic:
final classicToken = loginResult.accessToken as ClassicToken;
// token = classicToken.tokenString;
print('Token 字符串: ${classicToken.tokenString}');
print('classicToken值: ${classicToken}');
facebookAuthCredential = FacebookAuthProvider.credential(
classicToken.authenticationToken!,
);
break;
case AccessTokenType.limited:
final limitedToken = loginResult.accessToken as LimitedToken;
//token = limitedToken.tokenString;
print('Token 字符串: ${limitedToken.tokenString}');
print('LimitedToken值: ${limitedToken}');
facebookAuthCredential = OAuthCredential(
providerId: 'facebook.com',
signInMethod: 'oauth',
idToken: limitedToken.tokenString,
rawNonce: rawNonce,
);
break;
}
} else {
facebookAuthCredential = FacebookAuthProvider.credential(
loginResult.accessToken!.tokenString,
);
//token = loginResult.accessToken!.tokenString;
}
print('facebookAuthCredential值: ${facebookAuthCredential}');
if (facebookAuthCredential == null) {
throw Exception("Facebook Login Fail.");
}
final userData = await FacebookAuth.instance.getUserData();
name = userData["name"].toString();
email = userData["email"].toString();
pictureurl = userData["picture"]["data"]["url"].toString();
id = userData["id"].toString();
print("user login facebook ${email}${pictureurl}${name}${id}");
return FirebaseAuth.instance.signInWithCredential(facebookAuthCredential);
// final User? user = userCredential.user; // 获取用户信息
// token = await userCredential.user?.getIdToken();
// print("irebaseAuth.instance.signInWithCredential ${token}");
// return true;
// } catch (e) {
// print("Facebook 登录异常:$e");
// return false;
// }
}
// 退出Google登录
Future<void> signOutFacebook() async {
await FacebookAuth.instance.logOut();
}
}

82
lib/modules/login/controllers/googleLogin.dart

@ -0,0 +1,82 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:get/get.dart';
class Google {
String? token = '';
String? email = '';
String? displayName = '';
String? pictureurl = '';
String? id = '';
String? accessToken = '';
// 登录并返回
Future<bool> signInWithGoogle() async {
try {
// 确保用户已经登录,并获取当前用户的身份验证令牌
final currentUser = FirebaseAuth.instance.currentUser;
print('Future响应数据: $currentUser');
if (currentUser != null) {
// 这里处理您需要使用这个JWT令牌的逻辑,例如将它存储到本地存储中作为凭据。
email = currentUser.email;
displayName = currentUser.displayName;
id = currentUser.uid;
pictureurl = currentUser.photoURL;
print('Future响应数据pictureurl: $pictureurl');
token = await currentUser.getIdToken();
print('Future响应数据tokenResult: $token');
// IdTokenResult token1 = await currentUser.getIdTokenResult();
// print('Future响应数据token1: $token1');
return true;
} else {
UserCredential userCredential = await loginGoogle();
token = await userCredential.user?.getIdToken();
print('Future响应数据tokenResult: $token');
if (token != "") {
return true;
} else {
return false;
}
// logger.d("user login google: ${userCredential.user?.displayName}");
}
} catch (e) {
print("Google 登录异常:$e");
return false;
}
}
// Google登录
Future<UserCredential> loginGoogle() async {
print('loginGoogle:');
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
email = googleUser?.email;
id = googleUser?.id;
displayName = googleUser?.displayName;
pictureurl = googleUser?.photoUrl;
print('loginGoogle: $googleUser');
print("user login loginGoogle ${email}${id}${displayName}");
// Obtain the auth details from the request
final GoogleSignInAuthentication? googleAuth =
await googleUser?.authentication;
// Create a new credential,
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken,
idToken: googleAuth?.idToken,
);
// Once signed in, return the UserCredential
return await FirebaseAuth.instance.signInWithCredential(credential);
}
// 退出Google登录
Future<void> signOutGoogle() async {
await FirebaseAuth.instance.signOut();
await GoogleSignIn().signOut();
}
}

542
lib/modules/login/controllers/login_controller.dart

@ -1,15 +1,151 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:logger/web.dart';
import '../../../routes/app_routes.dart';
import 'dart:convert';
import 'dart:ffi';
import 'facebookLogin.dart';
import 'googleLogin.dart';
import 'weixinLogin.dart';
import '../../../data/services/network/api.dart';
import '../../../../data/models/user_Info.dart';
//import 'package:shared_preferences/shared_preferences.dart';
import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
enum LoginViewState {
welcome, // 欢迎页面
welcome, // 欢迎页面
verification // 验证码页面
}
class LoginController extends GetxController {
static LoginController get to => Get.find();
final GetStorage _storage = GetStorage();
String _deviceId = '';
// 添加国家编码相关状态
final _selectedCountryCode = ''.obs;
final _selectedCountryFlag = '✉️'.obs;
// 国家编码 Getter
String get selectedCountryCode => _selectedCountryCode.value;
String get selectedCountryFlag => _selectedCountryFlag.value;
// 预设国家编码列表
final List<Map<String, String>> countryCodes = [
{'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位
{'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位
// 按国家代码数字升序排列(去除+号后排序)
{'name': '美国', 'code': '+1', 'flag': '🇺🇸'},
{'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'},
{'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'},
{'name': '英国', 'code': '+44', 'flag': '🇬🇧'},
{'name': '德国', 'code': '+49', 'flag': '🇩🇪'},
{'name': '意大利', 'code': '+39', 'flag': '🇮🇹'},
{'name': '法国', 'code': '+33', 'flag': '🇫🇷'},
{'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'},
{'name': '南非', 'code': '+27', 'flag': '🇿🇦'},
{'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'},
{'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'},
{'name': '日本', 'code': '+81', 'flag': '🇯🇵'},
{'name': '韩国', 'code': '+82', 'flag': '🇰🇷'},
{'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'},
{'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'},
{'name': '泰国', 'code': '+66', 'flag': '🇹🇭'},
{'name': '越南', 'code': '+84', 'flag': '🇻🇳'},
{'name': '印度', 'code': '+91', 'flag': '🇮🇳'},
{'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'},
{'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'},
{'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'},
{'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'},
{'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'},
{'name': '巴西', 'code': '+55', 'flag': '🇧🇷'},
{'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'},
{'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'},
{'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'},
// 中国地区(保持最后)
{'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'},
{'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'},
{'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'},
// 添加的其他主要国家
{'name': '埃及', 'code': '+20', 'flag': '🇪🇬'},
{'name': '希腊', 'code': '+30', 'flag': '🇬🇷'},
{'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'},
{'name': '比利时', 'code': '+32', 'flag': '🇧🇪'},
{'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'},
{'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'},
{'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'},
{'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'},
{'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'},
{'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'},
{'name': '挪威', 'code': '+47', 'flag': '🇳🇴'},
{'name': '波兰', 'code': '+48', 'flag': '🇵🇱'},
{'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'},
{'name': '智利', 'code': '+56', 'flag': '🇨🇱'},
{'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'},
{'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'},
{'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'},
{'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'},
{'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'},
{'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'},
{'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'},
{'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'},
{'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'},
{'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'},
{'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'},
{'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'},
{'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'},
{'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'},
{'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'},
{'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'},
{'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'},
{'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'},
{'name': '捷克', 'code': '+420', 'flag': '🇨🇿'},
{'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'},
{'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'},
{'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'},
{'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'},
{'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'},
{'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'},
{'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'},
{'name': '海地', 'code': '+509', 'flag': '🇭🇹'},
{'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'},
{'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'},
{'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'},
{'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'},
{'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'},
{'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'},
{'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'},
{'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'},
{'name': '加纳', 'code': '+233', 'flag': '🇬🇭'},
{'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'},
{'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'},
{'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'},
{'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'},
{'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'},
{'name': '以色列', 'code': '+972', 'flag': '🇮🇱'},
{'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'},
{'name': '约旦', 'code': '+962', 'flag': '🇯🇴'},
{'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'},
{'name': '科威特', 'code': '+965', 'flag': '🇰🇼'},
{'name': '巴林', 'code': '+973', 'flag': '🇧🇭'},
{'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'},
{'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'},
{'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'},
{'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'},
{'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'},
{'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'},
{'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'},
{'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'},
{'name': '老挝', 'code': '+856', 'flag': '🇱🇦'},
{'name': '文莱', 'code': '+673', 'flag': '🇧🇳'},
{'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'},
{'name': '斐济', 'code': '+679', 'flag': '🇫🇯'},
{'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'},
];
// 状态管理
final _viewState = LoginViewState.welcome.obs;
final _isLoading = false.obs;
@ -17,7 +153,7 @@ class LoginController extends GetxController {
final _contactValue = ''.obs;
final _displayContact = ''.obs;
final _verificationCode = ['', '', '', ''].obs;
int loginType = -1;
// Getters
LoginViewState get viewState => _viewState.value;
bool get isLoading => _isLoading.value;
@ -25,29 +161,62 @@ class LoginController extends GetxController {
String get contactValue => _contactValue.value;
String get displayContact => _displayContact.value;
List<String> get verificationCode => _verificationCode;
// 文本控制器
final contactController = TextEditingController();
final verificationCodeController = TextEditingController();
@override
void onInit() {
super.onInit();
_viewState.value = LoginViewState.welcome;
_initDeviceInfo();
}
@override
void onClose() {
contactController.dispose();
verificationCodeController.dispose();
super.onClose();
}
// 初始化设备信息
Future<void> _initDeviceInfo() async {
try {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (GetPlatform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
_deviceId = androidInfo.id;
}else if (GetPlatform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
_deviceId = iosInfo.identifierForVendor ?? 'unknown';
}
Logger().d('deviceId: $_deviceId');
}catch(e){
Logger().e('获取设备信息失败: $e');
_deviceId = 'unknown';
}
}
// 更新选中的国家编码
void updateCountryCode(String code, String flag) {
_selectedCountryCode.value = code;
_selectedCountryFlag.value = flag;
// 根据选择更新输入框类型
if (code == '') {
contactController.clear(); // 清空当前输入
loginType = 0; // 设置为邮箱登录类型
} else {
loginType = 1; // 设置为手机号登录类型
}
}
// 获取验证码
void getVerificationCode() {
if (contactController.text.isEmpty) {
Get.snackbar(
'提示',
'提示',
'请输入手机号码或邮箱',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
@ -55,19 +224,18 @@ class LoginController extends GetxController {
);
return;
}
_contactValue.value = contactController.text;
_formatDisplayContact();
_startCountdown();
_sendVerificationCode();
}
// 使用验证码直接登录
void loginWithVerificationCode() {
//测试期间暂时关闭验证码登录
// if (contactController.text.isEmpty) {
// Get.snackbar(
// '提示',
// '提示',
// '请输入手机号码或邮箱',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
@ -75,10 +243,10 @@ class LoginController extends GetxController {
// );
// return;
// }
// if (verificationCodeController.text.isEmpty) {
// Get.snackbar(
// '提示',
// '提示',
// '请输入验证码',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
@ -86,10 +254,10 @@ class LoginController extends GetxController {
// );
// return;
// }
// if (verificationCodeController.text.length < 6) {
// Get.snackbar(
// '提示',
// '提示',
// '请输入完整的验证码',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
@ -97,9 +265,9 @@ class LoginController extends GetxController {
// );
// return;
// }
_isLoading.value = true;
// 模拟验证过程
Future.delayed(const Duration(seconds: 2), () {
_isLoading.value = false;
@ -107,12 +275,12 @@ class LoginController extends GetxController {
Get.offAllNamed(Routes.pairing);
});
}
// 切换到验证码页面
void showVerificationView() {
if (contactController.text.isEmpty) {
Get.snackbar(
'提示',
'提示',
'请输入手机号码或邮箱',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
@ -120,41 +288,48 @@ class LoginController extends GetxController {
);
return;
}
_contactValue.value = contactController.text;
_formatDisplayContact();
_viewState.value = LoginViewState.verification;
_startCountdown();
_sendVerificationCode();
}
// 返回欢迎页面
void showWelcomeView() {
_viewState.value = LoginViewState.welcome;
_resetVerificationCode();
}
// 格式化显示的联系方式(隐藏部分字符)
void _formatDisplayContact() {
String contact = _contactValue.value;
if (contact.contains('@')) { // 邮箱
if (contact.contains('@')) {
// 邮箱逻辑保持不变
final parts = contact.split('@');
if (parts[0].length > 3) {
_displayContact.value = parts[0].substring(0, 3) + '****@' + parts[1];
} else {
_displayContact.value = contact;
}
} else if (RegExp(r'^\d+$').hasMatch(contact)) { // 手机号
loginType = 0;
} else if (RegExp(r'^\d+$').hasMatch(contact)) {
// 手机号格式化时包含国家编码
if (contact.length > 7) {
_displayContact.value = '+86 ' + contact.substring(0, 3) + '****' + contact.substring(contact.length - 4);
_displayContact.value = '$selectedCountryCode ' +
contact.substring(0, 3) +
'****' +
contact.substring(contact.length - 4);
} else {
_displayContact.value = contact;
_displayContact.value = '$selectedCountryCode $contact';
}
loginType = 1;
} else {
_displayContact.value = contact;
}
}
// 开始倒计时
void _startCountdown() {
_countdownSeconds.value = 60;
@ -164,64 +339,264 @@ class LoginController extends GetxController {
return _countdownSeconds.value > 0;
});
}
// 发送验证码
void _sendVerificationCode() {
Future<void> _sendVerificationCode() async {
// 这里应该是实际发送验证码的逻辑
// 为了演示,我们只是模拟发送
if (isEmialContactController() || isPhoneContactController()) {
_startCountdown();
print(contactController.text);
var response = await Api.getCode({
"addr": selectedCountryCode + contactController.text,
"vtype": loginType,
});
if (response != null) {
print("请求后台数据验证码params=${response}");
//短信已发送
//EasyLoading.showInfo('codeSend'.tr);
}
} else {
Get.snackbar(
'提示',
'请正确的手机号码或邮箱',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
}
// 重新发送验证码
void resendVerificationCode() {
if (_countdownSeconds.value > 0) return;
_startCountdown();
_sendVerificationCode();
}
// 更新验证码输入
void updateVerificationCode(int index, String value) {
print("updateVerificationCode=${index}${value}");
if (index >= 0 && index < 4) {
var newList = [..._verificationCode];
newList[index] = value;
_verificationCode.value = newList;
}
}
// 重置验证码
void _resetVerificationCode() {
_verificationCode.value = ['', '', '', ''];
}
// 检查邮箱格式
bool isEmialContactController() {
// 检查每个元素是否为单个字母或数字字符
final text = contactController.text;
return (text.length > 5 &&
RegExp(r'^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
.hasMatch(text));
}
// 检查手机号码格式
bool isPhoneContactController() {
// 检查每个元素是否为单个字母或数字字符
final text = contactController.text;
return RegExp(r'^[0-9]+$').hasMatch(text);
}
// 检查验证码是否完整
bool isVerificationCodeComplete() {
return !_verificationCode.contains('');
// 检查每个元素是否为单个字母或数字字符
final text = verificationCodeController.text;
print("数据获取Facebook${text.length}");
return text.length == 4 && RegExp(r'^[0-9]+$').hasMatch(text);
}
// 登录
Future<void> login() async {
// 暂时关闭验证码验证
// if (!isVerificationCodeComplete()) {
// Get.snackbar(
// '提示',
// '请输入完整的验证码',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
// return;
// }
_isLoading.value = true;
try {
// 模拟验证过程
await Future.delayed(const Duration(seconds: 2));
// 验证成功,跳转到配对页面
Get.offAllNamed(Routes.pairing);
// 模拟验证过程
switch (loginType) {
case 0:
{
if (!isVerificationCodeComplete()) {
Get.snackbar(
'提示',
'请输入完整的验证码',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
break;
}
requestLogin({
"mail": contactController.text,
"phone": selectedCountryCode + contactController.text,
"stype": loginType,
"vcode": verificationCodeController.text,
"ttoken": "",
"phonemodel": "",
"phonemac": _deviceId,
"name": "",
"avatar": "",
});
break;
}
case 1:
{
if (!isVerificationCodeComplete()) {
Get.snackbar(
'提示',
'请输入完整的验证码',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
break;
}
requestLogin({
"mail": "",
"phone": selectedCountryCode + contactController.text,
"stype": loginType,
"vcode": verificationCodeController.text,
"ttoken": "",
"phonemodel": "",
"phonemac": _deviceId,
"name": "",
"avatar": "",
});
break;
}
case 4:
{
print("数据获取Facebook");
final facebook = Facebook();
bool authResult = await facebook.signInWithFacebook();
if (authResult) {
print("数据获取_Facebook.token${facebook.token}");
print("数据获取_Facebook.pictureurl${facebook.pictureurl}");
print("数据获取_Facebook.pictureurl${facebook.email}");
// SettingsController().userName = Facebook().name;
// SettingsController().avatar = Facebook().pictureurl;
// SettingsController().userId = Facebook().id;
//SettingsController().avatar = Google().email;
// 验证成功,跳转到配对页面
requestLogin({
"mail": facebook.email,
"phone": "",
"stype": 5,
"vcode": "",
"ttoken": facebook.token,
"phonemodel": "",
"phonemac": _deviceId,
"name": facebook.name,
"avatar": facebook.pictureurl,
});
}
break;
}
case 3:
{
print("数据获取_Google()");
final google = Google();
bool authResult = await google.signInWithGoogle();
print("数据获取_Google().token${authResult}");
if (authResult) {
print("数据获取_Google.token${google.token}");
print("数据获取_Google.pictureurl${google.pictureurl}");
print("数据获取_Google.email${google.email}");
print("数据获取_Google.email${google.displayName}");
// 验证成功,跳转到配对页面
// SettingsController().userName = Google().displayName!;
// //SettingsController().avatar = Google().email;
// SettingsController().userId = Google().id!;
requestLogin({
"mail": google.email,
"phone": "",
"stype": 5,
"vcode": "",
"ttoken": google.token,
"phonemodel": "",
"phonemac": _deviceId,
"name": google.displayName,
"avatar": google.pictureurl,
});
}
break;
}
case 2:
{
print("数据获取_FlWX()");
bool authResult = await FlWX().authBy(
NormalAuth(
scope: "snsapi_userinfo",
state: "wechat_sdk_demo_test",
),
getUserInfo: true, // 按需获取用户信息
);
if (authResult) {
// 确保 FlWX() 返回的是同一个实例,并且 token 已经被设置
// 这里假设 FlWX().token 在 authBy 成功后被设置
print("数据获取_FlWX().token${FlWX().token}");
// SettingsController().userName = FlWX().nickname;
// SettingsController().avatar = FlWX().headimgurl;
// SettingsController().userId = FlWX().openid;
requestLogin({
"mail": "",
"phone": "",
"stype": loginType,
"vcode": "",
"ttoken": FlWX().code,
"phonemodel": "",
"phonemac": _deviceId,
"name": FlWX().nickname,
"avatar": FlWX().headimgurl,
});
// 验证成功,跳转到配对页面
}
break;
}
case 6://游客登录
{
requestLogin({
"mail": "",
"phone": "",
"stype": loginType,
"vcode": "",
"ttoken": "",
"phonemodel": "",
"phonemac": _deviceId,
"name": "",
"avatar": "",
});
break;
}
default:
{
Get.snackbar(
'错误',
'登录失败,请重试',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
break;
}
}
} catch (e) {
Get.snackbar(
'错误',
'错误',
'登录失败,请重试',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
@ -231,16 +606,67 @@ class LoginController extends GetxController {
_isLoading.value = false;
}
}
// 使用登陆请求服务器
Future<void> requestLogin(Map params) async {
var response = await Api.login({
"mail": params["mail"],
"phone": params["phone"],
"stype": params["stype"],
"vcode": params["vcode"],
"ttoken": params["ttoken"],
"phonemodel": params["phonemodel"],
"phonemac": params["phonemac"],
"name": params["name"],
"avatar": params["avatar"],
});
if (response != null) {
print("请求后台数据login回复数据=${response}");
// final instance = await GetStorage();
await _storage.write('loginInfo', jsonEncode(response));
// 2. 将响应数据转换为User对象
try {
print('响应数据: ${response.toString()}'); // 调试输出原始数据
if (response['token'].toString() != '') {
// 新增:检查 user 字段是否存在且类型正确
// if (response.containsKey('data') && response['data'] is Map) {
final user = User.fromJson(response);
print('用户初始化成功: ${response['token'].toString()}');
await _storage.write("logintoken", response['token'].toString());
// 存储到 SharedPreferences (仅存储用户数据)
//final prefs = await GetStorage();
await _storage.write(
'user', jsonEncode(user.toJson())); // 使用 user.toJson()
Get.offAllNamed(Routes.pairing);
// } else {
// throw FormatException('API 响应缺少 user 字段');
// }
} else {
throw Exception('接口返回错误: ${response['msg']}');
}
} catch (e, stack) {
// 新增:打印堆栈跟踪
print('用户数据处理失败: $e');
print('堆栈跟踪: $stack');
Get.snackbar('错误', '用户数据解析失败,请检查网络');
}
// Get.offAll(() => IndexWidget());
}
}
// 使用密码登录
void usePasswordLogin() {
// 这里可以实现密码登录的逻辑
Get.snackbar(
'提示',
'提示',
'密码登录功能尚未实现',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue.withOpacity(0.1),
colorText: Colors.blue,
);
}
}
}

923
lib/modules/login/controllers/weixinLogin.dart

@ -0,0 +1,923 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:fluwx/fluwx.dart';
export 'package:fluwx/fluwx.dart';
import 'package:http/http.dart' as http;
typedef FlWeChatResponseCallback = void Function(WeChatResponse response);
/// 支持成功
typedef FlWeChatPayResponseResultCallback = void Function();
/// 支付结果回调
typedef FlWeChatPayResponseCallback = void Function(
WeChatPaymentResponse response);
/// LaunchFromWX 回调
typedef FlWeChatLaunchFromWXRequestCallback = void Function(
WeChatLaunchFromWXRequest response);
/// OpenBusinessWebView 回调
typedef FlWeChatOpenBusinessWebViewResponseCallback = void Function(
WeChatOpenBusinessWebviewResponse response);
/// WeChatOpenBusinessView 回调
typedef FlWeChatOpenBusinessViewResponseCallback = void Function(
WeChatOpenBusinessViewResponse response);
/// Share 回调
typedef FlWeChatShareResponseCallback = void Function(
WeChatShareResponse response);
/// Auth 回调
typedef FlWeChatAuthResponseCallback = Function(WeChatAuthResponse response);
/// Auth token 回调
typedef FlWeChatAuthResponseTokenCallback = Function(
WeChatAuthResponse response, WXTokenModel token);
/// Auth userinfo 回调
typedef FlWeChatAuthResponseUserinfoCallback = Function(
WeChatAuthResponse response,
WXTokenModel token,
WXUserModel userInfo,
);
/// 显示 toast
typedef FlWXToastCallback = void Function(String msg);
/// 日志 打印
typedef FlWXLogCallback = void Function(String msg);
/// http 请求
typedef FlWXHTTPCallback = Future<String?> Function(String url);
class FlWX {
factory FlWX() => _singleton ??= FlWX._();
FlWX._();
static FlWX? _singleton;
final Fluwx fluwx = Fluwx();
String? _appId;
/// 获取当前设置的 appId
String? get appId => _appId;
String? _appSecret;
/// 获取当前设置的 appSecret
String? get appSecret => _appSecret;
String _appName = '';
String token = '';
String code = '';
String refresh_token = '';
String openid = '';
String nickname = '';
String sex = '';
String headimgurl = '';
String city = '';
// 参数 说明
// access_token 接口调用凭证
// expires_in access_token 接口调用凭证超时时间,单位(秒)
// refresh_token 用户刷新 access_token
// openid 授权用户唯一标识
// scope 用户授权的作用域(snsapi_userinfo)
// unionid 当且仅当该移动应用已获得该用户的 userinfo 授权时,才会出现该字段
/// callback params
FlWXCallbackParams? _params;
/// 分享时的缩略图
Uint8List? _shareThumbnail;
/// register
bool _isRegister = false;
Future<bool> register({
required String appId,
required FlWXCallbackParams params,
String? universalLink,
bool doOnIOS = true,
bool doOnAndroid = true,
/// 用于获取用户信息
String? appSecret,
/// app名称(用于分享时使用)
String appName = '',
/// 分享时的缩略图
Uint8List? shareThumbnail,
}) {
assert(appId.isNotEmpty);
if (universalLink != null) assert(universalLink.isNotEmpty);
if (appSecret != null) {
assert(appSecret.isNotEmpty);
}
_isRegister = true;
_appId = appId;
_appSecret = appSecret;
_appName = appName;
_shareThumbnail = shareThumbnail;
_params = params;
return fluwx.registerApi(
appId: appId,
doOnAndroid: doOnAndroid,
doOnIOS: doOnIOS,
universalLink: universalLink,
);
}
/// 获取微信 用户token
String _getTokenUrl(String code) {
print("_getTokenUrl 数据获取${_appId},${_appSecret}");
assert(
_appId != null &&
_appId!.isNotEmpty &&
_appSecret != null &&
_appSecret!.isNotEmpty,
);
return 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=$_appId&secret=$_appSecret&code=$code&grant_type=authorization_code';
}
/// 获取微信 用户token
String _getRefreshTokenUrl(String code) {
print("_getRefreshTokenUrl 数据获取${_appId},${_appSecret}");
assert(
_appId != null &&
_appId!.isNotEmpty &&
_appSecret != null &&
_appSecret!.isNotEmpty,
);
return 'https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=$_appId&grant_type=refresh_token&&refresh_token=$refresh_token';
}
// GET请求示例
Future<Map<String, dynamic>> fetchData(String Url) async {
try {
final response = await http.get(Uri.parse(Url));
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
print('GET响应数据: $data');
return data;
} else {
print('GET请求失败,状态码: ${response.statusCode}');
return {};
}
} catch (e) {
print('GET请求异常: $e');
return {};
}
}
// POST请求示例
Future<bool> postData() async {
try {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {
'Content-Type': 'application/json',
}, // 设置请求头‌:ml-citation{ref="6" data="citationList"}
body: jsonEncode({
// 编码JSON数据‌:ml-citation{ref="3" data="citationList"}
'title': 'foo',
'body': 'bar',
'userId': 1,
}),
);
if (response.statusCode == 201) {
final data = jsonDecode(response.body);
print('POST响应数据: $data');
return true;
} else {
print('POST请求失败,状态码: ${response.statusCode}');
return false;
}
} catch (e) {
print('POST请求异常: $e');
return false;
}
}
/// 获取微信 用户信息
String _getUserInfoUrl({required String openId, required String token}) =>
'https://api.weixin.qq.com/sns/userinfo?access_token=$token&openid=$openId';
//检验授权凭证(access_token)是否有效
String _getUserTokenEffertive({
required String openId,
required String token,
}) =>
'https://api.weixin.qq.com/sns/auth?access_token=$token&openid=$openId';
/// 是否安装微信
Future<bool> get isInstalled async {
assert(_isRegister, '请先调用 FlWX().register()');
final installed = await fluwx.isWeChatInstalled;
if (!installed) _params!.onToast?.call('该服务需要先安装微信');
return installed;
}
/// isSupportOpenBusinessView
Future<bool> get isSupportOpenBusinessView async {
if (!await isInstalled) return false;
return await fluwx.isSupportOpenBusinessView;
}
/// 添加移除监听
FluwxCancelable _onListener(FlWeChatResponseCallback onResponse) {
late FluwxCancelable cancelable;
subscriber(response) {
onResponse(response);
Future.delayed(const Duration(milliseconds: 500), () {
cancelable.cancel();
});
}
return cancelable = fluwx.addSubscriber(subscriber);
}
Future<String?> getExtMsg() async {
if (!await isInstalled) return null;
return await fluwx.getExtMsg();
}
Future<void> attemptToResumeMsgFromWx({
Function(WeChatShowMessageFromWXRequest response)? onResponse,
}) async {
if (!await isInstalled) return;
await fluwx.attemptToResumeMsgFromWx();
if (onResponse != null) {
_onListener((response) {
if (response is WeChatShowMessageFromWXRequest) onResponse(response);
});
}
}
// graph TD
// A[开始] --> B{微信是否安装?}
// B -->|否| C[返回 false]
// B -->|是| D[注册监听器]
// D --> E[调起微信授权]
// E -->|失败| F[触发 completer(false)]
// E -->|成功| G[等待微信响应]
// G -->|用户取消/接口错误| H[触发 completer(false)]
// G -->|成功获取 code| I[获取 Token]
// I -->|Token 失败| H
// I -->|Token 成功| J[刷新 Token]
// J -->|刷新失败| H
// J -->|刷新成功| K{需用户信息?}
// K -->|是| L[获取用户信息]
// L -->|信息失败| H
// K -->|否| M[触发 completer(true)]
// L -->|信息成功| M
Future<bool> authBy(
AuthType which, {
FlWeChatAuthResponseCallback? onResponse,
FlWeChatAuthResponseTokenCallback? onToken,
FlWeChatAuthResponseUserinfoCallback? onUserinfo,
bool getUserInfo = false,
}) async {
if (!await isInstalled) {
_params?.onToast?.call('微信未安装');
return false;
}
final completer = Completer<bool>(); // 创建 Completer 控制 Future 完成时机
FluwxCancelable? subscription; // 用于取消监听
subscription = _onListener((response) async {
if (response is WeChatAuthResponse) {
onResponse?.call(response);
if (response.isSuccessful && response.code != null) {
code = response.code!;
completer.complete(true); // 所有操作成功时完成 Future
// try {
// code = response.code!;
// // 获取 token
// final tokenData = await fetchData(_getTokenUrl(response.code!));
// if (tokenData.isEmpty) {
// _params!.onLog?.call('Token 数据获取失败');
// completer.complete(false); // 失败时完成 Future
// return;
// }
// // 更新 token
// token = tokenData["access_token"].toString();
// openid = tokenData["openid"].toString();
// // Istoken = await fetchData(
// // _getUserTokenEffertive(openId: openid, token: token),
// // ); // 触发GET请求
// // print("数据获取_getUserTokenEffertive${Istoken}");
// // 可选:刷新 token 或获取用户信息
// if (getUserInfo) {
// final userInfo = await fetchData(
// _getUserInfoUrl(openId: openid, token: token));
// List<int> bytes = latin1.encode(userInfo["nickname"].toString());
// print("ISO-8859-1 字节数组: $bytes");
// nickname = String.fromCharCodes(bytes);
// // nickname = userInfo["nickname"].toString();
// sex = userInfo["sex"].toString();
// headimgurl = userInfo["headimgurl"].toString();
// city = userInfo["city"].toString();
// print("_getUserInfoUrl${nickname}");
// // onUserinfo?.call(userInfo);
// }
// completer.complete(true); // 所有操作成功时完成 Future
// } catch (e) {
// _params!.onLog?.call('授权流程异常: $e');
// completer.complete(false);
// }
} else {
completer.complete(false); // 用户取消授权或授权失败
}
}
});
// 调起微信授权
final result = await fluwx.authBy(which: which);
if (!result) {
subscription.cancel(); // 调起失败时取消监听
_params!.onToast?.call('微信授权失败');
completer.complete(false);
}
return completer.future; // 返回由回调控制的 Future
}
Future<bool> autoDeduct(
AutoDeduct data, {
FlWeChatResponseCallback? onResponse,
}) async {
final cancelable = _onListener((response) {
onResponse?.call(response);
});
final result = await fluwx.autoDeduct(data: data);
if (!result) {
cancelable.cancel();
_params!.onToast?.call('微信签约失败');
}
return result;
}
/// 调用微信支付
Future<bool> pay(
PayType which, {
FlWeChatPayResponseResultCallback? onSuccess,
FlWeChatPayResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatPaymentResponse) {
onResponse?.call(response);
_params!.onLog?.call('微信支付 result code: ${response.errCode} ');
if (response.errCode == 0) {
_params!.onToast?.call('支付成功');
onSuccess?.call();
} else if (response.errCode == -2) {
_params!.onToast?.call('已取消支付');
} else {
_params!.onToast?.call('支付失败');
}
}
});
final result = await fluwx.pay(which: which);
if (!result) {
cancelable.cancel();
_params!.onToast?.call('支付失败');
}
return result;
}
/// ****************** open ****************** ///
Future<bool> open(OpenType target) async {
if (!await isInstalled) return false;
return fluwx.open(target: target);
}
Future<bool> openWeChatApp({
FlWeChatLaunchFromWXRequestCallback? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatLaunchFromWXRequest) onResponse?.call(response);
});
final result = await fluwx.open(target: WeChatApp());
if (!result) cancelable.cancel();
return result;
}
Future<bool> openBrowser(
String url, {
FlWeChatOpenBusinessWebViewResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatOpenBusinessWebviewResponse) {
onResponse?.call(response);
}
});
final result = await fluwx.open(target: Browser(url));
if (!result) cancelable.cancel();
return result;
}
Future<bool> openRankList() async {
if (!await isInstalled) return false;
return fluwx.open(target: RankList());
}
Future<bool> openBusinessView({
required String businessType,
required String query,
FlWeChatOpenBusinessViewResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatOpenBusinessViewResponse) {
onResponse?.call(response);
}
});
final result = await fluwx.open(
target: BusinessView(businessType: businessType, query: query),
);
if (!result) cancelable.cancel();
return result;
}
Future<bool> openInvoice({
required String appId,
required String cardType,
String locationId = '',
String cardId = '',
String canMultiSelect = '',
Function(WeChatOpenInvoiceResponse response)? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatOpenInvoiceResponse) onResponse?.call(response);
});
final result = await fluwx.open(
target: Invoice(
appId: appId,
cardType: cardType,
locationId: locationId,
cardId: cardId,
canMultiSelect: canMultiSelect,
),
);
if (!result) cancelable.cancel();
return result;
}
Future<bool> openCustomerServiceChat({
required String corpId,
required String url,
Function(WeChatOpenCustomerServiceChatResponse response)? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatOpenCustomerServiceChatResponse) {
onResponse?.call(response);
}
});
final result = await fluwx.open(
target: CustomerServiceChat(corpId: corpId, url: url),
);
if (!result) cancelable.cancel();
return result;
}
Future<bool> openMiniProgram(
String username, {
String? path,
Function(WeChatLaunchMiniProgramResponse response)? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatLaunchMiniProgramResponse) {
onResponse?.call(response);
}
});
final result = await fluwx.open(
target: MiniProgram(username: username, path: path),
);
if (!result) {
cancelable.cancel();
_params?.onToast?.call('小程序打开失败');
}
return result;
}
Future<bool> openSubscribeMessage({
required String appId,
required int scene,
required String templateId,
String? reserved,
Function(WeChatSubscribeMsgResponse response)? onResponse,
}) async {
if (!await isInstalled) return false;
final cancelable = _onListener((response) {
if (response is WeChatSubscribeMsgResponse) onResponse?.call(response);
});
final result = await fluwx.open(
target: SubscribeMessage(
appId: appId,
scene: scene,
templateId: templateId,
reserved: reserved,
),
);
if (!result) cancelable.cancel();
return result;
}
/// ****************** share ****************** ///
Future<bool> share(
WeChatShareModel what, {
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(fluwx.share(what), onResponse: onResponse);
}
Future<bool> shareText(
String source, {
String? title,
String? description,
WeChatScene scene = WeChatScene.session,
String? messageExt,
String? messageAction,
String? mediaTagName,
String? msgSignature,
Uint8List? thumbData,
String? thumbDataHash,
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(
fluwx.share(
WeChatShareTextModel(
source,
title: title ?? _appName,
description: description ?? _appName,
scene: scene,
messageExt: messageExt,
messageAction: messageAction,
mediaTagName: mediaTagName,
msgSignature: msgSignature,
thumbData: thumbData ?? _shareThumbnail,
thumbDataHash: thumbDataHash,
),
),
onResponse: onResponse,
);
}
Future<bool> shareMiniProgram(
String webPage, {
required String webPageUrl,
WXMiniProgramType miniProgramType = WXMiniProgramType.release,
required String userName,
String path = "/",
String? title,
String? description,
bool withShareTicket = false,
String? messageExt,
String? messageAction,
String? mediaTagName,
String? msgSignature,
Uint8List? thumbData,
String? thumbDataHash,
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(
fluwx.share(
WeChatShareMiniProgramModel(
webPageUrl: webPageUrl,
userName: userName,
title: title ?? _appName,
description: description ?? _appName,
messageExt: messageExt,
messageAction: messageAction,
mediaTagName: mediaTagName,
msgSignature: msgSignature,
thumbData: thumbData ?? _shareThumbnail,
thumbDataHash: thumbDataHash,
),
),
onResponse: onResponse,
);
}
Future<bool> shareImage(
WeChatImageToShare source, {
String? title,
String? description,
WeChatScene scene = WeChatScene.session,
String? messageExt,
String? messageAction,
String? mediaTagName,
String? msgSignature,
Uint8List? thumbData,
String? thumbDataHash,
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(
fluwx.share(
WeChatShareImageModel(
source,
title: title ?? _appName,
description: description ?? _appName,
scene: scene,
messageExt: messageExt,
messageAction: messageAction,
mediaTagName: mediaTagName,
msgSignature: msgSignature,
thumbData: thumbData ?? _shareThumbnail,
thumbDataHash: thumbDataHash,
),
),
onResponse: onResponse,
);
}
Future<bool> shareMusic({
String? musicUrl,
String? musicDataUrl,
String? musicLowBandUrl,
String? musicLowBandDataUrl,
String? title,
String? description,
WeChatScene scene = WeChatScene.session,
String? messageExt,
String? messageAction,
String? mediaTagName,
String? msgSignature,
Uint8List? thumbData,
String? thumbDataHash,
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(
fluwx.share(
WeChatShareMusicModel(
musicUrl: musicUrl,
musicDataUrl: musicDataUrl,
musicLowBandUrl: musicLowBandUrl,
musicLowBandDataUrl: musicLowBandDataUrl,
title: title ?? _appName,
description: description ?? _appName,
scene: scene,
messageExt: messageExt,
messageAction: messageAction,
mediaTagName: mediaTagName,
msgSignature: msgSignature,
thumbData: thumbData ?? _shareThumbnail,
thumbDataHash: thumbDataHash,
),
),
onResponse: onResponse,
);
}
Future<bool> shareVideo(
String videoUrl, {
String? title,
String? description,
WeChatScene scene = WeChatScene.session,
String? messageExt,
String? messageAction,
String? mediaTagName,
String? msgSignature,
Uint8List? thumbData,
String? thumbDataHash,
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(
fluwx.share(
WeChatShareVideoModel(
title: title ?? _appName,
videoUrl: videoUrl,
description: description ?? _appName,
scene: scene,
messageExt: messageExt,
messageAction: messageAction,
mediaTagName: mediaTagName,
msgSignature: msgSignature,
thumbData: thumbData ?? _shareThumbnail,
thumbDataHash: thumbDataHash,
),
),
onResponse: onResponse,
);
}
Future<bool> shareWebPage(
String webPage, {
String? title,
String? description,
WeChatScene scene = WeChatScene.session,
String? messageExt,
String? messageAction,
String? mediaTagName,
String? msgSignature,
Uint8List? thumbData,
String? thumbDataHash,
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(
fluwx.share(
WeChatShareWebPageModel(
webPage,
title: title ?? _appName,
description: description ?? _appName,
scene: scene,
messageExt: messageExt,
messageAction: messageAction,
mediaTagName: mediaTagName,
msgSignature: msgSignature,
thumbData: thumbData ?? _shareThumbnail,
thumbDataHash: thumbDataHash,
),
),
onResponse: onResponse,
);
}
Future<bool> shareFile(
File source, {
required String suffix,
String? title,
String? description,
WeChatScene scene = WeChatScene.session,
String? messageExt,
String? messageAction,
String? mediaTagName,
String? msgSignature,
Uint8List? thumbData,
String? thumbDataHash,
FlWeChatShareResponseCallback? onResponse,
}) async {
if (!await isInstalled) return false;
return _shareResponse(
fluwx.share(
WeChatShareFileModel(
WeChatFile.file(source, suffix: suffix),
title: title ?? _appName,
description: description ?? _appName,
scene: scene,
messageExt: messageExt,
messageAction: messageAction,
mediaTagName: mediaTagName,
msgSignature: msgSignature,
thumbData: thumbData ?? _shareThumbnail,
thumbDataHash: thumbDataHash,
),
),
onResponse: onResponse,
);
}
Future<bool> _shareResponse(
Future<bool> share, {
FlWeChatShareResponseCallback? onResponse,
}) async {
final cancelable = _onListener((response) {
if (response is WeChatShareResponse) onResponse?.call(response);
});
final result = await share;
if (!result) cancelable.cancel();
return result;
}
}
class FlWXCallbackParams {
FlWXCallbackParams({this.onToast, this.onLog, this.onHttp});
/// toast 显示
final FlWXToastCallback? onToast;
/// 日志 打印
final FlWXLogCallback? onLog;
/// http 请求 返回 json
final FlWXHTTPCallback? onHttp;
}
class WXUserModel {
final String? openid;
final String? nickname;
final int? sex;
final String? language;
final String? city;
final String? province;
final String? country;
final String? headImgUrl;
final List<dynamic>? privilege;
final String? unionId;
WXUserModel.fromJson(Map<String, dynamic> json)
: openid = json['openid'] as String?,
nickname = json['nickname'] as String?,
sex = json['sex'] as int?,
language = json['language'] as String?,
city = json['city'] as String?,
province = json['province'] as String?,
country = json['country'] as String?,
headImgUrl = json['headimgurl'] as String?,
privilege = json['privilege'] as List<dynamic>?,
unionId = json['unionid'] as String?;
Map<String, dynamic> toMap() => {
'openid': openid,
'unionId': unionId,
'nickname': nickname,
'sex': sex,
'headImgUrl': headImgUrl,
'privilege': privilege,
'language': language,
'city': city,
'province': province,
'country': country,
};
}
class WXTokenModel {
final String? accessToken;
final int? expiresIn;
final String? refreshToken;
final String? openid;
final String? scope;
final String? unionId;
WXTokenModel.fromJson(Map<String, dynamic> json)
: accessToken = json['access_token'] as String?,
expiresIn = json['expires_in'] as int?,
refreshToken = json['refresh_token'] as String?,
openid = json['openid'] as String?,
scope = json['scope'] as String?,
unionId = json['unionid'] as String?;
Map<String, dynamic> toMap() => {
'openid': openid,
'unionId': unionId,
'accessToken': accessToken,
'expiresIn': expiresIn,
'refreshToken': refreshToken,
'scope': scope,
};
}
extension ExtensionMap on Map {
Payment? toPayment() {
try {
final appId = (this['appId'] ?? this['appid']);
final partnerId = (this['partnerId'] ??
this['partnerid'] ??
this['partner']) as String?;
final prepayId = (this['prepayId'] ?? this['prepayid'] ?? this['prepay']);
final packageValue =
(this['packageValue'] ?? this['packagevalue'] ?? this['package']);
final nonceStr = (this['nonceStr'] ?? this['noncestr'] ?? this['nonce']);
final timestamp = (this['timestamp'] ?? this['timeStamp']);
final sign = this['sign'];
if (appId != null &&
partnerId != null &&
prepayId != null &&
packageValue != null &&
nonceStr != null &&
timestamp != null &&
sign != null) {
return Payment(
appId: appId.toString(),
partnerId: partnerId.toString(),
prepayId: prepayId.toString(),
packageValue: packageValue.toString(),
nonceStr: nonceStr.toString(),
timestamp: int.parse(timestamp!.toString()),
sign: sign.toString(),
);
}
} catch (e) {
FlWX()._params?.onLog?.call(e.toString());
}
return null;
}
}

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

@ -3,7 +3,7 @@ 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 '../../../core/themes/app_theme.dart';
import '../../common/webview_page.dart';
class LoginView extends GetView<LoginController> {
const LoginView({Key? key}) : super(key: key);
@ -11,7 +11,7 @@ class LoginView extends GetView<LoginController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
body: SafeArea(
@ -33,11 +33,11 @@ class LoginView extends GetView<LoginController> {
// 应用标志和标题
SizedBox(height: 32.h),
_buildAppLogo(isDarkMode),
// 欢迎文本
SizedBox(height: 32.h),
_buildWelcomeText(isDarkMode),
// 输入框和继续按钮
SizedBox(height: 24.h),
_buildInputField(isDarkMode),
@ -45,11 +45,11 @@ class LoginView extends GetView<LoginController> {
_buildVerificationInputField(isDarkMode),
SizedBox(height: 16.h),
_buildContinueButton(isDarkMode),
// 社交媒体登录
SizedBox(height: 32.h),
_buildSocialLoginSection(isDarkMode),
// 用户协议
SizedBox(height: 40.h),
_buildTermsAndPrivacy(isDarkMode),
@ -71,23 +71,23 @@ class LoginView extends GetView<LoginController> {
children: [
// 返回按钮
_buildBackButton(isDarkMode),
// 验证码标题
SizedBox(height: 24.h),
_buildVerificationTitle(isDarkMode),
// 验证码输入框
SizedBox(height: 32.h),
_buildVerificationInputs(context, isDarkMode),
// 重新发送验证码
SizedBox(height: 16.h),
_buildResendButton(isDarkMode),
// 登录按钮
SizedBox(height: 32.h),
_buildLoginButton(isDarkMode),
// 使用密码登录
SizedBox(height: 16.h),
_buildUsePasswordButton(isDarkMode),
@ -128,7 +128,7 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(height: 16.h),
Text(
'DeepVoice',
'DeepSound',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
@ -137,7 +137,7 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(height: 8.h),
Text(
'连接世界,超越声音',
'slogan'.tr, // 连接世界,超越声音
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
@ -152,7 +152,7 @@ class LoginView extends GetView<LoginController> {
return Column(
children: [
Text(
'欢迎使用',
'welcome'.tr, // 欢迎使用
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
@ -161,7 +161,7 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(height: 4.h),
Text(
'请使用手机号或邮箱登录/注册',
'loginTips'.tr, // 请使用手机号或邮箱登录/注册
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
@ -175,24 +175,62 @@ class LoginView extends GetView<LoginController> {
Widget _buildInputField(bool isDarkMode) {
return Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900]!.withOpacity(0.4) : Colors.grey.withOpacity(0.1),
color: isDarkMode
? Colors.grey[900]!.withOpacity(0.4)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
child: Row(
children: [
Icon(
Icons.person_outline,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
size: 20.sp,
// 添加国家选择按钮
InkWell(
onTap: () => _showCountryPicker(isDarkMode),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Text(
'${controller.selectedCountryFlag}${controller.selectedCountryCode}',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
)),
Icon(
Icons.arrow_drop_down,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
size: 20.sp,
),
],
),
),
),
SizedBox(width: 12.w),
// 分隔线
Container(
height: 20.h,
width: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
margin: EdgeInsets.symmetric(horizontal: 8.w),
),
// Icon(
// Icons.person_outline,
// color: isDarkMode ? Colors.grey[400] : Colors.black54,
// size: 20.sp,
// ),
//SizedBox(width: 12.w),
Expanded(
child: TextField(
controller: controller.contactController,
// keyboardType: controller.selectedCountryFlag == '✉️'
// ? TextInputType.emailAddress
// : TextInputType.phone,
decoration: InputDecoration(
hintText: '手机号码或邮箱',
hintText: controller.selectedCountryFlag == '✉️'
? 'email'.tr // 需要在翻译文件中添加
: 'phoneNumber'.tr, // 需要在翻译文件中添加
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[600] : Colors.black38,
@ -213,11 +251,78 @@ class LoginView extends GetView<LoginController> {
);
}
// 添加国家选择底部弹窗
void _showCountryPicker(bool isDarkMode) {
Get.bottomSheet(
Container(
height: 400.h,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.r),
topRight: Radius.circular(20.r),
),
),
child: Column(
children: [
Text(
'selectCountry'.tr, // 选择国家/地区
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 16.h),
Expanded(
child: ListView.builder(
itemCount: controller.countryCodes.length,
itemBuilder: (context, index) {
final country = controller.countryCodes[index];
return ListTile(
onTap: () {
controller.updateCountryCode(
country['code']!,
country['flag']!,
);
Get.back();
},
leading: Text(
country['flag']!,
style: TextStyle(fontSize: 24.sp),
),
title: Text(
'${country['name']}'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
),
trailing: Text(
country['code']!,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
);
},
),
),
],
),
),
backgroundColor: Colors.transparent,
isScrollControlled: true,
);
}
// 验证码输入框
Widget _buildVerificationInputField(bool isDarkMode) {
return Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900]!.withOpacity(0.4) : Colors.grey.withOpacity(0.1),
color: isDarkMode
? Colors.grey[900]!.withOpacity(0.4)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
@ -236,7 +341,7 @@ class LoginView extends GetView<LoginController> {
keyboardType: TextInputType.number,
maxLength: 6,
decoration: InputDecoration(
hintText: '验证码',
hintText: 'verificationCode'.tr, // 验证码
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[600] : Colors.black38,
@ -253,25 +358,26 @@ class LoginView extends GetView<LoginController> {
),
),
Obx(() => TextButton(
onPressed: controller.countdownSeconds > 0
? null
: controller.getVerificationCode,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 16.w),
foregroundColor: const Color(0xFF3B82F6),
backgroundColor: Colors.transparent,
disabledForegroundColor: isDarkMode ? Colors.grey[600] : Colors.grey[400],
),
child: Text(
controller.countdownSeconds > 0
? '${controller.countdownSeconds}s后重新获取'
: '获取验证码',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
)),
onPressed: controller.countdownSeconds > 0
? null
: controller.getVerificationCode,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 16.w),
foregroundColor: const Color(0xFF3B82F6),
backgroundColor: Colors.transparent,
disabledForegroundColor:
isDarkMode ? Colors.grey[600] : Colors.grey[400],
),
child: Text(
controller.countdownSeconds > 0
? '${controller.countdownSeconds}${'reacquireAfter'.tr}' // s后重新获取
: 'getCode'.tr, // 获取验证码
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
)),
],
),
),
@ -283,35 +389,36 @@ class LoginView extends GetView<LoginController> {
return SizedBox(
width: double.infinity,
child: Obx(() => ElevatedButton(
onPressed: controller.isLoading ? null : () => controller.login(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
disabledBackgroundColor: const Color(0xFF0066CC).withOpacity(0.7),
disabledForegroundColor: Colors.white,
),
child: controller.isLoading
? SizedBox(
height: 20.h,
width: 20.h,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'继续',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
onPressed: controller.isLoading ? null : () => controller.login(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
)),
elevation: 0,
disabledBackgroundColor: const Color(0xFF0066CC).withOpacity(0.7),
disabledForegroundColor: Colors.white,
),
child: controller.isLoading
? SizedBox(
height: 20.h,
width: 20.h,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor:
const AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'continue'.tr, // 继续
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
)),
);
}
@ -321,18 +428,22 @@ class LoginView extends GetView<LoginController> {
children: [
Row(
children: [
Expanded(child: Divider(color: isDarkMode ? Colors.grey[800] : Colors.grey[300])),
Expanded(
child: Divider(
color: isDarkMode ? Colors.grey[800] : Colors.grey[300])),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text(
'或使用以下方式登录',
'otherWays'.tr, // 或使用以下方式登录
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
),
),
),
Expanded(child: Divider(color: isDarkMode ? Colors.grey[800] : Colors.grey[300])),
Expanded(
child: Divider(
color: isDarkMode ? Colors.grey[800] : Colors.grey[300])),
],
),
SizedBox(height: 24.h),
@ -341,7 +452,14 @@ class LoginView extends GetView<LoginController> {
children: [
_buildSocialButton(FontAwesomeIcons.google, Colors.red, isDarkMode),
SizedBox(width: 24.w),
_buildSocialButton(FontAwesomeIcons.weixin, Colors.green, isDarkMode),
_buildSocialButton(
FontAwesomeIcons.weixin, Colors.green, isDarkMode),
SizedBox(width: 24.w),
_buildSocialButton(
FontAwesomeIcons.facebook, Colors.blue, isDarkMode),
SizedBox(width: 24.w),
_buildSocialButton(
FontAwesomeIcons.userSecret, Colors.black, isDarkMode),//添加游客登录按钮
],
),
],
@ -356,31 +474,58 @@ class LoginView extends GetView<LoginController> {
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.white,
borderRadius: BorderRadius.circular(24.r),
boxShadow: isDarkMode ? [] : [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
boxShadow: isDarkMode
? []
: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: FaIcon(
icon,
color: color,
size: 20.sp,
),
onPressed: () {},
),
icon: FaIcon(
icon,
color: color,
size: 20.sp,
),
onPressed: () => SocialLogin(icon)),
);
}
Future<void> SocialLogin(IconData icon) async {
switch (icon) {
case FontAwesomeIcons.facebook:
{
controller.loginType = 4;
break;
}
case FontAwesomeIcons.google:
{
controller.loginType = 3;
break;
}
case FontAwesomeIcons.weixin:
{
controller.loginType = 2;
break;
}
case FontAwesomeIcons.userSecret: // 处理游客登录
{
controller.loginType = 6;
break;
}
}
controller.login();
}
// 用户协议和隐私政策
Widget _buildTermsAndPrivacy(bool isDarkMode) {
return Column(
children: [
Text(
'登录/注册即表示你同意',
'expressConsent'.tr, // 登录/注册即表示你同意
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
@ -391,9 +536,19 @@ class LoginView extends GetView<LoginController> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {},
onTap: () {
String url = 'https://www.deepsoud.com/yhxy.html';
print('----------${Get.locale?.toString()}');
if (Get.locale?.toString() != 'zh_CN') {
url = 'https://www.deepsoud.com/yhxy_en.html';
}
Get.to(() => WebViewPage(
url: url,
title: 'userAgreement'.tr,
));
},
child: Text(
'《用户协议》',
'userAgreement'.tr, // 《用户协议》
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF3B82F6),
@ -401,16 +556,25 @@ class LoginView extends GetView<LoginController> {
),
),
Text(
' 和 ',
'and'.tr, // 和
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
),
),
GestureDetector(
onTap: () {},
onTap: () {
String url = 'https://www.deepsoud.com/ysxy.html';
if (Get.locale?.toString() != 'zh_CN') {
url = 'https://www.deepsoud.com/ysxy_en.html';
}
Get.to(() => WebViewPage(
url: url,
title: 'privacyPolicy'.tr,
));
},
child: Text(
'《隐私政策》',
'privacyPolicy'.tr, // 《隐私政策》
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF3B82F6),
@ -458,12 +622,12 @@ class LoginView extends GetView<LoginController> {
),
SizedBox(height: 8.h),
Obx(() => Text(
'验证码已发送至 ${controller.displayContact}',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
)),
'验证码已发送至 ${controller.displayContact}',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
)),
],
);
}
@ -472,17 +636,21 @@ class LoginView extends GetView<LoginController> {
Widget _buildVerificationInputs(BuildContext context, bool isDarkMode) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(4, (index) => _buildVerificationInput(index, context, isDarkMode)),
children: List.generate(
4, (index) => _buildVerificationInput(index, context, isDarkMode)),
);
}
// 单个验证码输入框
Widget _buildVerificationInput(int index, BuildContext context, bool isDarkMode) {
Widget _buildVerificationInput(
int index, BuildContext context, bool isDarkMode) {
return Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900]!.withOpacity(0.3) : Colors.grey.withOpacity(0.1),
color: isDarkMode
? Colors.grey[900]!.withOpacity(0.3)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
@ -520,28 +688,28 @@ class LoginView extends GetView<LoginController> {
Widget _buildResendButton(bool isDarkMode) {
return Center(
child: Obx(() => TextButton.icon(
onPressed: controller.countdownSeconds > 0
? null
: controller.resendVerificationCode,
icon: Icon(
Icons.refresh,
size: 14.sp,
color: controller.countdownSeconds > 0
? (isDarkMode ? Colors.grey[600] : Colors.grey[500])
: const Color(0xFF3B82F6),
),
label: Text(
controller.countdownSeconds > 0
? '重新发送验证码 (${controller.countdownSeconds}s)'
: '重新发送验证码',
style: TextStyle(
fontSize: 14.sp,
color: controller.countdownSeconds > 0
? (isDarkMode ? Colors.grey[600] : Colors.grey[500])
: const Color(0xFF3B82F6),
),
),
)),
onPressed: controller.countdownSeconds > 0
? null
: controller.resendVerificationCode,
icon: Icon(
Icons.refresh,
size: 14.sp,
color: controller.countdownSeconds > 0
? (isDarkMode ? Colors.grey[600] : Colors.grey[500])
: const Color(0xFF3B82F6),
),
label: Text(
controller.countdownSeconds > 0
? '重新发送验证码 (${controller.countdownSeconds}s)'
: '重新发送验证码',
style: TextStyle(
fontSize: 14.sp,
color: controller.countdownSeconds > 0
? (isDarkMode ? Colors.grey[600] : Colors.grey[500])
: const Color(0xFF3B82F6),
),
),
)),
);
}
@ -550,35 +718,36 @@ class LoginView extends GetView<LoginController> {
return SizedBox(
width: double.infinity,
child: Obx(() => ElevatedButton(
onPressed: controller.isLoading ? null : controller.login,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B82F6),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
disabledBackgroundColor: const Color(0xFF3B82F6).withOpacity(0.7),
disabledForegroundColor: Colors.white,
),
child: controller.isLoading
? SizedBox(
height: 20.h,
width: 20.h,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'登录',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
onPressed: controller.isLoading ? null : controller.login,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B82F6),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
)),
elevation: 0,
disabledBackgroundColor: const Color(0xFF3B82F6).withOpacity(0.7),
disabledForegroundColor: Colors.white,
),
child: controller.isLoading
? SizedBox(
height: 20.h,
width: 20.h,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor:
const AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'登录',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
)),
);
}
@ -591,10 +760,11 @@ class LoginView extends GetView<LoginController> {
'使用密码登录',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? const Color(0xFF3B82F6) : const Color(0xFF3B82F6),
color:
isDarkMode ? const Color(0xFF3B82F6) : const Color(0xFF3B82F6),
),
),
),
);
}
}
}

10
lib/modules/meeting/bindings/meeting_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/meeting_controller.dart';
class MeetingBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingController>(() => MeetingController());
}
}

10
lib/modules/meeting/bindings/meeting_details_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/meeting_details_controller.dart';
class MeetingDetailsBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingDetailsController>(() => MeetingDetailsController());
}
}

11
lib/modules/meeting/bindings/meeting_edit_binding.dart

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/meeting_edit_controller.dart';
class MeetingEditBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingEditController>(() => MeetingEditController());
}
}

10
lib/modules/meeting/bindings/meeting_record_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/meeting_record_controller.dart';
class MeetingRecordBinding extends Bindings {
@override
void dependencies() {
// 注册控制器
Get.lazyPut<MeetingRecordController>(() => MeetingRecordController());
}
}

70
lib/modules/meeting/controllers/meeting_controller.dart

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/services/db/sqflite_api.dart';
class MeetingController extends GetxController {
final ScrollController listController = ScrollController();
// 是否还有更多数据
bool isLoadingMore = true;
int page = 1;
TextEditingController titleController = TextEditingController(); // 录音标题
RxList dataList = [].obs;
@override
void onInit() {
super.onInit();
listController.addListener(_onScroll);
_getDataList();
}
@override
void onClose() {
listController.dispose();
titleController.dispose();
super.onClose();
}
void _onScroll() {
if (isLoadingMore &&
listController.position.pixels >=
listController.position.maxScrollExtent - 100) {
page++;
_getDataList();
}
}
// 请求本地数据
void _getDataList() async {
List list = await SqfliteApi.getMeetingList(page);
dataList.addAll(list);
if (list.length < 10) {
isLoadingMore = false;
} else {
isLoadingMore = true;
}
}
// 修改标题
void editMeetingTitle(int id, String title) async {
if (title.isEmpty) return;
SqfliteApi.editMeetingTitle(id, title);
int index = dataList.indexWhere((item) => item['id'] == id);
if (index != -1) {
final dataMap = Map<String, dynamic>.from(dataList[index]);
dataMap['title'] = title;
dataList[index] = dataMap;
}
Get.back();
}
// 删除数据
void deleteMeeting(int id) {
dataList.removeWhere((item) => item['id'] == id);
// 删除数据库中的记录
SqfliteApi.deleteMeeting(id);
}
}

67
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../model/meeting_details_model.dart';
import '../model/meeting_model.dart';
class MeetingDetailsController extends GetxController
with GetSingleTickerProviderStateMixin {
late AnimationController lottieController;
final AudioPlayer audioPlayer = AudioPlayer();
int id = 0;
RxBool isLoading = true.obs;
Rx<MeetingModel> meetingData = MeetingModel().obs;
Rx<MeetingDetailsModel> dataDetails = MeetingDetailsModel().obs;
int seconds = 0;
RxBool isPlay = false.obs;
@override
void onInit() {
super.onInit();
lottieController = AnimationController(vsync: this);
final Map<String, dynamic> args = Get.arguments ?? {};
id = args['id'];
_getDataDetails();
}
@override
void onClose() {
lottieController.dispose();
audioPlayer.dispose();
super.onClose();
}
void _getDataDetails() async {
final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data);
dataDetails.value = MeetingDetailsModel.fromJson(data['details']);
audioPlayer.setAudioSource(AudioSource.file(dataDetails.value.audio));
audioPlayer.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
await audioPlayer.seek(Duration.zero);
audioPlayer.pause();
lottieController.stop();
isPlay.value = false;
}
});
isLoading.value = false;
}
void start() async {
if (isPlay.value) {
lottieController.stop();
audioPlayer.pause();
isPlay.value = false;
} else {
lottieController.repeat();
audioPlayer.play();
isPlay.value = true;
}
}
}

143
lib/modules/meeting/controllers/meeting_edit_controller.dart

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/volcano_ai_service.dart';
import 'meeting_controller.dart';
import 'meeting_details_controller.dart';
class MeetingEditController extends GetxController
with GetSingleTickerProviderStateMixin {
late AiService _aiService;
final MeetingController meetingController = Get.find<MeetingController>();
final MeetingDetailsController meetingDetailsController =
Get.find<MeetingDetailsController>();
late AnimationController lottieController;
late TextEditingController titleController;
late TextEditingController contentController;
final AudioPlayer audioPlayer = AudioPlayer();
int creationTime = 0;
RxString address = ''.obs;
String _longitude = '';
String _latitude = '';
int id = 0;
RxBool isPlay = false.obs;
@override
void onInit() {
super.onInit();
lottieController = AnimationController(vsync: this);
final Map<String, dynamic> args = Get.arguments ?? {};
id = args['id'];
_getDataDetails();
}
@override
void onClose() {
lottieController.dispose();
titleController.dispose();
contentController.dispose();
audioPlayer.dispose();
super.onClose();
}
void _getDataDetails() async {
titleController = TextEditingController(
text: meetingDetailsController.meetingData.value.title,
);
contentController = TextEditingController(
text: meetingDetailsController.dataDetails.value.content,
);
creationTime = meetingDetailsController.meetingData.value.creationtime;
address.value = meetingDetailsController.meetingData.value.address;
_longitude = meetingDetailsController.meetingData.value.longitude;
_latitude = meetingDetailsController.meetingData.value.latitude;
audioPlayer.setAudioSource(
AudioSource.file(meetingDetailsController.dataDetails.value.audio));
audioPlayer.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
await audioPlayer.seek(Duration.zero);
audioPlayer.pause();
lottieController.stop();
isPlay.value = false;
}
});
}
void start() async {
if (isPlay.value) {
lottieController.stop();
audioPlayer.pause();
isPlay.value = false;
} else {
lottieController.repeat();
audioPlayer.play();
isPlay.value = true;
}
}
void confirm() async {
EasyLoading.show();
_aiService = Get.find<VolcanoAIService>();
String aiSummary = await _aiService.sendMessage(
systemPrompt: '',
messages: [
{'role': 'user', 'content': contentController.text},
{'role': 'user', 'content': '帮我总结一下这次会议'},
],
);
String content = contentController.text;
String firstContent =
content.length >= 30 ? content.substring(0, 30) : content;
String title = titleController.text.isNotEmpty
? titleController.text
: meetingDetailsController.meetingData.value.title;
Map<String, Object?> data = {
'title': title,
'firstContent': firstContent.replaceAll("\n", ""),
'seconds': meetingDetailsController.seconds,
'content': contentController.text,
'aisummary': aiSummary,
'address': address.value,
'longitude': _longitude,
'latitude': _latitude,
};
final result = await SqfliteApi.editMeeting(id, data);
if (result != null) {
int index = meetingController.dataList.indexWhere(
(item) => item['id'] == id,
);
if (index != -1) {
final dataMap =
Map<String, dynamic>.from(meetingController.dataList[index]);
dataMap['title'] = title;
dataMap['content'] = firstContent.replaceAll("\n", "");
dataMap['seconds'] = meetingDetailsController.seconds;
dataMap['address'] = address.value;
meetingController.dataList[index] = dataMap;
}
meetingDetailsController.meetingData.value.title = title;
meetingDetailsController.meetingData.value.address = address.value;
meetingDetailsController.meetingData.value.longitude = _longitude;
meetingDetailsController.meetingData.value.latitude = _latitude;
meetingDetailsController.dataDetails.value.content =
contentController.text;
meetingDetailsController.dataDetails.value.aisummary = aiSummary;
meetingDetailsController.meetingData.refresh();
meetingDetailsController.dataDetails.refresh();
Get.back();
}
EasyLoading.dismiss();
}
}

312
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -0,0 +1,312 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/speech_impl/xunfei_asr_service.dart';
import '../../../data/services/volcano_ai_service.dart';
import 'meeting_controller.dart';
class MeetingRecordController extends GetxController
with GetSingleTickerProviderStateMixin {
late AiService _aiService;
final MeetingController meetingController = Get.find<MeetingController>();
late AnimationController lottieController;
late AsrService _asrService;
Timer? timer;
RxBool isRecording = false.obs; //是否录音中
RxInt seconds = 0.obs;
String title = '';
int creationTime = 0;
RxString address = '深圳市高新技术产业园区W2-A座'.obs;
String _longitude = '';
String _latitude = '';
RxString intermediateContent = ''.obs;
RxString finalContent = ''.obs;
StreamSubscription? _recognitionSubscription;
BytesBuilder _originaBytes = BytesBuilder(); //原始音频
@override
void onInit() {
super.onInit();
Permission.storage.request();
lottieController = AnimationController(vsync: this);
Get.put(XunfeiAsrService());
_asrService = Get.find<XunfeiAsrService>();
final Map<String, dynamic> args = Get.arguments ?? {};
title = args['title'];
creationTime = DateTime.now().millisecondsSinceEpoch;
}
@override
void onClose() {
lottieController.dispose();
timer?.cancel();
_asrService.dispose();
_recognitionSubscription?.cancel();
_originaBytes = BytesBuilder();
super.onClose();
}
// 点击开始、暂停按钮
void recorderHandle() {
if (isRecording.value) {
_pause();
} else {
_start();
}
}
// 开始
void _start() async {
await _startRecorder();
lottieController.repeat();
isRecording.value = true;
timer = Timer.periodic(const Duration(seconds: 1), (_) => seconds++);
}
// 暂停
Future<void> _pause() async {
await _stopRecorder();
lottieController.stop();
isRecording.value = false;
timer?.cancel();
}
//开始录音
Future<void> _startRecorder() async {
final recognitionStream = await _asrService.startContinuousRecognition();
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
);
}
//结束录音
Future<void> _stopRecorder() async {
await _asrService.stopContinuousRecognition();
}
// 处理语音识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.intermediateResult:
intermediateContent.value = event.text;
break;
case RecognitionEventType.finalResult:
finalContent.value += _spliceText(event);
intermediateContent.value = '';
_originaBytes.add(event.audio!);
break;
default:
// 不做任何处理
break;
}
}
String _spliceText(RecognitionEvent event) {
String eventText = event.text;
String text = '';
if (finalContent.value.isNotEmpty) {
if (event.role != '') {
final punctuations = {
',',
'。',
'!',
'?',
';',
':',
',',
'.',
':',
';',
'!',
'?'
};
final firstChar = eventText[0];
if (punctuations.contains(firstChar)) {
text += '$firstChar\n';
eventText = eventText.substring(1);
} else {
text += '\n';
}
text += '说话人${event.role}:';
}
} else {
text += '说话人${event.role}:';
}
text += eventText;
return text;
}
// 完成
void complete() async {
if (await Permission.storage.request().isGranted) {
EasyLoading.show();
await _pause();
_aiSummary();
} else {
Get.snackbar(
'error'.tr, // 错误
'请开启文件存储权限',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
}
void _aiSummary() async {
_aiService = Get.find<VolcanoAIService>();
String aiSummary = await _aiService.sendMessage(
systemPrompt: '',
messages: [
{'role': 'user', 'content': finalContent.value},
{'role': 'user', 'content': '帮我总结一下这次会议'},
],
);
_save(aiSummary);
}
void _save(String aiSummary) async {
String firstContent = finalContent.value.length >= 30
? finalContent.value.substring(0, 30)
: finalContent.value;
Uint8List audio = _originaBytes.toBytes();
Uint8List wavHeader = generateWavHeader(
sampleRate: 16000,
numberOfChannels: 1,
bitsPerSample: 16,
dataLength: audio.length,
);
Uint8List wavData = Uint8List.fromList([...wavHeader, ...audio]);
String audiopPath =
await _saveAudioToFile(wavData, 'meeting_$creationTime.wav');
if (audiopPath.isNotEmpty) {
Map<String, Object?> data = {
'title': title,
'firstContent': firstContent.replaceAll("\n", ""),
'seconds': seconds.value,
'creationTime': creationTime,
'audio': audiopPath,
'content': finalContent.value,
'aisummary': aiSummary,
'address': address.value,
'longitude': _longitude,
'latitude': _latitude,
};
final result = await SqfliteApi.insertMeeting(data);
if (result != null) {
meetingController.dataList.insert(0, {
'id': result,
'title': title,
'content': firstContent,
'seconds': seconds.value,
'creationtime': creationTime,
});
Get.back();
}
}
EasyLoading.dismiss();
}
Future<String> _saveAudioToFile(Uint8List audioData, String fileName) async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$fileName');
// 写入文件
await file.writeAsBytes(audioData);
return file.path;
} catch (e) {
Logger.error('保存失败: $e');
return '';
}
}
Uint8List generateWavHeader({
required int sampleRate,
required int numberOfChannels,
required int bitsPerSample,
required int dataLength,
}) {
final byteRate = sampleRate * numberOfChannels * (bitsPerSample ~/ 8);
final blockAlign = numberOfChannels * (bitsPerSample ~/ 8);
final riffSize = 36 + dataLength;
final header = Uint8List(44);
var pos = 0;
// RIFF 块
header.setRange(pos, pos + 4, [0x52, 0x49, 0x46, 0x46]); // 'RIFF'
pos += 4;
// 写入 RIFF 大小(小端)
header.setRange(pos, pos + 4, [
riffSize & 0xff,
(riffSize >> 8) & 0xff,
(riffSize >> 16) & 0xff,
(riffSize >> 24) & 0xff,
]);
pos += 4;
// WAVE 标识
header.setRange(pos, pos + 4, [0x57, 0x41, 0x56, 0x45]); // 'WAVE'
pos += 4;
// fmt 子块
header.setRange(pos, pos + 4, [0x66, 0x6d, 0x74, 0x20]); // 'fmt '
pos += 4;
header.setRange(pos, pos + 4, [16, 0, 0, 0]); // 块大小
pos += 4;
header.setRange(pos, pos + 2, [1, 0]); // PCM 格式
pos += 2;
header.setRange(pos, pos + 2, [numberOfChannels, 0]); // 声道数
pos += 2;
// 采样率(小端)
header.setRange(pos, pos + 4, [
sampleRate & 0xff,
(sampleRate >> 8) & 0xff,
(sampleRate >> 16) & 0xff,
(sampleRate >> 24) & 0xff,
]);
pos += 4;
// 字节率(小端)
header.setRange(pos, pos + 4, [
byteRate & 0xff,
(byteRate >> 8) & 0xff,
(byteRate >> 16) & 0xff,
(byteRate >> 24) & 0xff,
]);
pos += 4;
// 块对齐
header.setRange(pos, pos + 2, [blockAlign, 0]);
pos += 2;
// 位深度
header.setRange(pos, pos + 2, [bitsPerSample, 0]);
pos += 2;
// data 子块
header.setRange(pos, pos + 4, [0x64, 0x61, 0x74, 0x61]); // 'data'
pos += 4;
// 数据大小(小端)
header.setRange(pos, pos + 4, [
dataLength & 0xff,
(dataLength >> 8) & 0xff,
(dataLength >> 16) & 0xff,
(dataLength >> 24) & 0xff,
]);
return header;
}
}

27
lib/modules/meeting/model/meeting_details_model.dart

@ -0,0 +1,27 @@
class MeetingDetailsModel {
MeetingDetailsModel();
late int id;
late int meetingid;
late String audio;
late String content;
late String aisummary;
factory MeetingDetailsModel.fromJson(Map<String, dynamic> map) =>
MeetingDetailsModel()
..id = map["id"]
..meetingid = map["meetingid"]
..audio = map["audio"]
..content = map["content"]
..aisummary = map["aisummary"];
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['id'] = id;
data['meetingid'] = meetingid;
data['audio'] = audio;
data['content'] = content;
data['aisummary'] = aisummary;
return data;
}
}

35
lib/modules/meeting/model/meeting_model.dart

@ -0,0 +1,35 @@
class MeetingModel {
MeetingModel();
late int id;
late String title;
late String content;
late int seconds;
late String address;
late String longitude;
late String latitude;
late int creationtime;
factory MeetingModel.fromJson(Map<String, dynamic> map) => MeetingModel()
..id = map["id"]
..title = map["title"]
..content = map["content"]
..seconds = map["seconds"]
..address = map["address"]
..longitude = map["longitude"]
..latitude = map["latitude"]
..creationtime = map["creationtime"];
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['id'] = id;
data['title'] = title;
data['content'] = content;
data['seconds'] = seconds;
data['address'] = address;
data['longitude'] = longitude;
data['latitude'] = latitude;
data['creationtime'] = creationtime;
return data;
}
}

326
lib/modules/meeting/views/meeting_details_view.dart

@ -0,0 +1,326 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../controllers/meeting_details_controller.dart';
class MeetingDetailsView extends GetView<MeetingDetailsController> {
const MeetingDetailsView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: Obx(() => !controller.isLoading.value
? _buildBody(isDarkMode)
: const SizedBox()),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'录音详情',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
controller.meetingData.value.title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 17.sp,
),
),
),
10.horizontalSpace,
Text(
DateUtil.formatDateMs(
controller.meetingData.value.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
],
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Row(
children: [
Icon(
Icons.location_on,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Text(
controller.meetingData.value.address,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
],
),
),
_audioWave(isDarkMode),
_recordContent(isDarkMode),
_bottomButton(isDarkMode),
],
),
);
}
// 音波
Widget _audioWave(bool isDarkMode) {
return Container(
padding: EdgeInsets.only(bottom: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
SizedBox(
height: 70.w,
child: Row(
children: [
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
StreamBuilder<Duration>(
stream: controller.audioPlayer.positionStream,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
return Text(
DateUtil.formatDateMs(
position.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
),
GestureDetector(
onTap: controller.start,
child: Container(
width: 30.w,
height: 30.w,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(
12.w,
),
),
child: Icon(
controller.isPlay.value ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 25.w,
),
),
),
StreamBuilder<Duration?>(
stream: controller.audioPlayer.durationStream,
builder: (context, snapshot) {
final duration = snapshot.data ?? Duration.zero;
controller.seconds = duration.inSeconds;
return Text(
DateUtil.formatDateMs(
duration.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
),
],
),
],
),
);
}
// 记录内容
Widget _recordContent(bool isDarkMode) {
return Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: DefaultTabController(
length: 2,
child: Column(
children: [
SizedBox(
height: 40.w,
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: isDarkMode ? AppColors.primary : Colors.blue,
labelColor: isDarkMode ? AppColors.primary : Colors.blue,
labelStyle: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 14.sp,
),
tabs: const [
Tab(text: 'AI总结'),
Tab(text: '会议原文'),
],
),
),
10.verticalSpace,
Expanded(
child: TabBarView(
children: [
SingleChildScrollView(
child: Text(
controller.dataDetails.value.aisummary,
),
),
SingleChildScrollView(
child: Text(
controller.dataDetails.value.content,
),
),
],
),
),
],
),
),
),
);
}
// 底部按钮
Widget _bottomButton(bool isDarkMode) {
return Container(
padding: EdgeInsets.only(top: 20.w, bottom: 10.w),
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingEdit, arguments: {
'id': controller.id,
});
},
child: Container(
width: 130.w,
height: 48.w,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.edit_calendar,
color: Colors.white,
size: 20.w,
),
5.horizontalSpace,
Text(
'编辑',
style: TextStyle(color: Colors.white, fontSize: 16.sp),
),
],
),
),
),
);
}
}

298
lib/modules/meeting/views/meeting_edit_view.dart

@ -0,0 +1,298 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../controllers/meeting_edit_controller.dart';
class MeetingEditView extends GetView<MeetingEditController> {
const MeetingEditView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
actions: [
GestureDetector(
onTap: controller.confirm,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.w,
),
child: Text(
'保存',
style: TextStyle(
color: isDarkMode ? AppColors.primary : Colors.blue,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: controller.titleController,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
decoration: InputDecoration(
hintText: '最多50个字符',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 16.sp,
),
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10.w,
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Text(
DateUtil.formatDateMs(
controller.creationTime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
),
Row(
children: [
Icon(
Icons.location_on,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Obx(
() => Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
),
],
),
_audioWave(isDarkMode),
_recordContent(isDarkMode),
],
),
);
}
// 音波
Widget _audioWave(bool isDarkMode) {
return Container(
padding: EdgeInsets.only(bottom: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
SizedBox(
height: 70.w,
child: Row(
children: [
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
StreamBuilder<Duration>(
stream: controller.audioPlayer.positionStream,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
return Text(
DateUtil.formatDateMs(
position.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
),
GestureDetector(
onTap: controller.start,
child: Container(
width: 30.w,
height: 30.w,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(
12.w,
),
),
child: Obx(
() => Icon(
controller.isPlay.value ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 25.w,
),
),
),
),
StreamBuilder<Duration?>(
stream: controller.audioPlayer.durationStream,
builder: (context, snapshot) {
final duration = snapshot.data ?? Duration.zero;
return Text(
DateUtil.formatDateMs(
duration.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
),
],
),
],
),
);
}
// 记录内容
Widget _recordContent(bool isDarkMode) {
return Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: controller.contentController,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 14.sp,
),
maxLines: 50,
decoration: InputDecoration(
hintText: '编辑记录内容',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 14.sp,
),
fillColor: Colors.transparent,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
);
}
}

358
lib/modules/meeting/views/meeting_record_view.dart

@ -0,0 +1,358 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/select_location_map.dart';
import '../controllers/meeting_record_controller.dart';
class MeetingRecordView extends GetView<MeetingRecordController> {
const MeetingRecordView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
_showAlertDialog(isDarkMode);
},
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () {
_showAlertDialog(isDarkMode);
},
),
title: Padding(
padding: const EdgeInsets.only(right: 56),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(
() => Icon(
Icons.keyboard_voice,
color: isDarkMode
? controller.seconds.value > 0
? AppColors.primary
: Colors.white
: controller.seconds.value > 0
? Colors.blue
: Colors.black,
),
),
5.horizontalSpace,
Obx(
() => Text(
DateUtil.formatDateMs(
controller.seconds.value * 1000,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode
? controller.seconds.value > 0
? AppColors.primary
: Colors.white
: controller.seconds.value > 0
? Colors.blue
: Colors.black,
),
),
),
],
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
10.verticalSpace,
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Row(
children: [
Expanded(
child: Text(
controller.title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 17.sp,
),
),
),
10.horizontalSpace,
Text(
DateUtil.formatDateMs(
controller.creationTime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
],
),
),
GestureDetector(
onTap: () {
Get.to(() => const SelectLocationMap());
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
child: Row(
children: [
Icon(
Icons.location_on,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
),
],
),
),
),
_recordContent(isDarkMode),
_bottomButton(isDarkMode),
],
);
}
// 记录内容
Widget _recordContent(bool isDarkMode) {
return Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
reverse: true,
child: Obx(
() => Text(
'${controller.finalContent.value}${controller.intermediateContent.value}',
),
),
),
),
),
);
}
// 底部按钮
Widget _bottomButton(bool isDarkMode) {
return Container(
margin: EdgeInsets.only(top: 10.h),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 3,
offset: const Offset(0, -1),
),
],
),
child: SafeArea(
child: SizedBox(
height: 80.h,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 语音录制按钮
Container(
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(
child: Obx(
() => IconButton(
icon: Icon(
controller.isRecording.value
? Icons.mic
: Icons.mic_none,
),
color: controller.isRecording.value
? AppColors.primary
: Colors.grey.shade400,
iconSize: 28.r,
padding: EdgeInsets.zero,
onPressed: controller.recorderHandle,
),
),
),
),
10.horizontalSpace,
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
10.horizontalSpace,
// 关闭按钮
Container(
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(
child: IconButton(
icon: Icon(
Icons.close,
color: isDarkMode ? Colors.white : Colors.grey.shade600,
size: 28.r,
),
padding: EdgeInsets.zero,
onPressed: () {
_showAlertDialog(isDarkMode);
},
),
),
),
],
),
),
),
),
);
}
// 显示对话框
void _showAlertDialog(bool isDarkMode) {
if (controller.seconds.value > 0) {
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'注意',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Text(
'你想结束录音吗?',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
TextButton(
onPressed: () {
Get.back();
controller.complete();
},
child: Text(
'confirm'.tr, // 确定
style: TextStyle(
color: isDarkMode ? Colors.red[300] : Colors.red[600],
),
),
),
],
),
);
} else {
Get.back();
}
}
}

419
lib/modules/meeting/views/meeting_view.dart

@ -0,0 +1,419 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../controllers/meeting_controller.dart';
import '../model/meeting_model.dart';
class MeetingView extends GetView<MeetingController> {
const MeetingView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(context, isDarkMode),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'conferenceAssistant'.tr, // 商务会议助手
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) {
return Stack(
children: [
Obx(
() => ListView.builder(
itemCount: controller.dataList.length,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
itemBuilder: (BuildContext context, int index) {
MeetingModel data =
MeetingModel.fromJson(controller.dataList[index]);
return _item(context, isDarkMode, data);
},
),
),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: () {
_bottomSheet(context, isDarkMode);
},
child: Container(
width: 130.w,
height: 48.w,
margin: EdgeInsets.symmetric(vertical: 20.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.keyboard_voice, color: Colors.white),
5.horizontalSpace,
Text(
'开始会议',
style: TextStyle(color: Colors.white, fontSize: 16.sp),
),
],
),
),
),
),
],
);
}
Widget _item(BuildContext context, bool isDarkMode, MeetingModel data) {
return GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingDetails, arguments: {
'id': data.id,
});
},
onLongPressStart: (details) {
RenderBox? renderBox =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: [
_popupMenuEntry(
Icon(
Icons.edit_calendar,
color: isDarkMode ? Colors.white : Colors.black87,
),
'编辑',
isDarkMode ? Colors.white : Colors.black87,
() {
controller.titleController.text = data.title;
_bottomSheet(context, isDarkMode, id: data.id);
},
),
_popupMenuEntry(
const Icon(Icons.delete_outline, color: Colors.red),
'删除',
isDarkMode ? Colors.white : Colors.black87,
() {
controller.deleteMeeting(data.id);
},
),
],
);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
data.title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
overflow: TextOverflow.ellipsis,
),
),
10.horizontalSpace,
Text(
DateUtil.formatDateMs(
data.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 12.sp,
),
),
],
),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.content.isNotEmpty ? data.content : '无内容',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 12.sp,
),
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Icon(
Icons.location_on,
size: 14.w,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
Expanded(
child: Text(
data.address,
style: TextStyle(
color: isDarkMode
? Colors.white70
: Colors.black54,
fontSize: 12.sp,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
Container(
width: 76.w,
height: 28.w,
margin: EdgeInsets.only(left: 10.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(28.w),
),
child: Text(
DateUtil.formatDateMs(
data.seconds * 1000,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(color: Colors.white, fontSize: 14.sp),
),
),
],
),
],
),
),
);
}
PopupMenuEntry _popupMenuEntry(
Icon icon,
String title,
Color color,
Function() onTap,
) {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 120.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(color: color),
),
],
),
),
);
}
void _bottomSheet(
BuildContext context,
bool isDarkMode, {
int? id,
}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
clipBehavior: Clip.hardEdge,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w),
color: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
id != null ? '修改会议标题(非必填)' : '会议标题(非必填)',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
),
Container(
height: 48.w,
margin: EdgeInsets.only(top: 10.w, bottom: 20.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.05)
: Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: controller.titleController,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
decoration: InputDecoration(
hintText: '最多50个字符',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 16.sp,
),
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 10.w),
),
),
),
Padding(
padding: EdgeInsets.only(top: 30.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: () {
Get.back();
},
child: Container(
width: 120.w,
height: 40.w,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(
width: 2.w,
color:
isDarkMode ? AppColors.primary : Colors.blue,
),
borderRadius: BorderRadius.circular(12.w),
),
child: Text(
'取消',
style: TextStyle(
fontSize: 14.sp,
color:
isDarkMode ? AppColors.primary : Colors.blue,
),
),
),
),
GestureDetector(
onTap: () {
int time = DateTime.now().millisecondsSinceEpoch;
String title = controller.titleController.text;
if (id != null) {
controller.editMeetingTitle(id, title);
} else {
if (title.isEmpty) {
title =
'${DateUtil.formatDateMs(time, format: DateFormats.y_mo_d)} 会议';
}
Get.back();
Get.toNamed(Routes.meetingRecord, arguments: {
'title': title,
});
}
},
child: Container(
width: 120.w,
height: 40.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(12.w),
),
child: Text(
'确定',
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
),
),
),
),
],
),
),
],
),
),
);
},
).then((value) {
controller.titleController.text = '';
});
}
}

109
lib/modules/pairing/controllers/pairing_controller.dart

@ -1,3 +1,4 @@
import 'package:app_settings/app_settings.dart';
import 'package:get/get.dart';
import 'dart:async';
import '../../../routes/app_routes.dart';
@ -6,101 +7,102 @@ import '../../../core/utils/logger.dart';
import 'dart:io';
import '../../../data/services/voice_interaction_service.dart';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
// 经典蓝牙服务
final ClassicBluetoothService _bluetoothService = Get.find<ClassicBluetoothService>();
// 语音交互服务
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>();
final ClassicBluetoothService _bluetoothService =
Get.find<ClassicBluetoothService>();
// 语音交互服务
final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>();
// 是否开始搜索
final _isSearching = false.obs;
bool get isSearching => _isSearching.value;
// 设备连接状态
final _isConnecting = false.obs;
bool get isConnecting => _isConnecting.value;
// 蓝牙服务是否可用
final _isBluetoothServiceAvailable = true.obs;
bool get isBluetoothServiceAvailable => _isBluetoothServiceAvailable.value;
// 当前连接设备
Rx<BluetoothDevice?> get currentDevice => _bluetoothService.currentDevice;
// 连接设备列表
RxList<BluetoothDevice> get connectedDevices => _bluetoothService.connectedDevices;
RxList<BluetoothDevice> get connectedDevices =>
_bluetoothService.connectedDevices;
// 蓝牙状态
RxString get bluetoothState => _bluetoothService.bluetoothState;
// 错误信息
final errorMessage = ''.obs;
// 状态描述(用于UI显示)
final statusDescription = '正在初始化...'.obs;
final statusDescription = 'initializing'.tr.obs; // 正在初始化...
// 加载状态
final isLoading = false.obs;
// 是否正在跳转到主页
bool _isNavigatingToHome = false;
@override
void onInit() {
super.onInit();
_initialize();
}
/// 设置状态变化监听
void _setupStatusListeners() {
if (!_isBluetoothServiceAvailable.value) return;
// 监听蓝牙开关状态
ever(_bluetoothService.bluetoothState, (state) {
_updateStatusDescription();
});
// 监听当前设备状态
ever(_bluetoothService.currentDevice, (device) {
_updateStatusDescription();
// 如果有设备连接,自动跳转到主页
if (device != null) {
_autoNavigateToHome();
}
});
}
/// 更新状态描述
void _updateStatusDescription() {
if (_bluetoothService.bluetoothState.value == 'off') {
statusDescription.value = '蓝牙已关闭,请打开蓝牙';
statusDescription.value = 'bluetoothTurnedOff'.tr;// 蓝牙已关闭, 请打开蓝牙
return;
}
if (_bluetoothService.currentDevice.value != null) {
statusDescription.value = '已连接到设备: ${_bluetoothService.currentDevice.value!.name}';
statusDescription.value =
'已连接到设备: ${_bluetoothService.currentDevice.value!.name}';
return;
}
if (_bluetoothService.connectedDevices.isNotEmpty) {
statusDescription.value = '找到已连接的设备: ${_bluetoothService.connectedDevices.first.name}';
statusDescription.value =
'找到已连接的设备: ${_bluetoothService.connectedDevices.first.name}';
return;
}
if (isLoading.value) {
statusDescription.value = '正在搜索设备...';
return;
}
statusDescription.value = '未找到已连接的蓝牙设备';
}
@ -112,12 +114,12 @@ class PairingController extends GetxController {
isLoading.value = true;
statusDescription.value = '正在初始化...';
// 确保蓝牙服务已初始化
if (!await _bluetoothService.initialize()) {
Logger.error('蓝牙服务初始化失败');
statusDescription.value = '蓝牙服务初始化失败';
statusDescription.value =
'bluetoothInitializationFailure'.tr; // 蓝牙服务初始化失败
_isBluetoothServiceAvailable.value = false;
isLoading.value = false;
return;
@ -133,76 +135,75 @@ class PairingController extends GetxController {
return;
}
}
// 刷新设备列表
statusDescription.value = '正在搜索设备...';
await refreshConnectedDevices();
// 更新状态描述
_updateStatusDescription();
} catch (e) {
Logger.error('初始化配对控制器失败: $e');
statusDescription.value = '初始化失败';
statusDescription.value = 'initializationFailure'.tr; // 初始化失败
_isBluetoothServiceAvailable.value = false;
} finally {
isLoading.value = false;
}
}
/// 刷新设备
Future<void> refreshConnectedDevices() async {
try {
isLoading.value = true;
statusDescription.value = '正在搜索设备...';
statusDescription.value = 'searchingDevice'.tr; // 正在搜索设备...
final hasDevices = await _bluetoothService.refreshConnectedDevices();
// 更新状态描述
_updateStatusDescription();
} catch (e) {
Logger.error('刷新设备失败: $e');
statusDescription.value = '刷新设备失败';
statusDescription.value = 'failedRefreshDevice'.tr; // 刷新设备失败
} finally {
isLoading.value = false;
}
}
/// 自动跳转到主页
void _autoNavigateToHome() {
// 如果已经在跳转或设备列表为空,则不执行
if (_isNavigatingToHome || _bluetoothService.connectedDevices.isEmpty) return;
if (_isNavigatingToHome || _bluetoothService.connectedDevices.isEmpty)
return;
// 设置跳转标志
_isNavigatingToHome = true;
Logger.info('已找到连接的设备,自动跳转到主页');
Future.delayed(const Duration(milliseconds: 500), () {
Get.offAllNamed(Routes.home);
});
}
/// 打开系统蓝牙设置
void openBluetoothSettings() {
try {
// 使用app_settings插件打开蓝牙设置
// AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} catch (e) {
Logger.error('打开蓝牙设置失败: $e');
}
}
/// 刷新设备(兼容旧代码)
Future<void> refreshDevices() async {
await refreshConnectedDevices();
}
/// 跳过连接
void skipConnecting() {
// 设置跳转标志防止自动跳转再次触发
_isNavigatingToHome = true;
Get.offAllNamed(Routes.home);
}
}
}

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

@ -11,7 +11,7 @@ class PairingView extends GetView<PairingController> {
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
body: SafeArea(
@ -35,32 +35,33 @@ class PairingView extends GetView<PairingController> {
physics: BouncingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(Get.context!).size.height - 100.h, // 减去底部导航和状态栏高度
minHeight:
MediaQuery.of(Get.context!).size.height - 100.h, // 减去底部导航和状态栏高度
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 20.h), // 顶部间距
// 标题部分
_buildHeader(isDarkMode),
SizedBox(height: 20.h),
// 设备动画 - 更大尺寸
_buildDeviceAnimation(),
SizedBox(height: 20.h),
// 状态信息
_buildStatusInfo(isDarkMode),
SizedBox(height: 40.h),
// 底部按钮
_buildActionButtons(isDarkMode),
SizedBox(height: 20.h),
],
),
@ -74,7 +75,7 @@ class PairingView extends GetView<PairingController> {
return Column(
children: [
Text(
'DeepVoice',
'DeepSound',
style: TextStyle(
fontSize: 30.sp,
fontWeight: FontWeight.bold,
@ -83,7 +84,7 @@ class PairingView extends GetView<PairingController> {
),
SizedBox(height: 8.h),
Text(
'连接世界,超越声音',
'homeSlogan'.tr, // 连接世界,超越声音
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white60 : Colors.black54,
@ -127,7 +128,7 @@ class PairingView extends GetView<PairingController> {
// 重新开始动画
},
),
// 旋转边框
SizedBox(
width: 100.w,
@ -140,7 +141,7 @@ class PairingView extends GetView<PairingController> {
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
// 设备图标
Container(
width: 120.w,
@ -166,13 +167,13 @@ class PairingView extends GetView<PairingController> {
),
);
}
// 状态信息
Widget _buildStatusInfo(bool isDarkMode) {
return Obx(() {
final status = controller.statusDescription.value;
final isLoading = controller.isLoading.value;
return Column(
children: [
// 状态文本 - 更大更醒目
@ -185,19 +186,19 @@ class PairingView extends GetView<PairingController> {
),
textAlign: TextAlign.center,
),
SizedBox(height: 10.h),
// 子状态提示文本
Text(
'请打开耳机充电盒,将耳机放入您的耳朵',
'turnEarphones'.tr, // 请打开耳机充电盒,将耳机放入您的耳朵
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white60 : Colors.grey[600],
),
textAlign: TextAlign.center,
),
// 设备列表(有设备时才显示)
if (controller.connectedDevices.isNotEmpty) ...[
SizedBox(height: 24.h),
@ -207,23 +208,23 @@ class PairingView extends GetView<PairingController> {
);
});
}
// 已连接设备列表
Widget _buildConnectedDevicesList(bool isDarkMode) {
return Obx(() {
final devices = controller.connectedDevices;
if (devices.isEmpty) {
return const SizedBox.shrink();
}
return Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前已连接:',
'currentlyConnected'.tr, // 当前已连接:
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
@ -237,7 +238,7 @@ class PairingView extends GetView<PairingController> {
);
});
}
// 设备项
Widget _buildDeviceItem(device, bool isDarkMode) {
return Container(
@ -287,7 +288,7 @@ class PairingView extends GetView<PairingController> {
),
);
}
// 操作按钮
Widget _buildActionButtons(bool isDarkMode) {
return Container(
@ -308,16 +309,16 @@ class PairingView extends GetView<PairingController> {
minimumSize: Size(double.infinity, 56.h),
),
child: Text(
'去系统设置',
'unsystemSetup'.tr, // 去系统设置
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 12.h),
// 次要按钮
TextButton(
onPressed: () => controller.skipConnecting(),
@ -327,7 +328,7 @@ class PairingView extends GetView<PairingController> {
minimumSize: Size(double.infinity, 48.h),
),
child: Text(
'稍后再说',
'talkLater'.tr, // 稍后再说
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
@ -338,4 +339,4 @@ class PairingView extends GetView<PairingController> {
),
);
}
}
}

2
lib/modules/profile/bindings/profile_binding.dart

@ -6,4 +6,4 @@ class ProfileBinding extends Bindings {
void dependencies() {
Get.lazyPut<ProfileController>(() => ProfileController());
}
}
}

382
lib/modules/profile/controllers/profile_controller.dart

@ -1,47 +1,65 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:flutter/material.dart';
//import 'package:http/http.dart';
import 'package:image_picker/image_picker.dart';
import 'package:logger/logger.dart';
import '../../../data/models/user_Info.dart';
import '../../settings/controllers/settings_controller.dart';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
import '../../../data/services/network/api.dart';
import 'package:dio/dio.dart';
class ProfileController extends GetxController {
static ProfileController get to => Get.find();
final _userName = ''.obs;
final _email = ''.obs;
final _avatar = ''.obs;
final _isEditing = false.obs;
final _storage = GetStorage();
// final _userName = ''.obs;
// final _email = ''.obs;
// final _avatar = ''.obs;
// final _isEditing = false.obs;
// final _storage = GetStorage();
//用户信息
static const String userKey = 'user_info'; //用户信息存储键
final _storage = GetStorage(); //本地存储
final ImagePicker _picker = ImagePicker(); //图片选择器
final Rx<User> userInfo = User.instance.obs; // 响应式用户信息对象,使用单例模式确保全局唯一性
final RxString localAvatar = ''.obs; // 本地头像路径,便于响应式更新UI
// 使用响应式变量
final currentLanguage = 'zh'.obs;
String get userName => _userName.value;
String get email => _email.value;
String get avatar => _avatar.value;
bool get isEditing => _isEditing.value;
// String get userName => _userName.value;
// String get email => _email.value;
// String get avatar => _avatar.value;
// bool get isEditing => _isEditing.value;
@override
void onInit() {
super.onInit();
loadUserData();
loadUserInfo();
loadLanguagePreference();
}
void loadUserData() {
try {
_userName.value = _storage.read('userName') ?? 'User Name';
_email.value = _storage.read('userEmail') ?? 'user@example.com';
_avatar.value = _storage.read('userAvatar') ?? '';
} catch (e) {
Get.snackbar('Error', 'Failed to load user data');
}
}
// void loadUserData() {
// try {
// _userName.value = _storage.read('userName') ?? 'User Name';
// _email.value = _storage.read('userEmail') ?? 'user@example.com';
// _avatar.value = _storage.read('userAvatar') ?? '';
// } catch (e) {
// Get.snackbar('Error', 'Failed to load user data');
// }
// }
// 加载用户语言偏好设置
void loadLanguagePreference() {
try {
final String? languageCode = _storage.read('languageCode');
final String? countryCode = _storage.read('countryCode');
if (languageCode != null && countryCode != null) {
Get.updateLocale(Locale(languageCode, countryCode));
currentLanguage.value = languageCode;
@ -50,7 +68,7 @@ class ProfileController extends GetxController {
print('Failed to load language preference: $e');
}
}
// 保存用户语言偏好设置
Future<void> saveLanguagePreference(Locale locale) async {
try {
@ -60,7 +78,7 @@ class ProfileController extends GetxController {
print('Failed to save language preference: $e');
}
}
// 切换应用语言
void changeLanguage(String languageCode, String countryCode) {
// 使用Future.delayed来避免在构建过程中更新UI
@ -71,32 +89,298 @@ class ProfileController extends GetxController {
currentLanguage.value = languageCode;
});
}
Future<void> updateUserData({
String? userName,
String? email,
String? avatar,
}) async {
// Future<void> updateUserData({
// String? userName,
// String? email,
// String? avatar,
// }) async {
// try {
// if (userName != null) {
// await _storage.write('userName', userName);
// _userName.value = userName;
// }
// if (email != null) {
// await _storage.write('userEmail', email);
// _email.value = email;
// }
// if (avatar != null) {
// await _storage.write('userAvatar', avatar);
// _avatar.value = avatar;
// }
// Get.snackbar('Success', 'Profile updated successfully');
// } catch (e) {
// Get.snackbar('Error', 'Failed to update profile');
// }
// }
// void toggleEditing() {
// _isEditing.value = !_isEditing.value;
// }
// 验证base64图片数据
// Future<void> _verifyBase64Image(String base64String) async {
// try {
// // 解码base64数据
// List<int> imageBytes = base64Decode(base64String);
// // 获取应用文档目录
// final appDir = await getApplicationDocumentsDirectory();
// final verifyPath = '${appDir.path}/verify_base64_${DateTime.now().millisecondsSinceEpoch}.jpg';
// // 将解码后的数据写入文件
// final file = File(verifyPath);
// await file.writeAsBytes(imageBytes);
// // 记录验证文件的路径
// Logger().d('Base64验证图片已保存到: $verifyPath');
// // 显示图片预览对话框
// Get.dialog(
// Dialog(
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// Padding(
// padding: const EdgeInsets.all(16.0),
// child: Text('Base64 图片验证', style: TextStyle(fontWeight: FontWeight.bold)),
// ),
// Image.file(File(verifyPath)),
// Padding(
// padding: const EdgeInsets.all(16.0),
// child: ElevatedButton(
// onPressed: () => Get.back(),
// child: Text('关闭'),
// ),
// ),
// ],
// ),
// ),
// );
// Get.snackbar('验证', '已将Base64转换回图片,请查看预览');
// } catch (e) {
// Logger().e('验证Base64图片失败: $e');
// Get.snackbar('错误', 'Base64验证失败: $e');
// }
// }
// 更新用户信息到后端
Future<void> _syncWithBackend() async {
try {
if (userName != null) {
await _storage.write('userName', userName);
_userName.value = userName;
// 准备数据
Map<String, dynamic> data = {
"image": "", // 默认空字符串
"language": currentLanguage.value,//当前用户语言
"name": userInfo.value.name,//当前用户名
};
// 如果有头像,转换为base64
if (localAvatar.value.isNotEmpty) {
//如果本地头像路径不为空,即代表有头像
try {
File imageFile = File(localAvatar.value);
if (await imageFile.exists()) {
List<int> imageBytes = await imageFile.readAsBytes();
String base64Image = base64Encode(imageBytes);
data["image"] = base64Image;
// // 验证base64数据 - 将base64转回图片并保存到本地
// await _verifyBase64Image(base64Image);
} else {
Logger().e('头像文件不存在');
}
} catch (e) {
Logger().e('读取头像文件失败: $e');
}
}
if (email != null) {
await _storage.write('userEmail', email);
_email.value = email;
final dio = Dio(BaseOptions(
baseUrl: 'https://www.deapsound.com:443',
headers: {
'accept': 'application/json',
'Authorization': _storage.read<String>("logintoken"),
'Content-Type': 'application/json',
},
));
// 调用API前打印token
Logger().d('当前用户token: ${_storage.read<String>("logintoken")}');
Logger().d('请求数据: ${{"user": data}}');
final response = await dio.post(
'/api/home/user_setting',
data: data,
);
// 调用API
//final response = await Api.updateUserSetting(data);
if (response.statusCode == 200) {
if (response.data['code'] == 0) {
Logger().d('更新成功: ${response.data}');
Get.snackbar('成功', '同步到服务器成功');
userInfo.update((u) {
if (u != null) {
u.name = response.data['data']['name'] ?? '';
u.avatar = response.data['data']['avatar'] ?? '';
}
});
Logger().d('更新后的用户信息: ${userInfo.value.toJson()}');
} else {
throw '更新失败:${response.data['msg'] ?? '未知错误'}';
}
} else {
throw '服务器响应错误: ${response.statusCode}';
}
if (avatar != null) {
await _storage.write('userAvatar', avatar);
_avatar.value = avatar;
} catch (e) {
Logger().e('同步到后端失败: $e');
Get.snackbar('错误', '同步到服务器失败,请重试');
throw e;
}
}
void loadUserInfo() {
final userJson = _storage.read(userKey);
if (userJson != null) {
userInfo.value = User.instance;
Logger().d('从本地加载用户信息: $userJson');
// 如果有远程头像地址,下载并保存到本地
if (userInfo.value.avatar.isNotEmpty) {
Logger().d('远程头像地址: ${userInfo.value.avatar}');
_downloadAndSaveAvatar(userInfo.value.avatar);
}
}
}
// 下载并保存远程头像
Future<void> _downloadAndSaveAvatar(String url) async {
try {
Logger().d('开始下载头像: $url');
final dio = Dio(BaseOptions(
headers: {
'accept': '*/*',
'Authorization': _storage.read<String>("logintoken"),
},
));
final response = await dio.get(
url,
options: Options(responseType: ResponseType.bytes),
);
Logger().d('下载状态: ${response.statusCode}');
if (response.statusCode == 200) {
// 保存到应用文档目录
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final localPath = '${appDir.path}/$fileName';
// 写入文件
final file = File(localPath);
await file.writeAsBytes(response.data);
// 更新本地头像路径
localAvatar.value = localPath;
await _storage.write('local_avatar', localPath);
Logger().d('头像已下载到本地: $localPath');
//Get.snackbar('头像', '头像已下载到本地');
}
Get.snackbar('Success', 'Profile updated successfully');
} catch (e) {
Get.snackbar('Error', 'Failed to update profile');
Logger().e('下载头像失败: $e');
}
}
void toggleEditing() {
_isEditing.value = !_isEditing.value;
Future<void> saveUserInfo() async {
await _storage.write(userKey, userInfo.value.toJson());
}
// 更新用户头像
Future<void> updateUserAvatar() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,// 从相册选择
maxWidth: 800,
maxHeight: 800,
imageQuality: 85,
);
if (image != null) {
// 检查文件大小
final file = File(image.path);
var fileSize = await file.length();
var quality = 85;
// 如果文件大于1MB,进行压缩
while (fileSize > 1024 * 1024 && quality > 30) {
final compressedImage = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
file.absolute.path, // 覆盖原文件
quality: quality,
minWidth: 800,
minHeight: 800,
);
if (compressedImage == null) {
Get.snackbar('错误', '图片压缩失败');
return;
}
//更新文件大小和质量
fileSize = await compressedImage.length();
quality -= 5;
Logger().d('压缩后文件大小:$fileSize');
}
userInfo.update((user) {
if (user != null) {
user.avatar = image.path;
}
});
// 更新本地头像路径
localAvatar.value = image.path;
await _storage.write('local_avatar', image.path);
// await saveUserInfo(); //保存到本地
//await _syncWithBackend(); //同步到后端
// 通知 SettingsController 更新
// if (Get.isRegistered<SettingsController>()) {
// Get.find<SettingsController>().loadUserInfo();
// }
}
}
// 更新用户名
Future<void> updateUserNameAndAvatar(String name) async {
if (name.trim().isEmpty) {
Logger().e('无效名称: $name');
return;
}
try {
userInfo.update((user) {
if (user != null) {
user.name = name.trim();
Logger().d('名称更新为: ${user.name}');
}
});
//await saveUserInfo(); //保存到本地
await _syncWithBackend(); //同步到后端
//同步更新 SettingsController
if (Get.isRegistered<SettingsController>()) {
final settingsController = Get.find<SettingsController>();
settingsController.userInfo.value.name = userInfo.value.name;
settingsController.userInfo.value.avatar = '${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}';
//settingsController.loadUserInfo();
settingsController.userInfo.refresh();
}
} catch (e) {
Logger().e('更新用户名和头像错误: $e');
Get.snackbar('错误', '更新用户名和头像失败');
}
}
}
}

144
lib/modules/profile/views/profile_view.dart

@ -4,7 +4,9 @@ import '../controllers/profile_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../routes/app_routes.dart';
import 'dart:io';
// ProfileView: 用户设置页面
class ProfileView extends GetView<ProfileController> {
const ProfileView({Key? key}) : super(key: key);
@ -61,7 +63,6 @@ class ProfileView extends GetView<ProfileController> {
body: ListView(
children: [
SizedBox(height: 12.h),
const Divider(),
_buildMenuItem(
title: '关于'.tr,
@ -70,7 +71,6 @@ class ProfileView extends GetView<ProfileController> {
// TODO: 实现关于页面
},
),
const Divider(),
_buildMenuItem(
title: '火山语音合成测试'.tr,
@ -80,8 +80,6 @@ class ProfileView extends GetView<ProfileController> {
Get.toNamed(Routes.TTS_TEST);
},
),
const Divider(),
_buildMenuItem(
title: '火山语音识别测试'.tr,
@ -100,7 +98,7 @@ class ProfileView extends GetView<ProfileController> {
Get.toNamed(Routes.AZURE_ASR_TEST);
},
),
const Divider(),
const Divider(),
_buildMenuItem(
title: 'Azure语音合成测试'.tr,
icon: Icons.record_voice_over,
@ -132,4 +130,138 @@ class ProfileView extends GetView<ProfileController> {
bottomNavigationBar: const CommonBottomNav(currentIndex: 2),
);
}
}
}
// EditProfileView: 编辑个人资料页面
class EditProfileView extends GetView<ProfileController> {
const EditProfileView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final nameController =
TextEditingController(text: controller.userInfo.value.name);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
elevation: 0,
backgroundColor: isDarkMode ? Colors.black : Colors.white,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: isDarkMode? Colors.white : Colors.black,
),
onPressed: () => Get.back(),
),
title: Text(
'editProfile'.tr,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
// 头像编辑
Center(
child: GestureDetector(
onTap: () => controller.updateUserAvatar(),
child: Stack(
children: [
Obx(() => CircleAvatar(
radius: 50.r,
backgroundImage: controller.localAvatar.value.isNotEmpty
? FileImage(File(controller.localAvatar.value))
: (controller.userInfo.value.avatar.isNotEmpty
? NetworkImage(
controller.userInfo.value.avatar)
: null) as ImageProvider<Object>?,
child: controller.userInfo.value.avatar.isEmpty
? Icon(Icons.person, size: 50.r)
: null,
)),
Positioned(
right: 0,
bottom: 0,
child: Container(
padding: EdgeInsets.all(4.w),
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: Icon(
Icons.camera_alt,
size: 20.w,
color: Colors.white,
)),
)
],
)),
),
SizedBox(height: 20.h),
// 昵称编辑 - 使用外部定义的controller而不是每次重建
Row(
children: [
Expanded(
child: TextField(
controller: nameController,
decoration: InputDecoration(
labelText: 'nickname'.tr,
border: const OutlineInputBorder(),
suffixIcon: nameController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
nameController.clear();
},
)
: null,
),
),
),
],
),
// ... 其他编辑项 ...
SizedBox(height: 30.h),
// 添加一个大的保存按钮
SizedBox(
width: double.infinity,
height: 50.h,
child: ElevatedButton(
onPressed: () {
// 保存用户名
controller.updateUserNameAndAvatar(nameController.text);
// 返回上一级页面
Get.back();
},
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? Colors.blue[700] : Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
),
),
child: Text(
'save'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
);
}
}

283
lib/modules/settings/controllers/settings_controller.dart

@ -1,41 +1,237 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/speech_factory.dart';
import '../models/language_model.dart';
import 'package:logger/logger.dart';
import '../../../data/models/user_Info.dart';
import '../../../modules/login/controllers/googleLogin.dart';
import '../../../modules/login/controllers/facebookLogin.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import '../../../routes/app_routes.dart';
class SettingsController extends GetxController {
// 语言设置
final List<LanguageModel> languageList = [
LanguageModel(
name: 'followingSystem', // 跟随系统.tr
alias: 'followingSystem', // 跟随系统.tr
value: 'system'),
LanguageModel(name: 'chinese', alias: '中文', value: 'zh_CN'), // 中文.tr
LanguageModel(name: 'english', alias: 'English', value: 'en_US'), // 英语.tr
];
Rx<LanguageModel> language = LanguageModel().obs;
final GetStorage _storage = GetStorage();
// 主题服务
final ThemeService _themeService = ThemeService();
// 是否是暗黑模式
final RxBool isDarkMode = false.obs;
// 翻译语言对
final RxString sourceLanguage = '中文'.obs;
final RxString targetLanguage = '英文'.obs;
final List<String> availableLanguages = ['中文', '英文', '日文', '韩文', '法文', '德文', '西班牙文', '俄文'];
final List<String> availableLanguages = [
'中文',
'英文',
'日文',
'韩文',
'法文',
'德文',
'西班牙文',
'俄文'
];
// 语音服务类型
final Rx<SpeechServiceType> speechServiceType = SpeechServiceType.azure.obs;
final List<Map<String, dynamic>> availableSpeechServices = [
{'type': SpeechServiceType.azure, 'name': 'Azure 语音服务', 'description': '微软云端语音服务,支持高质量语音合成和识别'},
{'type': SpeechServiceType.flutter, 'name': 'Flutter 本地语音服务', 'description': '设备本地语音服务,无需联网即可使用'},
{
'type': SpeechServiceType.azure,
'name': 'azureVoiceService', // Azure 语音服务.tr
'description': 'azureVoiceServiceInfo' // 微软云端语音服务,支持高质量语音合成和识别.tr
},
{
'type': SpeechServiceType.flutter,
'name': 'flutterVoiceService', // Flutter 本地语音服务.tr
'description': 'flutterVoiceServiceInfo' // 设备本地语音服务,无需联网即可使用.tr
},
];
RxString timbreName = 'defaultTimbre'.obs;
//用户信息
static const String userKey = 'user_info';
//final _storage=GetStorage();
//final ImagePicker _picker = ImagePicker();
final Rx<User> userInfo = User.instance.obs; //使用单例实例
@override
void onInit() {
super.onInit();
// 获取系统语言
_getLanguage();
_getTimbre();
// 从存储中加载上次保存的主题设置
final savedDarkMode = _themeService.isSavedDarkMode();
// 设置当前主题状态
isDarkMode.value = savedDarkMode;
// 获取当前语音服务类型
_loadCurrentSpeechServiceType();
//获取用户信息
loadUserInfo();
}
void loadUserInfo() {
final userJson = _storage.read(userKey);
Logger().d('加载用户信息: $userJson');
if (userJson != null) {
userInfo.value = User.instance;
Logger().d('用户信息已加载: ${userInfo.value.toJson()}');
if (userInfo.value.avatar.isNotEmpty) {
Logger().d('开始下载头像');
_downloadAndSaveAvatar(userInfo.value.avatar);
} else {
Logger().d('用户没有头像');
}
} else {
Logger().d('没有找到用户信息');
}
}
Future<void> saveUserInfo() async {
await _storage.write(userKey, userInfo.value.toJson());
}
// 下载并保存远程头像
Future<void> _downloadAndSaveAvatar(String url) async {
try {
Logger().d('开始下载头像: $url');
final dio = Dio(BaseOptions(
headers: {
'accept': '*/*',
'Authorization': _storage.read<String>("logintoken"),
},
));
final response = await dio.get(
url,
options: Options(responseType: ResponseType.bytes),
);
Logger().d('下载状态: ${response.statusCode}');
if (response.statusCode == 200) {
// 保存到应用文档目录
final appDir = await getApplicationDocumentsDirectory();
final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg';
final localPath = '${appDir.path}/$fileName';
// 写入文件
final file = File(localPath);
await file.writeAsBytes(response.data);
// 更新本地头像路径
//localAvatar.value = localPath;
await _storage.write('local_avatar', localPath);
Logger().d('头像已下载到本地: $localPath');
//Get.snackbar('头像', '头像已下载到本地');
}
} catch (e) {
Logger().e('下载头像失败: $e');
}
}
// Future<void> updateUserAvatar() async {
// final XFile? image = await _picker.pickImage(
// source: ImageSource.gallery,
// maxWidth: 800,
// maxHeight: 800,
// imageQuality: 85,
// );
// if (image != null) {
// userInfo.update((user) {
// if (user != null) {
// user.avatar = image.path;
// }
// });
// await saveUserInfo();
// }
// }
Future<void> updateUserName(String name) async {
if (name.trim().isNotEmpty) {
userInfo.update((user) {
if (user != null) {
user.name = name.trim();
}
});
await saveUserInfo();
}
}
// Future<void> updateDeviceName(String deviceName) async {
// userInfo.update((user) {
// if (user != null) {
// user.deviceName = deviceName;
// }
// });
// await saveUserInfo();
// }
// }
void _getLanguage() {
String? languageValue = _storage.read('language');
LanguageModel data = languageList.firstWhere(
(element) => element.value == languageValue,
orElse: () => languageList[0],
);
language.value = data;
}
// 获取音色
void _getTimbre() {
List? timbreList = _storage.read('timbreList');
String? timbre = _storage.read('timbre');
if (timbreList != null && timbre != null) {
Map data = timbreList.firstWhere(
(element) => element['type'] == timbre,
orElse: () => {},
);
if (data.isNotEmpty) {
timbreName.value = data['name'];
}
}
}
void setLanguage(LanguageModel data) {
if (data.value != 'system') {
List languageList = data.value.split('_');
Get.updateLocale(Locale(languageList[0], languageList[1]));
} else {
Get.updateLocale(Get.deviceLocale!);
}
language.value = data;
_storage.write('language', data.value);
Get.back();
}
// 加载当前语音服务类型
void _loadCurrentSpeechServiceType() {
try {
@ -45,19 +241,19 @@ class SettingsController extends GetxController {
// 如果找不到SpeechFactory实例,使用默认值
}
}
// 切换主题
Future<void> toggleTheme() async {
// 切换暗黑模式状态
isDarkMode.value = !isDarkMode.value;
// 更新全局主题
Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
// 保存用户选择的主题设置 - 确保立即写入存储
await _themeService.saveThemeMode(isDarkMode.value);
}
// 设置语音服务类型
void setSpeechServiceType(SpeechServiceType type) {
try {
@ -68,23 +264,64 @@ class SettingsController extends GetxController {
// 处理错误
}
}
void setSourceLanguage(String language) {
sourceLanguage.value = language;
}
void setTargetLanguage(String language) {
targetLanguage.value = language;
}
void swapLanguages() {
final temp = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp;
}
void signOut() {
// 在此实现退出登录逻辑
Get.offAllNamed('/login');
Future<void> signOut() async {
try {
//显示加载提示
// Get.dialog(
// const Center(child: CircularProgressIndicator()),
// barrierDismissible: false,
// );
// 清理所有存储
await _storage.erase();
// 或者逐个清理关键存储项
await _storage.write("logintoken", null);
await _storage.write("user_info", null);
await _storage.write("local_avatar", null);
print('清理存储成功');
// 第三方登录退出
try {
final google = Google();
final facebook = Facebook();
await Future.wait([
google.signOutGoogle(),
facebook.signOutFacebook(),
]);
} catch (e) {
print('第三方登录退出错误: $e');
// 继续执行,不中断退出流程
}
// 关闭加载提示
//Get.back();
// 确保导航到登录页面
if (Get.currentRoute != Routes.login) {
await Get.offAllNamed(Routes.login);
}
Get.snackbar('成功', '已退出登录');
print('退出登录成功');
} catch (e) {
//Get.back(); // 关闭加载提示
Get.snackbar('错误', '退出登录失败,请重试');
print('退出登录错误: $e');
}
}
}
}

11
lib/modules/settings/models/language_model.dart

@ -0,0 +1,11 @@
class LanguageModel {
final String name;
final String alias;
final String value;
LanguageModel({
this.name = '',
this.alias = '',
this.value = '',
});
}

717
lib/modules/settings/views/settings_view.dart

File diff suppressed because it is too large

125
lib/modules/splash/controllers/splash_controller.dart

@ -1,87 +1,116 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart';
class SplashController extends GetxController {
final GetStorage _storage = GetStorage();
// 进度条动画控制
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 版本号
final String version = 'v1.0.0';
// 资源加载状态
final _isResourceLoaded = false.obs;
bool get isResourceLoaded => _isResourceLoaded.value;
// 版本号
final String version = 'v1.0.0';
// 主题服务
final ThemeService _themeService = ThemeService();
@override
void onInit() {
super.onInit();
// 确保页面先渲染,再执行其他操作
WidgetsBinding.instance.addPostFrameCallback((_) {
// 启动进度条动画
_startLoadingAnimation();
// 异步加载应用资源
_loadAppResources();
});
}
// 进度条动画
// 进度条动画(持续2秒)
void _startLoadingAnimation() {
// 模拟0.5秒的加载进度
Timer.periodic(const Duration(milliseconds: 5), (timer) {
const totalDuration = 2000; // 动画总时长2秒
const steps = totalDuration ~/ 50; // 40次更新(50ms间隔)
final stepValue = 1.0 / steps;
Timer.periodic(const Duration(milliseconds: 50), (timer) {
if (_loadingProgress.value < 1.0) {
_loadingProgress.value += 0.01;
_loadingProgress.value += stepValue;
} else {
timer.cancel();
// 当进度条动画完成且资源加载完毕时,跳转到登录页面
if (_isResourceLoaded.value) {
_navigateToLogin();
}
_checkNavigateCondition();
}
});
}
// 异步加载应用资源
Future<void> _loadAppResources() async {
try {
// 在这里可以添加应用初始化需要的资源加载
// 例如:预加载图片、初始化数据库、加载配置等
// 模拟耗时操作
await Future.delayed(const Duration(milliseconds: 100));
// TODO: 在此处添加实际的资源加载代码
// 标记资源加载完成
// 模拟资源加载(如API配置、本地缓存等)
await Future.delayed(const Duration(milliseconds: 1500));
// 实际资源加载代码
// await _loadImages();
// await _initDatabase();
_isResourceLoaded.value = true;
// 如果进度条已经走完,则跳转到登录页面
if (_loadingProgress.value >= 1.0) {
_navigateToLogin();
}
_checkNavigateCondition();
} catch (e) {
// 处理资源加载错误
// 即使加载失败,也应该允许用户继续使用应用
_isResourceLoaded.value = true;
// 记录错误日志
Get.snackbar('错误', '资源加载失败,请检查网络连接');
_isResourceLoaded.value = true; // 允许用户继续使用
print('资源加载错误: $e');
}
}
// 跳转到登录页面
void _navigateToLogin() {
// 添加一个小延迟,确保UI完全渲染
Future.delayed(const Duration(milliseconds: 200), () {
Get.offAllNamed(Routes.login);
});
// 检查跳转条件
void _checkNavigateCondition() {
if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) {
_navigateToLogin();
}
}
// 获取登录Token并跳转
Future<void> _navigateToLogin() async {
bool isLoggedIn = false;
final token = _storage.read('logintoken');
print('getLoginToken:进来了${token}');
if (token != null && token.isNotEmpty) {
isLoggedIn = await getLoginToken();
}
if (isLoggedIn) {
Get.offAllNamed(Routes.pairing);
} else {
Get.offAllNamed(Routes.login); // 跳转到登录页
}
}
Future<bool> getLoginToken() async {
try {
print('getLoginToken:进来了');
//if (User.getname() != "") {
final response = await Api.tokenlogin();
print('getLoginToken: $response');
if (response == null) return false;
if (response['user'] != '') {
print('getLoginToken:进来了');
// 根据API设计调整
final user = User.fromJson(response);
// 存储用户信息(示例)
await _storage.write('user_info', user.toJson());
return true;
}
//return true;
//}
return false;
} catch (e) {
print('Token获取失败: $e');
return false;
}
}
}
}

22
lib/modules/splash/views/splash_view.dart

@ -9,7 +9,7 @@ class SplashView extends GetView<SplashController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF5F5F7),
body: Stack(
@ -21,24 +21,24 @@ class SplashView extends GetView<SplashController> {
children: [
// Logo动画
_buildAnimatedLogo(isDarkMode),
// 应用名称
_buildAppTitle(isDarkMode),
// 标语
_buildSlogan(isDarkMode),
// 加载进度条
SizedBox(height: 40.h),
_buildLoadingBar(isDarkMode),
// 额外信息
SizedBox(height: 16.h),
_buildExtraInfo(isDarkMode),
],
),
),
// 版本号
_buildVersionInfo(isDarkMode),
],
@ -55,7 +55,7 @@ class SplashView extends GetView<SplashController> {
builder: (context, value, child) {
// 确保opacity值不超过1.0
final opacity = value <= 1.0 ? value : 1.0;
return Transform.scale(
scale: value,
child: AnimatedOpacity(
@ -105,7 +105,7 @@ class SplashView extends GetView<SplashController> {
child: Padding(
padding: EdgeInsets.only(top: 24.h),
child: Text(
'DeepVoice',
'DeepSound',
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.bold,
@ -130,7 +130,7 @@ class SplashView extends GetView<SplashController> {
child: Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
'连接世界,岂止声音',
'slogan'.tr, // 连接世界,岂止声音
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -189,7 +189,7 @@ class SplashView extends GetView<SplashController> {
return Opacity(
opacity: value,
child: Text(
'AI语音专家 · 随时随地',
'sloganInfo'.tr, // AI语音专家 · 随时随地
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[400],
@ -217,4 +217,4 @@ class SplashView extends GetView<SplashController> {
),
);
}
}
}

9
lib/modules/timbre/bindings/timbre_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/timbre_controller.dart';
class TimbreBinding extends Bindings {
@override
void dependencies() {
Get.put<TimbreController>(TimbreController());
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save