101 changed files with 5515 additions and 1045 deletions
@ -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 |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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) { |
|||
// 反射方法可能不存在,忽略 |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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 |
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
@ -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) { |
|||
// 反射方法可能不存在,忽略 |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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('悬浮窗被点击'); |
|||
// 可以发送事件给其他组件处理 |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
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; |
|||
} |
|||
} |
|||
// iOS不需要特殊权限 |
|||
return true; |
|||
} |
|||
|
|||
/// 请求悬浮窗权限 |
|||
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; |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -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), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -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, |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue