Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into 重写ble

newdev_shunjiawei
fdp 1 year ago
parent
commit
1568a5ab30
  1. 17
      android/app/build.gradle.kts
  2. 10
      android/app/src/main/AndroidManifest.xml
  3. 133
      android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt
  4. 376
      android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt
  5. 245
      android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt
  6. 150
      android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt
  7. 66
      android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt
  8. 116
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  9. 10
      ios/BroadcastExtension/BroadcastExtension.entitlements
  10. 38
      ios/BroadcastExtension/Info.plist
  11. 216
      ios/BroadcastExtension/SampleHandler.swift
  12. 180
      ios/ReplayKit_Implementation_Summary.md
  13. 142
      ios/ReplayKit_Integration_Guide.md
  14. 10
      ios/Runner/AppDelegate.swift
  15. 341
      ios/Runner/FloatingWindow.swift
  16. 154
      ios/Runner/FloatingWindowPlugin.swift
  17. 5
      ios/Runner/Info.plist
  18. 40
      ios/Runner/OverlayPermissionPlugin.swift
  19. 247
      ios/Runner/ReplayKitManager.swift
  20. 4
      ios/Runner/Runner.entitlements
  21. 8
      lib/core/bindings/initial_binding.dart
  22. 199
      lib/core/services/floating_window_manager.dart
  23. 4
      lib/core/translations/app_translations.dart
  24. 2
      lib/core/translations/language/bg_bg.dart
  25. 2
      lib/core/translations/language/cs_cz.dart
  26. 2
      lib/core/translations/language/da_dk.dart
  27. 2
      lib/core/translations/language/de_de.dart
  28. 2
      lib/core/translations/language/el_gr.dart
  29. 2
      lib/core/translations/language/en_us.dart
  30. 2
      lib/core/translations/language/es_es.dart
  31. 2
      lib/core/translations/language/et_ee.dart
  32. 2
      lib/core/translations/language/fi_fi.dart
  33. 2
      lib/core/translations/language/fr_fr.dart
  34. 2
      lib/core/translations/language/hi_in.dart
  35. 2
      lib/core/translations/language/hr_hr.dart
  36. 2
      lib/core/translations/language/hu_hu.dart
  37. 2
      lib/core/translations/language/id_id.dart
  38. 2
      lib/core/translations/language/is_is.dart
  39. 2
      lib/core/translations/language/it_it.dart
  40. 2
      lib/core/translations/language/ja_jp.dart
  41. 2
      lib/core/translations/language/ko_kr.dart
  42. 2
      lib/core/translations/language/lt_lt.dart
  43. 2
      lib/core/translations/language/lv_lv.dart
  44. 2
      lib/core/translations/language/ms_my.dart
  45. 2
      lib/core/translations/language/nb_no.dart
  46. 2
      lib/core/translations/language/nl_nl.dart
  47. 2
      lib/core/translations/language/pl_pl.dart
  48. 2
      lib/core/translations/language/pt_pt.dart
  49. 2
      lib/core/translations/language/ro_ro.dart
  50. 2
      lib/core/translations/language/ru_ru.dart
  51. 2
      lib/core/translations/language/sk_sk.dart
  52. 2
      lib/core/translations/language/sl_si.dart
  53. 2
      lib/core/translations/language/sr_rs.dart
  54. 2
      lib/core/translations/language/sv_se.dart
  55. 2
      lib/core/translations/language/th_th.dart
  56. 2
      lib/core/translations/language/tl_ph.dart
  57. 2
      lib/core/translations/language/tr_tr.dart
  58. 2
      lib/core/translations/language/uk_ua.dart
  59. 2
      lib/core/translations/language/vi_vn.dart
  60. 4
      lib/core/translations/language/zh_cn.dart
  61. 2
      lib/core/translations/language/zh_hk.dart
  62. 79
      lib/core/utils/overlay_permission_util.dart
  63. 4
      lib/data/models/appconfig.dart
  64. 4
      lib/data/services/bluetooth_manager.dart
  65. 9
      lib/data/services/network/api.dart
  66. 89
      lib/data/services/ota_check_service.dart
  67. 362
      lib/data/services/pay_service.dart
  68. 199
      lib/main.dart
  69. 50
      lib/modules/agent/controllers/agent_controller.dart
  70. 6
      lib/modules/agent/views/message_bubble.dart
  71. 9
      lib/modules/login/controllers/weixinLogin.dart
  72. 65
      lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart
  73. 6
      lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
  74. 2
      lib/modules/ota_upgrade/views/ota_upgrade_popup_view.dart
  75. 2
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  76. 111
      lib/modules/pairing/controllers/pairing_controller.dart
  77. 87
      lib/modules/pairing/views/pairing_view.dart
  78. 9
      lib/modules/payment/bindings/payment_binding.dart
  79. 143
      lib/modules/payment/controllers/payment_controller.dart
  80. 506
      lib/modules/payment/views/payment_view.dart
  81. 2
      lib/modules/permissions/controllers/permissions_controller.dart
  82. 19
      lib/modules/profile/views/profile_view.dart
  83. 8
      lib/modules/settings/controllers/settings_controller.dart
  84. 72
      lib/modules/settings/views/settings_view.dart
  85. 31
      lib/modules/splash/controllers/splash_controller.dart
  86. 115
      lib/modules/splash/views/splash_view.dart
  87. 148
      lib/modules/translation/controllers/translation_controller.dart
  88. 162
      lib/modules/translation/controllers/translation_detail_controller.dart
  89. 46
      lib/modules/translation/controllers/translation_list_controller.dart
  90. 172
      lib/modules/translation/views/dialogs/delete_confirm_dialog.dart
  91. 187
      lib/modules/translation/views/dialogs/translation_long_press_menu.dart
  92. 81
      lib/modules/translation/views/dialogs/translation_mode_selector.dart
  93. 352
      lib/modules/translation/views/translation_detail_view.dart
  94. 358
      lib/modules/translation/views/translation_view.dart
  95. 37
      lib/modules/translation/views/widgets/draggable_floating_button.dart
  96. 9
      lib/routes/app_pages.dart
  97. 3
      lib/routes/app_routes.dart
  98. 407
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  99. 357
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  100. 17
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

17
android/app/build.gradle.kts

@ -61,7 +61,8 @@ android {
manifestPlaceholders.putAll(
mapOf(
"redirectSchemeName" to "spotify-sdk",
"redirectHostName" to "auth"
"redirectHostName" to "auth",
"applicationName" to "com.saitong.voitrans.MyApp" // 新增:声明自定义 Application
)
)
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
@ -105,7 +106,11 @@ android {
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
// signingConfig = signingConfigs.getByName("debug")
// Release 版本只保留 arm64-v8a 和 armeabi-v7a(移除 x86/x86_64)
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}
debug {
isMinifyEnabled = false
@ -115,6 +120,11 @@ android {
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
// Debug 版本保留所有 ABI(包括 x86/x86_64,用于模拟器)
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
}
}
@ -122,6 +132,9 @@ android {
dependencies {
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加 CardView 依赖
implementation("androidx.cardview:cardview:1.0.0")
}
flutter {

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

@ -2,6 +2,7 @@
xmlns:tools="http://schemas.android.com/tools">
<!-- 添加必要的权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 13+ 前台服务特定类型权限 -->
@ -84,8 +85,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
@ -121,6 +121,12 @@
android:value="78e7946dd602778df82c6ae143a88184"/>
<service android:name="com.amap.api.location.APSService"></service>
<!-- 悬浮窗服务 -->
<service
android:name="com.saitong.voitrans.FloatingWindowService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

133
android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt

@ -0,0 +1,133 @@
package com.saitong.voitrans
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class FloatingWindowPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "floating_window")
channel.setMethodCallHandler(this)
// 设置回调通道
FloatingWindowService.methodChannel = channel
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"enableFloatingWindow" -> {
enableFloatingWindow(result)
}
"disableFloatingWindow" -> {
disableFloatingWindow(result)
}
"showFloatingWindow" -> {
showFloatingWindow(result)
}
"hideFloatingWindow" -> {
hideFloatingWindow(result)
}
"updateContent" -> {
updateContent(call, result)
}
"setPosition" -> {
setPosition(call, result)
}
else -> {
result.notImplemented()
}
}
}
private fun enableFloatingWindow(result: Result) {
try {
val intent = Intent(context, FloatingWindowService::class.java)
context.startService(intent)
result.success(true)
} catch (e: Exception) {
result.error("ENABLE_ERROR", "Failed to enable floating window: ${e.message}", null)
}
}
private fun disableFloatingWindow(result: Result) {
try {
val intent = Intent(context, FloatingWindowService::class.java)
context.stopService(intent)
result.success(true)
} catch (e: Exception) {
result.error("DISABLE_ERROR", "Failed to disable floating window: ${e.message}", null)
}
}
private fun showFloatingWindow(result: Result) {
try {
// 服务已经在运行,不需要额外操作
result.success(true)
} catch (e: Exception) {
result.error("SHOW_ERROR", "Failed to show floating window: ${e.message}", null)
}
}
private fun hideFloatingWindow(result: Result) {
try {
FloatingWindowService.serviceInstance?.stopSelf()
result.success(true)
} catch (e: Exception) {
result.error("HIDE_ERROR", "Failed to hide floating window: ${e.message}", null)
}
}
private fun updateContent(call: MethodCall, result: Result) {
try {
val args = call.arguments as? Map<String, Any>
if (args != null) {
val intent = Intent(context, FloatingWindowService::class.java).apply {
action = FloatingWindowService.ACTION_UPDATE_CONTENT
putExtra(FloatingWindowService.EXTRA_SOURCE_TEXT, args["sourceText"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_TRANSLATED_TEXT, args["translatedText"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_SOURCE_LANGUAGE, args["sourceLanguage"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_TARGET_LANGUAGE, args["targetLanguage"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_TIMESTAMP, (args["timestamp"] as? Number)?.toLong() ?: 0L)
putExtra(FloatingWindowService.EXTRA_IS_INTERMEDIATE, args["isIntermediate"] as? Boolean ?: false)
}
context.startService(intent)
}
result.success(true)
} catch (e: Exception) {
result.error("UPDATE_ERROR", "Failed to update content: ${e.message}", null)
}
}
private fun setPosition(call: MethodCall, result: Result) {
try {
val args = call.arguments as? Map<String, Any>
if (args != null) {
val intent = Intent(context, FloatingWindowService::class.java).apply {
action = FloatingWindowService.ACTION_SET_POSITION
putExtra(FloatingWindowService.EXTRA_X, (args["x"] as? Number)?.toInt() ?: 0)
putExtra(FloatingWindowService.EXTRA_Y, (args["y"] as? Number)?.toInt() ?: 0)
}
context.startService(intent)
}
result.success(true)
} catch (e: Exception) {
result.error("POSITION_ERROR", "Failed to set position: ${e.message}", null)
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
FloatingWindowService.methodChannel = null
}
}

376
android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt

@ -0,0 +1,376 @@
package com.saitong.voitrans
import android.app.Service
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.view.*
import android.widget.TextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.FrameLayout
import androidx.cardview.widget.CardView
import io.flutter.plugin.common.MethodChannel
import java.text.SimpleDateFormat
import java.util.*
class FloatingWindowService : Service() {
private var windowManager: WindowManager? = null
private var floatingView: View? = null
private var isMinimized = true
private lateinit var sourceTextView: TextView
private lateinit var translatedTextView: TextView
private lateinit var languageInfoView: TextView
private lateinit var timestampView: TextView
private lateinit var minimizedView: CardView
private lateinit var expandedView: CardView
companion object {
var methodChannel: MethodChannel? = null
var serviceInstance: FloatingWindowService? = null
const val ACTION_UPDATE_CONTENT = "UPDATE_CONTENT"
const val ACTION_SET_POSITION = "SET_POSITION"
const val EXTRA_SOURCE_TEXT = "source_text"
const val EXTRA_TRANSLATED_TEXT = "translated_text"
const val EXTRA_SOURCE_LANGUAGE = "source_language"
const val EXTRA_TARGET_LANGUAGE = "target_language"
const val EXTRA_TIMESTAMP = "timestamp"
const val EXTRA_IS_INTERMEDIATE = "is_intermediate"
const val EXTRA_X = "x"
const val EXTRA_Y = "y"
}
override fun onCreate() {
super.onCreate()
serviceInstance = this
createFloatingWindow()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_UPDATE_CONTENT -> updateContent(intent)
ACTION_SET_POSITION -> setPosition(intent)
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createFloatingWindow() {
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
// 创建布局
floatingView = createFloatingLayout()
// 初始化视图组件
initViews()
// 设置点击事件
setupClickListeners()
// 设置窗口参数
val layoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
format = PixelFormat.TRANSLUCENT
gravity = Gravity.TOP or Gravity.START
x = 100
y = 100
}
windowManager?.addView(floatingView, layoutParams)
// 默认显示最小化状态
updateViewState(true)
}
private fun createFloatingLayout(): View {
val context = this
val density = resources.displayMetrics.density
// 创建根布局
val rootLayout = FrameLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
// 创建最小化视图
val minimizedCard = CardView(context).apply {
id = View.generateViewId()
layoutParams = FrameLayout.LayoutParams(
(60 * density).toInt(),
(60 * density).toInt()
).apply {
setMargins((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
}
radius = 30 * density
cardElevation = 8 * density
setCardBackgroundColor(0xFFE3F2FD.toInt())
}
val minimizedContent = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
val icon = TextView(context).apply {
text = "译"
textSize = 14f
setTextColor(0xFF1976D2.toInt())
gravity = Gravity.CENTER
}
minimizedContent.addView(icon)
minimizedCard.addView(minimizedContent)
// 创建展开视图
val expandedCard = CardView(context).apply {
id = View.generateViewId()
layoutParams = FrameLayout.LayoutParams(
(320 * density).toInt(),
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
}
radius = 16 * density
cardElevation = 12 * density
setCardBackgroundColor(0xFFFFFFFF.toInt())
visibility = View.GONE
}
val expandedContent = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
(16 * density).toInt(),
(16 * density).toInt(),
(16 * density).toInt(),
(16 * density).toInt()
)
}
// 标题栏
val titleLayout = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}
val titleText = TextView(context).apply {
text = "实时翻译"
textSize = 16f
setTextColor(0xFF333333.toInt())
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
}
val closeButton = TextView(context).apply {
id = View.generateViewId()
text = "✕"
textSize = 18f
setTextColor(0xFF666666.toInt())
gravity = Gravity.CENTER
setPadding((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
}
titleLayout.addView(titleText)
titleLayout.addView(closeButton)
// 语言信息
val languageInfo = TextView(context).apply {
id = View.generateViewId()
text = "中文 → 英文"
textSize = 12f
setTextColor(0xFF666666.toInt())
setPadding(0, (8 * density).toInt(), 0, (8 * density).toInt())
}
// 源文本
val sourceText = TextView(context).apply {
id = View.generateViewId()
text = "正在识别..."
textSize = 14f
setTextColor(0xFF333333.toInt())
setPadding(0, (8 * density).toInt(), 0, (4 * density).toInt())
maxLines = 3
}
// 翻译文本
val translatedText = TextView(context).apply {
id = View.generateViewId()
text = "翻译中..."
textSize = 14f
setTextColor(0xFF1976D2.toInt())
setPadding(0, (4 * density).toInt(), 0, (8 * density).toInt())
maxLines = 3
}
// 时间戳
val timestamp = TextView(context).apply {
id = View.generateViewId()
text = ""
textSize = 10f
setTextColor(0xFF999999.toInt())
gravity = Gravity.END
}
expandedContent.addView(titleLayout)
expandedContent.addView(languageInfo)
expandedContent.addView(sourceText)
expandedContent.addView(translatedText)
expandedContent.addView(timestamp)
expandedCard.addView(expandedContent)
rootLayout.addView(minimizedCard)
rootLayout.addView(expandedCard)
// 保存引用
minimizedView = minimizedCard
expandedView = expandedCard
sourceTextView = sourceText
translatedTextView = translatedText
languageInfoView = languageInfo
timestampView = timestamp
return rootLayout
}
private fun initViews() {
// Views are already initialized in createFloatingLayout
}
private fun setupClickListeners() {
// 最小化状态点击展开
minimizedView.setOnClickListener {
isMinimized = false
updateViewState(false)
notifyStateChange("expanded")
}
// 展开状态点击最小化
expandedView.setOnClickListener {
isMinimized = true
updateViewState(true)
notifyStateChange("minimized")
}
// 长按关闭
floatingView!!.setOnLongClickListener {
closeFloatingWindow()
true
}
// 拖拽功能
setupDragListener()
}
private fun setupDragListener() {
var initialX = 0
var initialY = 0
var initialTouchX = 0f
var initialTouchY = 0f
floatingView!!.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
initialX = layoutParams.x
initialY = layoutParams.y
initialTouchX = event.rawX
initialTouchY = event.rawY
true
}
MotionEvent.ACTION_MOVE -> {
val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
layoutParams.x = initialX + (event.rawX - initialTouchX).toInt()
layoutParams.y = initialY + (event.rawY - initialTouchY).toInt()
windowManager?.updateViewLayout(floatingView, layoutParams)
true
}
else -> false
}
}
}
private fun updateViewState(minimized: Boolean) {
if (minimized) {
minimizedView.visibility = View.VISIBLE
expandedView.visibility = View.GONE
} else {
minimizedView.visibility = View.GONE
expandedView.visibility = View.VISIBLE
}
}
private fun updateContent(intent: Intent) {
val sourceText = intent.getStringExtra(EXTRA_SOURCE_TEXT) ?: ""
val translatedText = intent.getStringExtra(EXTRA_TRANSLATED_TEXT) ?: ""
val sourceLanguage = intent.getStringExtra(EXTRA_SOURCE_LANGUAGE) ?: ""
val targetLanguage = intent.getStringExtra(EXTRA_TARGET_LANGUAGE) ?: ""
val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, 0)
val isIntermediate = intent.getBooleanExtra(EXTRA_IS_INTERMEDIATE, false)
// 更新UI
sourceTextView.text = if (sourceText.isNotEmpty()) sourceText else "正在识别..."
translatedTextView.text = if (translatedText.isNotEmpty()) translatedText else "翻译中..."
languageInfoView.text = "$sourceLanguage → $targetLanguage"
if (timestamp > 0) {
val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
timestampView.text = dateFormat.format(Date(timestamp))
}
// 如果是中间结果,可以添加一些视觉提示
if (isIntermediate) {
translatedTextView.alpha = 0.7f
} else {
translatedTextView.alpha = 1.0f
}
}
private fun setPosition(intent: Intent) {
val x = intent.getIntExtra(EXTRA_X, 0)
val y = intent.getIntExtra(EXTRA_Y, 0)
floatingView?.let { view ->
val layoutParams = view.layoutParams as WindowManager.LayoutParams
layoutParams.x = x
layoutParams.y = y
windowManager?.updateViewLayout(view, layoutParams)
}
}
private fun closeFloatingWindow() {
stopSelf()
notifyFloatingWindowClosed()
}
private fun notifyStateChange(state: String) {
methodChannel?.invokeMethod("onFloatingWindowStateChanged", mapOf("state" to state))
}
private fun notifyFloatingWindowClosed() {
methodChannel?.invokeMethod("onFloatingWindowClosed", null)
}
override fun onDestroy() {
super.onDestroy()
floatingView?.let { windowManager?.removeView(it) }
serviceInstance = null
}
}

245
android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt

@ -0,0 +1,245 @@
package com.saitong.voitrans
import android.app.PendingIntent
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.Intent
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.plugin.common.MethodChannel // 添加这行导入
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"
private var pendingDeepLink: Intent? = null
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "🚀 MainActivity onCreate 开始")
super.onCreate(savedInstanceState)
// 检查 Intent 信息
intent?.let {
Log.d(TAG, "Intent action: ${it.action}")
Log.d(TAG, "Intent data: ${it.data}")
Log.d(TAG, "Intent extras: ${it.extras}")
}
// 暂存深链接,等Flutter引擎完全准备好后处理
if (intent?.action == Intent.ACTION_VIEW && intent?.data != null) {
pendingDeepLink = intent
Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成")
}
// 设置日志级别,减少系统级日志
suppressSystemLogs()
Log.d(TAG, "✅ MainActivity onCreate 完成")
}
// 添加 onNewIntent 方法处理深链接
override fun onNewIntent(intent: Intent) {
Log.d(TAG, "🔗 onNewIntent 被调用")
super.onNewIntent(intent)
setIntent(intent) // 更新当前 Intent
// 检查新的 Intent 信息
Log.d(TAG, "New Intent action: ${intent.action}")
Log.d(TAG, "New Intent data: ${intent.data}")
Log.d(TAG, "New Intent extras: ${intent.extras}")
// 处理深链接
handleDeepLink(intent)
}
override fun provideFlutterEngine(context: Context): FlutterEngine? {
Log.d(TAG, "🔍 provideFlutterEngine 被调用")
val engine = FlutterEngineCache.getInstance().get("main_engine")
if (engine == null) {
Log.w(TAG, "❌ FlutterEngine 缓存为空,这可能导致问题")
// 如果缓存为空,让 Flutter 框架创建默认 Engine
return null
}
// 检查 Engine 状态 - 使用更兼容的方式
try {
// 检查 Dart 执行器是否正在运行
val isExecutorRunning = engine.dartExecutor.isExecutingDart
Log.d(TAG, "FlutterEngine Dart 执行状态: $isExecutorRunning")
// 如果需要,可以检查其他状态
if (!isExecutorRunning) {
Log.w(TAG, "⚠️ FlutterEngine Dart 执行器未运行,可能需要重新初始化")
}
} catch (e: Exception) {
Log.w(TAG, "❌ 检查 FlutterEngine 状态时出错", e)
}
Log.d(TAG, "✅ 使用缓存的 FlutterEngine")
return engine
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
Log.d(TAG, "🔧 configureFlutterEngine 开始")
// 检查插件是否已经注册,避免重复注册
val pluginRegistry = flutterEngine.plugins
// 只有在插件未注册时才调用 GeneratedPluginRegistrant
if (!pluginRegistry.has(io.flutter.plugins.pathprovider.PathProviderPlugin::class.java)) {
Log.d(TAG, "📦 注册 Flutter 插件")
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
// 注册悬浮窗相关插件
flutterEngine.plugins.add(FloatingWindowPlugin())
flutterEngine.plugins.add(OverlayPermissionPlugin())
} else {
Log.d(TAG, "⚠️ 插件已注册,跳过重复注册")
}
// Flutter引擎配置完成后,处理待处理的深链接
Handler(Looper.getMainLooper()).postDelayed({
pendingDeepLink?.let { intent ->
Log.d(TAG, "🔗 处理待处理的深链接")
handleDeepLink(intent)
pendingDeepLink = null
}
}, 1000) // 延迟1秒确保Flutter完全准备好
Log.d(TAG, "✅ configureFlutterEngine 完成")
}
// 处理深链接的统一方法
private fun handleDeepLink(intent: Intent?) {
intent?.let {
if (it.action == Intent.ACTION_VIEW && it.data != null) {
val uri = it.data
Log.d(TAG, "🔗 处理深链接: $uri")
try {
Log.d(TAG, "🔗 开始发送深链接到 Flutter")
// 通过 MethodChannel 将深链接信息传递给 Flutter
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
Log.d(TAG, "🔗 获取到 BinaryMessenger")
val channel = MethodChannel(
messenger,
"com.saitong.voitrans/deeplink"
)
val arguments = mapOf(
"url" to uri.toString(),
"scheme" to uri?.scheme,
"host" to uri?.host,
"path" to uri?.path,
"query" to uri?.query,
"queryParameters" to uri?.queryParameterNames?.associateWith {
uri.getQueryParameter(it)
}
)
Log.d(TAG, "🔗 准备发送参数: $arguments")
channel.invokeMethod("handleDeepLink", arguments)
Log.d(TAG, "✅ 深链接信息已发送到 Flutter")
} ?: run {
Log.w(TAG, "❌ BinaryMessenger 为空,无法发送深链接")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 处理深链接时出错", e)
}
} else {
Log.d(TAG, "🔗 Intent 不是深链接或数据为空")
}
}
}
override fun onDestroy() {
super.onDestroy()
}
/**
* 抑制系统级组件的日志输出
*/
private fun suppressSystemLogs() {
// 更全面的系统标签列表
val systemTags = arrayOf(
"MediaCodec",
"CCodec",
"CCodecBufferChannel",
"OpusManager",
"MediaCodecList",
"MediaPlayer",
"AudioTrack",
"AudioManager",
"AudioFlinger",
"AudioService",
"AudioEffect",
"AppOpsManager",
"AudioRecord",
"OpenGLRenderer",
"Choreographer",
"InputMethodManager",
"ViewRootImpl",
"Surface",
"BufferQueueProducer",
"ActivityManager",
"WindowManager",
"SensorManager"
)
// 尝试多种方式设置日志级别
systemTags.forEach { tag ->
try {
// 方式1:标准属性设置
System.setProperty("log.tag.$tag", "ERROR")
// 方式2:尝试使用反射设置(针对某些特殊标签)
setLogLevelViaReflection(tag, Log.ERROR)
} catch (e: Exception) {
// 忽略设置失败的情况
}
}
// 方式3:尝试设置全局日志级别
try {
// 设置更高的阈值,减少verbose和debug日志
System.setProperty("log.tag", "WARN")
} catch (e: Exception) {
// 忽略
}
}
/**
* 通过反射尝试设置日志级别
*/
private fun setLogLevelViaReflection(tag: String, level: Int) {
try {
val logClass = Class.forName("android.util.Log")
val isLoggableMethod = logClass.getDeclaredMethod(
"isLoggable",
String::class.java,
Int::class.javaPrimitiveType
)
isLoggableMethod.isAccessible = true
// 某些系统可能支持直接设置
val setLogLevelMethod = logClass.getDeclaredMethod(
"setLogLevel",
String::class.java,
Int::class.javaPrimitiveType
)
setLogLevelMethod.isAccessible = true
setLogLevelMethod.invoke(null, tag, level)
} catch (e: Exception) {
// 反射方法可能不存在,忽略
}
}
}

150
android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt

@ -0,0 +1,150 @@
package com.saitong.voitrans
import android.app.ActivityManager
import android.content.Context
import android.util.Log
import io.flutter.app.FlutterApplication
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugins.GeneratedPluginRegistrant
import com.saitong.voitrans.FloatingWindowPlugin
import com.saitong.voitrans.OverlayPermissionPlugin
// Application.kt
class MyApp : FlutterApplication() {
companion object {
private const val TAG = "MyApp"
private const val ENGINE_ID = "main_engine"
// 标记 Engine 是否已经执行过 Dart 入口点
@Volatile
private var isDartEntrypointExecuted = false
// 记录应用启动时间,用于判断是否为新启动
@Volatile
private var appStartTime = 0L
}
override fun onCreate() {
super.onCreate()
appStartTime = System.currentTimeMillis()
Log.d(TAG, "MyApp onCreate - 进程启动")
val cache = FlutterEngineCache.getInstance()
val existingEngine = cache.get(ENGINE_ID)
if (existingEngine == null) {
Log.d(TAG, "创建新的 FlutterEngine")
createAndCacheEngine(cache)
} else {
Log.d(TAG, "FlutterEngine 已存在,检查状态")
// 检查现有 Engine 的状态
try {
val isExecuting = existingEngine.dartExecutor.isExecutingDart
Log.d(TAG, "Engine Dart执行状态: $isExecuting")
if (!isExecuting && isMainProcess()) {
Log.d(TAG, "Engine 存在但Dart未执行,重新执行")
existingEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
isDartEntrypointExecuted = true
}
} catch (e: Exception) {
Log.e(TAG, "检查或重新执行 Dart 入口点失败", e)
// 如果失败,重新创建 Engine
cache.remove(ENGINE_ID)
createAndCacheEngine(cache)
}
}
}
private fun createAndCacheEngine(cache: FlutterEngineCache) {
// 在主进程中总是创建并执行Dart入口点
if (isMainProcess()) {
Log.d(TAG, "✅ 在主进程创建 FlutterEngine 并执行 Dart")
val flutterEngine = FlutterEngine(this)
// 注册插件(只在这里注册一次)
Log.d(TAG, "📦 注册 Flutter 插件")
GeneratedPluginRegistrant.registerWith(flutterEngine)
// 注册悬浮窗相关插件
flutterEngine.plugins.add(FloatingWindowPlugin())
flutterEngine.plugins.add(OverlayPermissionPlugin())
try {
Log.d(TAG, "🚀 开始执行 Dart 入口点")
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
isDartEntrypointExecuted = true
cache.put(ENGINE_ID, flutterEngine)
Log.d(TAG, "✅ FlutterEngine 创建并缓存成功")
} catch (e: Exception) {
Log.e(TAG, "❌ 执行 Dart 入口点失败", e)
// 如果执行失败,仍然缓存 Engine,但标记为未执行
isDartEntrypointExecuted = false
cache.put(ENGINE_ID, flutterEngine)
}
} else {
Log.d(TAG, "⚠️ 在非主进程,仅创建 FlutterEngine")
// 非主进程只创建 Engine,不执行 Dart 入口点
val flutterEngine = FlutterEngine(this)
cache.put(ENGINE_ID, flutterEngine)
}
}
private fun isMainProcess(): Boolean {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processes = am.runningAppProcesses ?: return true
val mainProcessName = packageName
val myPid = android.os.Process.myPid()
for (process in processes) {
if (process.pid == myPid) {
return process.processName == mainProcessName
}
}
return true
}
private fun isAppInForeground(): Boolean {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processes = am.runningAppProcesses ?: return false
val pkg = packageName
for (p in processes) {
if (p.processName == pkg &&
p.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true
}
}
return false
}
private fun isAppBeingLaunched(): Boolean {
// 改进的启动检测逻辑
val currentTime = System.currentTimeMillis()
val isRecentStart = (currentTime - appStartTime) < 3000 // 3秒内认为是新启动
if (isRecentStart) {
Log.d(TAG, "检测到应用新启动")
return true
}
// 备用检测方法(兼容旧版本)
return try {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val tasks = am.getRunningTasks(1)
val result = tasks.isNotEmpty() && tasks[0].topActivity?.packageName == packageName
Log.d(TAG, "通过 RunningTasks 检测启动状态: $result")
result
} catch (e: Exception) {
Log.w(TAG, "无法获取 RunningTasks,可能缺少权限", e)
// 如果无法获取任务信息,默认认为是启动状态
true
}
}
}

66
android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt

@ -0,0 +1,66 @@
package com.saitong.voitrans
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class OverlayPermissionPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "overlay_permission")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"hasOverlayPermission" -> {
result.success(hasOverlayPermission())
}
"requestOverlayPermission" -> {
requestOverlayPermission(result)
}
else -> {
result.notImplemented()
}
}
}
private fun hasOverlayPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(context)
} else {
true
}
}
private fun requestOverlayPermission(result: Result) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
data = Uri.parse("package:${context.packageName}")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
result.success(true)
} else {
result.success(true)
}
} catch (e: Exception) {
result.error("REQUEST_ERROR", "Failed to request overlay permission: ${e.message}", null)
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

116
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -1,116 +0,0 @@
package com.saitong.voitrans
import android.app.PendingIntent
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.Intent
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置日志级别,减少系统级日志
suppressSystemLogs()
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 注册标准Flutter插件
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
override fun onDestroy() {
super.onDestroy()
}
/**
* 抑制系统级组件的日志输出
*/
private fun suppressSystemLogs() {
// 更全面的系统标签列表
val systemTags = arrayOf(
"MediaCodec",
"CCodec",
"CCodecBufferChannel",
"OpusManager",
"MediaCodecList",
"MediaPlayer",
"AudioTrack",
"AudioManager",
"AudioFlinger",
"AudioService",
"AudioEffect",
"AppOpsManager",
"AudioRecord",
"OpenGLRenderer",
"Choreographer",
"InputMethodManager",
"ViewRootImpl",
"Surface",
"BufferQueueProducer",
"ActivityManager",
"WindowManager",
"SensorManager"
)
// 尝试多种方式设置日志级别
systemTags.forEach { tag ->
try {
// 方式1:标准属性设置
System.setProperty("log.tag.$tag", "ERROR")
// 方式2:尝试使用反射设置(针对某些特殊标签)
setLogLevelViaReflection(tag, Log.ERROR)
} catch (e: Exception) {
// 忽略设置失败的情况
}
}
// 方式3:尝试设置全局日志级别
try {
// 设置更高的阈值,减少verbose和debug日志
System.setProperty("log.tag", "WARN")
} catch (e: Exception) {
// 忽略
}
}
/**
* 通过反射尝试设置日志级别
*/
private fun setLogLevelViaReflection(tag: String, level: Int) {
try {
val logClass = Class.forName("android.util.Log")
val isLoggableMethod = logClass.getDeclaredMethod(
"isLoggable",
String::class.java,
Int::class.javaPrimitiveType
)
isLoggableMethod.isAccessible = true
// 某些系统可能支持直接设置
val setLogLevelMethod = logClass.getDeclaredMethod(
"setLogLevel",
String::class.java,
Int::class.javaPrimitiveType
)
setLogLevelMethod.isAccessible = true
setLogLevelMethod.invoke(null, tag, level)
} catch (e: Exception) {
// 反射方法可能不存在,忽略
}
}
}

10
ios/BroadcastExtension/BroadcastExtension.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>com.apple.security.application-groups</key>
<array>
<string>group.com.saitong.voitrans.shared</string>
</array>
</dict>
</plist>

38
ios/BroadcastExtension/Info.plist

@ -0,0 +1,38 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Voitrans 字幕悬浮窗</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.broadcast-services-upload</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SampleHandler</string>
<key>RPBroadcastProcessMode</key>
<string>RPBroadcastProcessModeSampleBuffer</string>
</dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

216
ios/BroadcastExtension/SampleHandler.swift

@ -0,0 +1,216 @@
import ReplayKit
import UIKit
import AVFoundation
/// Broadcast Upload Extension 的核心处理类
/// 负责接收屏幕内容并叠加字幕悬浮窗
class SampleHandler: RPBroadcastSampleHandler {
// MARK: - 属性
private var overlayWindow: UIWindow?
private var subtitleLabel: UILabel?
private var containerView: UIView?
private let appGroupIdentifier = "group.com.saitong.voitrans.shared"
// MARK: - 生命周期方法
override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
print("🎬 ReplayKit 广播开始")
setupOverlayWindow()
startMonitoringSubtitleUpdates()
}
override func broadcastPaused() {
print("⏸️ ReplayKit 广播暂停")
hideOverlay()
}
override func broadcastResumed() {
print("▶️ ReplayKit 广播恢复")
showOverlay()
}
override func broadcastFinished() {
print("🛑 ReplayKit 广播结束")
cleanupOverlay()
}
// MARK: - 样本处理方法
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case RPSampleBufferType.video:
// 处理视频帧 - 这里我们主要用于触发悬浮窗显示
break
case RPSampleBufferType.audioApp:
// 处理应用音频
break
case RPSampleBufferType.audioMic:
// 处理麦克风音频
break
@unknown default:
break
}
}
// MARK: - 悬浮窗设置
/// 设置悬浮窗口
private func setupOverlayWindow() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// 创建悬浮窗口
self.overlayWindow = UIWindow(frame: UIScreen.main.bounds)
self.overlayWindow?.windowLevel = UIWindow.Level.alert + 1000
self.overlayWindow?.backgroundColor = UIColor.clear
self.overlayWindow?.isHidden = false
// 创建容器视图
self.containerView = UIView()
self.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
self.containerView?.layer.cornerRadius = 12
self.containerView?.layer.masksToBounds = true
self.containerView?.translatesAutoresizingMaskIntoConstraints = false
// 创建字幕标签
self.subtitleLabel = UILabel()
self.subtitleLabel?.textColor = .white
self.subtitleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
self.subtitleLabel?.numberOfLines = 0
self.subtitleLabel?.textAlignment = .center
self.subtitleLabel?.translatesAutoresizingMaskIntoConstraints = false
self.subtitleLabel?.text = "等待翻译内容..."
// 添加视图层次
guard let containerView = self.containerView,
let subtitleLabel = self.subtitleLabel,
let overlayWindow = self.overlayWindow else { return }
containerView.addSubview(subtitleLabel)
overlayWindow.addSubview(containerView)
// 设置约束
NSLayoutConstraint.activate([
// 容器视图约束 - 位于屏幕顶部
containerView.topAnchor.constraint(equalTo: overlayWindow.safeAreaLayoutGuide.topAnchor, constant: 20),
containerView.leadingAnchor.constraint(greaterThanOrEqualTo: overlayWindow.leadingAnchor, constant: 20),
containerView.trailingAnchor.constraint(lessThanOrEqualTo: overlayWindow.trailingAnchor, constant: -20),
containerView.centerXAnchor.constraint(equalTo: overlayWindow.centerXAnchor),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60),
// 字幕标签约束
subtitleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
subtitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
subtitleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -12)
])
print("✅ 悬浮窗口设置完成")
}
}
/// 显示悬浮窗
private func showOverlay() {
DispatchQueue.main.async { [weak self] in
self?.overlayWindow?.isHidden = false
self?.overlayWindow?.alpha = 1.0
}
}
/// 隐藏悬浮窗
private func hideOverlay() {
DispatchQueue.main.async { [weak self] in
UIView.animate(withDuration: 0.3) {
self?.overlayWindow?.alpha = 0.0
} completion: { _ in
self?.overlayWindow?.isHidden = true
}
}
}
/// 清理悬浮窗
private func cleanupOverlay() {
DispatchQueue.main.async { [weak self] in
self?.overlayWindow?.isHidden = true
self?.overlayWindow = nil
self?.subtitleLabel = nil
self?.containerView = nil
}
}
// MARK: - 字幕更新监听
/// 开始监听字幕更新
private func startMonitoringSubtitleUpdates() {
// 使用 UserDefaults 监听字幕更新
if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) {
// 设置初始值监听
userDefaults.addObserver(self, forKeyPath: "subtitle_text", options: [.new], context: nil)
userDefaults.addObserver(self, forKeyPath: "subtitle_source_language", options: [.new], context: nil)
userDefaults.addObserver(self, forKeyPath: "subtitle_target_language", options: [.new], context: nil)
print("✅ 开始监听字幕更新")
}
}
/// KVO 监听回调
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "subtitle_text" || keyPath == "subtitle_source_language" || keyPath == "subtitle_target_language" {
updateSubtitleDisplay()
}
}
/// 更新字幕显示
private func updateSubtitleDisplay() {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
let subtitleText = userDefaults.string(forKey: "subtitle_text") ?? ""
let sourceLanguage = userDefaults.string(forKey: "subtitle_source_language") ?? ""
let targetLanguage = userDefaults.string(forKey: "subtitle_target_language") ?? ""
let isIntermediate = userDefaults.bool(forKey: "subtitle_is_intermediate")
DispatchQueue.main.async { [weak self] in
guard let self = self, let subtitleLabel = self.subtitleLabel else { return }
if subtitleText.isEmpty {
subtitleLabel.text = "等待翻译内容..."
subtitleLabel.alpha = 0.6
} else {
// 构建显示文本
var displayText = subtitleText
if !sourceLanguage.isEmpty && !targetLanguage.isEmpty {
displayText = "\(sourceLanguage) → \(targetLanguage)\n\(subtitleText)"
}
subtitleLabel.text = displayText
subtitleLabel.alpha = isIntermediate ? 0.8 : 1.0
// 添加淡入动画
UIView.animate(withDuration: 0.2) {
self.containerView?.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
} completion: { _ in
UIView.animate(withDuration: 0.2) {
self.containerView?.transform = .identity
}
}
}
print("📝 字幕更新: \(subtitleText)")
}
}
// MARK: - 清理
deinit {
// 移除 KVO 监听
if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) {
userDefaults.removeObserver(self, forKeyPath: "subtitle_text")
userDefaults.removeObserver(self, forKeyPath: "subtitle_source_language")
userDefaults.removeObserver(self, forKeyPath: "subtitle_target_language")
}
cleanupOverlay()
print("🗑️ SampleHandler 已清理")
}
}

180
ios/ReplayKit_Implementation_Summary.md

@ -0,0 +1,180 @@
# iOS ReplayKit 悬浮窗实现总结
## 🎯 实现目标
✅ **已完成**:使用 ReplayKit + Broadcast Upload Extension 实现 iOS 全局字幕悬浮窗功能,与 Android 版本保持功能一致。
## 📁 文件结构
### 主应用文件
```
ios/Runner/
├── FloatingWindowPlugin.swift # Flutter 平台通道插件
├── ReplayKitManager.swift # ReplayKit 管理器
├── OverlayPermissionPlugin.swift # 权限管理插件(兼容性)
├── AppDelegate.swift # 应用委托(已更新)
└── Runner.entitlements # 主应用权限配置
```
### Extension 文件
```
ios/BroadcastExtension/
├── SampleHandler.swift # 广播处理器(核心实现)
├── Info.plist # Extension 配置
└── BroadcastExtension.entitlements # Extension 权限配置
```
### 配置和文档
```
ios/
├── ReplayKit_Integration_Guide.md # Xcode 集成指南
└── ReplayKit_Implementation_Summary.md # 实现总结(本文件)
```
## 🔧 核心技术实现
### 1. ReplayKit 广播控制 (`ReplayKitManager.swift`)
- **功能**:管理屏幕广播的启动、停止和状态监控
- **关键方法**:
- `startBroadcast()`: 启动 ReplayKit 广播
- `stopBroadcast()`: 停止广播
- `updateSubtitle()`: 更新字幕内容
- **数据通信**:使用 App Groups 与 Extension 共享数据
### 2. 广播处理器 (`SampleHandler.swift`)
- **功能**:接收屏幕内容并显示悬浮窗字幕
- **关键特性**:
- 创建全局悬浮窗口 (`UIWindow`)
- 实时监听字幕更新 (KVO + UserDefaults)
- 自适应字幕显示和动画效果
- **显示位置**:屏幕顶部中央,半透明黑色背景
### 3. Flutter 接口 (`FloatingWindowPlugin.swift`)
- **功能**:提供与 Android 一致的 Flutter 调用接口
- **方法映射**:
- `enableFloatingWindow` → 启动 ReplayKit 广播
- `disableFloatingWindow` → 停止广播
- `updateContent` → 更新字幕内容
- `setPosition` → 兼容性方法(位置固定)
## 📱 用户体验流程
### 启动流程
1. 用户在翻译界面点击"悬浮窗"按钮
2. 系统弹出"开始直播屏幕"选择界面
3. 用户选择 "Voitrans 字幕悬浮窗" 选项
4. 系统开始屏幕录制,状态栏显示红色录制指示器
5. 悬浮窗出现在屏幕顶部,显示"等待翻译内容..."
### 使用流程
1. 应用进行语音识别和翻译
2. 实时更新悬浮窗中的字幕内容
3. 支持源语言和目标语言显示
4. 中间结果和最终结果有不同的透明度显示
5. 用户可以在任何应用中看到翻译字幕
### 停止流程
1. 用户再次点击"悬浮窗"按钮或停止翻译
2. 应用调用停止广播方法
3. 悬浮窗消失,录制指示器消失
## 🔐 权限和配置
### App Groups 配置
- **标识符**:`group.com.saitong.voitrans.shared`
- **用途**:主应用与 Extension 之间的数据共享
- **数据键**:
- `subtitle_text`: 字幕文本
- `subtitle_source_language`: 源语言
- `subtitle_target_language`: 目标语言
- `subtitle_is_intermediate`: 是否为中间结果
### 系统权限
- **屏幕录制权限**:用户首次使用时系统自动请求
- **麦克风权限**:如果需要录制音频(可选)
- **摄像头权限**:ReplayKit 框架要求(实际不使用)
## 🎨 UI 设计特点
### 悬浮窗样式
- **位置**:屏幕顶部中央,距离安全区域 20pt
- **背景**:黑色半透明 (alpha: 0.8)
- **圆角**:12pt 圆角矩形
- **字体**:系统字体 16pt,白色文字
- **布局**:垂直布局,支持多行文本
### 动画效果
- **出现动画**:淡入效果 (0.3s)
- **更新动画**:轻微缩放效果 (1.05x → 1.0x)
- **状态指示**:中间结果透明度 0.8,最终结果透明度 1.0
## 🔄 与 Android 版本对比
### 功能一致性
| 功能 | Android | iOS (ReplayKit) | 状态 |
|------|---------|-----------------|------|
| 全局悬浮窗 | ✅ 系统悬浮窗 | ✅ ReplayKit 悬浮窗 | 一致 |
| 实时字幕更新 | ✅ | ✅ | 一致 |
| 拖拽移动 | ✅ | ❌ 位置固定 | 差异 |
| 最小化/展开 | ✅ | ❌ 固定样式 | 差异 |
| 权限管理 | ✅ 悬浮窗权限 | ✅ 录制权限 | 一致 |
### Flutter 接口一致性
- ✅ 所有方法签名保持一致
- ✅ 回调事件保持一致
- ✅ 数据模型保持一致
- ✅ 错误处理保持一致
## 🚀 部署和发布
### 开发环境要求
- Xcode 14.0+
- iOS 16.0+ (ReplayKit 2 要求)
- 有效的 Apple Developer 账号
- App Groups 权限配置
### App Store 审核要点
- ✅ 使用官方 ReplayKit API,符合审核规范
- ✅ 明确的用户权限请求和说明
- ✅ 合理的功能用途(翻译字幕显示)
- ✅ 不涉及隐私数据收集
### 发布注意事项
1. 确保 Extension 的 Bundle ID 正确配置
2. App Groups 在 Developer Portal 中正确设置
3. 两个 target 使用相同的签名证书
4. 测试在不同 iOS 版本上的兼容性
## 📊 技术优势
### 相比应用内悬浮窗
- ✅ 真正的全局显示能力
- ✅ 符合 iOS 平台规范
- ✅ 通过 App Store 审核
- ✅ 系统级权限管理
### 相比其他方案
- ✅ 无需越狱或私有 API
- ✅ 稳定可靠的系统支持
- ✅ 良好的用户体验
- ✅ 与现有架构无缝集成
## 🔮 未来扩展
### 可能的改进
1. **自定义样式**:支持更多字幕样式选项
2. **位置调整**:探索在录制流中实现位置调整
3. **多语言支持**:优化多语言字幕显示
4. **性能优化**:减少内存占用和电池消耗
### 技术演进
- 关注 iOS 新版本的 ReplayKit 功能更新
- 考虑集成 Live Activities (iOS 16+) 作为补充方案
- 探索 WidgetKit 在锁屏界面的字幕显示
---
**实现状态**: ✅ 完成
**测试状态**: ✅ 基础功能测试通过
**文档状态**: ✅ 完整的集成和使用文档
**发布准备**: ⏳ 需要 Xcode 项目配置

142
ios/ReplayKit_Integration_Guide.md

@ -0,0 +1,142 @@
# iOS ReplayKit 悬浮窗集成指南
## 📋 概述
本指南说明如何在 Xcode 中配置 ReplayKit Broadcast Upload Extension 以实现全局字幕悬浮窗功能。
## 🔧 Xcode 项目配置步骤
### 1. 添加 Broadcast Upload Extension Target
1. 在 Xcode 中打开 `ios/Runner.xcworkspace`
2. 选择项目根节点 → 点击 "+" 添加新 Target
3. 选择 "Broadcast Upload Extension"
4. 配置信息:
- Product Name: `BroadcastExtension`
- Bundle Identifier: `com.saitong.voitrans.BroadcastExtension`
- Language: Swift
- 确保 "Include UI Extension" 未选中
### 2. 配置 Extension Target
#### 2.1 替换默认文件
- 删除自动生成的 `SampleHandler.swift`
- 将项目中的 `ios/BroadcastExtension/SampleHandler.swift` 复制到 Extension target
- 将项目中的 `ios/BroadcastExtension/Info.plist` 替换 Extension 的 Info.plist
#### 2.2 添加 Entitlements
- 在 Extension target 的 Build Settings 中设置 Code Signing Entitlements
- 指向 `ios/BroadcastExtension/BroadcastExtension.entitlements`
#### 2.3 配置 Build Settings
- iOS Deployment Target: 16.0 (与主应用保持一致)
- Swift Language Version: Swift 5
- Enable Bitcode: No
### 3. 配置 App Groups
#### 3.1 在 Apple Developer Portal 中:
1. 创建 App Group: `group.com.saitong.voitrans.shared`
2. 将主应用和 Extension 都添加到此 App Group
#### 3.2 在 Xcode 中:
1. 主应用 Target → Signing & Capabilities → 添加 "App Groups" capability
2. Extension Target → Signing & Capabilities → 添加 "App Groups" capability
3. 两个 target 都勾选 `group.com.saitong.voitrans.shared`
### 4. 更新主应用配置
#### 4.1 添加 ReplayKit 框架
- 主应用 Target → Build Phases → Link Binary With Libraries
- 添加 `ReplayKit.framework`
#### 4.2 更新 Info.plist
在主应用的 Info.plist 中添加:
```xml
<key>NSCameraUsageDescription</key>
<string>需要摄像头权限以支持屏幕录制功能</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限以支持音频录制功能</string>
```
## 📱 使用说明
### Flutter 调用方式
```dart
// 启动悬浮窗(会弹出系统录屏选择界面)
await floatingWindowManager.enableFloatingWindow();
// 更新字幕内容
await floatingWindowManager.updateFloatingWindowContent(
FloatingWindowData(
sourceText: '你好',
translatedText: 'Hello',
sourceLanguage: '中文',
targetLanguage: '英语',
timestamp: DateTime.now(),
isIntermediate: false,
),
);
// 停止悬浮窗
await floatingWindowManager.disableFloatingWindow();
```
### 用户操作流程
1. 用户点击"启用悬浮窗"按钮
2. 系统弹出"开始直播屏幕"选择界面
3. 用户选择 "Voitrans 字幕悬浮窗" 选项
4. 系统开始屏幕录制,悬浮窗显示在屏幕顶部
5. 应用实时更新字幕内容
6. 用户可以在任何应用中看到翻译字幕
## ⚠️ 注意事项
### 开发阶段
- 确保两个 target 使用相同的开发者账号签名
- App Groups 必须在 Apple Developer Portal 中正确配置
- Extension 的 Bundle ID 必须是主应用的子域名
### 发布阶段
- Extension 会随主应用一起打包发布
- 用户首次使用时需要授权屏幕录制权限
- 符合 App Store 审核规范,因为使用的是官方 ReplayKit API
### 用户体验
- 录制期间状态栏会显示红色录制指示器
- 悬浮窗位置固定在屏幕顶部中央
- 支持实时字幕更新和语言切换
- 可以通过停止录制来关闭悬浮窗
## 🔍 故障排除
### 常见问题
1. **Extension 无法启动**
- 检查 Bundle ID 配置
- 确认 App Groups 权限配置正确
- 验证代码签名设置
2. **字幕不更新**
- 检查 App Groups 数据同步
- 确认 UserDefaults 键名一致
- 查看 Extension 日志输出
3. **编译错误**
- 确保 iOS Deployment Target 一致
- 检查 Swift 版本设置
- 验证框架依赖配置
### 调试方法
1. 使用 Xcode 的 Device Console 查看 Extension 日志
2. 在 SampleHandler 中添加 print 语句进行调试
3. 检查 App Groups 中的数据是否正确写入
## 📚 相关文档
- [Apple ReplayKit Documentation](https://developer.apple.com/documentation/replaykit)
- [Broadcast Upload Extension Guide](https://developer.apple.com/documentation/replaykit/rpbroadcastsamplehandler)
- [App Groups Documentation](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups)

10
ios/Runner/AppDelegate.swift

@ -8,6 +8,16 @@ import UIKit
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// 注册悬浮窗相关插件
if let registrar = self.registrar(forPlugin: "FloatingWindowPlugin") {
FloatingWindowPlugin.register(with: registrar)
}
if let registrar = self.registrar(forPlugin: "OverlayPermissionPlugin") {
OverlayPermissionPlugin.register(with: registrar)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

341
ios/Runner/FloatingWindow.swift

@ -0,0 +1,341 @@
import UIKit
/// 悬浮窗实现类
/// 使用 UIWindow 创建应用内悬浮窗口
class FloatingWindow: UIWindow {
// MARK: - 属性
private weak var delegate: FloatingWindowDelegate?
private var floatingViewController: FloatingViewController?
private var isMinimized = true
private var lastPosition = CGPoint(x: 100, y: 100)
// MARK: - 初始化
init(delegate: FloatingWindowDelegate) {
self.delegate = delegate
// 创建窗口
super.init(frame: CGRect(x: 100, y: 100, width: 300, height: 120))
setupWindow()
setupViewController()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - 私有方法
/// 设置窗口属性
private func setupWindow() {
// 设置窗口层级,确保在最顶层显示
windowLevel = UIWindow.Level.alert + 1
backgroundColor = UIColor.clear
// 设置窗口为可见但不成为主窗口
isHidden = true
// 设置圆角
layer.cornerRadius = 12
layer.masksToBounds = true
}
/// 设置视图控制器
private func setupViewController() {
floatingViewController = FloatingViewController()
floatingViewController?.delegate = self
rootViewController = floatingViewController
}
// MARK: - 公共方法
/// 显示悬浮窗
func show() {
isHidden = false
makeKeyAndVisible()
// 添加显示动画
alpha = 0
UIView.animate(withDuration: 0.3) {
self.alpha = 1
}
delegate?.floatingWindowDidChangeState(isMinimized ? "minimized" : "expanded")
}
/// 隐藏悬浮窗
func hide() {
UIView.animate(withDuration: 0.3, animations: {
self.alpha = 0
}) { _ in
self.isHidden = true
self.resignKey()
}
delegate?.floatingWindowDidChangeState("hidden")
}
/// 更新内容
func updateContent(_ data: FloatingWindowContentData) {
floatingViewController?.updateContent(data)
}
/// 设置位置
func setPosition(x: Double, y: Double) {
let newPosition = CGPoint(x: x, y: y)
lastPosition = newPosition
UIView.animate(withDuration: 0.3) {
self.frame.origin = newPosition
}
}
/// 切换最小化/展开状态
func toggleMinimized() {
isMinimized.toggle()
let newSize: CGSize
if isMinimized {
newSize = CGSize(width: 120, height: 60)
} else {
newSize = CGSize(width: 300, height: 120)
}
UIView.animate(withDuration: 0.3) {
self.frame.size = newSize
}
floatingViewController?.setMinimized(isMinimized)
delegate?.floatingWindowDidChangeState(isMinimized ? "minimized" : "expanded")
}
}
// MARK: - FloatingViewControllerDelegate
extension FloatingWindow: FloatingViewControllerDelegate {
func floatingViewControllerDidTap() {
toggleMinimized()
delegate?.floatingWindowDidClick()
}
func floatingViewControllerDidPan(translation: CGPoint) {
let newOrigin = CGPoint(
x: lastPosition.x + translation.x,
y: lastPosition.y + translation.y
)
// 限制在屏幕范围内
let screenBounds = UIScreen.main.bounds
let constrainedX = max(0, min(newOrigin.x, screenBounds.width - frame.width))
let constrainedY = max(0, min(newOrigin.y, screenBounds.height - frame.height))
frame.origin = CGPoint(x: constrainedX, y: constrainedY)
}
func floatingViewControllerDidEndPan() {
lastPosition = frame.origin
}
func floatingViewControllerDidRequestClose() {
hide()
delegate?.floatingWindowDidClose()
}
}
/// 悬浮窗视图控制器
private class FloatingViewController: UIViewController {
// MARK: - 属性
weak var delegate: FloatingViewControllerDelegate?
// UI 组件
private let containerView = UIView()
private let sourceTextLabel = UILabel()
private let translatedTextLabel = UILabel()
private let languageInfoLabel = UILabel()
private let timestampLabel = UILabel()
private let closeButton = UIButton(type: .system)
// 手势识别器
private var panGestureRecognizer: UIPanGestureRecognizer!
private var tapGestureRecognizer: UITapGestureRecognizer!
private var isMinimized = true
// MARK: - 生命周期
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupGestures()
}
// MARK: - 私有方法
/// 设置 UI
private func setupUI() {
view.backgroundColor = UIColor.clear
// 容器视图
containerView.backgroundColor = UIColor.black.withAlphaComponent(0.8)
containerView.layer.cornerRadius = 12
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
// 源文本标签
sourceTextLabel.textColor = .white
sourceTextLabel.font = UIFont.systemFont(ofSize: 14)
sourceTextLabel.numberOfLines = 2
sourceTextLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(sourceTextLabel)
// 翻译文本标签
translatedTextLabel.textColor = .systemBlue
translatedTextLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
translatedTextLabel.numberOfLines = 2
translatedTextLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(translatedTextLabel)
// 语言信息标签
languageInfoLabel.textColor = .lightGray
languageInfoLabel.font = UIFont.systemFont(ofSize: 10)
languageInfoLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(languageInfoLabel)
// 时间戳标签
timestampLabel.textColor = .lightGray
timestampLabel.font = UIFont.systemFont(ofSize: 10)
timestampLabel.textAlignment = .right
timestampLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(timestampLabel)
// 关闭按钮
closeButton.setTitle("×", for: .normal)
closeButton.setTitleColor(.white, for: .normal)
closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold)
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
containerView.addSubview(closeButton)
setupConstraints()
}
/// 设置约束
private func setupConstraints() {
NSLayoutConstraint.activate([
// 容器视图
containerView.topAnchor.constraint(equalTo: view.topAnchor),
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// 关闭按钮
closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 4),
closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -4),
closeButton.widthAnchor.constraint(equalToConstant: 20),
closeButton.heightAnchor.constraint(equalToConstant: 20),
// 源文本标签
sourceTextLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8),
sourceTextLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
sourceTextLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -4),
// 翻译文本标签
translatedTextLabel.topAnchor.constraint(equalTo: sourceTextLabel.bottomAnchor, constant: 4),
translatedTextLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
translatedTextLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
// 语言信息标签
languageInfoLabel.topAnchor.constraint(equalTo: translatedTextLabel.bottomAnchor, constant: 4),
languageInfoLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
languageInfoLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4),
// 时间戳标签
timestampLabel.topAnchor.constraint(equalTo: translatedTextLabel.bottomAnchor, constant: 4),
timestampLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
timestampLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4)
])
}
/// 设置手势识别器
private func setupGestures() {
// 拖拽手势
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
view.addGestureRecognizer(panGestureRecognizer)
// 点击手势
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
view.addGestureRecognizer(tapGestureRecognizer)
}
// MARK: - 手势处理
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view.superview)
switch gesture.state {
case .changed:
delegate?.floatingViewControllerDidPan(translation: translation)
case .ended, .cancelled:
delegate?.floatingViewControllerDidEndPan()
default:
break
}
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
delegate?.floatingViewControllerDidTap()
}
@objc private func closeButtonTapped() {
delegate?.floatingViewControllerDidRequestClose()
}
// MARK: - 公共方法
/// 更新内容
func updateContent(_ data: FloatingWindowContentData) {
sourceTextLabel.text = data.sourceText.isEmpty ? "正在识别..." : data.sourceText
translatedTextLabel.text = data.translatedText.isEmpty ? "翻译中..." : data.translatedText
languageInfoLabel.text = "\(data.sourceLanguage) → \(data.targetLanguage)"
// 格式化时间戳
if data.timestamp > 0 {
let date = Date(timeIntervalSince1970: TimeInterval(data.timestamp / 1000))
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
timestampLabel.text = formatter.string(from: date)
}
// 中间结果的视觉提示
translatedTextLabel.alpha = data.isIntermediate ? 0.7 : 1.0
}
/// 设置最小化状态
func setMinimized(_ minimized: Bool) {
isMinimized = minimized
// 在最小化状态下隐藏部分元素
sourceTextLabel.isHidden = minimized
languageInfoLabel.isHidden = minimized
timestampLabel.isHidden = minimized
closeButton.isHidden = minimized
// 调整翻译文本标签的约束
if minimized {
translatedTextLabel.numberOfLines = 1
translatedTextLabel.font = UIFont.systemFont(ofSize: 12, weight: .medium)
} else {
translatedTextLabel.numberOfLines = 2
translatedTextLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
}
}
}
/// 悬浮窗视图控制器代理协议
private protocol FloatingViewControllerDelegate: AnyObject {
func floatingViewControllerDidTap()
func floatingViewControllerDidPan(translation: CGPoint)
func floatingViewControllerDidEndPan()
func floatingViewControllerDidRequestClose()
}

154
ios/Runner/FloatingWindowPlugin.swift

@ -0,0 +1,154 @@
import Flutter
import UIKit
/// iOS 悬浮窗插件 - ReplayKit 方案
/// 使用 ReplayKit + Broadcast Upload Extension 实现全局字幕悬浮窗
public class FloatingWindowPlugin: NSObject, FlutterPlugin {
// MARK: - 常量
private static let channelName = "floating_window"
// MARK: - 属性
private var methodChannel: FlutterMethodChannel?
private let replayKitManager = ReplayKitManager.shared
// MARK: - 插件注册
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = FloatingWindowPlugin()
// 创建方法通道
let methodChannel = FlutterMethodChannel(
name: channelName,
binaryMessenger: registrar.messenger()
)
instance.methodChannel = methodChannel
registrar.addMethodCallDelegate(instance, channel: methodChannel)
// 设置 ReplayKit 回调
instance.setupReplayKitCallbacks()
print("FloatingWindowPlugin (ReplayKit) 注册成功")
}
// MARK: - 方法调用处理
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "enableFloatingWindow":
enableFloatingWindow(result: result)
case "disableFloatingWindow":
disableFloatingWindow(result: result)
case "showFloatingWindow":
showFloatingWindow(result: result)
case "hideFloatingWindow":
hideFloatingWindow(result: result)
case "updateContent":
if let arguments = call.arguments as? [String: Any] {
updateContent(arguments: arguments, result: result)
} else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的参数", details: nil))
}
case "setPosition":
// ReplayKit 方案中位置固定,但保持接口兼容性
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - ReplayKit 回调设置
/// 设置 ReplayKit 回调
private func setupReplayKitCallbacks() {
replayKitManager.onBroadcastStarted = { [weak self] in
self?.methodChannel?.invokeMethod("onFloatingWindowStateChanged", arguments: ["state": "minimized"])
}
replayKitManager.onBroadcastFinished = { [weak self] in
self?.methodChannel?.invokeMethod("onFloatingWindowClosed", arguments: nil)
}
replayKitManager.onBroadcastError = { [weak self] error in
self?.methodChannel?.invokeMethod("onFloatingWindowError", arguments: ["error": error.localizedDescription])
}
}
// MARK: - 悬浮窗控制方法
/// 启用悬浮窗功能(启动 ReplayKit 广播)
private func enableFloatingWindow(result: @escaping FlutterResult) {
replayKitManager.startBroadcast { success, error in
if success {
result(true)
} else {
result(FlutterError(
code: "ENABLE_ERROR",
message: "启用悬浮窗失败: \(error?.localizedDescription ?? "未知错误")",
details: nil
))
}
}
}
/// 禁用悬浮窗功能(停止 ReplayKit 广播)
private func disableFloatingWindow(result: @escaping FlutterResult) {
replayKitManager.stopBroadcast { success, error in
if success {
result(true)
} else {
result(FlutterError(
code: "DISABLE_ERROR",
message: "禁用悬浮窗失败: \(error?.localizedDescription ?? "未知错误")",
details: nil
))
}
}
}
/// 显示悬浮窗(ReplayKit 方案中自动显示)
private func showFloatingWindow(result: @escaping FlutterResult) {
if replayKitManager.isBroadcasting {
result(true)
} else {
result(FlutterError(code: "NOT_BROADCASTING", message: "广播未启动", details: nil))
}
}
/// 隐藏悬浮窗(ReplayKit 方案中通过停止广播实现)
private func hideFloatingWindow(result: @escaping FlutterResult) {
disableFloatingWindow(result: result)
}
/// 更新悬浮窗内容
private func updateContent(arguments: [String: Any], result: @escaping FlutterResult) {
let sourceText = arguments["sourceText"] as? String ?? ""
let translatedText = arguments["translatedText"] as? String ?? ""
let sourceLanguage = arguments["sourceLanguage"] as? String ?? ""
let targetLanguage = arguments["targetLanguage"] as? String ?? ""
let isIntermediate = arguments["isIntermediate"] as? Bool ?? false
// 构建显示文本
var displayText = ""
if !sourceText.isEmpty && !translatedText.isEmpty {
displayText = "\(sourceText)\n\(translatedText)"
} else if !translatedText.isEmpty {
displayText = translatedText
} else if !sourceText.isEmpty {
displayText = sourceText
}
replayKitManager.updateSubtitle(
text: displayText,
sourceLanguage: sourceLanguage,
targetLanguage: targetLanguage,
isIntermediate: isIntermediate
)
result(true)
}

5
ios/Runner/Info.plist

@ -91,6 +91,9 @@
<string>google.navigation</string>
<string>googlemaps</string>
<string>comgooglemaps</string>
<string>baidumap</string>
<string>amapuri</string>
<string>maps</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
@ -154,6 +157,8 @@
<string>Calendar permission is required to schedule and manage your audio meetings and recording tasks</string>
<key>NSCameraUsageDescription</key>
<string>Camera permission is required for QR code scanning and video calling functions</string>
<key>NSContactsUsageDescription</key>
<string>Contacts access is required to help you make phone calls to your contacts through voice commands. This enables the AI assistant to find and dial your contacts when you ask it to call someone.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Location access enables the app to provide location-based voice translation services, such as translating local place names and providing contextual information relevant to your current location during conversations.</string>
<key>NSLocationWhenInUseUsageDescription</key>

40
ios/Runner/OverlayPermissionPlugin.swift

@ -0,0 +1,40 @@
import Flutter
import UIKit
/// iOS 悬浮窗权限插件
/// 为了与 Android 版本保持接口一致性而创建
public class OverlayPermissionPlugin: NSObject, FlutterPlugin {
// MARK: - 常量
private static let channelName = "overlay_permission"
// MARK: - 插件注册
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = OverlayPermissionPlugin()
// 创建方法通道
let methodChannel = FlutterMethodChannel(
name: channelName,
binaryMessenger: registrar.messenger()
)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
print("OverlayPermissionPlugin 注册成功")
}
// MARK: - 方法调用处理
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "hasOverlayPermission":
// iOS 使用应用内悬浮窗,不需要特殊权限
result(true)
case "requestOverlayPermission":
// iOS 使用应用内悬浮窗,不需要特殊权限
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
}

247
ios/Runner/ReplayKitManager.swift

@ -0,0 +1,247 @@
import ReplayKit
import UIKit
/// ReplayKit 管理器
/// 负责管理屏幕广播的启动、停止和状态监控
class ReplayKitManager: NSObject {
// MARK: - 单例
static let shared = ReplayKitManager()
// MARK: - 属性
private var broadcastController: RPBroadcastController?
private let appGroupIdentifier = "group.com.saitong.voitrans.shared"
private var isRecording = false
// 回调闭包
var onBroadcastStarted: (() -> Void)?
var onBroadcastFinished: (() -> Void)?
var onBroadcastError: ((Error) -> Void)?
// MARK: - 初始化
private override init() {
super.init()
setupNotifications()
}
// MARK: - 公共方法
/// 启动屏幕广播
/// - Parameter completion: 完成回调
func startBroadcast(completion: @escaping (Bool, Error?) -> Void) {
print("🎬 准备启动 ReplayKit 广播")
// 检查是否已经在录制
if isRecording {
print("⚠️ 广播已在进行中")
completion(false, NSError(domain: "ReplayKitManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "广播已在进行中"]))
return
}
// 获取广播选择器
RPBroadcastActivityViewController.load { [weak self] (broadcastActivityViewController, error) in
DispatchQueue.main.async {
guard let self = self else { return }
if let error = error {
print("❌ 加载广播选择器失败: \(error.localizedDescription)")
completion(false, error)
return
}
guard let broadcastActivityViewController = broadcastActivityViewController else {
let error = NSError(domain: "ReplayKitManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "无法加载广播选择器"])
completion(false, error)
return
}
// 设置代理
broadcastActivityViewController.delegate = self
// 显示广播选择器
if let topViewController = self.getTopViewController() {
broadcastActivityViewController.modalPresentationStyle = .formSheet
topViewController.present(broadcastActivityViewController, animated: true) {
print("✅ 广播选择器已显示")
completion(true, nil)
}
} else {
let error = NSError(domain: "ReplayKitManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "无法获取顶层视图控制器"])
completion(false, error)
}
}
}
}
/// 停止屏幕广播
/// - Parameter completion: 完成回调
func stopBroadcast(completion: @escaping (Bool, Error?) -> Void) {
print("🛑 准备停止 ReplayKit 广播")
guard isRecording else {
print("⚠️ 当前没有进行中的广播")
completion(false, NSError(domain: "ReplayKitManager", code: -4, userInfo: [NSLocalizedDescriptionKey: "当前没有进行中的广播"]))
return
}
broadcastController?.finishBroadcast { [weak self] error in
DispatchQueue.main.async {
if let error = error {
print("❌ 停止广播失败: \(error.localizedDescription)")
completion(false, error)
} else {
print("✅ 广播已停止")
self?.isRecording = false
self?.broadcastController = nil
completion(true, nil)
}
}
}
}
/// 更新字幕内容
/// - Parameters:
/// - text: 字幕文本
/// - sourceLanguage: 源语言
/// - targetLanguage: 目标语言
/// - isIntermediate: 是否为中间结果
func updateSubtitle(text: String, sourceLanguage: String = "", targetLanguage: String = "", isIntermediate: Bool = false) {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
print("❌ 无法访问 App Group UserDefaults")
return
}
// 更新字幕数据
userDefaults.set(text, forKey: "subtitle_text")
userDefaults.set(sourceLanguage, forKey: "subtitle_source_language")
userDefaults.set(targetLanguage, forKey: "subtitle_target_language")
userDefaults.set(isIntermediate, forKey: "subtitle_is_intermediate")
userDefaults.set(Date().timeIntervalSince1970, forKey: "subtitle_timestamp")
// 强制同步
userDefaults.synchronize()
print("📝 字幕已更新: \(text)")
}
/// 检查广播状态
var isBroadcasting: Bool {
return isRecording
}
// MARK: - 私有方法
/// 设置通知监听
private func setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
/// 应用进入后台
@objc private func applicationDidEnterBackground() {
print("📱 应用进入后台,广播状态: \(isRecording)")
}
/// 应用即将进入前台
@objc private func applicationWillEnterForeground() {
print("📱 应用即将进入前台,广播状态: \(isRecording)")
}
/// 获取顶层视图控制器
private func getTopViewController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else {
return nil
}
var topViewController = window.rootViewController
while let presentedViewController = topViewController?.presentedViewController {
topViewController = presentedViewController
}
return topViewController
}
// MARK: - 清理
deinit {
NotificationCenter.default.removeObserver(self)
print("🗑️ ReplayKitManager 已清理")
}
}
// MARK: - RPBroadcastActivityViewControllerDelegate
extension ReplayKitManager: RPBroadcastActivityViewControllerDelegate {
func broadcastActivityViewController(_ broadcastActivityViewController: RPBroadcastActivityViewController, didFinishWith broadcastController: RPBroadcastController?, error: Error?) {
DispatchQueue.main.async { [weak self] in
// 关闭选择器
broadcastActivityViewController.dismiss(animated: true)
if let error = error {
print("❌ 广播设置失败: \(error.localizedDescription)")
self?.onBroadcastError?(error)
return
}
guard let broadcastController = broadcastController else {
print("❌ 无法获取广播控制器")
let error = NSError(domain: "ReplayKitManager", code: -5, userInfo: [NSLocalizedDescriptionKey: "无法获取广播控制器"])
self?.onBroadcastError?(error)
return
}
// 保存广播控制器
self?.broadcastController = broadcastController
broadcastController.delegate = self
// 开始广播
broadcastController.startBroadcast { error in
DispatchQueue.main.async {
if let error = error {
print("❌ 开始广播失败: \(error.localizedDescription)")
self?.onBroadcastError?(error)
} else {
print("✅ 广播已开始")
self?.isRecording = true
self?.onBroadcastStarted?()
}
}
}
}
}
}
// MARK: - RPBroadcastControllerDelegate
extension ReplayKitManager: RPBroadcastControllerDelegate {
func broadcastController(_ broadcastController: RPBroadcastController, didFinishWithError error: Error?) {
DispatchQueue.main.async { [weak self] in
self?.isRecording = false
self?.broadcastController = nil
if let error = error {
print("❌ 广播结束时发生错误: \(error.localizedDescription)")
self?.onBroadcastError?(error)
} else {
print("✅ 广播正常结束")
self?.onBroadcastFinished?()
}
}
}
func broadcastController(_ broadcastController: RPBroadcastController, didUpdateServiceInfo serviceInfo: [String : NSCoding & NSObjectProtocol]) {
print("📊 广播服务信息更新: \(serviceInfo)")
}
}

4
ios/Runner/Runner.entitlements

@ -30,5 +30,9 @@
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.saitong.voitrans.shared</string>
</array>
</dict>
</plist>

8
lib/core/bindings/initial_binding.dart

@ -1,3 +1,4 @@
import 'package:voitrans/data/services/pay_service.dart';
import 'package:voitrans/data/services/qq_music.dart';
import '../../../data/services/user_portrait.dart';
@ -22,6 +23,7 @@ import '../../data/services/account_service.dart';
import '../../data/services/version_update_service.dart';
import '../../data/services/ota_check_service.dart';
import '../../core/utils/app_lifecycle_manager.dart';
import '../services/floating_window_manager.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@ -87,6 +89,12 @@ class InitialBinding extends Bindings {
// OTA检查服务
Get.lazyPut<OtaCheckService>(() => OtaCheckService(), fenix: true);
// 悬浮窗管理器
Get.lazyPut<FloatingWindowManager>(() => FloatingWindowManager(),
fenix: true);
// 支付服务
Get.lazyPut<PayService>(() => PayService(), fenix: true);
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)

199
lib/core/services/floating_window_manager.dart

@ -0,0 +1,199 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../utils/logger.dart';
import '../utils/overlay_permission_util.dart';
/// 悬浮窗状态
enum FloatingWindowState {
hidden, // 隐藏
minimized, // 最小化
expanded, // 展开
}
/// 悬浮窗数据模型
class FloatingWindowData {
final String sourceText;
final String translatedText;
final String sourceLanguage;
final String targetLanguage;
final DateTime timestamp;
final bool isIntermediate;
FloatingWindowData({
required this.sourceText,
required this.translatedText,
required this.sourceLanguage,
required this.targetLanguage,
required this.timestamp,
this.isIntermediate = false,
});
Map<String, dynamic> toMap() {
return {
'sourceText': sourceText,
'translatedText': translatedText,
'sourceLanguage': sourceLanguage,
'targetLanguage': targetLanguage,
'timestamp': timestamp.millisecondsSinceEpoch,
'isIntermediate': isIntermediate,
};
}
}
/// 悬浮窗管理器
class FloatingWindowManager extends GetxController {
static const String TAG = 'FloatingWindowManager';
static const MethodChannel _channel = MethodChannel('floating_window');
final _isEnabled = false.obs;
final _currentState = FloatingWindowState.hidden.obs;
final _hasPermission = false.obs;
// Getters
bool get isEnabled => _isEnabled.value;
FloatingWindowState get currentState => _currentState.value;
bool get hasPermission => _hasPermission.value;
@override
void onInit() {
super.onInit();
_setupMethodCallHandler();
_checkPermissions();
}
/// 设置方法调用处理器
void _setupMethodCallHandler() {
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'onFloatingWindowStateChanged':
final String state = call.arguments['state'];
_updateState(_parseState(state));
break;
case 'onFloatingWindowClosed':
_currentState.value = FloatingWindowState.hidden;
_isEnabled.value = false;
break;
case 'onFloatingWindowClicked':
// 悬浮窗被点击,可以执行相应操作(如返回主应用)
_handleFloatingWindowClick();
break;
}
});
}
/// 检查权限
Future<void> _checkPermissions() async {
_hasPermission.value = await OverlayPermissionUtil.hasOverlayPermission();
}
/// 启用悬浮窗功能
Future<bool> enableFloatingWindow() async {
if (!_hasPermission.value) {
final bool granted =
await OverlayPermissionUtil.requestOverlayPermission();
if (!granted) {
Get.snackbar('权限不足'.tr, '需要悬浮窗权限才能启用此功能'.tr);
return false;
}
_hasPermission.value = true;
}
try {
await _channel.invokeMethod('enableFloatingWindow');
_isEnabled.value = true;
Logger.info('悬浮窗功能已启用');
return true;
} catch (e) {
Logger.error('启用悬浮窗失败: $e');
return false;
}
}
/// 禁用悬浮窗功能
Future<void> disableFloatingWindow() async {
try {
await _channel.invokeMethod('disableFloatingWindow');
_isEnabled.value = false;
_currentState.value = FloatingWindowState.hidden;
Logger.info('悬浮窗功能已禁用');
} catch (e) {
Logger.error('禁用悬浮窗失败: $e');
}
}
/// 显示悬浮窗
Future<void> showFloatingWindow() async {
if (!_isEnabled.value) return;
try {
await _channel.invokeMethod('showFloatingWindow');
_currentState.value = FloatingWindowState.minimized;
Logger.info('悬浮窗已显示');
} catch (e) {
Logger.error('显示悬浮窗失败: $e');
}
}
/// 隐藏悬浮窗
Future<void> hideFloatingWindow() async {
try {
await _channel.invokeMethod('hideFloatingWindow');
_currentState.value = FloatingWindowState.hidden;
Logger.info('悬浮窗已隐藏');
} catch (e) {
Logger.error('隐藏悬浮窗失败: $e');
}
}
/// 更新悬浮窗内容
Future<void> updateFloatingWindowContent(FloatingWindowData data) async {
if (!_isEnabled.value ||
_currentState.value == FloatingWindowState.hidden) {
return;
}
try {
await _channel.invokeMethod('updateContent', data.toMap());
Logger.info('悬浮窗内容已更新');
} catch (e) {
Logger.error('更新悬浮窗内容失败: $e');
}
}
/// 设置悬浮窗位置
Future<void> setFloatingWindowPosition(double x, double y) async {
try {
await _channel.invokeMethod('setPosition', {'x': x, 'y': y});
} catch (e) {
Logger.error('设置悬浮窗位置失败: $e');
}
}
/// 解析状态字符串
FloatingWindowState _parseState(String state) {
switch (state) {
case 'minimized':
return FloatingWindowState.minimized;
case 'expanded':
return FloatingWindowState.expanded;
case 'hidden':
default:
return FloatingWindowState.hidden;
}
}
/// 更新状态
void _updateState(FloatingWindowState state) {
_currentState.value = state;
Logger.info('悬浮窗状态更新为: $state');
}
/// 处理悬浮窗点击事件
void _handleFloatingWindowClick() {
// 这里可以实现点击悬浮窗后的操作,比如返回主应用
Logger.info('悬浮窗被点击');
// 可以发送事件给其他组件处理
}
}

4
lib/core/translations/app_translations.dart

@ -186,11 +186,11 @@ class AppTranslations extends Translations {
return fallbackLocale;
}
return _getMatchedLocale(systemLocale);
return getMatchedLocale(systemLocale);
}
/// 智能匹配系统语言到支持的语言
static Locale _getMatchedLocale(Locale systemLocale) {
static Locale getMatchedLocale(Locale systemLocale) {
String languageCode = systemLocale.languageCode.toLowerCase();
String? countryCode = systemLocale.countryCode?.toUpperCase();
String? scriptCode = systemLocale.scriptCode;

2
lib/core/translations/language/bg_bg.dart

@ -824,7 +824,7 @@ const Map<String, String> bgBG = {
"charging": "Зареждане",
"updateContent": "Съдържание на актуализацията",
"removeEarbudsEnsureBattery":
"Извадете двете слушалки и се уверете, че батерията на двете е над 30%",
"Извадете двете слушалки и се уверете, че батерията на двете е над 20%",
// 音乐列表页面
"noMusic": "Няма музика", // 暂无音乐

2
lib/core/translations/language/cs_cz.dart

@ -810,7 +810,7 @@ const Map<String, String> csCZ = {
"charging": "Nabíjení",
"updateContent": "Obsah aktualizace",
"removeEarbudsEnsureBattery":
"Vyjměte obě sluchátka a ujistěte se, že baterie obou je nad 30%",
"Vyjměte obě sluchátka a ujistěte se, že baterie obou je nad 20%",
// 音乐列表页面
"noMusic": "Žádná hudba", // 暂无音乐

2
lib/core/translations/language/da_dk.dart

@ -809,7 +809,7 @@ const Map<String, String> daDK = {
"charging": "Oplader",
"updateContent": "Opdateringsindhold",
"removeEarbudsEnsureBattery":
"Tag begge øretelefoner ud og sørg for at batteriet på begge er over 30%",
"Tag begge øretelefoner ud og sørg for at batteriet på begge er over 20%",
// 音乐列表页面
"noMusic": "Ingen musik", // 暂无音乐

2
lib/core/translations/language/de_de.dart

@ -829,7 +829,7 @@ const Map<String, String> deDE = {
"charging": "Laden",
"updateContent": "Update-Inhalt",
"removeEarbudsEnsureBattery":
"Entfernen Sie beide Ohrhörer und stellen Sie sicher, dass der Batteriestand über 30% liegt",
"Entfernen Sie beide Ohrhörer und stellen Sie sicher, dass der Batteriestand über 20% liegt",
// 音乐列表页面
"noMusic": "Keine Musik", // 暂无音乐

2
lib/core/translations/language/el_gr.dart

@ -819,7 +819,7 @@ const Map<String, String> elGR = {
"charging": "Φόρτιση",
"updateContent": "Περιεχόμενο ενημέρωσης",
"removeEarbudsEnsureBattery":
"Αφαιρέστε και τα δύο ακουστικά και βεβαιωθείτε ότι η μπαταρία και των δύο είναι πάνω από 30%",
"Αφαιρέστε και τα δύο ακουστικά και βεβαιωθείτε ότι η μπαταρία και των δύο είναι πάνω από 20%",
// 音乐列表页面
"noMusic": "Δεν υπάρχει μουσική", // 暂无音乐

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

@ -1137,7 +1137,7 @@ const Map<String, String> enUS = {
"charging": "Charging",
"updateContent": "Update Content",
"removeEarbudsEnsureBattery":
"Remove both earbuds and ensure battery level is above 30%",
"Remove both earbuds and ensure battery level is above 20%",
// 音乐列表页面
"noMusic": "No Music", // 暂无音乐

2
lib/core/translations/language/es_es.dart

@ -823,7 +823,7 @@ const Map<String, String> esES = {
"charging": "Cargando",
"updateContent": "Contenido de actualización",
"removeEarbudsEnsureBattery":
"Retira ambos auriculares y asegúrate de que la batería de ambos esté por encima del 30%",
"Retira ambos auriculares y asegúrate de que la batería de ambos esté por encima del 20%",
// 音乐列表页面
"noMusic": "Sin música", // 暂无音乐

2
lib/core/translations/language/et_ee.dart

@ -798,7 +798,7 @@ const Map<String, String> etEE = {
"charging": "Laadimine",
"updateContent": "Uuenduse sisu",
"removeEarbudsEnsureBattery":
"Võta mõlemad kõrvaklapid välja ja veendu, et mõlema aku on üle 30%",
"Võta mõlemad kõrvaklapid välja ja veendu, et mõlema aku on üle 20%",
// 音乐列表页面
"noMusic": "Muusikat pole", // 暂无音乐

2
lib/core/translations/language/fi_fi.dart

@ -793,7 +793,7 @@ const Map<String, String> fiFI = {
"charging": "Ladataan",
"updateContent": "Päivityksen sisältö",
"removeEarbudsEnsureBattery":
"Poista molemmat kuulokkeet ja varmista, että molempien akun varaus on yli 30%",
"Poista molemmat kuulokkeet ja varmista, että molempien akun varaus on yli 20%",
// 音乐列表页面
"noMusic": "Ei musiikkia",

2
lib/core/translations/language/fr_fr.dart

@ -832,7 +832,7 @@ const Map<String, String> frFR = {
"charging": "En charge",
"updateContent": "Contenu de la mise à jour",
"removeEarbudsEnsureBattery":
"Retirez les deux écouteurs et assurez-vous que le niveau de batterie est supérieur à 30%",
"Retirez les deux écouteurs et assurez-vous que le niveau de batterie est supérieur à 20%",
// 音乐列表页面
"noMusic": "Aucune musique", // 暂无音乐

2
lib/core/translations/language/hi_in.dart

@ -797,7 +797,7 @@ const Map<String, String> hiIN = {
"charging": "चार्ज हो रहा है",
"updateContent": "अपडेट सामग्री",
"removeEarbudsEnsureBattery":
"दोनों ईयरबड निकालें, और सुनिश्चित करें कि दोनों की बैटरी 30% से ऊपर है",
"दोनों ईयरबड निकालें, और सुनिश्चित करें कि दोनों की बैटरी 20% से ऊपर है",
// 音乐列表页面
"noMusic": "कोई संगीत नहीं",

2
lib/core/translations/language/hr_hr.dart

@ -803,7 +803,7 @@ const Map<String, String> hrHR = {
"charging": "Punjenje",
"updateContent": "Sadržaj ažuriranja",
"removeEarbudsEnsureBattery":
"Izvadite obje slušalice i provjerite je li baterija obje iznad 30%",
"Izvadite obje slušalice i provjerite je li baterija obje iznad 20%",
// 音乐列表页面
"noMusic": "Nema glazbe",

2
lib/core/translations/language/hu_hu.dart

@ -802,7 +802,7 @@ const Map<String, String> huHU = {
"charging": "Töltés",
"updateContent": "Frissítés tartalma",
"removeEarbudsEnsureBattery":
"Vegye ki mindkét fülhallgatót, és győződjön meg róla, hogy mindkettő akkumulátora 30% felett van",
"Vegye ki mindkét fülhallgatót, és győződjön meg róla, hogy mindkettő akkumulátora 20% felett van",
// 音乐列表页面
"noMusic": "Nincs zene",

2
lib/core/translations/language/id_id.dart

@ -795,7 +795,7 @@ const Map<String, String> idID = {
"charging": "Mengisi daya",
"updateContent": "Konten Pembaruan",
"removeEarbudsEnsureBattery":
"Keluarkan kedua earbud, dan pastikan baterai keduanya di atas 30%",
"Keluarkan kedua earbud, dan pastikan baterai keduanya di atas 20%",
// 音乐列表页面
"noMusic": "Tidak Ada Musik",

2
lib/core/translations/language/is_is.dart

@ -796,7 +796,7 @@ const Map<String, String> isIS = {
"charging": "Hleður",
"updateContent": "Innihald uppfærslu",
"removeEarbudsEnsureBattery":
"Taktu báðar heyrnartólin úr og gakktu úr skugga um að rafhlaðan á báðum sé yfir 30%",
"Taktu báðar heyrnartólin úr og gakktu úr skugga um að rafhlaðan á báðum sé yfir 20%",
// 音乐列表页面
"noMusic": "Engin tónlist",

2
lib/core/translations/language/it_it.dart

@ -818,7 +818,7 @@ const Map<String, String> itIT = {
"charging": "In carica",
"updateContent": "Contenuto dell'aggiornamento",
"removeEarbudsEnsureBattery":
"Rimuovi entrambi gli auricolari e assicurati che la batteria di entrambi sia superiore al 30%",
"Rimuovi entrambi gli auricolari e assicurati che la batteria di entrambi sia superiore al 20%",
// 音乐列表页面
"noMusic": "Nessuna musica",

2
lib/core/translations/language/ja_jp.dart

@ -746,7 +746,7 @@ const Map<String, String> jaJP = {
"doNotDisconnect": "切断しないでください",
"charging": "充電中",
"updateContent": "更新内容",
"removeEarbudsEnsureBattery": "両方のイヤホンを取り出し、バッテリーレベルが30%以上であることを確認してください",
"removeEarbudsEnsureBattery": "両方のイヤホンを取り出し、バッテリーレベルが20%以上であることを確認してください",
// 音乐列表页面
"noMusic": "音楽がありません", // 暂无音乐

2
lib/core/translations/language/ko_kr.dart

@ -733,7 +733,7 @@ const Map<String, String> koKR = {
"doNotDisconnect": "연결을 끊지 마세요",
"charging": "충전 중",
"updateContent": "업데이트 내용",
"removeEarbudsEnsureBattery": "양쪽 이어버드를 꺼내고 배터리가 모두 30% 이상인지 확인하세요",
"removeEarbudsEnsureBattery": "양쪽 이어버드를 꺼내고 배터리가 모두 20% 이상인지 확인하세요",
// 音乐列表页面
"noMusic": "음악 없음",

2
lib/core/translations/language/lt_lt.dart

@ -804,7 +804,7 @@ const Map<String, String> ltLT = {
"charging": "Kraunama",
"updateContent": "Atnaujinimo turinys",
"removeEarbudsEnsureBattery":
"Išimkite abi ausines ir įsitikinkite, kad abiejų baterijos lygis yra didesnis nei 30%",
"Išimkite abi ausines ir įsitikinkite, kad abiejų baterijos lygis yra didesnis nei 20%",
// 音乐列表页面
"noMusic": "Nėra muzikos",

2
lib/core/translations/language/lv_lv.dart

@ -805,7 +805,7 @@ const Map<String, String> lvLV = {
"charging": "Uzlādējas",
"updateContent": "Atjauninājuma saturs",
"removeEarbudsEnsureBattery":
"Izņemiet abas austiņas un pārliecinieties, ka abu baterijas līmenis ir virs 30%",
"Izņemiet abas austiņas un pārliecinieties, ka abu baterijas līmenis ir virs 20%",
// 音乐列表页面
"noMusic": "Nav mūzikas",

2
lib/core/translations/language/ms_my.dart

@ -795,7 +795,7 @@ const Map<String, String> msMY = {
"charging": "Mengecas",
"updateContent": "Kandungan Kemas Kini",
"removeEarbudsEnsureBattery":
"Keluarkan kedua-dua fon telinga, dan pastikan bateri kedua-duanya melebihi 30%",
"Keluarkan kedua-dua fon telinga, dan pastikan bateri kedua-duanya melebihi 20%",
// 音乐列表页面
"noMusic": "Tiada Muzik",

2
lib/core/translations/language/nb_no.dart

@ -794,7 +794,7 @@ const Map<String, String> nbNO = {
"charging": "Lader",
"updateContent": "Oppdateringsinnhold",
"removeEarbudsEnsureBattery":
"Ta ut begge øreproppene og sørg for at batterinivået på begge er over 30 %",
"Ta ut begge øreproppene og sørg for at batterinivået på begge er over 20 %",
// 音乐列表页面
"noMusic": "Ingen musikk",

2
lib/core/translations/language/nl_nl.dart

@ -804,7 +804,7 @@ const Map<String, String> nlNL = {
"charging": "Opladen",
"updateContent": "Update-inhoud",
"removeEarbudsEnsureBattery":
"Haal beide oortjes eruit en zorg ervoor dat de batterij van beide meer dan 30% is",
"Haal beide oortjes eruit en zorg ervoor dat de batterij van beide meer dan 20% is",
// 音乐列表页面
"noMusic": "Geen muziek",

2
lib/core/translations/language/pl_pl.dart

@ -810,7 +810,7 @@ const Map<String, String> plPL = {
"charging": "Ładowanie",
"updateContent": "Treść aktualizacji",
"removeEarbudsEnsureBattery":
"Wyjmij obie słuchawki i upewnij się, że poziom naładowania baterii obu wynosi ponad 30%",
"Wyjmij obie słuchawki i upewnij się, że poziom naładowania baterii obu wynosi ponad 20%",
// 音乐列表页面
"noMusic": "Brak muzyki",

2
lib/core/translations/language/pt_pt.dart

@ -812,7 +812,7 @@ const Map<String, String> ptPT = {
"charging": "A carregar",
"updateContent": "Conteúdo da Atualização",
"removeEarbudsEnsureBattery":
"Retire ambos os auriculares e certifique-se de que a bateria de ambos está acima de 30%",
"Retire ambos os auriculares e certifique-se de que a bateria de ambos está acima de 20%",
// 音乐列表页面
"noMusic": "Sem Música",

2
lib/core/translations/language/ro_ro.dart

@ -813,7 +813,7 @@ const Map<String, String> roRO = {
"charging": "Se încarcă",
"updateContent": "Conținut actualizare",
"removeEarbudsEnsureBattery":
"Scoateți ambele căști și asigurați-vă că bateria ambelor este peste 30%",
"Scoateți ambele căști și asigurați-vă că bateria ambelor este peste 20%",
// 音乐列表页面
"noMusic": "Fără muzică",

2
lib/core/translations/language/ru_ru.dart

@ -805,7 +805,7 @@ const Map<String, String> ruRU = {
"charging": "Зарядка",
"updateContent": "Содержание обновления",
"removeEarbudsEnsureBattery":
"Извлеките оба наушника и убедитесь, что уровень заряда батареи в обоих превышает 30%",
"Извлеките оба наушника и убедитесь, что уровень заряда батареи в обоих превышает 20%",
// 音乐列表页面
"noMusic": "Нет музыки",

2
lib/core/translations/language/sk_sk.dart

@ -799,7 +799,7 @@ const Map<String, String> skSK = {
"charging": "Nabíja sa",
"updateContent": "Obsah aktualizácie",
"removeEarbudsEnsureBattery":
"Vyberte obe slúchadlá a uistite sa, že batéria oboch je nad 30%",
"Vyberte obe slúchadlá a uistite sa, že batéria oboch je nad 20%",
// 音乐列表页面
"noMusic": "Žiadna hudba",

2
lib/core/translations/language/sl_si.dart

@ -804,7 +804,7 @@ const Map<String, String> slSI = {
"charging": "Polnjenje",
"updateContent": "Vsebina posodobitve",
"removeEarbudsEnsureBattery":
"Odstranite obe slušalki in se prepričajte, da je baterija obeh nad 30%",
"Odstranite obe slušalki in se prepričajte, da je baterija obeh nad 20%",
// 音乐列表页面
"noMusic": "Ni glasbe",

2
lib/core/translations/language/sr_rs.dart

@ -798,7 +798,7 @@ const Map<String, String> srRS = {
"charging": "Punjenje",
"updateContent": "Sadržaj ažuriranja",
"removeEarbudsEnsureBattery":
"Izvadite obe slušalice i uverite se da je baterija obe iznad 30%",
"Izvadite obe slušalice i uverite se da je baterija obe iznad 20%",
// 音乐列表页面
"noMusic": "Nema muzike",

2
lib/core/translations/language/sv_se.dart

@ -794,7 +794,7 @@ const Map<String, String> svSE = {
"charging": "Laddar",
"updateContent": "Uppdateringsinnehåll",
"removeEarbudsEnsureBattery":
"Ta ut båda hörlurarna och se till att batterinivån är över 30 % på båda",
"Ta ut båda hörlurarna och se till att batterinivån är över 20 % på båda",
// 音乐列表页面
"noMusic": "Ingen musik",

2
lib/core/translations/language/th_th.dart

@ -780,7 +780,7 @@ const Map<String, String> thTH = {
"charging": "กำลังชาร์จ",
"updateContent": "เนื้อหาการอัปเดต",
"removeEarbudsEnsureBattery":
"ถอดหูฟังทั้งสองข้างออก และตรวจสอบให้แน่ใจว่าแบตเตอรี่ทั้งสองข้างมีพลังงานมากกว่า 30%",
"ถอดหูฟังทั้งสองข้างออก และตรวจสอบให้แน่ใจว่าแบตเตอรี่ทั้งสองข้างมีพลังงานมากกว่า 20%",
// 音乐列表页面
"noMusic": "ไม่มีเพลง",

2
lib/core/translations/language/tl_ph.dart

@ -816,7 +816,7 @@ const Map<String, String> tlPH = {
"charging": "Nagcha-charge",
"updateContent": "Nilalaman ng Update",
"removeEarbudsEnsureBattery":
"Alisin ang parehong earbuds, at tiyaking parehong may 30% o higit pang baterya",
"Alisin ang parehong earbuds, at tiyaking parehong may 20% o higit pang baterya",
// 音乐列表页面
"noMusic": "Walang Musika",

2
lib/core/translations/language/tr_tr.dart

@ -796,7 +796,7 @@ const Map<String, String> trTR = {
"charging": "Şarj oluyor",
"updateContent": "Güncelleme İçeriği",
"removeEarbudsEnsureBattery":
"Her iki kulaklığı da çıkarın ve pil seviyelerinin %30'un üzerinde olduğundan emin olun",
"Her iki kulaklığı da çıkarın ve pil seviyelerinin %20'un üzerinde olduğundan emin olun",
// 音乐列表页面
"noMusic": "Müzik Yok",

2
lib/core/translations/language/uk_ua.dart

@ -804,7 +804,7 @@ const Map<String, String> ukUA = {
"charging": "Зарядка",
"updateContent": "Зміст оновлення",
"removeEarbudsEnsureBattery":
"Вийміть обидва навушники та переконайтеся, що заряд батареї в обох перевищує 30%",
"Вийміть обидва навушники та переконайтеся, що заряд батареї в обох перевищує 20%",
// 音乐列表页面
"noMusic": "Немає музики",

2
lib/core/translations/language/vi_vn.dart

@ -788,7 +788,7 @@ const Map<String, String> viVN = {
"charging": "Đang sạc",
"updateContent": "Nội dung cập nhật",
"removeEarbudsEnsureBattery":
"Lấy cả hai tai nghe ra và đảm bảo pin của cả hai đều trên 30%",
"Lấy cả hai tai nghe ra và đảm bảo pin của cả hai đều trên 20%",
// 音乐列表页面
"noMusic": "Không có nhạc",

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

@ -960,7 +960,7 @@ const Map<String, String> zhCN = {
"pairingSuccessRedirecting": "配对成功!即将跳转到主页...",
// 配对错误状态文本
"ensureConnectedDeepSoundDevice": "请确保连接的是Voitrans设备",
"ensureConnectedDeepSoundDevice": "当前使用设备:%s,请确保连接的是Voitrans支持的设备",
"deviceAlreadyBoundToOtherAccount": "该设备已被其他账号绑定",
"deviceAssociationFailed": "设备关联失败,请重试",
"scanTimeoutEnsureDeviceNearby": "扫描超时,请确保设备在附近",
@ -1036,7 +1036,7 @@ const Map<String, String> zhCN = {
"doNotDisconnect": "请勿断开连接",
"charging": "充电中",
"updateContent": "更新内容",
"removeEarbudsEnsureBattery": "取出双耳机,并保证电量均为30%以上的状态",
"removeEarbudsEnsureBattery": "取出双耳机,并保证电量均为20%以上的状态",
// 音乐列表页面
"noMusic": "暂无音乐", // 暂无音乐

2
lib/core/translations/language/zh_hk.dart

@ -734,7 +734,7 @@ const Map<String, String> zhHK = {
"doNotDisconnect": "請勿斷開連接",
"charging": "充電中",
"updateContent": "更新內容",
"removeEarbudsEnsureBattery": "取出雙耳機,並保證電量均為30%以上的狀態",
"removeEarbudsEnsureBattery": "取出雙耳機,並保證電量均為20%以上的狀態",
// 音樂列表頁面
"noMusic": "暫無音樂",

79
lib/core/utils/overlay_permission_util.dart

@ -0,0 +1,79 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'logger.dart';
/// 悬浮窗权限管理工具类
class OverlayPermissionUtil {
static const MethodChannel _channel = MethodChannel('overlay_permission');
/// 检查是否有悬浮窗权限
static Future<bool> hasOverlayPermission() async {
if (Platform.isAndroid) {
try {
final bool hasPermission = await _channel.invokeMethod('hasOverlayPermission');
Logger.info('检查悬浮窗权限: $hasPermission');
return hasPermission;
} catch (e) {
Logger.error('检查悬浮窗权限失败: $e');
return false;
}
} else if (Platform.isIOS) {
// iOS 使用应用内悬浮窗,不需要特殊权限
Logger.info('iOS 平台:使用应用内悬浮窗,无需特殊权限');
return true;
}
// 其他平台不支持
return false;
}
/// 请求悬浮窗权限
static Future<bool> requestOverlayPermission() async {
if (Platform.isAndroid) {
// 首先检查是否已有权限
if (await hasOverlayPermission()) {
return true;
}
// 显示说明对话框
bool? userAgreed = await Get.dialog<bool>(
AlertDialog(
title: Text('悬浮窗权限'.tr),
content: Text('为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。'.tr),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text('取消'.tr),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
child: Text('去设置'.tr),
),
],
),
barrierDismissible: false,
);
if (userAgreed == true) {
try {
// 跳转系统设置页
await _channel.invokeMethod('requestOverlayPermission');
// 等待用户操作完成后重新检查权限
await Future.delayed(const Duration(seconds: 1));
return await hasOverlayPermission();
} catch (e) {
Logger.error('请求悬浮窗权限失败: $e');
return false;
}
}
return false;
} else if (Platform.isIOS) {
// iOS 使用应用内悬浮窗,不需要特殊权限,但需要检查应用状态
Logger.info('iOS 平台:使用应用内悬浮窗,无需特殊权限');
return true;
}
// 其他平台不支持
return false;
}
}

4
lib/data/models/appconfig.dart

@ -49,8 +49,8 @@ class AppConfig {
//根据产品名称获取产品配置
static DBProduct? getproduct(String name) {
final foundProduct =
config.products.firstWhere((product) => product.devicename == name);
final foundProduct = config.products
.firstWhere((product) => product.devicename.contains(name));
return foundProduct;
}
}

4
lib/data/services/bluetooth_manager.dart

@ -76,7 +76,9 @@ class BluetoothManager extends GetxService {
currentDeviceRx.value = newDevice;
// 检查当前是否在OTA升级界面,如果是则不跳转
if (!_isInOtaUpgradePage() && !_bleManager.isConnected) {
if (!_isInOtaUpgradePage() &&
!_bleManager.isConnected &&
Get.currentRoute != Routes.pairing) {
Get.toNamed(Routes.pairing, arguments: newDevice);
} else {
Logger.info('当前在OTA升级界面,跳过配对页面跳转');

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

@ -171,4 +171,13 @@ class Api {
params: params,
);
}
//创建支付订单
static paycreateorder([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/pay_createorder',
params: params,
);
}
}

89
lib/data/services/ota_check_service.dart

@ -9,49 +9,50 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 负责管理全局OTA检查状态,确保在应用生命周期内只检查一次
class OtaCheckService extends GetxService {
static const String _tag = 'OtaCheckService';
// 全局状态管理
static bool _hasCheckedInCurrentSession = false;
static bool _isCheckingInProgress = false;
final _otaCoreService = OtaCoreService();
/// 获取服务实例
static OtaCheckService get to => Get.find<OtaCheckService>();
@override
void onInit() {
super.onInit();
Logger.d(_tag, 'OTA检查服务已初始化');
}
/// 重置检查状态(仅在应用重启时调用)
static void resetCheckStatus() {
_hasCheckedInCurrentSession = false;
_isCheckingInProgress = false;
Logger.d(_tag, 'OTA检查状态已重置');
}
/// 检查是否应该进行OTA检查
bool shouldCheckOta() {
return !_hasCheckedInCurrentSession && !_isCheckingInProgress;
}
/// 标记为已检查
void _markAsChecked() {
_hasCheckedInCurrentSession = true;
_isCheckingInProgress = false;
Logger.d(_tag, 'OTA检查状态已标记为完成');
}
/// 检查当前是否在Home页面
bool _isCurrentlyOnHomePage() {
final currentRoute = Get.currentRoute;
final isOnHome = currentRoute == Routes.home || currentRoute == Routes.initial;
final isOnHome =
currentRoute == Routes.home || currentRoute == Routes.initial;
Logger.d(_tag, '当前路由: $currentRoute, 是否在Home页面: $isOnHome');
return isOnHome;
}
/// 执行OTA升级检查
/// [isFullyConnected] 设备是否完全连接
/// 返回是否成功执行了检查
@ -62,53 +63,54 @@ class OtaCheckService extends GetxService {
Logger.d(_tag, '跳过OTA检查 - 已检查过或正在检查中');
return false;
}
// 检查设备连接状态
if (!isFullyConnected) {
Logger.d(_tag, '设备未完全连接,跳过OTA检查');
return false;
}
// 检查当前页面
if (!_isCurrentlyOnHomePage()) {
Logger.d(_tag, '当前不在Home页面,跳过OTA检查');
return false;
}
// 标记为检查中
_isCheckingInProgress = true;
Logger.d(_tag, '开始执行OTA固件升级检查...');
// 获取当前版本信息
final currentInfo = await _otaCoreService.getCurrentVersionInfo();
final currentVersion = currentInfo['currentVersion'] as String;
// 获取最新版本信息
final latestInfo = await _otaCoreService.getLatestVersionInfo();
final latestVersion = latestInfo['latestVersion'] as String;
Logger.d(_tag, '当前版本: $currentVersion, 最新版本: $latestVersion');
// 检查是否有更新
final hasUpdate = _otaCoreService.isNewVersionAvailable(currentVersion, latestVersion);
final hasUpdate =
_otaCoreService.isNewVersionAvailable(currentVersion, latestVersion);
if (hasUpdate) {
Logger.d(_tag, '发现新版本,准备显示升级提醒');
// 再次确认当前在Home页面(防止异步执行期间页面切换)
if (_isCurrentlyOnHomePage()) {
_showOtaUpgradeDialog(latestVersion, latestInfo['updateDescription'] as String?);
_showOtaUpgradeDialog(
latestVersion, latestInfo['updateDescription'] as String?);
} else {
Logger.d(_tag, '检查完成时已不在Home页面,取消显示弹窗');
}
} else {
Logger.d(_tag, '当前已是最新版本');
}
// 标记为已检查(无论是否有更新都标记为已检查)
_markAsChecked();
return true;
} catch (e) {
Logger.e(_tag, '检查OTA升级失败: $e');
// 即使检查失败,也标记为已检查,避免重复尝试
@ -116,7 +118,7 @@ class OtaCheckService extends GetxService {
return false;
}
}
/// 显示OTA升级提醒对话框
/// [latestVersion] 最新版本号
/// [updateDescription] 更新描述
@ -126,24 +128,32 @@ class OtaCheckService extends GetxService {
Logger.d(_tag, '显示弹窗时发现不在Home页面,取消显示');
return;
}
final isDarkMode = Get.isDarkMode;
Logger.d(_tag, '显示OTA升级提醒对话框');
Get.dialog(
AlertDialog(
title: Row(
children: [
Icon(
Icons.headset,
color: Colors.blue[600],
color: isDarkMode ? const Color(0xFF0066CC) : Colors.blue[600],
size: 24.sp,
),
SizedBox(width: 8.w),
Text(
'ota_newVersionFound'.tr,
style: TextStyle(
color: Colors.blue[700],
fontWeight: FontWeight.w600,
// 使用Flexible确保标题文本能够自适应不同语言长度,避免溢出
Flexible(
child: Text(
'ota_newVersionFound'.tr, // 发现新版本
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.blue[700],
fontWeight: FontWeight.w600,
fontSize: 20.sp, // 设置合适的字体大小
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
maxLines: 2, // 最多显示两行
),
),
],
@ -155,6 +165,7 @@ class OtaCheckService extends GetxService {
Text(
'${'latestFirmwareVersion'.tr}: $latestVersion',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontWeight: FontWeight.w500,
fontSize: 16.sp,
),
@ -173,7 +184,9 @@ class OtaCheckService extends GetxService {
},
child: Text(
'talkLater'.tr,
style: TextStyle(color: Colors.grey[600]),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
// 立即升级 (靠右)
@ -184,11 +197,15 @@ class OtaCheckService extends GetxService {
Get.toNamed(Routes.OTA_UPGRADE);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
backgroundColor:
isDarkMode ? const Color(0xFF0066CC) : Colors.blue[600],
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.sp),
),
side: isDarkMode
? BorderSide(color: Colors.grey[800]!, width: 1)
: BorderSide.none,
),
child: Text(
'ota_updateNow'.tr,
@ -201,12 +218,12 @@ class OtaCheckService extends GetxService {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.sp),
),
backgroundColor: Colors.white,
backgroundColor: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
),
barrierDismissible: true, // 允许点击外部关闭
);
}
/// 获取当前检查状态(用于调试)
Map<String, dynamic> getCheckStatus() {
return {

362
lib/data/services/pay_service.dart

@ -0,0 +1,362 @@
import 'package:get/get.dart';
import 'package:voitrans/data/services/network/api.dart';
import 'package:tobias/tobias.dart';
import '../../modules/login/controllers/weixinLogin.dart';
typedef PaymentResultCallback = void Function(bool success, String message);
/// 支付服务
class PayService extends GetxService {
late FlWX _flwx;
late Tobias _tobias; // 添加 Tobias 实例
@override
void onInit() {
super.onInit();
//微信
FlWX().register(
appSecret: '9ceb39df312b3ba95a229b2aa0d15496',
appId: 'wx4b5ec8957ad5e425',
partnerId: '1719983673',
universalLink: 'https://voitrans.net/app/',
params: FlWXCallbackParams(onHttp: (url) async {
return '';
}));
_flwx = FlWX();
// 初始化支付宝SDK
_tobias = Tobias(); // 初始化 Tobias 实例
_initAlipay();
}
/// 初始化支付宝SDK
Future<void> _initAlipay() async {
try {
// 检查支付宝是否安装 - 修复用法
final isInstalled = await _tobias.isAliPayInstalled;
print('支付宝是否安装: $isInstalled');
} catch (e) {
print('初始化支付宝SDK失败: $e');
}
}
/// 查询支付结果
///
/// [orderId] 订单ID
/// 返回支付状态:success, failed, pending
Future<String?> queryPaymentStatus(String orderId) async {
try {
// TODO: 调用后端API查询支付状态
// final response = await Api.queryPaymentStatus({'orderId': orderId});
// return response['status'] as String?;
// 临时返回,实际使用时需要删除
return 'pending';
} catch (e) {
print('查询支付状态失败: $e');
return null;
}
}
/// 微信支付
///
/// [paymentData] 支付参数,包含以下字段:
/// - appId: 应用ID
/// - partnerId: 商户号
/// - prepayId: 预支付交易会话ID
/// - packageValue: 扩展字段,固定值"Sign=WXPay"
/// - nonceStr: 随机字符串
/// - timestamp: 时间戳
/// - sign: 签名
///
/// [onResult] 支付结果回调
Future<bool> wechatPay({
required Map<String, dynamic> paymentData,
PaymentResultCallback? onResult,
}) async {
try {
// 检查微信是否安装
if (!await _flwx.isInstalled) {
onResult?.call(false, '微信未安装');
return false;
}
// 将支付数据转换为Payment对象
final payment = paymentData.toPayment();
if (payment == null) {
onResult?.call(false, '支付参数格式错误');
return false;
}
// 调用微信支付
final result = await _flwx.pay(
payment,
onSuccess: () {
onResult?.call(true, '支付成功');
},
onResponse: (response) {
if (response.errCode == 0) {
onResult?.call(true, '支付成功');
} else if (response.errCode == -2) {
onResult?.call(false, '用户取消支付');
} else {
onResult?.call(false, '支付失败: ${response.errStr}');
}
},
);
return result;
} catch (e) {
onResult?.call(false, '支付异常: $e');
return false;
}
}
/// 创建微信支付订单
///
/// [productId] 商品ID
/// [productName] 商品名称
/// [productDesc] 商品描述
/// [amount] 支付金额(分)
/// [paymentMethod] 支付方式(1=微信支付)
/// [clientIp] 客户端IP(可选)
/// [notifyUrl] 通知URL(可选)
/// [extraData] 扩展数据(可选)
///
/// 返回支付参数,用于调用 [wechatPay] 方法
Future<Map<String, dynamic>?> createWechatPayOrder({
required String productId,
required String productName,
String? productDesc,
required int amount,
int paymentMethod = 4, // 1表示微信支付
String? clientIp,
String? notifyUrl,
String? extraData,
}) async {
try {
// 调用后端API创建支付订单
final response = await Api.paycreateorder({
'product_id': productId,
'product_name': productName,
'product_desc': productDesc ?? '',
'amount': amount,
'payment_method': paymentMethod,
'client_ip': clientIp ?? '',
'notify_url': notifyUrl ?? '',
'extra_data': extraData ?? '',
});
print('创建支付订单: $response');
// 检查响应是否成功
if (response != null) {
final pay_params = response["pay_params"] as Map<String, dynamic>;
// 返回微信支付所需的参数
return {
'appId': pay_params['appid'],
'partnerId': pay_params['partnerid'],
'prepayId': pay_params['prepayid'],
'packageValue': pay_params['package'] ?? 'Sign=WXPay',
'nonceStr': pay_params['noncestr'],
'timestamp': pay_params['timestamp'],
'sign': pay_params['sign'],
};
} else {
print('创建支付订单失败: ${response?['message'] ?? '未知错误'}');
return null;
}
} catch (e) {
print('创建支付订单异常: $e');
return null;
}
}
Future<bool> processWechatPayment({
required String productId,
required int amount,
required String description,
String? userId,
PaymentResultCallback? onResult,
}) async {
try {
// 1. 创建支付订单
final paymentData = await createWechatPayOrder(
productId: productId,
productName: '会员充值',
productDesc: '1个月会员服务',
amount: 1, // 29.99元,单位:分
paymentMethod: 4, // 微信支付
notifyUrl: 'https://api.ideapsound.com/api/home/pay_wechatnotify',
);
if (paymentData == null) {
onResult?.call(false, '创建支付订单失败');
return false;
}
// 2. 调用微信支付
return await wechatPay(
paymentData: paymentData,
onResult: onResult,
);
} catch (e) {
onResult?.call(false, '支付流程异常: $e');
return false;
}
}
/// 完整的支付宝支付流程
///
/// [productId] 产品ID
/// [amount] 支付金额(分)
/// [description] 商品描述
/// [onResult] 支付结果回调
Future<bool> processAlipayPayment({
required String productId,
required int amount,
required String description,
PaymentResultCallback? onResult,
}) async {
try {
// 检查支付宝是否安装
final isInstalled = await _tobias.isAliPayInstalled;
if (!isInstalled) {
onResult?.call(false, '支付宝未安装');
return false;
}
// 调用现有的 createAlipayOrder 方法创建订单
final orderInfo = await createAlipayOrder(
productId: productId,
productName: description,
productDesc: description,
amount: amount,
paymentMethod: 3, // 支付宝支付
notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify',
);
if (orderInfo == null) {
onResult?.call(false, '创建支付订单失败');
return false;
}
print('支付宝支付订单: $orderInfo');
// 调用支付宝支付
final result = await _tobias.pay(orderInfo);
print('支付宝支付结果: $result');
// 处理支付结果
if (result != null) {
final resultStatus = result['resultStatus'] as String?;
final memo = result['memo'] as String?;
if (resultStatus == '9000') {
onResult?.call(true, '支付成功');
return true;
} else {
onResult?.call(false, memo ?? '支付失败');
return false;
}
} else {
onResult?.call(false, '支付结果为空');
return false;
}
} catch (e) {
onResult?.call(false, '支付异常: $e');
return false;
}
}
/// 支付宝支付
///
/// [orderInfo] 支付宝订单信息字符串
/// [onResult] 支付结果回调
Future<bool> alipay({
required String orderInfo,
PaymentResultCallback? onResult,
}) async {
try {
// 检查支付宝是否安装 - 使用实例方法
final isInstalled = await _tobias.isAliPayInstalled;
if (!isInstalled) {
onResult?.call(false, '支付宝未安装');
return false;
}
// 调用支付宝支付 - 使用实例方法
final result = await _tobias.pay(orderInfo);
if (result != null) {
// 解析支付结果
final resultStatus = result['resultStatus'] as String?;
final memo = result['memo'] as String?;
if (resultStatus == '9000') {
// 支付成功
onResult?.call(true, '支付成功');
return true;
} else {
// 支付失败或取消
onResult?.call(false, memo ?? '支付失败');
return false;
}
} else {
onResult?.call(false, '支付结果为空');
return false;
}
} catch (e) {
onResult?.call(false, '支付异常: $e');
return false;
}
}
/// 创建支付宝支付订单
///
/// [productId] 商品ID
/// [productName] 商品名称
/// [productDesc] 商品描述
/// [amount] 支付金额(分)
/// [paymentMethod] 支付方式(2=支付宝)
/// [clientIp] 客户端IP(可选)
/// [notifyUrl] 通知URL(可选)
/// [extraData] 扩展数据(可选)
///
/// 返回支付宝订单信息字符串,用于调用 [alipay] 方法
Future<String?> createAlipayOrder({
required String productId,
required String productName,
String? productDesc,
required int amount,
int paymentMethod = 2, // 2表示支付宝
String? clientIp,
String? notifyUrl,
String? extraData,
}) async {
try {
// 调用后端API创建支付订单
final response = await Api.paycreateorder({
'product_id': productId,
'product_name': productName,
'product_desc': productDesc ?? '',
'amount': amount,
'payment_method': paymentMethod,
'client_ip': clientIp ?? '',
'notify_url': notifyUrl ?? '',
'extra_data': extraData ?? '',
});
print('创建支付宝订单: $response');
// 检查响应是否成功
if (response != null && response['alipay_pay_params'] != null) {
// 返回支付宝订单信息字符串
return response['alipay_pay_params'] as String?;
} else {
print('创建支付宝订单失败: ${response?['message'] ?? '未知错误'}');
return null;
}
} catch (e) {
print('创建支付宝订单异常: $e');
return null;
}
}
}

199
lib/main.dart

@ -4,34 +4,104 @@ import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_intro/flutter_intro.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';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/bindings/initial_binding.dart';
import 'routes/app_pages.dart';
import 'routes/app_routes.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';
import 'core/bindings/initial_binding.dart';
import 'data/services/login_service.dart';
import 'dart:ui' as ui;
import 'modules/splash/controllers/splash_controller.dart';
void main() async {
// 确保Flutter绑定初始化
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// 初始化存储 - 在创建应用之前初始化
// 初始化存储
await GetStorage.init();
// 初始化环境变量
await dotenv.load();
// 加载环境变量
await dotenv.load(fileName: ".env");
print("环境变量加载成功");
// 初始化登录服务
Get.put(LoginService());
// 延迟初始化深链接处理器,确保Flutter完全准备好
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 1500), () {
_initDeepLinkHandler();
});
});
runApp(MainApp());
}
// 初始化深链接处理器
void _initDeepLinkHandler() {
print("🔗 初始化深链接处理器");
const platform = MethodChannel('com.saitong.voitrans/deeplink');
// 初始化登录服务(包含Firebase初始化)
LoginService();
platform.setMethodCallHandler((call) async {
print("🔗 Flutter 收到深链接调用: ${call.method}");
// 运行应用
runApp(const MainApp());
if (call.method == 'handleDeepLink') {
final arguments = call.arguments as Map<dynamic, dynamic>;
print("🔗 深链接参数: $arguments");
final url = arguments['url'] as String?;
if (url != null) {
print("🔗 处理深链接 URL: $url");
await _handleDeepLinkNavigation(url);
}
}
});
print("✅ 深链接处理器初始化完成");
}
// 处理深链接导航
Future<void> _handleDeepLinkNavigation(String url) async {
print("🔗 开始处理深链接导航: $url");
try {
final uri = Uri.parse(url);
print("🔗 解析URI: scheme=${uri.scheme}, host=${uri.host}, path=${uri.path}");
if (uri.scheme == 'deepvoice' && uri.host == 'qqmusic') {
print("🔗 检测到QQ音乐登录回调");
// 等待SplashController注册
int retryCount = 0;
const maxRetries = 10;
while (retryCount < maxRetries) {
try {
if (Get.isRegistered<SplashController>()) {
print("🔗 找到SplashController,处理QQ音乐登录");
final controller = Get.find<SplashController>();
controller.handleQQMusicLogin();
return;
}
} catch (e) {
print("🔗 获取SplashController失败: $e");
}
retryCount++;
print("🔗 等待SplashController注册 ($retryCount/$maxRetries)");
await Future.delayed(const Duration(milliseconds: 500));
}
print("❌ 超时等待SplashController注册");
}
} catch (e) {
print("❌ 处理深链接时出错: $e");
}
}
class MainApp extends StatelessWidget {
@ -39,48 +109,71 @@ class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取保存的主题设置
// 在应用构建时立即初始化深链接处理
_initDeepLinkHandler();
// 获取保存的主题设置
final themeMode = ThemeService().getThemeMode();
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, child) {
return GetMaterialApp(
title: 'Voitrans',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode, // 使用提前获取的主题模式
initialBinding: InitialBinding(),
initialRoute: Routes.splash,
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
translations: AppTranslations(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: AppTranslations.deviceLocale(),
fallbackLocale: AppTranslations.getSmartFallbackLocale(),
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: Intro(
padding: EdgeInsets.zero,
child: widget!,
),
);
},
),
);
},
);
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, child) {
return GetMaterialApp(
title: 'Voitrans',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode, // 使用提前获取的主题模式
initialBinding: InitialBinding(),
initialRoute: Routes.splash,
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
translations: AppTranslations(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: AppTranslations.deviceLocale(),
fallbackLocale: AppTranslations.getSmartFallbackLocale(),
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: Intro(
padding: EdgeInsets.zero,
child: widget!,
),
);
},
),
);
});
// // 使用 ScreenUtilInit 包装整个应用
// return ScreenUtilInit(
// designSize: const Size(375, 812), // 设计稿尺寸,根据你的设计稿调整
// minTextAdapt: true,
// splitScreenMode: true,
// builder: (context, child) {
// return GetMaterialApp(
// title: 'VoiTrans',
// theme: AppTheme.lightTheme,
// darkTheme: AppTheme.darkTheme,
// themeMode: ThemeService().getThemeMode(),
// initialRoute: Routes.splash,
// getPages: AppPages.pages,
// initialBinding: InitialBinding(),
// translations: AppTranslations(),
// locale: Get.deviceLocale,
// fallbackLocale: const Locale('en', 'US'),
// debugShowCheckedModeBanner: false,
// );
// },
// );
}
}

50
lib/modules/agent/controllers/agent_controller.dart

@ -721,7 +721,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// messages.refresh();
}
Get.snackbar('error'.tr, '${event.data['message']}');
// Get.snackbar('error'.tr, '${event.data['message']}');
Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break;
@ -770,7 +770,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
final responseId = event.data['responseId'] ?? '';
// final result = event.data['result'];
final metaStr = event.data['meta'] ?? '';
// final functionCall = event.data['function_call'] ?? '';
final functionCall = event.data['function_call'] ?? '';
Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr');
if (metaStr.isNotEmpty) {
Map<String, dynamic> metaResult = {};
@ -805,26 +805,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
);
}
}
final musicKey = metaResult.keys.firstWhere(
(key) => key.contains("music"),
orElse: () => "",
);
if (musicKey.isNotEmpty) {
// final qqmusic_search_songs =
// metaResult['qqmusic_searchandplay_songs']
// as Map<String, dynamic>;
// final data = qqmusic_search_songs['data'];
// if (data != null && data is List) {
Future.delayed(Duration(seconds: 2), () {
_qqmusicManager.syncCurrentPlayInfo(isOpenPlay: true);
});
// }
} else if (metaResult.containsKey("maps_route_navigation")) {
if (metaResult.containsKey("maps_route_navigation")) {
final maps_route_navigation =
metaResult["maps_route_navigation"];
if (maps_route_navigation is Map<String, dynamic>) {
final card_navigation =
maps_route_navigation["card_navigation"];
final isinternal = metaResult["isinternal"] ?? false;
if (card_navigation is Map<String, dynamic>) {
//导航
final start = card_navigation["start"] as String;
@ -841,9 +828,15 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Future.delayed(Duration(seconds: 2), () {
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
// 2秒后执行的代码
openNavigation(false, start, destination, end_latitude,
end_longitude, "");
final navigationMode =
AppConfig.env('APP_NAVIGATION_MODE') ??
'external_auto';
if (navigationMode == 'internal_gaode' || isinternal) {
_navigationManager.open(
start, "$end_longitude,$end_latitude", "driving");
}
// openNavigation(false, start, destination, end_latitude,
// end_longitude, "");
});
}
}
@ -871,6 +864,12 @@ class AgentController extends GetxController with WidgetsBindingObserver {
}
}
}
if (functionCall.contains("music")) {
Future.delayed(Duration(seconds: 1), () {
_qqmusicManager.syncCurrentPlayInfo(isOpenPlay: false);
});
// }
}
break;
default:
break;
@ -2099,12 +2098,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
return;
}
}
// 5. 最后尝试内置高德导航
_navigationManager.open(start, "$endlongitude,$endlatitude", mode);
// 5. 如果都没有安装,提示用户
print('未找到可用的导航应用');
final availableApps =
Platform.isIOS ? '百度地图、高德地图、谷歌地图或苹果地图' : '百度地图、高德地图或谷歌地图';
Get.snackbar('提示', '未找到可用的导航应用,请安装$availableApps');
// print('未找到可用的导航应用');
// final availableApps =
// Platform.isIOS ? '百度地图、高德地图、谷歌地图或苹果地图' : '百度地图、高德地图或谷歌地图';
// Get.snackbar('提示', '未找到可用的导航应用,请安装$availableApps');
} catch (e) {
print('自动选择导航失败: $e');
Get.snackbar('错误', '启动导航失败');

6
lib/modules/agent/views/message_bubble.dart

@ -160,7 +160,7 @@ class MessageBubble extends GetView<AgentController> {
);
}
if (!isUser && message.isEmpty) {
if (!isUser && message.isEmpty && meta == null) {
return _buildTypingIndicator();
}
@ -236,9 +236,9 @@ class MessageBubble extends GetView<AgentController> {
//音乐播放列表
return buildMusicContent(context, playlist);
}
} else if (meta!.keys.contains('qqmusic_searchandplay_songs')) {
} else if (meta!.keys.contains('qqmusic_search_songs')) {
final qqmusic_search_songs =
meta!['qqmusic_searchandplay_songs'] as Map<String, dynamic>;
meta!['qqmusic_search_songs'] as Map<String, dynamic>;
final data = qqmusic_search_songs['data'];
if (data != null && data is List) {
//qq音乐

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

@ -73,6 +73,11 @@ class FlWX {
/// 获取当前设置的 appSecret
String? get appSecret => _appSecret;
String? _partnerId;
/// 获取当前设置的 partnerId
String? get partnerId => _partnerId;
String _appName = '';
String token = '';
String code = '';
@ -109,6 +114,9 @@ class FlWX {
/// 用于获取用户信息
String? appSecret,
/// 商户号
String? partnerId,
/// app名称(用于分享时使用)
String appName = '',
@ -123,6 +131,7 @@ class FlWX {
_isRegister = true;
_appId = appId;
_appSecret = appSecret;
_partnerId = partnerId;
_appName = appName;
_shareThumbnail = shareThumbnail;
_params = params;

65
lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart

@ -128,7 +128,8 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
onTap: _controller.meetingDetails.value.tasktype >= 3
? () {
Get.back();
_copy(_controller.meetingDetails.value.summary);
_copy(_markdownToPlainText(
_controller.meetingDetails.value.summary));
}
: null),
]),
@ -168,8 +169,9 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
await getTemporaryDirectory();
final file = File(
'${tempDir.path}/${_controller.meetingData.value.title}_summary.txt');
await file.writeAsString(_controller
.meetingDetails.value.summary);
await file.writeAsString(
_markdownToPlainText(_controller
.meetingDetails.value.summary));
_share(file.path,
'exportSummary'.tr); // 对应中文:分享会议总结
} else {
@ -320,6 +322,63 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
);
}
/// 将Markdown格式转换为纯文本
String _markdownToPlainText(String markdown) {
String text = markdown;
// 移除粗体标记 **text** 或 __text__
text = text.replaceAllMapped(
RegExp(r'\*\*(.*?)\*\*'), (match) => match.group(1) ?? '');
text = text.replaceAllMapped(
RegExp(r'__(.*?)__'), (match) => match.group(1) ?? '');
// 移除斜体标记 *text* 或 _text_
text = text.replaceAllMapped(
RegExp(r'\*(.*?)\*'), (match) => match.group(1) ?? '');
text = text.replaceAllMapped(
RegExp(r'_(.*?)_'), (match) => match.group(1) ?? '');
// 移除删除线标记 ~~text~~
text = text.replaceAllMapped(
RegExp(r'~~(.*?)~~'), (match) => match.group(1) ?? '');
// 移除代码块标记 ```code```
text = text.replaceAll(RegExp(r'```[\s\S]*?```'), '');
// 移除行内代码标记 `code`
text = text.replaceAllMapped(
RegExp(r'`(.*?)`'), (match) => match.group(1) ?? '');
// 移除链接标记 [text](url)
text = text.replaceAllMapped(
RegExp(r'\[([^\]]+)\]\([^\)]+\)'), (match) => match.group(1) ?? '');
// 移除图片标记 ![alt](url)
text = text.replaceAllMapped(
RegExp(r'!\[([^\]]*)\]\([^\)]+\)'), (match) => match.group(1) ?? '');
// 移除标题标记 # ## ### 等
text = text.replaceAll(RegExp(r'^#{1,6}\s+', multiLine: true), '');
// 移除引用标记 >
text = text.replaceAll(RegExp(r'^>\s*', multiLine: true), '');
// 移除列表标记 - * +
text = text.replaceAll(RegExp(r'^[\s]*[-\*\+]\s+', multiLine: true), '');
// 移除有序列表标记 1. 2. 等
text = text.replaceAll(RegExp(r'^[\s]*\d+\.\s+', multiLine: true), '');
// 移除水平分割线
text =
text.replaceAll(RegExp(r'^[\s]*[-\*_]{3,}[\s]*$', multiLine: true), '');
// 清理多余的空行
text = text.replaceAll(RegExp(r'\n\s*\n\s*\n'), '\n\n');
return text.trim();
}
String _getAllText() {
String text = '';
for (var element in _controller.textList) {

6
lib/modules/ota_upgrade/controllers/ota_core_contropller.dart

@ -188,8 +188,8 @@ class OtaCoreService {
final leftBattery = deviceInfo.leftEarBattery;
final rightBattery = deviceInfo.rightEarBattery;
// 检查左右耳电量是否都大于30%
if (leftBattery <= 30 || rightBattery <= 30) {
// 检查左右耳电量是否都大于20%
if (leftBattery <= 20 || rightBattery <= 20) {
_showLowBatteryDialog(leftBattery, rightBattery);
return false;
}
@ -206,7 +206,7 @@ class OtaCoreService {
/// [rightBattery] 右耳电量
void _showLowBatteryDialog(int leftBattery, int rightBattery) {
String message = '设备电量不足,无法进行固件升级\n';
message += '升级要求:左右耳电量均需大于30%\n';
message += '升级要求:左右耳电量均需大于20%\n';
message += '当前电量:左耳 ${leftBattery}%,右耳 ${rightBattery}%\n';
message += '请充电后再试';

2
lib/modules/ota_upgrade/views/ota_upgrade_popup_view.dart

@ -80,7 +80,7 @@ class OtaUpgradePopUp {
ElevatedButton(
onPressed: () {
Get.back();
Get.toNamed(Routes.pairing);
Get.offAllNamed(Routes.pairing);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],

2
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -925,7 +925,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
SizedBox(width: 12.w),
Expanded(
child: Text(
'removeEarbudsEnsureBattery'.tr, //取出双耳机,并保证电量均为30%以上的状态
'removeEarbudsEnsureBattery'.tr, //取出双耳机,并保证电量均为20%以上的状态
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode

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

@ -1,20 +1,25 @@
import 'dart:io';
import 'dart:async';
import 'package:app_settings/app_settings.dart';
import 'package:voitrans/data/models/appconfig_model.dart';
import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart';
import '/data/services/network/api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../../../routes/app_routes.dart';
import '../../../core/utils/logger.dart';
import 'dart:io';
import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart';
import '/data/services/network/api.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../../core/utils/permission_util.dart';
import '../../../data/services/version_update_service.dart';
/// 配对状态枚举 - 简化版本
enum PairingState {
@ -48,6 +53,7 @@ enum ErrorType {
class PairingController extends GetxController with WidgetsBindingObserver {
static const String _tag = 'PairingCtrl';
static PairingController get to => Get.find();
final _storage = GetStorage();
// ========== 状态管理 ==========
final _currentState = PairingState.idle.obs;
@ -89,6 +95,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
// ========== 当前设备信息 ==========
BluetoothDevice? _currentClassicDevice;
int? _verifiedDevicePid;
String? _verifiedDeviceName;
String? _verifiedDeviceAddress;
TextEditingController codeInputController = TextEditingController();
@ -96,6 +103,12 @@ class PairingController extends GetxController with WidgetsBindingObserver {
// Public getter for view access
BluetoothDevice? get currentDevice => _currentClassicDevice;
//华为设备权限检查标记
bool isHuaweiHasChecked = false;
// 缓存的设备品牌信息
final versionService = Get.find<VersionUpdateService>();
bool isHuaweiDevice = false;
@override
void onInit() {
super.onInit();
@ -106,6 +119,8 @@ class PairingController extends GetxController with WidgetsBindingObserver {
WidgetsBinding.instance.addObserver(this);
Logger.i(_tag, '应用生命周期监听器注册完成');
isHuaweiDevice = _isHuaweiDevice();
// 确保服务已初始化
_bluetoothManager = Get.find<BluetoothManager>();
_bleManager = Get.find<BleManager>();
@ -113,6 +128,11 @@ class PairingController extends GetxController with WidgetsBindingObserver {
// 集中设置所有监听器
_setupAllListeners();
if (isHuaweiDevice) {
//检查华为设备是否检查权限检查过
isHuaweiHasChecked = _storage.read('isHuaweiHasChecked') ?? false;
}
// 开始初始状态检查
// 延迟稍长时间,确保BluetoothManager有足够时间获取已连接设备
Future.delayed(const Duration(milliseconds: 300), () {
@ -548,8 +568,26 @@ class PairingController extends GetxController with WidgetsBindingObserver {
return _bluetoothManager.bluetoothState != 'off';
}
/// 检查是否为华为设备
bool _isHuaweiDevice() {
final deviceBrand = versionService.cachedDeviceBrand;
return deviceBrand.isNotEmpty &&
(deviceBrand.contains('huawei') || deviceBrand.contains('华为'));
}
/// 检查权限
Future<bool> _checkPermissions() async {
//如果是华为设备,且已经检查过,直接返回权限状态
if (isHuaweiDevice && isHuaweiHasChecked) {
final bluetoothScanStatus = await Permission.bluetoothScan.status;
final bluetoothConnectStatus = await Permission.bluetoothConnect.status;
final locationStatus = await Permission.locationWhenInUse.status;
return bluetoothScanStatus.isGranted &&
bluetoothConnectStatus.isGranted &&
locationStatus.isGranted;
}
// Android权限检查
if (Platform.isAndroid) {
Logger.i(_tag, '开始检查Android权限');
@ -575,6 +613,9 @@ class PairingController extends GetxController with WidgetsBindingObserver {
permanentDenialText: 'locationPermanentDenial'.tr,
);
if (isHuaweiDevice) {
_storage.write('isHuaweiHasChecked', true);
}
return bluetoothScanGranted && bluetoothConnectGranted && locationGranted;
}
return true;
@ -730,10 +771,10 @@ class PairingController extends GetxController with WidgetsBindingObserver {
final deviceName = device.name;
DBProduct? product;
// 检查是否已有BLE绑定的设备
/// 检查是否已有BLE绑定的设备
// 检查是否已有BLE绑定的设备
if (AppConfig.config.products.isNotEmpty) {
final match = AppConfig.config.products.where(
(d) => d.devicename == device.name,
(d) => d.devicename.contains(deviceName),
);
product = match.isNotEmpty ? match.first : null;
}
@ -746,7 +787,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
final existingDevice = User.instance.devices.firstWhere(
(d) =>
d.devicetype == 1 &&
d.devicename == device.name &&
d.devicename.contains(deviceName) &&
d.devicemac == device.address,
orElse: () => UserDevice(
id: -1,
@ -772,13 +813,12 @@ class PairingController extends GetxController with WidgetsBindingObserver {
_bleManager.disconnect();
}
}
// 检查是否已有BLE绑定的设备
if (Platform.isAndroid) {
// 检查是否已有BLE绑定的设备
final existingDevice = User.instance.devices.firstWhere(
(d) =>
d.devicetype == 2 &&
d.devicename == device.name &&
d.devicename.contains(deviceName) &&
d.devicemac == device.address,
orElse: () => UserDevice(
id: -1,
@ -797,7 +837,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
}
print(
'获取到 deviceName: $deviceName address: ${device.address} uuids:$uuids');
if (names.contains(deviceName)) {
if (product != null && product.devicetype != 1) {
if (currentBleState == 2) {
//已连接直接返回
return 9;
@ -821,10 +861,13 @@ class PairingController extends GetxController with WidgetsBindingObserver {
String macFromHex = extractMacFromHex(hexData, device.address);
if (macFromHex.isNotEmpty) {
String code = "";
// 找到匹配的设备,提取授权码
if (Platform.isIOS) {
// iOS设备,提取13-22字节的授权码
code = extractAndReverseBytes(hexData, 13, 22);
_verifiedDevicePid =
extractAndReverseBytesToInt(hexData, 4, 5);
print('找到匹配设备,获取到 code: $code');
if (code != "") {
_bleManager.stopScan();
@ -859,6 +902,8 @@ class PairingController extends GetxController with WidgetsBindingObserver {
} else if (Platform.isAndroid) {
// Android设备,提取15-24字节的授权码
code = extractAndReverseBytes(hexData, 15, 24);
_verifiedDevicePid =
extractAndReverseBytesToInt(hexData, 6, 7);
print('找到匹配设备,获取到 code: $code');
if (code != "") {
_bleManager.stopScan();
@ -898,12 +943,10 @@ class PairingController extends GetxController with WidgetsBindingObserver {
await Future.delayed(Duration(seconds: 1));
}
_bleManager.stopScan();
if (product != null && product.devicetype != 0) {
//ble设备 必须要扫描到结果
return 0;
} else {
return 1;
}
// if (product != null && product.devicetype != 1) {
// //ble设备 必须要扫描到结果
// return 0;
// }
}
return 0;
//可以跳转1
@ -970,7 +1013,14 @@ class PairingController extends GetxController with WidgetsBindingObserver {
//确认绑定设备
void confirmBindDevice() async {
if (currentDevice == null) {
Logger.e(_tag, '当前设备为空!');
_transition(PairingState.verifying);
return;
}
final response = await Api.binddevice({
"pid": _verifiedDevicePid,
"code": deviceCode.value,
"devicename": currentDevice?.name,
"devicemac": currentDevice?.address, // 使用 address 替代 deviceId
@ -1010,6 +1060,31 @@ class PairingController extends GetxController with WidgetsBindingObserver {
.toUpperCase();
}
int extractAndReverseBytesToInt(String hex, int start, int end) {
List<int> bytes = [];
// 将十六进制字符串解析为字节列表
for (int i = 0; i < hex.length; i += 2) {
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
}
// 截取指定范围的字节(包含end索引)
List<int> slice = bytes.sublist(start, end + 1);
// 反转字节顺序
List<int> reversed = slice.toList();
String pid = reversed
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
Logger.i(_tag, 'liwei-------------- 解析道Pid $pid');
// 将反转后的字节列表转换为整数
int result = 0;
for (int b in reversed) {
result = (result << 8) | (b & 0xFF); // 按字节拼接成整数
}
return result;
}
/// 用户点击授权码输入框
void onAutoCodeInputTap() async {
// 调用BLE管理器的自动绑定方法

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

@ -72,56 +72,60 @@ class _PairingViewState extends State<PairingView>
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
body: SafeArea(
child: _buildMainContent(isDarkMode),
child: Column(
children: [
// 固定顶部区域 - 不受下方内容影响
_buildFixedTopSection(isDarkMode),
// 可滚动的动态内容区域
Expanded(
child: _buildDynamicContentSection(isDarkMode),
),
],
),
),
// bottomNavigationBar: Opacity(
// opacity: 0.5,
// child: const CommonBottomNav(currentIndex: 0),
// ),
);
}
// 主要内容
Widget _buildMainContent(bool isDarkMode) {
return Container(
width: double.infinity,
height: MediaQuery.of(Get.context!).size.height,
padding: EdgeInsets.symmetric(horizontal: 24.w),
// 添加SingleChildScrollView以防止溢出
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
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),
// 固定顶部区域
Widget _buildFixedTopSection(bool isDarkMode) {
return Padding(
padding: EdgeInsets.only(top: 60.h, left: 24.w, right: 24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 标题部分
_buildHeader(isDarkMode),
SizedBox(height: 20.h),
// 设备动画 - 根据状态显示不同动画
_buildDeviceAnimation(isDarkMode),
_buildDeviceAnimation(isDarkMode),
],
),
);
}
SizedBox(height: 20.h),
// 动态内容区域
Widget _buildDynamicContentSection(bool isDarkMode) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 30.h),
// 状态信息
_buildStatusInfo(isDarkMode),
// 状态信息
_buildStatusInfo(isDarkMode),
SizedBox(height: 40.h),
SizedBox(height: 40.h),
// 底部按钮
_buildActionButtons(isDarkMode),
// 底部按钮
_buildActionButtons(isDarkMode),
SizedBox(height: 20.h),
],
),
SizedBox(height: 20.h),
],
),
),
);
@ -488,7 +492,8 @@ class _PairingViewState extends State<PairingView>
return 'connectDeepSoundViaSystemBluetooth'
.tr; // 请先通过系统蓝牙连接Voitrans设备
case ErrorType.deviceNotMatch:
return 'ensureConnectedDeepSoundDevice'.tr; // 请确保连接的是Voitrans设备
return 'ensureConnectedDeepSoundDevice'.tr.trArgs(
[controller.currentDevice?.name ?? '']); // 请确保连接的是Voitrans设备
case ErrorType.deviceBound:
return 'deviceAlreadyBoundToOtherAccount'.tr; // 该设备已被其他账号绑定
case ErrorType.associationFailed:

9
lib/modules/payment/bindings/payment_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/payment_controller.dart';
class PaymentBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<PaymentController>(() => PaymentController());
}
}

143
lib/modules/payment/controllers/payment_controller.dart

@ -0,0 +1,143 @@
import 'package:get/get.dart';
import '../../../data/services/pay_service.dart';
class PaymentController extends GetxController {
final PayService _payService = Get.find<PayService>();
// 支付状态
final RxBool isLoading = false.obs;
final RxString paymentStatus = ''.obs;
final RxString errorMessage = ''.obs;
// 测试订单信息
final RxString testProductId = 'vip_101'.obs;
final RxDouble testAmount = 0.01.obs; // 测试金额 1分钱
// 支付方式选择
final RxInt selectedPaymentMethod = 1.obs; // 1=微信支付, 2=支付宝支付
@override
void onInit() {
super.onInit();
// 生成测试订单ID
// testProductId.value = 'test_${DateTime.now().millisecondsSinceEpoch}';
}
/// 测试微信支付
Future<void> testWechatPayment() async {
try {
isLoading.value = true;
paymentStatus.value = '正在发起微信支付...';
errorMessage.value = '';
// 调用支付服务,使用回调处理结果
final launched = await _payService.processWechatPayment(
productId: testProductId.value,
amount: (testAmount.value * 100).toInt(), // 转换为分
description: '微信支付测试',
onResult: (success, message) {
// 支付结果回调
isLoading.value = false;
if (success) {
paymentStatus.value = '微信支付成功!';
errorMessage.value = '';
Get.snackbar(
'支付成功',
message ?? '微信支付测试完成',
snackPosition: SnackPosition.TOP,
);
} else {
paymentStatus.value = '微信支付失败';
errorMessage.value = message ?? '未知错误';
Get.snackbar(
'支付失败',
message ?? '未知错误',
snackPosition: SnackPosition.TOP,
);
}
},
);
if (!launched) {
// 如果支付没有成功启动
isLoading.value = false;
paymentStatus.value = '微信支付启动失败';
errorMessage.value = '无法启动微信支付';
Get.snackbar('支付启动失败', '无法启动微信支付');
}
} catch (e) {
isLoading.value = false;
paymentStatus.value = '微信支付异常';
errorMessage.value = e.toString();
Get.snackbar('支付异常', e.toString());
}
}
/// 测试支付宝支付
Future<void> testAlipayPayment() async {
try {
isLoading.value = true;
paymentStatus.value = '正在发起支付宝支付...';
errorMessage.value = '';
// 调用支付宝支付服务
final launched = await _payService.processAlipayPayment(
productId: testProductId.value, // 这里传递的是产品ID
amount: (testAmount.value * 100).toInt(), // 转换为分
description: '支付宝支付测试',
onResult: (success, message) {
// 支付结果回调
isLoading.value = false;
if (success) {
paymentStatus.value = '支付宝支付成功!';
errorMessage.value = '';
} else {
paymentStatus.value = '支付宝支付失败';
errorMessage.value = message;
}
},
);
if (!launched) {
// 如果支付没有成功启动
isLoading.value = false;
paymentStatus.value = '支付宝支付启动失败';
errorMessage.value = '无法启动支付宝支付';
Get.snackbar('支付启动失败', '无法启动支付宝支付');
}
} catch (e) {
isLoading.value = false;
paymentStatus.value = '支付宝支付异常';
errorMessage.value = e.toString();
Get.snackbar('支付异常', e.toString());
}
}
/// 根据选择的支付方式进行支付
Future<void> processPayment() async {
if (selectedPaymentMethod.value == 1) {
await testWechatPayment();
} else if (selectedPaymentMethod.value == 2) {
await testAlipayPayment();
}
}
/// 选择支付方式
void selectPaymentMethod(int method) {
selectedPaymentMethod.value = method;
}
/// 重置状态
void resetStatus() {
paymentStatus.value = '';
errorMessage.value = '';
// testProductId.value = 'test_${DateTime.now().millisecondsSinceEpoch}';
}
/// 更新测试金额
void updateTestAmount(double amount) {
testAmount.value = amount;
}
}

506
lib/modules/payment/views/payment_view.dart

@ -0,0 +1,506 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/payment_controller.dart';
class PaymentView extends GetView<PaymentController> {
const PaymentView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'支付测试',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () => Get.back(),
),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTestInfoCard(isDarkMode),
SizedBox(height: 20.h),
_buildPaymentMethodSelector(isDarkMode),
SizedBox(height: 20.h),
_buildAmountSelector(isDarkMode),
SizedBox(height: 20.h),
_buildPaymentButton(isDarkMode),
SizedBox(height: 20.h),
_buildStatusCard(isDarkMode),
SizedBox(height: 20.h),
_buildInstructions(isDarkMode),
],
),
),
);
}
/// 测试信息卡片
Widget _buildTestInfoCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'测试订单信息',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 12.h),
Obx(() => _buildInfoRow(
'商品ID', controller.testProductId.value, isDarkMode)),
SizedBox(height: 8.h),
_buildInfoRow('商品描述', '微信支付测试', isDarkMode),
SizedBox(height: 8.h),
Obx(() => _buildInfoRow(
'支付金额',
'¥${controller.testAmount.value.toStringAsFixed(2)}',
isDarkMode)),
],
),
);
}
/// 信息行
Widget _buildInfoRow(String label, String value, bool isDarkMode) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80.w,
child: Text(
'$label:',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
],
);
}
/// 金额选择器
Widget _buildAmountSelector(bool isDarkMode) {
final amounts = [0.01, 0.10, 1.00, 10.00];
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择测试金额',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 12.h),
Wrap(
spacing: 12.w,
runSpacing: 8.h,
children: amounts.map((amount) {
return Obx(() => GestureDetector(
onTap: () => controller.updateTestAmount(amount),
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: controller.testAmount.value == amount
? Colors.blue[600]
: (isDarkMode
? Colors.grey[800]
: Colors.grey[200]),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
'¥${amount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 14.sp,
color: controller.testAmount.value == amount
? Colors.white
: (isDarkMode ? Colors.white : Colors.black),
),
),
),
));
}).toList(),
),
],
),
);
}
/// 支付方式选择器
Widget _buildPaymentMethodSelector(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择支付方式',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 16.h),
// 移除外层的 Obx,因为内部的 _buildPaymentMethodOption 已经有 Obx 了
Column(
children: [
_buildPaymentMethodOption(
method: 1,
title: '微信支付',
icon: Icons.wechat,
iconColor: Colors.green,
isDarkMode: isDarkMode,
),
SizedBox(height: 12.h),
_buildPaymentMethodOption(
method: 2,
title: '支付宝支付',
icon: Icons.account_balance_wallet,
iconColor: Colors.blue,
isDarkMode: isDarkMode,
),
],
),
],
),
);
}
/// 支付方式选项
Widget _buildPaymentMethodOption({
required int method,
required String title,
required IconData icon,
required Color iconColor,
required bool isDarkMode,
}) {
return Obx(() => GestureDetector(
onTap: () => controller.selectPaymentMethod(method),
child: Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: controller.selectedPaymentMethod.value == method
? (isDarkMode
? Colors.blue.withOpacity(0.2)
: Colors.blue.withOpacity(0.1))
: (isDarkMode ? Colors.grey[800] : Colors.grey[100]),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: controller.selectedPaymentMethod.value == method
? Colors.blue
: Colors.transparent,
width: 1.5,
),
),
child: Row(
children: [
Icon(
icon,
color: iconColor,
size: 24.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
if (controller.selectedPaymentMethod.value == method)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.sp,
),
],
),
),
));
}
/// 支付按钮
Widget _buildPaymentButton(bool isDarkMode) {
return Obx(() => Container(
width: double.infinity,
height: 50.h,
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: () {
controller.processPayment();
},
style: ElevatedButton.styleFrom(
backgroundColor: controller.isLoading.value
? Colors.grey
: (controller.selectedPaymentMethod.value == 1
? Colors.green
: Colors.blue),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
elevation: 2,
),
child: controller.isLoading.value
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
),
SizedBox(width: 12.w),
Text(
'支付中...',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
controller.selectedPaymentMethod.value == 1
? Icons.wechat
: Icons.account_balance_wallet,
color: Colors.white,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
controller.selectedPaymentMethod.value == 1
? '微信支付 ¥${controller.testAmount.value.toStringAsFixed(2)}'
: '支付宝支付 ¥${controller.testAmount.value.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
));
}
/// 状态卡片
Widget _buildStatusCard(bool isDarkMode) {
return Obx(() {
if (controller.paymentStatus.value.isEmpty) {
return const SizedBox.shrink();
}
final isError = controller.errorMessage.value.isNotEmpty;
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isError
? (isDarkMode ? Colors.red[900] : Colors.red[50])
: (isDarkMode ? Colors.green[900] : Colors.green[50]),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isError ? Colors.red[300]! : Colors.green[300]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isError ? Icons.error : Icons.check_circle,
color: isError ? Colors.red[600] : Colors.green[600],
size: 20.w,
),
SizedBox(width: 8.w),
Text(
'支付状态',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isError ? Colors.red[600] : Colors.green[600],
),
),
],
),
SizedBox(height: 8.h),
Text(
controller.paymentStatus.value,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
if (controller.errorMessage.value.isNotEmpty) ...[
SizedBox(height: 8.h),
Text(
'错误信息: ${controller.errorMessage.value}',
style: TextStyle(
fontSize: 12.sp,
color: Colors.red[600],
),
),
],
SizedBox(height: 12.h),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: controller.resetStatus,
style: OutlinedButton.styleFrom(
side: BorderSide(
color: isError ? Colors.red[600]! : Colors.green[600]!,
),
),
child: Text(
'重新测试',
style: TextStyle(
color: isError ? Colors.red[600] : Colors.green[600],
),
),
),
),
],
),
);
});
}
/// 使用说明
Widget _buildInstructions(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.blue[50],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.blue[200]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Colors.blue[600],
size: 20.w,
),
SizedBox(width: 8.w),
Text(
'使用说明',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.blue[600],
),
),
],
),
SizedBox(height: 12.h),
Text(
'1. 选择测试金额(建议使用0.01元进行测试)\n'
'2. 点击"发起微信支付"按钮\n'
'3. 系统将调用微信支付SDK\n'
'4. 在微信中完成支付流程\n'
'5. 返回应用查看支付结果\n\n'
'注意:这是测试环境,实际不会产生真实扣费',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
height: 1.5,
),
),
],
),
);
}
}

2
lib/modules/permissions/controllers/permissions_controller.dart

@ -184,7 +184,7 @@ class PermissionsController extends GetxController {
);
if (hasPermission) {
_showSuccessMessage();
// _showSuccessMessage();
if (Platform.isIOS && permission == Permission.calendarFullAccess) {
if (_isCalendarFullAccess == null) {
_storage.write('record_permission_calendar', true);

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

@ -750,11 +750,11 @@ class EditProfileView extends GetView<ProfileController> {
title: 'clean'.tr, // 清理缓存
icon: Icons.cleaning_services,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
: Colors.red[50]!,
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
titleColor:
isDarkMode ? Colors.red[300]! : Colors.red[600]!,
? Colors.blue[900]!.withOpacity(0.3)
: Colors.blue[50]!,
iconColor:
isDarkMode ? Colors.blue[300]! : Colors.blue[600]!,
titleColor: isDarkMode ? Colors.white : Colors.black87,
onTap: () {
// 退出登录确认对话框
Get.dialog(
@ -816,11 +816,12 @@ class EditProfileView extends GetView<ProfileController> {
title: 'logout'.tr, // 退出登录
icon: Icons.logout,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
: Colors.red[50]!,
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[50]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
titleColor:
isDarkMode ? Colors.red[300]! : Colors.red[600]!,
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
onTap: () {
// 退出登录确认对话框
Get.dialog(

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

@ -301,9 +301,11 @@ class SettingsController extends GetxController {
}
}
/// 设置语言
Future<void> setLanguage(LanguageModel data) async {
try {
Locale? targetLocale;
// print('lxm--设置语言: ${data.value}');
if (data.value != 'system') {
// 用户手动选择的语言
@ -311,7 +313,11 @@ class SettingsController extends GetxController {
targetLocale = Locale(languageList[0], languageList[1]);
} else {
// 跟随系统语言,使用智能匹配
targetLocale = AppTranslations.deviceLocale();
targetLocale = Get.deviceLocale;
if (targetLocale != null) {
// 智能匹配系统语言到支持的语言
targetLocale = AppTranslations.getMatchedLocale(targetLocale);
}
}
if (targetLocale != null) {

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

@ -514,28 +514,56 @@ class SettingsView extends GetView<SettingsController> {
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
//BLE测试
_buildSimpleNavigationSetting(
title: 'opus解码测试',
subtitle: '测试opus解码',
icon: Icons.bluetooth_searching,
iconBgColor: isDarkMode
? Colors.green[900]!.withOpacity(0.3)
: Colors.green[100]!,
iconColor:
isDarkMode ? Colors.green[300]! : Colors.green[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// //BLE测试
// 支付测试
// _buildSimpleNavigationSetting(
// title: '支付测试',
// subtitle: '测试微信支付功能',
// icon: Icons.payment,
// iconBgColor: isDarkMode
// ? Colors.green[900]!.withOpacity(0.3)
// : Colors.green[100]!,
// iconColor:
// isDarkMode ? Colors.green[300]! : Colors.green[600]!,
// titleColor: isDarkMode ? Colors.white : null,
// subtitleColor: isDarkMode ? Colors.white70 : null,
// onTap: () {
// Get.toNamed(Routes.payment);
// },
// isDarkMode: isDarkMode,
// ),
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
Obx(() => controller.showDebugInfo.value
? _buildSimpleNavigationSetting(
title: 'opus解码测试',
subtitle: '测试opus解码',
icon: Icons.bluetooth_searching,
iconBgColor: isDarkMode
? Colors.green[900]!.withOpacity(0.3)
: Colors.green[100]!,
iconColor: isDarkMode
? Colors.green[300]!
: Colors.green[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
)
: const SizedBox.shrink()),
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// BLE测试
// _buildSimpleNavigationSetting(

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

@ -27,12 +27,20 @@ class SplashController extends GetxController {
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 隐私政策同意状态 - 新增
final _hasAcceptedPrivacy = false.obs;
RxBool get hasAcceptedPrivacy => _hasAcceptedPrivacy;
// 使用一个反应式变量来决定是否可以导航,替代所有布尔标记
final _canNavigate = false.obs;
@override
void onInit() {
super.onInit();
// 初始化隐私政策状态
_hasAcceptedPrivacy.value =
_storage.read<bool>('hasAcceptedPrivacy') ?? false;
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航
once(_canNavigate, (_) => _navigateToTargetPage());
// 启动整个初始化流程
@ -59,10 +67,17 @@ class SplashController extends GetxController {
_canNavigate.value = true;
}
/// 切换隐私政策同意状态 - 新增方法
void togglePrivacyAcceptance() {
_hasAcceptedPrivacy.value = !_hasAcceptedPrivacy.value;
_storage.write('hasAcceptedPrivacy', _hasAcceptedPrivacy.value);
}
/// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作
/// 返回用户是否同意
Future<bool> _checkAndRequestPrivacy() async {
if (_storage.read<bool>('hasAcceptedPrivacy') ?? false) {
_hasAcceptedPrivacy.value = true;
return true;
}
@ -71,6 +86,7 @@ class SplashController extends GetxController {
PrivacyPolicyDialog.show(
onAccept: () {
_storage.write('hasAcceptedPrivacy', true);
_hasAcceptedPrivacy.value = true;
Get.back(); // 关闭弹窗
if (!completer.isCompleted) completer.complete(true);
},
@ -289,6 +305,21 @@ class SplashController extends GetxController {
return false;
}
}
/// 处理QQ音乐登录回调
void handleQQMusicLogin() {
try {
Logger.i(_tag, '处理QQ音乐登录回调');
// 这里可以添加具体的QQ音乐登录处理逻辑
// 例如:验证登录状态、更新用户信息、导航到相应页面等
// 如果需要导航到特定页面,可以使用:
// Get.toNamed(Routes.qqmusic);
} catch (e) {
Logger.e(_tag, '处理QQ音乐登录回调失败: $e');
}
}
}
/// 显示无网络连接对话框并等待用户操作(阻塞版本)

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

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/splash_controller.dart';
import '../../common/webview_page.dart';
class SplashView extends GetView<SplashController> {
const SplashView({Key? key}) : super(key: key);
@ -39,6 +40,14 @@ class SplashView extends GetView<SplashController> {
),
),
// 隐私政策组件 - 添加在底部
Positioned(
bottom: 90.h,
left: 0,
right: 0,
child: _buildTermsAndPrivacy(isDarkMode),
),
// 版本号
//_buildVersionInfo(isDarkMode),
],
@ -206,6 +215,112 @@ class SplashView extends GetView<SplashController> {
);
}
// 用户协议和隐私政策
Widget _buildTermsAndPrivacy(bool isDarkMode) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 将勾选框和文本包装在 GestureDetector 中
GestureDetector(
//onTap: () => controller.togglePrivacyAcceptance(),
behavior: HitTestBehavior.opaque, // 确保透明区域也能响应点击
child: Row(
children: [
SizedBox(
width: 12.w,
height: 16.w,
child: Transform.scale(
scale: 0.7,
child: Obx(() => Checkbox(
value: controller.hasAcceptedPrivacy.value,
onChanged: (value) => controller
.hasAcceptedPrivacy.value = value ?? false,
activeColor: const Color(0xFF3B82F6),
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
side: BorderSide(
width: 1.6,
color: Colors.grey[600]!,
),
)),
),
),
SizedBox(width: 4.w),
Text(
'expressConsent'.tr, // 登录/注册即表示你同意
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
],
),
),
],
),
//SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
// onTap: () {
// String url = 'https://www.voitrans.net/doc/yhxy.html'; // 用户协议链接
// if (Get.locale?.languageCode != 'zh' ||
// Get.locale?.countryCode != 'CN') {
// url = 'https://www.voitrans.net/doc/yhxy_en.html'; // 用户协议英文链接
// }
// Get.to(() => WebViewPage(
// url: url,
// title: 'userAgreement'.tr,
// ));
// },
child: Text(
'userAgreement'.tr, // 《用户协议》
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF3B82F6),
),
),
),
Text(
'and'.tr, // 和
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
),
),
GestureDetector(
// onTap: () {
// String url = 'https://www.voitrans.net/doc/ysxy.html'; // 隐私政策链接
// if (Get.locale?.languageCode != 'zh' ||
// Get.locale?.countryCode != 'CN') {
// url = 'https://www.voitrans.net/doc/ysxy_en.html'; // 隐私政策英文链接
// }
// Get.to(() => WebViewPage(
// url: url,
// title: 'privacyPolicy'.tr,
// ));
// },
child: Text(
'privacyPolicy'.tr, // 《隐私政策》
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF3B82F6),
),
),
),
],
),
],
);
}
// 版本信息
// 暂时不需要版本号显示,保留代码以备将来使用
// Widget _buildVersionInfo(bool isDarkMode) {

148
lib/modules/translation/controllers/translation_controller.dart

@ -16,6 +16,7 @@ import '../../../data/services/tts_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/permission_util.dart';
import '../../../core/services/floating_window_manager.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/translation_usage_service.dart';
@ -23,6 +24,7 @@ import '../../../data/services/ble_manager.dart';
import 'record_timer_controller.dart';
import 'translation_history_controller.dart';
import '../models/translation_models.dart';
import '../views/dialogs/translation_mode_selector.dart';
/// 音频输出设备枚举
enum AudioOutputDevice {
@ -47,6 +49,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
final BleManager bleManager = Get.find<BleManager>();
final _musiceManager = Get.find<MusicManager>();
late final TranslationUsageService _usageService;
late final FloatingWindowManager _floatingWindowManager;
// ==================== 悬浮窗相关 ====================
final isFloatingWindowEnabled = false.obs;
// ==================== ASR统计相关 ====================
DateTime? _asrSessionStartTime; // ASR会话开始时间
@ -129,6 +135,100 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 底部控制栏高度
double _bottomBarHeight = 0.0;
/// 初始化悬浮窗管理器
void _initFloatingWindowManager() {
try {
if (!Get.isRegistered<FloatingWindowManager>()) {
Get.put(FloatingWindowManager(), permanent: true);
}
_floatingWindowManager = Get.find<FloatingWindowManager>();
Logger.info('悬浮窗管理器初始化完成');
} catch (e) {
Logger.error('悬浮窗管理器初始化失败: $e');
}
}
/// 切换悬浮窗功能
Future<void> toggleFloatingWindow() async {
if (isFloatingWindowEnabled.value) {
await _disableFloatingWindow();
} else {
await _enableFloatingWindow();
}
}
/// 启用悬浮窗功能
Future<void> _enableFloatingWindow() async {
// 检查是否在支持悬浮窗的模式
if (!_isFloatingWindowSupportedMode()) {
// Get.snackbar(
// 'unsupportedMode'.tr,
// '悬浮窗仅支持通话和音视频翻译模式',
// snackPosition: SnackPosition.TOP,
// );
return;
}
final bool success = await _floatingWindowManager.enableFloatingWindow();
if (success) {
isFloatingWindowEnabled.value = true;
await _floatingWindowManager.showFloatingWindow();
// 如果当前有翻译内容,立即更新到悬浮窗
if (translationHistory.isNotEmpty) {
final latestItem = translationHistory.last;
await _updateFloatingWindowWithTranslation(latestItem);
}
// Get.snackbar(
// '悬浮窗已启用',
// '翻译结果将在悬浮窗中显示',
// snackPosition: SnackPosition.TOP,
// );
}
}
/// 禁用悬浮窗功能
Future<void> _disableFloatingWindow() async {
await _floatingWindowManager.disableFloatingWindow();
isFloatingWindowEnabled.value = false;
// Get.snackbar(
// '悬浮窗已禁用',
// '翻译结果将在应用内显示',
// snackPosition: SnackPosition.TOP,
// );
}
/// 检查当前模式是否支持悬浮窗
bool _isFloatingWindowSupportedMode() {
return currentMode.value == 'call' || currentMode.value == 'audioVideo';
}
/// 更新悬浮窗翻译内容
Future<void> _updateFloatingWindowWithTranslation(
TranslationItem item) async {
if (!isFloatingWindowEnabled.value) return;
final sourceLanguageName =
_languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ??
'未知';
final targetLanguageName =
_languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ??
'未知';
final data = FloatingWindowData(
sourceText: item.sourceText,
translatedText: item.translatedText,
sourceLanguage: sourceLanguageName,
targetLanguage: targetLanguageName,
timestamp: item.timestamp,
isIntermediate: item.isIntermediate,
);
await _floatingWindowManager.updateFloatingWindowContent(data);
}
/// 更改翻译模式
/// [mode] 翻译模式:simultaneous(同声传译)、audioVideo(音视频)、call(通话)、faceToFace(面对面)
Future<void> changeTranslationMode(String mode) async {
@ -136,6 +236,26 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
currentModeTitle.value = _getModeTitle(mode);
stopAll();
// 检查悬浮窗支持情况
if (isFloatingWindowEnabled.value && !_isFloatingWindowSupportedMode()) {
// 当前模式不支持悬浮窗,自动禁用
await _disableFloatingWindow();
// Get.snackbar(
// '模式已切换',
// '悬浮窗仅支持通话和音视频翻译模式',
// snackPosition: SnackPosition.TOP,
// );
} else if (!isFloatingWindowEnabled.value &&
_isFloatingWindowSupportedMode()) {
// 切换到支持悬浮窗的模式,提示用户可以启用
// Get.snackbar(
// '模式已切换',
// '可以启用悬浮窗显示翻译结果',
// snackPosition: SnackPosition.TOP,
// duration: const Duration(seconds: 4),
// );
}
if (mode == 'call') {
await _initializeCallModeTranslationService();
} else {
@ -252,6 +372,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
_timerManager = RecordTimerController();
_historyManager = TranslationHistoryManager();
// 初始化悬浮窗管理器
_initFloatingWindowManager();
// 预先检查权限,避免在用户交互时阻塞
_preCheckPermissions();
@ -285,6 +408,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker
WidgetsBinding.instance.addObserver(this);
// 延迟显示模式选择器,确保页面已经完全加载
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// // 确保上下文有效
// if (Get.context == null || !Get.context!.mounted) return;
// _showModeSelector();
// });
}
/// 预先检查权限
@ -339,6 +470,18 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
}
}
/// 显示模式选择器
void _showModeSelector() {
try {
final isDarkMode = Get.theme.brightness == Brightness.dark;
// 直接使用现有的翻译模式选择器组件
TranslationModeSelector.show(isDarkMode);
} catch (e) {
Logger.error('显示模式选择器失败: $e');
}
}
/// 保存悬浮按钮位置
void saveFloatingButtonPosition(Offset position) {
floatingButtonPosition.value = position;
@ -984,6 +1127,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
translationHistory[index].targetLanguageCode = languageCodes['target']!;
translationHistory.refresh();
// 更新悬浮窗内容
if (isFloatingWindowEnabled.value) {
await _updateFloatingWindowWithTranslation(translationHistory[index]);
}
// 添加TranslationItem日志打印
if (isFinal) {
final item = translationHistory[index];

162
lib/modules/translation/controllers/translation_detail_controller.dart

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/services/volcano_ai_service.dart';
// import '../../../data/services/volcano_ai_service.dart';// 注释掉火山AI服务导入
import '../controllers/translation_controller.dart';
import 'translation_audiohistory_controller.dart';
import '../models/translation_models.dart'; // 添加这行导入
@ -12,7 +12,7 @@ import '../../../data/services/translation_usage_service.dart';
//详情页管理
class TranslationDetailController extends GetxController {
// 依赖注入
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
//final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final GetStorage _storage = GetStorage();
late final TranslationUsageService _usageService;
@ -124,89 +124,89 @@ class TranslationDetailController extends GetxController {
sessionItems.map((e) => e.translatedText).join('\n\n');
// 使用延长的超时时间重试提取摘要
Future<String> retryExtractSummaryWithExtendedTimeout(String text) async {
return _extractSummaryInternal(text, _extendedTimeoutDuration);
}
// Future<String> retryExtractSummaryWithExtendedTimeout(String text) async {
// return _extractSummaryInternal(text, _extendedTimeoutDuration);
// }
// 提取摘要
Future<String> extractSummary(String text) async {
return _extractSummaryInternal(text, _defaultTimeoutDuration);
}
// Future<String> extractSummary(String text) async {
// return _extractSummaryInternal(text, _defaultTimeoutDuration);
// }
// 内部摘要提取方法,避免代码重复
Future<String> _extractSummaryInternal(
String text, int timeoutDuration) async {
if (text.isEmpty) {
return '无法生成摘要:文本为空';
}
isGeneratingSummary.value = true;
try {
// 构建提示词
final messages = [
{'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'},
];
// 系统提示词
const systemPrompt = '''
你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。
请按照以下格式生成摘要:
1. 首先用1-2句话对整体内容进行简要概括
2. 然后使用项目符号列表(•)列出关键点或重要信息
3. 如果是对话内容,请特别标注出讨论的结论或达成的共识
请遵循以下原则:
1. 保留原文的核心信息和关键点
2. 使用简洁、清晰的语言
3. 保持客观,不添加个人观点
4. 摘要总长度应该是原文的1/3或更短
5. 确保列表中的每个要点都简明扼要,突出重点
''';
String result;
try {
// 使用超时限制
result = await _aiService
.sendMessage(
messages: messages,
systemPrompt: systemPrompt,
)
.timeout(Duration(seconds: timeoutDuration), onTimeout: () {
throw TimeoutException('请求超时', Duration(seconds: timeoutDuration));
});
} on TimeoutException catch (_) {
final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
? '生成摘要超时,请稍后重试'
: '生成摘要超时,请检查网络连接后重试';
summaryResult.value = timeoutMessage;
return summaryResult.value;
}
// 更新结果并保存
if (result.isNotEmpty) {
summaryResult.value = result;
saveSummary(result);
// 记录AI摘要API调用统计
try {
_usageService.recordAiSummaryApiCall();
} catch (e) {
Logger.error('记录AI摘要统计失败: $e');
}
}
return result;
} catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}';
Logger.error('TranslationDetailController', errorMessage);
summaryResult.value = errorMessage;
return errorMessage;
} finally {
isGeneratingSummary.value = false;
}
}
// Future<String> _extractSummaryInternal(
// String text, int timeoutDuration) async {
// if (text.isEmpty) {
// return '无法生成摘要:文本为空';
// }
// isGeneratingSummary.value = true;
// try {
// // 构建提示词
// final messages = [
// {'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'},
// ];
// // 系统提示词
// const systemPrompt = '''
// 你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。
// 请按照以下格式生成摘要:
// 1. 首先用1-2句话对整体内容进行简要概括
// 2. 然后使用项目符号列表(•)列出关键点或重要信息
// 3. 如果是对话内容,请特别标注出讨论的结论或达成的共识
// 请遵循以下原则:
// 1. 保留原文的核心信息和关键点
// 2. 使用简洁、清晰的语言
// 3. 保持客观,不添加个人观点
// 4. 摘要总长度应该是原文的1/3或更短
// 5. 确保列表中的每个要点都简明扼要,突出重点
// ''';
// String result;
// try {
// // 使用超时限制
// result = await _aiService
// .sendMessage(
// messages: messages,
// systemPrompt: systemPrompt,
// )
// .timeout(Duration(seconds: timeoutDuration), onTimeout: () {
// throw TimeoutException('请求超时', Duration(seconds: timeoutDuration));
// });
// } on TimeoutException catch (_) {
// final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
// ? '生成摘要超时,请稍后重试'
// : '生成摘要超时,请检查网络连接后重试';
// summaryResult.value = timeoutMessage;
// return summaryResult.value;
// }
// // 更新结果并保存
// if (result.isNotEmpty) {
// summaryResult.value = result;
// saveSummary(result);
// // 记录AI摘要API调用统计
// try {
// _usageService.recordAiSummaryApiCall();
// } catch (e) {
// Logger.error('记录AI摘要统计失败: $e');
// }
// }
// return result;
// } catch (e) {
// final errorMessage = '生成摘要失败: ${e.toString()}';
// Logger.error('TranslationDetailController', errorMessage);
// summaryResult.value = errorMessage;
// return errorMessage;
// } finally {
// isGeneratingSummary.value = false;
// }
// }
// 删除单条翻译记录
Future<void> deleteTranslationItem(TranslationItem item) async {

46
lib/modules/translation/controllers/translation_list_controller.dart

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
@ -93,24 +94,43 @@ class TranslationListController extends GetxController {
// 请求存储权限
void _requestStoragePermission() async {
// final BuildContext context = Get.overlayContext!;
PermissionUtil.instance
.requestPermission(
// context: context,
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
)
.then((granted) {
// 只在Android 10及以下版本请求存储权限
if (Platform.isAndroid) {
try {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
final sdkVersion = androidInfo.version.sdkInt;
// Android 11+ (API 30+) 不需要存储权限
if (sdkVersion >= 30) {
Logger.d("Permission", "Android 11+版本,无需请求存储权限");
return; // 直接返回,表示可以使用存储
}
} catch (e) {
Logger.e("Permission", "获取Android版本信息失败: $e");
// 如果获取版本失败,为了兼容性还是请求权限
}
}
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.storage,
permissionName: 'storagePermission'.tr,
explanationText: 'storagePermissionExplanation'.tr,
permanentDenialText: 'storagePermissionPermanentDenial'.tr,
);
if (granted) {
Logger.d("Permission", "用户授予了存储权限");
//return true;
} else {
Logger.d("Permission", "用户未授予存储权限");
// 可以提示用户无法进行文件操作
//return false;
}
});
} catch (e) {
Logger.e("Permission", "请求存储权限时出错: $e");
//return false;
}
}
// 添加方法:获取音频时长

172
lib/modules/translation/views/dialogs/delete_confirm_dialog.dart

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../../core/theme/app_colors.dart';
import '../../models/translation_models.dart';
/// 删除确认对话框
/// 提供翻译记录的删除确认功能
class DeleteConfirmDialog {
/// 显示删除单条翻译记录确认对话框
static void showDeleteItemDialog({
required TranslationItem item,
required Function() onConfirm,
}) {
final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'deleteTranslationRecord'.tr, // 删除翻译记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'deleteTranslationConfirm'.tr, // 确定要删除这条翻译记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
SizedBox(height: 12.h),
// 显示要删除的记录预览
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]?.withOpacity(0.5)
: Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.sourceText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (item.translatedText.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
onConfirm();
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
/// 显示清除所有历史记录确认对话框
static void showClearHistoryDialog({
required Function() onConfirm,
}) {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'clearHistory'.tr, // 清除历史记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'clearHistoryConfirm'.tr, // 确定要清除所有翻译历史记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
onConfirm();
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
}

187
lib/modules/translation/views/dialogs/translation_long_press_menu.dart

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../../core/theme/app_colors.dart';
import '../../models/translation_models.dart';
/// 翻译长按菜单对话框
/// 负责处理翻译条目的长按菜单显示和交互
class TranslationLongPressMenu {
/// 显示翻译长按菜单
static void show({
required BuildContext context,
required bool isDarkMode,
required LongPressStartDetails details,
required TranslationItem item,
required Function(TranslationItem) onCopy,
required Function(TranslationItem) onDeleteItem,
required Function() onClearHistory,
}) {
// 只对完成态记录显示长按菜单
if (item.isIntermediate) return;
// 触发触觉反馈
Haptics.vibrate(HapticsType.success);
// 计算弹出菜单位置
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: _buildMenuItems(
context: context,
isDarkMode: isDarkMode,
item: item,
onCopy: onCopy,
onDeleteItem: onDeleteItem,
onClearHistory: onClearHistory,
),
);
}
/// 构建菜单项
static List<PopupMenuEntry> _buildMenuItems({
required BuildContext context,
required bool isDarkMode,
required TranslationItem item,
required Function(TranslationItem) onCopy,
required Function(TranslationItem) onDeleteItem,
required Function() onClearHistory,
}) {
return [
// 复制选项
_buildPopupMenuItem(
Icon(
Icons.copy,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
'copy'.tr, // 复制
isDarkMode ? Colors.white : Colors.black87,
() => onCopy(item),
),
// 分隔线
PopupMenuItem(
height: 1,
enabled: false,
child: Divider(
height: 1,
thickness: 0.8,
indent: 0,
endIndent: 0,
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
),
// 删除该条记录选项
_buildPopupMenuItem(
Icon(
Icons.delete_outline,
size: 20.w,
color: isDarkMode ? Colors.orange[300] : Colors.orange,
),
'delete'.tr, // 删除
isDarkMode ? Colors.orange[300]! : Colors.orange,
() => onDeleteItem(item),
),
// 分隔线
PopupMenuItem(
height: 1,
enabled: false,
child: Divider(
height: 1,
thickness: 0.8,
indent: 0,
endIndent: 0,
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
),
// 删除所有记录选项
_buildPopupMenuItem(
Icon(
Icons.delete_sweep,
size: 20.w,
color: Colors.red,
),
'clearHistory'.tr, // 清除历史记录
Colors.red,
onClearHistory,
),
];
}
/// 构建弹出菜单项
static PopupMenuEntry _buildPopupMenuItem(
Icon icon,
String title,
Color color,
Function() onTap,
) {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 150.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(
color: color,
fontSize: 14.sp,
),
),
],
),
),
);
}
/// 复制翻译条目内容
/// 将原文和翻译文本复制到剪贴板
static void copyTranslationItem(TranslationItem item) {
// 构建复制文本:原文 + 翻译
String copyText = item.sourceText;
if (item.translatedText.isNotEmpty) {
copyText += '\n${item.translatedText}';
}
Clipboard.setData(ClipboardData(text: copyText)).then((_) {
Get.snackbar(
'success'.tr,
'copySuccess'.tr, // 复制成功
backgroundColor: Colors.blue.withOpacity(0.8),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(milliseconds: 1200),
icon: Icon(
Icons.check_circle_outline,
color: Colors.white,
size: 24.sp,
),
);
});
}
}

81
lib/modules/translation/views/dialogs/translation_mode_selector.dart

@ -8,21 +8,82 @@ import '../../controllers/translation_controller.dart';
/// 翻译模式选择对话框
class TranslationModeSelector {
/// 显示翻译模式选择器
static void show(bool isDarkMode) {
static void show(bool isDarkMode, {Offset? anchor}) {
final controller = Get.find<TranslationController>();
Get.bottomSheet(
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: _buildTranslationModeSelector(controller),
),
isScrollControlled: true,
backgroundColor: Colors.transparent,
final Size screenSize = MediaQuery.of(Get.context!).size;
Alignment alignment;
if (anchor != null) {
// 使用传入的锚点计算对齐点
final double ax = (anchor.dx / screenSize.width) * 2 - 1;
final double ay = (anchor.dy / screenSize.height) * 2 - 1;
alignment = Alignment(ax, ay);
} else {
final Offset floatingButtonPos = controller.floatingButtonPosition.value;
// 计算悬浮按钮的中心点位置(从右下角偏移转换为屏幕坐标)
final double buttonSize = 56.0.w; // 悬浮按钮的大小
final double buttonCenterX =
screenSize.width - floatingButtonPos.dx - buttonSize / 2;
final double buttonCenterY =
screenSize.height - floatingButtonPos.dy - buttonSize / 2;
// 计算缩放对齐点(-1..1)
final double ax = (buttonCenterX / screenSize.width) * 2 - 1;
final double ay = (buttonCenterY / screenSize.height) * 2 - 1;
alignment = Alignment(ax, ay);
}
Get.generalDialog(
barrierLabel: 'translation_mode_selector',
barrierDismissible: true, // 允许点击背景关闭
barrierColor: Colors.transparent, // 设置为透明,让自定义模糊层处理点击
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) {
return const SizedBox.shrink();
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic);
return Stack(
children: [
// 修复:将模糊背景放在可点击区域
GestureDetector(
onTap: () => Get.back(), // 添加点击关闭功能
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Container(
color: isDarkMode
? Colors.black.withOpacity(0.55)
: Colors.grey.withOpacity(0.95), // 添加半透明背景
),
),
),
// 内容区域
ScaleTransition(
alignment: alignment,
scale: Tween<double>(begin: 0.1, end: 1.0).animate(curved),
child: FadeTransition(
opacity: curved,
child: SizedBox.expand(
child: Align(
alignment: Alignment.bottomCenter,
child: _buildTranslationModeSelector(controller),
),
),
),
),
],
);
},
);
}
// 构建翻译模式选择器 - 参考会议模块样式
static Widget _buildTranslationModeSelector(TranslationController controller) {
static Widget _buildTranslationModeSelector(
TranslationController controller) {
return Builder(
builder: (BuildContext context) {
return SafeArea(

352
lib/modules/translation/views/translation_detail_view.dart

@ -142,9 +142,9 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
if (controller.selectedTabIndex.value == 0)
..._buildOriginalRecords(isDarkMode)
else
_buildSummaryWidget(isDarkMode),
//_buildSummaryWidget(isDarkMode),
SizedBox(height: 24.h),
SizedBox(height: 24.h),
],
);
}),
@ -323,23 +323,23 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
}
// 构建总结视图
Widget _buildSummaryWidget(bool isDarkMode) {
final String combinedSourceText = _getCombinedSourceText();
// Widget _buildSummaryWidget(bool isDarkMode) {
// final String combinedSourceText = _getCombinedSourceText();
return Column(
children: [
// 摘要内容容器
_buildSummaryContainer(isDarkMode),
// return Column(
// children: [
// // 摘要内容容器
// _buildSummaryContainer(isDarkMode),
// 提取摘要按钮
Obx(() => _buildExtractSummaryButton(combinedSourceText, isDarkMode)),
// // 提取摘要按钮
// Obx(() => _buildExtractSummaryButton(combinedSourceText, isDarkMode)),
// 重新生成按钮
Obx(() =>
_buildRegenerateSummaryButton(combinedSourceText, isDarkMode)),
],
);
}
// // 重新生成按钮
// Obx(() =>
// _buildRegenerateSummaryButton(combinedSourceText, isDarkMode)),
// ],
// );
// }
// 构建摘要容器
Widget _buildSummaryContainer(bool isDarkMode) {
@ -484,163 +484,163 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
}
// 构建提取摘要按钮
Widget _buildExtractSummaryButton(
String combinedSourceText, bool isDarkMode) {
final bool isDisabled = controller.isGeneratingSummary.value ||
controller.summaryResult.value.isNotEmpty;
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
child: ElevatedButton(
onPressed:
isDisabled ? null : () => _extractSummary(combinedSourceText),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? AppColors.primary : Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
disabledBackgroundColor: controller.isGeneratingSummary.value
? isDarkMode
? AppColors.primary.withAlpha(128)
: Colors.blue.withAlpha(128)
: Colors.grey.withAlpha(128),
),
child: controller.isGeneratingSummary.value
? _buildGeneratingIndicator(isDarkMode)
: Text(
controller.summaryResult.value.isEmpty
? 'extractSummary'.tr
: 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
);
}
// Widget _buildExtractSummaryButton(
// String combinedSourceText, bool isDarkMode) {
// final bool isDisabled = controller.isGeneratingSummary.value ||
// controller.summaryResult.value.isNotEmpty;
// return Container(
// width: double.infinity,
// margin: EdgeInsets.only(bottom: 16.h),
// child: ElevatedButton(
// onPressed:
// isDisabled ? null : () => _extractSummary(combinedSourceText),
// style: ElevatedButton.styleFrom(
// backgroundColor: isDarkMode ? AppColors.primary : Colors.blue,
// foregroundColor: Colors.white,
// padding: EdgeInsets.symmetric(vertical: 12.h),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16.r),
// ),
// elevation: 0,
// disabledBackgroundColor: controller.isGeneratingSummary.value
// ? isDarkMode
// ? AppColors.primary.withAlpha(128)
// : Colors.blue.withAlpha(128)
// : Colors.grey.withAlpha(128),
// ),
// child: controller.isGeneratingSummary.value
// ? _buildGeneratingIndicator(isDarkMode)
// : Text(
// controller.summaryResult.value.isEmpty
// ? 'extractSummary'.tr
// : 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要
// style: TextStyle(
// fontSize: 16.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// );
// }
// 构建生成中指示器
Widget _buildGeneratingIndicator(bool isDarkMode) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20.w,
height: 20.h,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.0,
),
),
SizedBox(width: 12.w),
Text(
'generating'.tr, // 生成中...
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
],
);
}
// Widget _buildGeneratingIndicator(bool isDarkMode) {
// return Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// SizedBox(
// width: 20.w,
// height: 20.h,
// child: CircularProgressIndicator(
// color: Colors.white,
// strokeWidth: 2.0,
// ),
// ),
// SizedBox(width: 12.w),
// Text(
// 'generating'.tr, // 生成中...
// style: TextStyle(
// fontSize: 16.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ],
// );
// }
// 构建重新生成按钮
Widget _buildRegenerateSummaryButton(
String combinedSourceText, bool isDarkMode) {
if (controller.summaryResult.value.isEmpty ||
controller.isGeneratingSummary.value) {
return SizedBox.shrink();
}
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
child: OutlinedButton(
onPressed: () => _extractSummary(combinedSourceText),
style: OutlinedButton.styleFrom(
foregroundColor: isDarkMode ? AppColors.primary : Colors.blue,
side: BorderSide(color: isDarkMode ? AppColors.primary : Colors.blue),
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
),
child: Text(
'regenerate'.tr, // 重新生成
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
);
}
// Widget _buildRegenerateSummaryButton(
// String combinedSourceText, bool isDarkMode) {
// if (controller.summaryResult.value.isEmpty ||
// controller.isGeneratingSummary.value) {
// return SizedBox.shrink();
// }
// return Container(
// width: double.infinity,
// margin: EdgeInsets.only(bottom: 16.h),
// child: OutlinedButton(
// onPressed: () => _extractSummary(combinedSourceText),
// style: OutlinedButton.styleFrom(
// foregroundColor: isDarkMode ? AppColors.primary : Colors.blue,
// side: BorderSide(color: isDarkMode ? AppColors.primary : Colors.blue),
// padding: EdgeInsets.symmetric(vertical: 12.h),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16.r),
// ),
// ),
// child: Text(
// 'regenerate'.tr, // 重新生成
// style: TextStyle(
// fontSize: 16.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// );
// }
// 提取摘要方法
void _extractSummary(String text) async {
final result = await controller.extractSummary(text);
// void _extractSummary(String text) async {
// final result = await controller.extractSummary(text);
// 检查是否超时,如果超时则显示重试选项
if (result.contains('超时')) {
_showTimeoutDialog(text);
}
}
// // 检查是否超时,如果超时则显示重试选项
// if (result.contains('超时')) {
// _showTimeoutDialog(text);
// }
// }
// 显示超时对话框
void _showTimeoutDialog(String text) {
Get.dialog(
AlertDialog(
title: Text(
'requestTimeout'.tr, // 请求超时
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
content: Text(
'summaryTimeoutMessage'.tr, // 生成摘要请求超时,可能是网络问题或服务器繁忙。
style: TextStyle(
fontSize: 16.sp,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
// 使用延长的超时时间重试
controller.retryExtractSummaryWithExtendedTimeout(text);
},
child: Text(
'retry'.tr, // 重试
style: TextStyle(
fontSize: 16.sp,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// void _showTimeoutDialog(String text) {
// Get.dialog(
// AlertDialog(
// title: Text(
// 'requestTimeout'.tr, // 请求超时
// style: TextStyle(
// fontSize: 18.sp,
// fontWeight: FontWeight.bold,
// ),
// ),
// content: Text(
// 'summaryTimeoutMessage'.tr, // 生成摘要请求超时,可能是网络问题或服务器繁忙。
// style: TextStyle(
// fontSize: 16.sp,
// ),
// ),
// actions: [
// TextButton(
// onPressed: () => Get.back(),
// child: Text(
// 'cancel'.tr, // 取消
// style: TextStyle(
// fontSize: 16.sp,
// color: Colors.grey[700],
// ),
// ),
// ),
// TextButton(
// onPressed: () {
// Get.back();
// // 使用延长的超时时间重试
// controller.retryExtractSummaryWithExtendedTimeout(text);
// },
// child: Text(
// 'retry'.tr, // 重试
// style: TextStyle(
// fontSize: 16.sp,
// color: Colors.blue,
// fontWeight: FontWeight.bold,
// ),
// ),
// ),
// ],
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.r),
// ),
// ),
// );
// }
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
@ -750,18 +750,18 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
'${'originalText'.tr}:\n$sourceText\n\n${'translatedText'.tr}:\n$translatedText'; // 原文:译文:
} else {
// 分享摘要
if (controller.summaryResult.value.isNotEmpty) {
shareText =
'${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要:
} else {
Get.snackbar(
'cannotShare'.tr, // 无法分享
'generateSummaryFirst'.tr, // 请先生成摘要
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2),
);
return;
}
// if (controller.summaryResult.value.isNotEmpty) {
// shareText =
// '${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要:
// } else {
// Get.snackbar(
// 'cannotShare'.tr, // 无法分享
// 'generateSummaryFirst'.tr, // 请先生成摘要
// snackPosition: SnackPosition.BOTTOM,
// duration: Duration(seconds: 2),
// );
// return;
// }
}
// 调用系统分享

358
lib/modules/translation/views/translation_view.dart

@ -2,8 +2,10 @@ import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../controllers/translation_controller.dart';
import '../models/translation_models.dart';
import '../views/translation_history_view.dart';
@ -16,6 +18,7 @@ import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局
import 'widgets/draggable_floating_button.dart'; // 可拖拽的悬浮按钮
import 'dialogs/language_selection_dialog.dart'; // 语言选择对话框
import 'dialogs/translation_mode_selector.dart'; // 翻译模式选择器
import 'dialogs/translation_long_press_menu.dart'; // 长按菜单
// import 'package:intl/intl.dart';
@ -42,10 +45,10 @@ class TranslationView extends GetView<TranslationController> {
children: [
_buildBody(isDarkMode),
// 可拖拽的悬浮按钮
DraggableFloatingButton(
isDarkMode: isDarkMode,
controller: controller,
),
// DraggableFloatingButton(
// isDarkMode: isDarkMode,
// controller: controller,
// ),
],
),
);
@ -141,6 +144,15 @@ class TranslationView extends GetView<TranslationController> {
title: Obx(() => GestureDetector(
onTap: () {
// 点击标题时弹出翻译模式选择器
// 从拖拽按钮位置作为锚点打开
// final pos = controller.floatingButtonPosition.value;
// final Size screen = MediaQuery.of(Get.context!).size;
// // 已保存的是距右、下的偏移,需要换算为屏幕坐标中的中心点
// final double buttonSize = 56.w;
// final double left = screen.width - pos.dx - buttonSize;
// final double top = screen.height - pos.dy - buttonSize;
// final Offset anchor =
// Offset(left + buttonSize / 2, top + buttonSize / 2);
TranslationModeSelector.show(isDarkMode);
},
child: Text(
@ -191,9 +203,31 @@ class TranslationView extends GetView<TranslationController> {
),
);
} else {
// 未录音:显示红色录音按钮 + 文件夹按钮
// 未录音:显示红色录音按钮 + 文件夹按钮 + 悬浮窗按钮
return Row(
children: [
// 悬浮窗切换按钮(仅在支持的模式下显示)
Obx(() {
if (controller.currentMode.value == 'call' ||
controller.currentMode.value == 'audioVideo') {
return IconButton(
icon: Icon(
controller.isFloatingWindowEnabled.value
? Icons.picture_in_picture
: Icons.picture_in_picture_alt_outlined,
color: controller.isFloatingWindowEnabled.value
? Colors.blue
: (isDarkMode ? Colors.white : Colors.black),
size: 24.sp,
),
onPressed: controller.toggleFloatingWindow,
tooltip: controller.isFloatingWindowEnabled.value
? '禁用悬浮窗'
: '启用悬浮窗',
);
}
return const SizedBox.shrink();
}),
IconButton(
icon: Icon(
Icons.radio_button_checked,
@ -343,7 +377,17 @@ class TranslationView extends GetView<TranslationController> {
}
Widget _buildBody(bool isDarkMode) {
return Column(
return
// GestureDetector(
// onPanUpdate: (details) {
// // 检测左滑手势
// if (details.delta.dx < -10) {
// // 左滑距离超过10像素时触发
// _handleSwipeLeft();
// }
// },
// child:
Column(
children: [
Expanded(
child: _buildTranslationContent(isDarkMode),
@ -351,8 +395,15 @@ class TranslationView extends GetView<TranslationController> {
_buildControlBar(isDarkMode),
],
);
// );
}
// 处理左滑手势
// void _handleSwipeLeft() async {
// await controller.stopAll();
// Get.toNamed(Routes.translationHistory);
// }
Widget _buildTranslationContent(bool isDarkMode) {
return Obx(() {
// 其他模式使用原有的列表布局
@ -397,103 +448,116 @@ class TranslationView extends GetView<TranslationController> {
Widget _buildTranslationItemWidget(
BuildContext context, TranslationItem item) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: item.isIntermediate
? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色
: (item.sourceLanguageCode == controller.sourceLanguageCode.value
? Theme.of(context)
.colorScheme
.primary
.withOpacity(0.08) // 源语言匹配时使用主题色底色
: Theme.of(context)
.colorScheme
.secondary
.withOpacity(0.08)), // 否则使用次要色底色
borderRadius: BorderRadius.circular(12),
border: Border.all(
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// 定义自适应颜色
final selfColor = isDarkMode
? Colors.blue[800]!.withOpacity(0.15) // 深色模式:深蓝+透明度
: Colors.blue[50]!; // 浅色模式:浅蓝
final otherColor = isDarkMode
? Colors.green[800]!.withOpacity(0.15) // 深色模式:深绿+透明度
: Colors.green[50]!; // 浅色模式:浅绿
return GestureDetector(
onLongPressStart: (details) => _onLongPressTranslationItem(
context,
isDarkMode,
details,
item,
),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: item.isIntermediate
? Colors.grey.withOpacity(0.2)
: Colors.transparent,
width: 1,
? Colors.grey.withOpacity(isDarkMode ? 0.15 : 0.1) // 中间结果
: (item.sourceLanguageCode == controller.sourceLanguageCode.value
? selfColor // 自己说的话
: otherColor), // 对方说的话
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: item.isIntermediate
? Colors.grey.withOpacity(isDarkMode ? 0.3 : 0.2)
: Colors.transparent,
width: 1,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本和删除按钮行
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Expanded(
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本和删除按钮行
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Expanded(
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
),
),
// 删除按钮 - 只对完成态记录显示,与源文本平齐
if (!item.isIntermediate)
GestureDetector(
onTap: () => _showDeleteItemConfirmDialog(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.delete_outline,
size: 18.sp,
color: Colors.grey[500],
// 删除按钮 - 只对完成态记录显示,与源文本平齐
if (!item.isIntermediate)
GestureDetector(
onTap: () => _showDeleteItemConfirmDialog(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.delete_outline,
size: 18.sp,
color: Colors.grey[500],
),
),
),
),
],
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
],
),
],
// 语言信息和时间戳
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
),
// 播放按钮
if (!item.isIntermediate)
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
],
// 语言信息和时间戳
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
),
],
),
],
// 播放按钮
if (!item.isIntermediate)
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
],
),
),
);
}
@ -569,8 +633,26 @@ class TranslationView extends GetView<TranslationController> {
if (isRecognizing) {
controller.stopAll();
} else {
// 弹出翻译模式选择器
TranslationModeSelector.show(isDarkMode);
// 获取底部按钮的位置
if (controlBarKey.currentContext != null) {
final RenderBox renderBox =
controlBarKey.currentContext!.findRenderObject()
as RenderBox;
final buttonPosition =
renderBox.localToGlobal(Offset.zero);
final buttonSize = renderBox.size;
// 计算按钮中心点
final Offset anchor = buttonPosition +
Offset(buttonSize.width / 2,
buttonSize.height / 2);
TranslationModeSelector.show(isDarkMode,
anchor: anchor);
//controller.startRecognition();
} else {
TranslationModeSelector.show(isDarkMode);
}
}
},
child: Container(
@ -858,6 +940,24 @@ class TranslationView extends GetView<TranslationController> {
});
}
/// 处理翻译条目长按事件
void _onLongPressTranslationItem(
BuildContext context,
bool isDarkMode,
LongPressStartDetails details,
TranslationItem item,
) {
TranslationLongPressMenu.show(
context: context,
isDarkMode: isDarkMode,
details: details,
item: item,
onCopy: (item) => TranslationLongPressMenu.copyTranslationItem(item),
onDeleteItem: (item) => _deleteTranslationItem(item),
onClearHistory: _showClearHistoryConfirmDialog,
);
}
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
@ -969,14 +1069,14 @@ class TranslationView extends GetView<TranslationController> {
controller.deleteTranslationItem(item.sessionId, item.timestamp);
// 显示删除成功提示
Get.snackbar(
'success'.tr, // deleteSuccess
'已删除该翻译记录', // deleteTranslationSuccess
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(milliseconds: 800),
);
// Get.snackbar(
// 'success'.tr, // deleteSuccess
// '已删除该翻译记录', // deleteTranslationSuccess
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
// colorText: Get.isDarkMode ? Colors.white : Colors.black,
// duration: const Duration(milliseconds: 800),
// );
} catch (e) {
// 显示删除失败提示
Get.snackbar(
@ -990,6 +1090,62 @@ class TranslationView extends GetView<TranslationController> {
}
}
// 显示删除所有记录确认对话框
void _showClearHistoryConfirmDialog() {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'clearHistory'.tr,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'clearHistoryConfirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
// 清除历史记录
controller.clearTranslationHistory();
Get.back();
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// 构建语言选择栏
Widget _buildLanguageSelectionBar(bool isDarkMode) {
return Container(

37
lib/modules/translation/views/widgets/draggable_floating_button.dart

@ -55,19 +55,7 @@ class DraggableFloatingButton extends StatelessWidget {
),
),
onDragEnd: (details) {
// 计算新位置(相对于屏幕右下角)
final screenSize = MediaQuery.of(Get.context!).size;
final newRight = screenSize.width - details.offset.dx - 56.w;
final newBottom = screenSize.height - details.offset.dy - 56.w;
// 边界检查,确保按钮不会超出屏幕
final clampedRight = newRight.clamp(0.0, screenSize.width - 56.w);
final clampedBottom =
newBottom.clamp(0.0, screenSize.height - 56.w);
// 保存新位置
controller.saveFloatingButtonPosition(
Offset(clampedRight, clampedBottom));
_handleDragEnd(context, details);
},
child: GestureDetector(
onTap: () => _showTranslationModeSelector(isDarkMode),
@ -96,8 +84,31 @@ class DraggableFloatingButton extends StatelessWidget {
));
}
/// 处理拖拽结束事件
void _handleDragEnd(BuildContext context, DraggableDetails details) {
final screenSize = MediaQuery.of(context).size;
final appBarHeight = Scaffold.of(context).appBarMaxHeight ?? kToolbarHeight;
final statusBarHeight = MediaQuery.of(context).padding.top;
final safeAreaTop = statusBarHeight + appBarHeight;
// 计算新位置(相对于屏幕右下角)
final newRight = screenSize.width - details.offset.dx - 56.w;
final newBottom = screenSize.height - details.offset.dy - 56.w;
// 边界检查,确保按钮不会超出屏幕或被AppBar遮盖
final clampedRight = newRight.clamp(0.0, screenSize.width - 56.w);
final clampedBottom = newBottom.clamp(
0.0, // 底部边界
screenSize.height - safeAreaTop - 56.w, // 顶部边界(避免被AppBar遮盖)
);
// 保存新位置
controller.saveFloatingButtonPosition(Offset(clampedRight, clampedBottom));
}
/// 显示翻译模式选择器
void _showTranslationModeSelector(bool isDarkMode) {
// 直接调用模式选择器,它会自动从悬浮按钮位置展开
TranslationModeSelector.show(isDarkMode);
}
}

9
lib/routes/app_pages.dart

@ -63,6 +63,8 @@ import '../modules/music/views/music_playlist_view.dart';
import '../modules/music/bindings/music_playlist_binding.dart';
import '../modules/FTFTranslation/bindings/FTFTranslation_binding.dart';
import '../modules/FTFTranslation/views/FTFTranslation_view.dart';
import '../modules/payment/bindings/payment_binding.dart';
import '../modules/payment/views/payment_view.dart';
abstract class AppPages {
static final pages = [
@ -288,5 +290,12 @@ abstract class AppPages {
page: () => const QqmusicPlaylistView(),
binding: QQMusicPlaylistBinding(),
),
GetPage(
name: Routes.payment,
page: () => const PaymentView(),
binding: PaymentBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
];
}

3
lib/routes/app_routes.dart

@ -63,4 +63,7 @@ abstract class Routes {
static const ble = '/ble';
// ble设备管理
static const qqmusic = '/qqmusic';
// 支付相关
static const payment = '/payment'; // 支付测试页面
}

407
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -36,6 +36,11 @@ import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date
// 添加后台启动相关导入
import android.app.PendingIntent
import android.app.NotificationManager
import android.app.NotificationChannel
import androidx.core.app.NotificationCompat
/**
@ -1035,7 +1040,7 @@ object AgentService : CoroutineScope {
if (reply) {
sendEvent("assistant_response", responseData)
}
Log.d(TAG, "liwei--------------- AI Call onComplete:$response")
if (response.isNotEmpty()) {
// 添加AI回复到历史记录
addToHistoryMessages(createAssistantMessage(response))
@ -1101,11 +1106,11 @@ object AgentService : CoroutineScope {
audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val resultmap = autoHandleFcunCallResult(name, functionCallResult);
aiMetadata = resultmap["meta"] as String;
broadcast = resultmap["broadcast"] as Boolean;
reply = !(resultmap["notreply"] as Boolean);
iscard = resultmap["iscard"] as Boolean;
if (iscard){
aiMetadata = resultmap["meta"] as String;
sendEvent(
"function_call_result", mapOf(
"function_call" to functionCall.toString(),
@ -1113,7 +1118,19 @@ object AgentService : CoroutineScope {
"meta" to aiMetadata,
)
)
}else{
sendEvent(
"function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
)
)
}
Log.e(
TAG,
"liwei--------------- AI Call onFunctionCallResult broadcast:$broadcast reply:$reply iscard:$iscard"
)
} catch (e: Exception) {
Log.e(
TAG,
@ -1879,13 +1896,13 @@ object AgentService : CoroutineScope {
toolname: String,
functionCallResult: JSONObject
): Map<String, Any> {
println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}")
// println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}")
try {
val contentStr = functionCallResult.optString("context")
println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}")
// println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}")
val content = JSONObject(contentStr)
val textStr = content.optString("text")
println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}")
// println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}")
val metadata: MutableMap<String, Any> = mutableMapOf()
var broadcast = true;
var notreply = false;
@ -1904,7 +1921,7 @@ object AgentService : CoroutineScope {
metadata[toolname] = meta // 现在可以赋值
metadata["notreply"] = notreply // 现在可以赋值
val metaStr = JSONObject(metadata as Map<*, *>).toString()
// Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
@ -1951,7 +1968,7 @@ object AgentService : CoroutineScope {
} else {
Log.w(TAG, "card_musiclist.musics 不是一个数组")
}
} else if (toolname == "qqmusic_searchandplay_songs" && meta.has("data")) { //qq音乐
} else if (toolname == "qqmusic_search_songs" && meta.has("data")) { //qq音乐
//"{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}"
val musics = meta.optJSONArray("data")
if (musics != null) {
@ -1987,9 +2004,24 @@ object AgentService : CoroutineScope {
}else if (meta.has("card_navigation")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "")
val end = cardNavigation.optString("end", "")
processNavigation(start, end);
val destination = cardNavigation.optString("destination", "")
val end_longitude = cardNavigation.optString("end_longitude", "")
val end_latitude = cardNavigation.optString("end_latitude", "")
// final start = card_navigation["start"] as String;
// final destination =
// card_navigation["destination"] as String;
// final end_longitude =
// card_navigation["end_longitude"] as String;
// final end_latitude =
// card_navigation["end_latitude"] as String;
var isinternal = processNavigation(start, destination,end_latitude,end_longitude,"driving");
metadata["isinternal"] = isinternal // 是否是内部导航
}else if (toolname == "map_weather") { //导航服务
iscard = true
}else if (toolname == "maps_weather") { //导航服务
iscard = true
}
val metaStr = JSONObject(metadata as Map<*, *>).toString()
return mapOf("meta" to metaStr, "broadcast" to broadcast, "notreply" to notreply,"iscard" to iscard);
}
} catch (e: Exception) {
@ -2071,25 +2103,352 @@ object AgentService : CoroutineScope {
}
/**
* 处理导航
* 将ai返回的音乐连接 在后台启动播放
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* 启动导航服务到指定目的地
* @param start 起点地址
* @param destination 目的地地址
* @param endlatitude 目的地纬度
* @param endlongitude 目的地经度
* @param mode 导航模式:driving(驾车)、transit(公交)、walking(步行)、cycling(骑行)
* @return 是否成功开始处理
*/
fun processNavigation(start: String, end: String) {
if (navigationMode == "internal_gaode"){
// 如果 playlist 中有有效的歌曲,开始播放
if (!start.isNullOrEmpty() && !end.isNullOrEmpty()) {
NavigationServiceHelper.startNavigation(
context,
"start",
startpos = start,
endpos = end
)
fun processNavigation(start: String, destination: String,
endlatitude: String, endlongitude: String, mode: String):Boolean {
Log.d(TAG, "processNavigation: $start -> $destination ($endlongitude,$endlatitude,), mode: $mode")
if (destination.isEmpty()) {
Log.i(TAG, "启动导航服务失败:目的地为空")
return false
}
// 获取导航模式配置,默认为自动选择
val configNavigationMode = navigationMode.ifEmpty { "external_auto" }
Log.d(TAG, "导航模式配置: $configNavigationMode")
when (configNavigationMode) {
"internal_gaode" -> {
// 使用内置高德导航
navigateWithInternalGaode(start, destination, endlatitude, endlongitude, mode)
return true
}
"external_baidu" -> {
// 使用外部百度地图
navigateWithBaiduMap(destination, endlatitude, endlongitude, mode)
return false
}
"external_gaode" -> {
// 使用外部高德地图
navigateWithGaodeMap(destination, endlatitude, endlongitude, mode)
return false
}
"external_google" -> {
// 使用外部谷歌地图
navigateWithGoogleMap(destination, mode)
return false
}
else -> {
// 自动选择导航应用,优先级:百度 > 高德 > 谷歌 > 内置高德
return autoSelectNavigation(start, destination, endlatitude, endlongitude, mode)
}
}
}
/**
* 使用内置高德导航
*/
private fun navigateWithInternalGaode(start: String, destination: String,
endlatitude: String, endlongitude: String, mode: String) {
try {
val endpos = if (endlatitude.isNotEmpty() && endlongitude.isNotEmpty()) {
"$endlongitude,$endlatitude"
} else {
destination
}
NavigationServiceHelper.startNavigation(
context,
"start",
startpos = start,
endpos = "$endlongitude,$endlatitude"
)
Log.i(TAG, "启动内置高德导航服务成功")
} catch (e: Exception) {
Log.e(TAG, "启动内置高德导航失败: ${e.message}", e)
}
}
/**
* 使用百度地图导航
*/
private fun navigateWithBaiduMap(destination: String, endlatitude: String,
endlongitude: String, mode: String) {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val src = packageInfo.packageName
// 百度地图导航模式映射
val baiduMode = when (mode) {
"transit" -> "transit"
"walking" -> "walking"
"cycling" -> "riding"
else -> "driving"
}
// 构建目的地参数
val destinationParam = if (endlatitude.isNotEmpty() && endlongitude.isNotEmpty()) {
"name:$destination|latlng:$endlatitude,$endlongitude"
} else {
destination
}
val uri = android.net.Uri.parse("baidumap://map/direction?" +
"destination=$destinationParam&" +
"coord_type=gcj02&" +
"mode=$baiduMode&" +
"src=$src")
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
// 添加包名确保启动正确的应用
setPackage("com.baidu.BaiduMap")
}
if (isAppInstalled("com.baidu.BaiduMap")) {
try {
context.startActivity(intent)
Log.i(TAG, "已启动百度地图导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动百度地图: ${e.message}")
startActivityWithPendingIntent(intent, "百度地图导航")
} catch (e: Exception) {
Log.e(TAG, "启动百度地图失败: ${e.message}", e)
throw e
}
} else {
Log.w(TAG, "百度地图未安装")
throw Exception("百度地图未安装")
}
} catch (e: Exception) {
Log.e(TAG, "启动百度地图失败: ${e.message}", e)
// 回退到其他导航方式
navigateWithGaodeMap(destination, endlatitude, endlongitude, mode)
}
}
/**
* 使用高德地图导航
*/
private fun navigateWithGaodeMap(destination: String, endlatitude: String,
endlongitude: String, mode: String) {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val src = packageInfo.packageName
// 高德地图导航模式映射
val gaodeMode = when (mode) {
"transit" -> "1"
"walking" -> "2"
"cycling" -> "3"
else -> "0"
}
val uriBuilder = StringBuilder("androidamap://route/plan?")
if (endlatitude.isNotEmpty() && endlongitude.isNotEmpty()) {
uriBuilder.append("dlat=$endlatitude&")
uriBuilder.append("dlon=$endlongitude&")
}
uriBuilder.append("dname=$destination&")
uriBuilder.append("dev=0&")
uriBuilder.append("t=$gaodeMode&")
uriBuilder.append("sourceApplication=$src")
val uri = android.net.Uri.parse(uriBuilder.toString())
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
// 添加包名确保启动正确的应用
setPackage("com.autonavi.minimap")
}
if (isAppInstalled("com.autonavi.minimap")) {
try {
context.startActivity(intent)
Log.i(TAG, "已启动高德地图导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动高德地图: ${e.message}")
startActivityWithPendingIntent(intent, "高德地图导航")
} catch (e: Exception) {
Log.e(TAG, "启动高德地图失败: ${e.message}", e)
throw e
}
} else {
Log.w(TAG, "高德地图未安装")
throw Exception("高德地图未安装")
}
} catch (e: Exception) {
Log.e(TAG, "启动高德地图失败: ${e.message}", e)
// 回退到其他导航方式
navigateWithGoogleMap(destination, mode)
}
}
/**
* 使用谷歌地图导航
*/
private fun navigateWithGoogleMap(destination: String, mode: String) {
try {
// 谷歌地图导航模式映射
val googleMode = when (mode) {
"transit" -> "transit"
"walking" -> "walking"
"cycling" -> "bicycling"
else -> "driving"
}
// 尝试使用Google Maps应用
val appUri = android.net.Uri.parse("comgooglemaps://?saddr=Current+Location&daddr=$destination&directionsmode=$googleMode")
val appIntent = Intent(Intent.ACTION_VIEW, appUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
// 添加包名确保启动正确的应用
setPackage("com.google.android.apps.maps")
}
if (isAppInstalled("com.google.android.apps.maps")) {
try {
context.startActivity(appIntent)
Log.i(TAG, "已启动谷歌地图导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动谷歌地图: ${e.message}")
startActivityWithPendingIntent(appIntent, "谷歌地图导航")
} catch (e: Exception) {
Log.e(TAG, "启动谷歌地图失败: ${e.message}", e)
throw e
}
} else {
Log.i(TAG, "启动导航服务失败")
// 回退到网页版
val webUri = android.net.Uri.parse("https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=$destination&travelmode=$googleMode")
val webIntent = Intent(Intent.ACTION_VIEW, webUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
}
try {
context.startActivity(webIntent)
Log.i(TAG, "已启动谷歌地图网页版导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动谷歌地图网页版: ${e.message}")
startActivityWithPendingIntent(webIntent, "谷歌地图网页版导航")
} catch (e: Exception) {
Log.e(TAG, "启动谷歌地图网页版失败: ${e.message}", e)
throw e
}
}
} catch (e: Exception) {
Log.e(TAG, "启动谷歌地图失败: ${e.message}", e)
// 最后回退到内置导航
navigateWithInternalGaode("", destination, "", "", mode)
}
}
/**
* 自动选择导航应用
*/
private fun autoSelectNavigation(start: String, destination: String,
endlatitude: String, endlongitude: String, mode: String):Boolean {
Log.d(TAG, "自动选择导航应用")
// 1. 优先尝试百度地图
if (isAppInstalled("com.baidu.BaiduMap")) {
Log.d(TAG, "自动选择:使用百度地图")
navigateWithBaiduMap(destination, endlatitude, endlongitude, mode)
return false
}
// 2. 其次尝试高德地图
if (isAppInstalled("com.autonavi.minimap")) {
Log.d(TAG, "自动选择:使用高德地图")
navigateWithGaodeMap(destination, endlatitude, endlongitude, mode)
return false
}
// 3. 然后尝试谷歌地图
if (isAppInstalled("com.google.android.apps.maps")) {
Log.d(TAG, "自动选择:使用谷歌地图")
navigateWithGoogleMap(destination, mode)
return false
}
// 4. 最后使用内置高德导航
Log.d(TAG, "自动选择:使用内置高德导航")
navigateWithInternalGaode(start, destination, endlatitude, endlongitude, mode)
return true
}
/**
* 使用 PendingIntent 启动 Activity(适用于后台启动场景)
*/
private fun startActivityWithPendingIntent(intent: android.content.Intent, description: String) {
try {
val pendingIntent = PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(), // 使用时间戳作为唯一ID
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 创建通知渠道(Android 8.0+)
val channelId = "navigation_channel"
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"导航服务",
NotificationManager.IMPORTANCE_HIGH
).apply {
this.description = "用于启动导航应用"
enableVibration(true)
enableLights(true)
}
notificationManager.createNotificationChannel(channel)
}
// 创建通知
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_map)
.setContentTitle("启动导航")
.setContentText("点击启动$description")
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.build()
notificationManager.notify(1001, notification)
Log.i(TAG, "已通过通知启动$description")
} catch (e: Exception) {
Log.e(TAG, "PendingIntent 启动失败: ${e.message}", e)
}
}
/**
* 检查应用是否已安装
*/
private fun isAppInstalled(packageName: String): Boolean {
return try {
context.packageManager.getPackageInfo(packageName, 0)
true
} catch (e: Exception) {
false
}
}

357
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -1,4 +1,5 @@
import Foundation
import UIKit
import AVFoundation
import ble_service
import location_service
@ -318,19 +319,24 @@ class AgentServiceImpl: NSObject {
// 根据模式决定空闲检测策略
switch mode {
case "phone_call":
azureAsrHelper?.restoreOriginalAudioState()
// 通话模式:禁用空闲检测,保持持续激活
os_log("通话模式:禁用空闲检测", log: logger, type: .info)
return
case "ble_wakeup":
azureAsrHelper?.disableBluetoothAudio()
// BLE唤醒模式:使用默认的空闲检测
startIdleCheck(idleSeconds: maxIdleSeconds)
case "push_to_talk":
azureAsrHelper?.disableBluetoothAudio()
// 按住说话模式:使用默认的空闲检测
startIdleCheck(idleSeconds: maxIdleSeconds)
case "normal":
azureAsrHelper?.disableBluetoothAudio()
// 普通模式:使用默认的空闲检测
startIdleCheck(idleSeconds: maxIdleSeconds)
default:
azureAsrHelper?.disableBluetoothAudio()
// 未知模式:使用默认的空闲检测
os_log("未知识别模式: %{public}@,使用默认空闲检测", log: logger, type: .error, mode)
startIdleCheck(idleSeconds: maxIdleSeconds)
@ -834,6 +840,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult:
private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -> [String: Any] {
var broadcast = true
var notreply = false
var iscard = false
var metadata: [String: Any] = [:]
metadata[toolName] = jsonDict
@ -843,7 +850,11 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
if let _notreply = jsonDict["notreply"] as? Bool {
notreply = _notreply
}
if let _iscard = jsonDict["iscard"] as? Bool {
iscard = _iscard
}
metadata["notreply"] = notreply
metadata["iscard"] = iscard
// 处理卡片数据
// if let cardMusic = jsonDict["card_music"] as? [String: Any] {
// processMusicCard(cardMusic)
@ -851,11 +862,16 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
if let cardMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "music_searchandplay_songs" {
processMusicListCard(cardMusiclist)
} else if let cardNavigation = jsonDict["card_navigation"] as? [String: Any] {
processNavigationCard(cardNavigation)
let isinternal = processNavigationCard(cardNavigation)
metadata["isinternal"] = isinternal
} else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" {
processQQMusicPlayList(cardQQMusiclist)
} else if toolName == "maps_weather" {
iscard = true
}else if toolName == "map_weather" {
iscard = true
}
metadata["iscard"] = iscard
let metaStr: String
if let jsonData = try? JSONSerialization.data(withJSONObject: metadata, options: []),
let jsonString = String(data: jsonData, encoding: .utf8) {
@ -973,11 +989,317 @@ private func jsonToString(_ json: [String: Any]) -> String? {
}
// 处理导航卡片
private func processNavigationCard(_ cardNavigation: [String: Any]) {
private func processNavigationCard(_ cardNavigation: [String: Any]) -> Bool {
let start = cardNavigation["start"] as? String ?? ""
let end = cardNavigation["end"] as? String ?? ""
// 实际处理导航逻辑
print("启动导航: 从 \(start) 到 \(end)")
let destination = cardNavigation["destination"] as? String ?? ""
let endlatitude = cardNavigation["endlatitude"] as? String ?? ""
let endlongitude = cardNavigation["endlongitude"] as? String ?? ""
let mode = cardNavigation["mode"] as? String ?? "driving"
print("启动导航: 从 \(start) 到 \(destination), 坐标: \(endlatitude),\(endlongitude), 模式: \(mode)")
// 检查应用状态
let appState = UIApplication.shared.applicationState
// 如果应用在后台,直接使用内置导航
if appState == .background || appState == .inactive {
print("应用在后台,直接启动内置导航模式")
// startInternalNavigation(start: start, destination: destination,
// latitude: endlatitude, longitude: endlongitude, mode: mode)
return true
}
// 应用在前台时的原有逻辑
// 根据导航模式选择不同的导航方式
switch mode {
case "internal_gaode":
// 使用内置高德导航
// startInternalNavigation(start: start, destination: destination,
// latitude: endlatitude, longitude: endlongitude, mode: mode)
return true
case "external_baidu":
// 使用外部百度地图
openBaiduMap(destination: destination, latitude: endlatitude,
longitude: endlongitude, mode: mode)
return false
case "external_gaode":
// 使用外部高德地图
openGaodeMap(destination: destination, latitude: endlatitude,
longitude: endlongitude, mode: mode)
return false
case "external_google":
// 使用外部谷歌地图
openGoogleMap(destination: destination, mode: mode)
return false
default:
// 默认使用自动选择
return autoSelectNavigation(start: start, destination: destination,
latitude: endlatitude, longitude: endlongitude, mode: mode)
}
}
// 启动内置高德导航
private func startInternalNavigation(start: String, destination: String,
latitude: String, longitude: String, mode: String) {
// 发送导航事件通知
NotificationCenter.default.post(name: .navigationdEvent, object: nil, userInfo: [
"start": start,
"destination": destination,
"latitude": latitude,
"longitude": longitude,
"mode": mode
])
print("启动内置导航: 从 \(start) 到 \(destination)")
}
// 打开百度地图
private func openBaiduMap(destination: String, latitude: String, longitude: String, mode: String) {
guard let bundleId = Bundle.main.bundleIdentifier else {
print("无法获取应用包名")
return
}
let src = "ios.\(bundleId)"
// 百度地图导航模式映射
let baiduMode: String
switch mode {
case "transit":
baiduMode = "transit" // 公交
case "walking":
baiduMode = "walking" // 步行
case "cycling":
baiduMode = "riding" // 骑行
case "driving":
fallthrough
default:
baiduMode = "driving" // 驾车
}
var components = URLComponents()
components.scheme = "baidumap"
components.host = "map"
components.path = "/direction"
components.queryItems = [
URLQueryItem(name: "destination", value: "name:\(destination)|latlng:\(latitude),\(longitude)"),
URLQueryItem(name: "coord_type", value: "gcj02"),
URLQueryItem(name: "mode", value: baiduMode),
URLQueryItem(name: "src", value: src)
]
guard let url = components.url else {
print("构建百度地图URL失败")
return
}
print("百度导航链接: \(url.absoluteString)")
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
print("成功打开百度地图")
} else {
print("打开百度地图失败")
}
}
} else {
print("百度地图未安装")
// 可以在这里添加提示用户安装百度地图的逻辑
}
}
}
// 打开高德地图
private func openGaodeMap(destination: String, latitude: String, longitude: String, mode: String) {
guard let bundleId = Bundle.main.bundleIdentifier else {
print("无法获取应用包名")
return
}
let src = "ios.\(bundleId)"
// 导航模式映射
let gaodeMode: String
switch mode {
case "transit":
gaodeMode = "1" // 公交
case "walking":
gaodeMode = "2" // 步行
case "cycling":
gaodeMode = "3" // 骑行
case "driving":
fallthrough
default:
gaodeMode = "0" // 驾车
}
var components = URLComponents()
components.scheme = "amapuri"
components.host = "route"
components.path = "/plan"
components.queryItems = [
URLQueryItem(name: "dlat", value: latitude),
URLQueryItem(name: "dlon", value: longitude),
URLQueryItem(name: "dname", value: destination),
URLQueryItem(name: "dev", value: "0"), // 0: gcj02
URLQueryItem(name: "t", value: gaodeMode),
URLQueryItem(name: "sourceApplication", value: src)
]
guard let url = components.url else {
print("构建高德地图URL失败")
return
}
print("高德导航链接: \(url.absoluteString)")
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
print("成功打开高德地图")
} else {
print("打开高德地图失败")
}
}
} else {
print("高德地图未安装")
// 可以在这里添加提示用户安装高德地图的逻辑
}
}
}
// 打开谷歌地图
private func openGoogleMap(destination: String, mode: String) {
// 方案1: 尝试使用Google Maps应用的URL scheme
var components = URLComponents()
components.scheme = "comgooglemaps"
components.queryItems = [
URLQueryItem(name: "saddr", value: "Current Location"),
URLQueryItem(name: "daddr", value: destination),
URLQueryItem(name: "directionsmode", value: mode)
]
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if let appUrl = components.url, UIApplication.shared.canOpenURL(appUrl) {
UIApplication.shared.open(appUrl, options: [:]) { success in
if success {
print("成功打开谷歌地图应用")
} else {
print("打开谷歌地图应用失败")
}
}
return
}
// 方案2: 使用网页版谷歌地图
let webUrlString = "https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=\(destination)&travelmode=\(mode)"
if let webUrl = URL(string: webUrlString) {
UIApplication.shared.open(webUrl, options: [:]) { success in
if success {
print("成功打开谷歌地图网页版")
} else {
print("打开谷歌地图网页版失败")
}
}
}
}
}
// 打开苹果地图
private func openAppleMap(destination: String, mode: String) {
var components = URLComponents()
components.scheme = "maps"
components.queryItems = [
URLQueryItem(name: "daddr", value: destination),
URLQueryItem(name: "dirflg", value: mode == "driving" ? "d" : "r") // d=驾车, r=公交
]
guard let url = components.url else {
print("构建苹果地图URL失败")
return
}
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
print("成功打开苹果地图")
} else {
print("打开苹果地图失败")
}
}
} else {
print("苹果地图不可用")
}
}
}
// 自动选择导航应用
private func autoSelectNavigation(start: String, destination: String,
latitude: String, longitude: String, mode: String) -> Bool {
// 1. 优先尝试百度地图
if canOpenBaiduMap() {
print("自动选择:使用百度地图")
openBaiduMap(destination: destination, latitude: latitude, longitude: longitude, mode: mode)
return false
}
// 2. 其次尝试高德地图
if canOpenGaodeMap() {
print("自动选择:使用高德地图")
openGaodeMap(destination: destination, latitude: latitude, longitude: longitude, mode: mode)
return false
}
// 3. 然后尝试谷歌地图
if canOpenGoogleMap() {
print("自动选择:使用谷歌地图")
openGoogleMap(destination: destination, mode: mode)
return false
}
// 4. 尝试苹果地图
if canOpenAppleMap() {
print("自动选择:使用苹果地图")
openAppleMap(destination: destination, mode: mode)
return false
}
// 5. 最后尝试内置高德导航
print("自动选择:使用内置导航")
// startInternalNavigation(start: start, destination: destination,
// latitude: latitude, longitude: longitude, mode: mode)
return true
}
// 检查是否可以打开百度地图
private func canOpenBaiduMap() -> Bool {
guard let url = URL(string: "baidumap://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 检查是否可以打开高德地图
private func canOpenGaodeMap() -> Bool {
guard let url = URL(string: "amapuri://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 检查是否可以打开谷歌地图
private func canOpenGoogleMap() -> Bool {
guard let url = URL(string: "comgooglemaps://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 检查是否可以打开苹果地图
private func canOpenAppleMap() -> Bool {
guard let url = URL(string: "maps://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 后台播放单曲
@ -1285,6 +1607,7 @@ class ChatApiStreamCallback: StreamCallback {
private var responseBuilder = ""
private var metadata = ""
private var userMetadata:[String: Any] = [:]
private var iscard = false;
private var reply = true; //是否回复
private var broadcast = true //是否播发
@ -1434,15 +1757,19 @@ class ChatApiStreamCallback: StreamCallback {
metadata = resultmap["meta"] as? String ?? ""
broadcast = resultmap["broadcast"] as? Bool ?? true
reply = !(resultmap["notreply"] as? Bool ?? true)
agentService.sendEvent(name: "function_call_result", data: [
"function_call": functionCall,
"result": functionCallResult,
"meta":metadata,
])
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@",
log: agentService.logger,
type: .info,
"\(broadcast)", "\(reply)", "\(metadata)")
iscard = !(resultmap["iscard"] as? Bool ?? false)
if (iscard){
agentService.sendEvent(name: "function_call_result", data: [
"function_call": functionCall,
"result": functionCallResult,
"meta":metadata,
])
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@",
log: agentService.logger,
type: .info,
"\(broadcast)", "\(reply)", "\(metadata)")
}
}catch{
print("liwei--------------- AI Call onFunctionCallResult 异常: \(error)")
}

17
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -547,19 +547,20 @@ class AzureAsrHelper(private val context: Context) {
Log.e(tag, "启动录音前台服务失败: ${e.message}")
}
}
// Initialize if not already done
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
audioStream.setupStream()
if (audioStream.recordfile == null) {
audioStream.recordfile = RecordFile()
}
if (!audioStream.isContinuousRecognitionActive) {
// 启动音频处理
Log.i(tag, "开启录音:audioSourceType${audioSourceType}")
// Initialize if not already done
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
audioStream.setupStream()
audioStream.startAudioRecord( when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
@ -635,7 +636,7 @@ class AzureAsrHelper(private val context: Context) {
}
Log.i(tag, "关闭录音:")
audioStream.recordfile ?: return
if (!audioStream.isContinuousRecognitionActive) {
audioStream.stopMicrophoneCapture()

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

Loading…
Cancel
Save