From 0d90c92923b97a3da63a3b173a32f0761231f9a7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 5 Feb 2026 20:23:45 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(android):=20=E5=90=8C=E6=AD=A5Flutter?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=88=B0?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E5=89=8D=E5=8F=B0=E6=9C=8D=E5=8A=A1=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 NativeLocalizationSync 服务,用于将Flutter应用的本地化字符串同步到Android原生前台服务通知中。在应用启动、语言切换和应用生命周期恢复时自动同步,确保原生通知内容与当前应用语言保持一致。 主要修改: 1. 新增 native_localization_sync.dart 服务,通过MethodChannel将本地化字符串传递给Android原生 2. 在MainApp、SettingsController和AppLifecycleManager中添加同步调用 3. 更新所有Android前台服务(音频录制、BLE代理、QQ音乐、导航、音乐服务)以使用同步的本地化字符串 4. 添加中英文通知文本到翻译文件 5. 在MainActivity中实现原生端字符串接收和存储逻辑 --- .../com/saitong/voitrans/MainActivity.kt | 40 +++++++++ .../services/native_localization_sync.dart | 87 +++++++++++++++++++ lib/core/translations/language/en_us.dart | 16 ++++ lib/core/translations/language/zh_cn.dart | 13 +++ lib/core/utils/app_lifecycle_manager.dart | 4 + lib/main.dart | 4 + .../controllers/settings_controller.dart | 2 + .../agent_service/BleMediaService.kt | 24 ++++- .../amap_service/NavigationService.kt | 29 ++++++- .../tools/AudioRecordingForegroundService.kt | 24 ++++- .../music_service/MusicService.kt | 37 ++++++-- .../qq_music/pcm/PlayerService.kt | 26 +++++- 12 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 lib/core/services/native_localization_sync.dart diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt index acf5653b8..135cbbf48 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt @@ -19,6 +19,7 @@ import android.net.Uri import android.provider.OpenableColumns import java.io.File import java.io.FileOutputStream +import org.json.JSONObject class MainActivity: FlutterActivity() { private val TAG = "MainActivity" @@ -34,6 +35,10 @@ class MainActivity: FlutterActivity() { private val DEEPLINK_CHANNEL = "com.saitong.voitrans/deeplink" private val DELIVERY_RETRY_DELAY_MS = 300L private val MAX_DELIVERY_RETRIES = 12 + private val NATIVE_LOCALIZATION_CHANNEL = "com.saitong.voitrans/native_localization" + private val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" + private val NATIVE_LOCALIZATION_PREF_KEY_LOCALE = "locale" + private val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "🚀 MainActivity onCreate 开始") @@ -147,6 +152,8 @@ class MainActivity: FlutterActivity() { } else { Log.d(TAG, "⚠️ 插件已注册,跳过重复注册") } + + setupNativeLocalizationChannel(flutterEngine) // Flutter引擎配置完成后,处理待处理的深链接 Handler(Looper.getMainLooper()).postDelayed({ @@ -172,6 +179,39 @@ class MainActivity: FlutterActivity() { Log.d(TAG, "✅ configureFlutterEngine 完成") } + private fun setupNativeLocalizationChannel(flutterEngine: FlutterEngine) { + val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NATIVE_LOCALIZATION_CHANNEL) + channel.setMethodCallHandler { call, result -> + when (call.method) { + "updateNativeStrings" -> { + try { + val args = call.arguments as? Map<*, *> + val locale = args?.get("locale") as? String + val strings = args?.get("strings") as? Map<*, *> + + val obj = JSONObject() + strings?.forEach { (k, v) -> + val key = k as? String ?: return@forEach + val value = v as? String ?: return@forEach + obj.put(key, value) + } + + val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) + prefs.edit() + .putString(NATIVE_LOCALIZATION_PREF_KEY_LOCALE, locale) + .putString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, obj.toString()) + .apply() + + result.success(null) + } catch (e: Exception) { + result.error("NATIVE_LOCALIZATION_ERROR", e.message, null) + } + } + else -> result.notImplemented() + } + } + } + private fun deliverOrQueueIntent(intent: Intent?) { intent ?: return diff --git a/lib/core/services/native_localization_sync.dart b/lib/core/services/native_localization_sync.dart new file mode 100644 index 000000000..5c3d74820 --- /dev/null +++ b/lib/core/services/native_localization_sync.dart @@ -0,0 +1,87 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import '../translations/app_translations.dart'; + +class NativeLocalizationSync { + static const MethodChannel _channel = + MethodChannel('com.saitong.voitrans/native_localization'); + + static const List nativeKeys = [ + 'native.notification.appName', + 'native.notification.navigation.title', + 'native.notification.navigation.keepRunning', + 'native.notification.bleAgent.running', + 'native.notification.audioRecording.recording', + 'native.notification.music.waiting', + 'native.notification.music.previous', + 'native.notification.music.next', + 'native.notification.music.play', + 'native.notification.music.pause', + 'native.notification.qqMusic.playing', + 'unknownSong', + 'unknownArtist', + ]; + + static String? _lastLocaleKey; + static int? _lastPayloadHash; + static bool _isSyncing = false; + + static Future syncToNative({bool force = false}) async { + if (!Platform.isAndroid) return; + if (_isSyncing) return; + + final Locale locale = Get.locale ?? + AppTranslations.deviceLocale() ?? + AppTranslations.fallbackLocale; + final String localeKey = _toAppTranslationsLocaleKey(locale); + + final Map payload = _buildPayload(localeKey); + final int payloadHash = Object.hashAll( + payload.entries.map((e) => Object.hash(e.key, e.value)), + ); + + if (!force && + _lastLocaleKey == localeKey && + _lastPayloadHash == payloadHash) { + return; + } + + _isSyncing = true; + try { + await _channel.invokeMethod('updateNativeStrings', { + 'locale': localeKey, + 'strings': payload, + }); + _lastLocaleKey = localeKey; + _lastPayloadHash = payloadHash; + } finally { + _isSyncing = false; + } + } + + static Map _buildPayload(String localeKey) { + final Map> all = AppTranslations().keys; + final Map? current = all[localeKey]; + final Map? fallback = + all['en_US'] ?? all[AppTranslations.fallbackLocale.toString()]; + + final Map result = {}; + for (final String key in nativeKeys) { + final String? value = current?[key] ?? fallback?[key]; + if (value == null || value.isEmpty) continue; + result[key] = value; + } + return result; + } + + static String _toAppTranslationsLocaleKey(Locale locale) { + final String language = locale.languageCode; + final String? country = locale.countryCode; + if (country == null || country.isEmpty) { + return language.toLowerCase(); + } + return '${language.toLowerCase()}_${country.toUpperCase()}'; + } +} diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index bce2e24c8..fae882d12 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -1083,6 +1083,22 @@ const Map enUS = { 'storagePermissionPermanentDenial': 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', // VOITRANS + 'native.notification.navigation.title': 'Navigation in progress', // 导航进行中 + 'native.notification.navigation.keepRunning': + 'Navigating, keep the app running', // 正在导航中,请保持应用运行 + 'native.notification.bleAgent.running': + 'Voice assistant agent is running', // 语音助手正在运行 + 'native.notification.audioRecording.recording': + 'Recording audio, tap to return to app', // 正在录音,点击返回应用 + 'native.notification.music.waiting': 'Waiting to play', // 等待播放 + 'native.notification.music.previous': 'Previous', // 上一首 + 'native.notification.music.next': 'Next', // 下一首 + 'native.notification.music.play': 'Play', // 播放 + 'native.notification.music.pause': 'Pause', // 暂停 + 'native.notification.qqMusic.playing': 'Playing music', // 正在播放音乐 + //permission_util.dart 'permissionUtilNeed': 'Need ', 'permissionUtilPermission': ' Permission', diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index a858ca3ec..3bc96f722 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -981,6 +981,19 @@ const Map zhCN = { 'storagePermissionPermanentDenial': '检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', + 'native.notification.navigation.title': '导航进行中', + 'native.notification.navigation.keepRunning': '正在导航中,请保持应用运行', + 'native.notification.bleAgent.running': '语音助手正在运行', + 'native.notification.audioRecording.recording': '正在录音,点击返回应用', + 'native.notification.music.waiting': '等待播放', + 'native.notification.music.previous': '上一首', + 'native.notification.music.next': '下一首', + 'native.notification.music.play': '播放', + 'native.notification.music.pause': '暂停', + 'native.notification.qqMusic.playing': '正在播放音乐', + //permission_util.dart 'permissionUtilNeed': '需要', 'permissionUtilPermission': '权限', diff --git a/lib/core/utils/app_lifecycle_manager.dart b/lib/core/utils/app_lifecycle_manager.dart index 5ba7e1399..3f0f744b8 100644 --- a/lib/core/utils/app_lifecycle_manager.dart +++ b/lib/core/utils/app_lifecycle_manager.dart @@ -7,6 +7,7 @@ import 'package:get_storage/get_storage.dart'; import '../../modules/meeting/controllers/meeting_record_controller.dart'; import '../../routes/app_routes.dart'; import 'logger.dart'; +import '../services/native_localization_sync.dart'; import '../translations/app_translations.dart'; //import '../../modules/settings/controllers/settings_controller.dart'; @@ -36,6 +37,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { // 注册应用生命周期观察者 WidgetsBinding.instance.addObserver(this); Logger.info('AppLifecycleManager 已初始化'); + unawaited(NativeLocalizationSync.syncToNative(force: true)); unawaited(refreshBleWakeupPolicy()); } @@ -174,6 +176,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { // 延迟执行,确保系统语言变化处理完成 Future.delayed(const Duration(milliseconds: 100), () { Get.updateLocale(userLocale); + unawaited(NativeLocalizationSync.syncToNative(force: true)); Logger.info('已恢复用户设置的语言: ${userLocale.toString()}'); }); } else { @@ -183,6 +186,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { Locale? newLocale = AppTranslations.deviceLocale(); if (newLocale != null) { Get.updateLocale(newLocale); + unawaited(NativeLocalizationSync.syncToNative(force: true)); // 同步更新设置控制器中的语言状态 try { diff --git a/lib/main.dart b/lib/main.dart index 758f45e4a..6d6509671 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'core/themes/app_theme.dart'; import 'core/themes/theme_service.dart'; import 'core/bindings/initial_binding.dart'; import 'core/utils/logger.dart'; +import 'core/services/native_localization_sync.dart'; import 'data/services/login_service.dart'; import 'modules/meeting/controllers/meeting_home_controller.dart'; import 'modules/splash/controllers/splash_controller.dart'; @@ -165,6 +166,9 @@ class MainApp extends StatelessWidget { Widget build(BuildContext context) { // 在应用构建时立即初始化深链接处理 _initDeepLinkHandler(); + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(NativeLocalizationSync.syncToNative()); + }); // 获取保存的主题设置 final themeMode = ThemeService().getThemeMode(); diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 4cbc4de90..78f1f078b 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -8,6 +8,7 @@ import 'package:logger/logger.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../../../core/themes/theme_service.dart'; +import '../../../core/services/native_localization_sync.dart'; import '../../../core/translations/app_translations.dart'; import '../../../data/services/network/dio_manager.dart'; import '../../../routes/app_routes.dart'; @@ -128,6 +129,7 @@ class SettingsController extends GetxController { Get.updateLocale(targetLocale); Logger().i("使用fallback语言: ${targetLocale.toString()}"); } + await NativeLocalizationSync.syncToNative(force: true); // 保存用户选择 currentLanguage.value = selectedLanguage; diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 5c6415e49..2e9c91893 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -12,6 +12,7 @@ import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import android.util.Log +import org.json.JSONObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -47,6 +48,8 @@ class BleMediaService : Service(), CoroutineScope { private const val CHANNEL_ID = "ble_agent_service_channel" private const val NOTIFICATION_ID = 1002 private const val PREF_NAME = "deep_voice_config" + private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" + private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" // 目标蓝牙设备名称 private const val TARGET_DEVICE_NAME = "DeapSound" @@ -882,11 +885,16 @@ private var companionManager: BleCompanionManager? = null } val appIconResId = applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info + val title = getNativeLocalizedString("native.notification.appName", "Voitrans") + val content = getNativeLocalizedString( + "native.notification.bleAgent.running", + "Voice assistant agent is running" + ) // 构建通知 val builder = NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("Voitrans") - .setContentText("Voice assistant agent is running") + .setContentTitle(title) + .setContentText(content) .setSmallIcon(appIconResId) .setOngoing(true) .setOnlyAlertOnce(true) @@ -899,4 +907,16 @@ private var companionManager: BleCompanionManager? = null return builder.build() } + + private fun getNativeLocalizedString(key: String, defaultValue: String): String { + return try { + val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) + val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue + val obj = JSONObject(json) + val value = obj.optString(key, defaultValue) + if (value.isBlank()) defaultValue else value + } catch (_: Exception) { + defaultValue + } + } } diff --git a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt index c926010b0..6ec4d9e2e 100644 --- a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt +++ b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt @@ -14,6 +14,7 @@ import android.util.Base64 import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import org.json.JSONObject import com.amap.api.navi.AMapNavi import com.amap.api.navi.AMapNaviListener import com.amap.api.navi.enums.PathPlanningStrategy @@ -42,6 +43,8 @@ class NavigationService: Service(),AMapNaviListener { private const val CHANNEL_ID = "navigation_channel" private const val CHANNEL_Name = "导航服务" private const val NOTIFICATION_ID = 1003 + private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" + private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" private val isRunning = AtomicBoolean(false) private var startpos: String = "" @@ -135,9 +138,14 @@ class NavigationService: Service(),AMapNaviListener { manager.createNotificationChannel(channel) } + val title = getNativeLocalizedString("native.notification.navigation.title", "导航进行中") + val content = getNativeLocalizedString( + "native.notification.navigation.keepRunning", + "正在导航中,请保持应用运行" + ) val notification = NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("导航进行中") - .setContentText("正在导航中,请保持应用运行") + .setContentTitle(title) + .setContentText(content) .setSmallIcon(R.drawable.ic_navigation) // 使用你自己的图标资源 .setOngoing(true) .build() @@ -146,8 +154,9 @@ class NavigationService: Service(),AMapNaviListener { } private fun updateNotification(contentText: String, iconBitmap: Bitmap?) { + val title = getNativeLocalizedString("native.notification.navigation.title", "导航进行中") val builder = NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("导航进行中") + .setContentTitle(title) .setContentText(contentText) .setSmallIcon(R.drawable.ic_navigation) // 通知栏图标(小) .setOnlyAlertOnce(true) @@ -162,6 +171,18 @@ class NavigationService: Service(),AMapNaviListener { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(NOTIFICATION_ID, notification) } + + private fun getNativeLocalizedString(key: String, defaultValue: String): String { + return try { + val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) + val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue + val obj = JSONObject(json) + val value = obj.optString(key, defaultValue) + if (value.isBlank()) defaultValue else value + } catch (_: Exception) { + defaultValue + } + } private fun tintBitmap(source: Bitmap, tintColor: Int): Bitmap { val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888) val canvas = android.graphics.Canvas(result) @@ -435,4 +456,4 @@ class NavigationService: Service(),AMapNaviListener { isRunning.set(false) super.onDestroy() } -} \ No newline at end of file +} diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt index 620c08dda..77f3fbacb 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt @@ -11,6 +11,7 @@ import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import android.util.Log +import org.json.JSONObject /** * 音频录制前台服务 @@ -24,6 +25,8 @@ class AudioRecordingForegroundService : Service() { private const val CHANNEL_ID = "audio_recording_channel" private const val CHANNEL_NAME = "音频录制服务" private const val ACTION_STOP = "com.yunqiinnovation.azure_speech.tools.STOP_SERVICE" + private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" + private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" /** * 启动前台服务 @@ -124,9 +127,14 @@ class AudioRecordingForegroundService : Service() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) + val title = getNativeLocalizedString("native.notification.appName", "Voitrans") + val content = getNativeLocalizedString( + "native.notification.audioRecording.recording", + "Recording audio, tap to return to app" + ) return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("Voitrans") - .setContentText("Recording audio, tap to return to app") + .setContentTitle(title) + .setContentText(content) .setSmallIcon(applicationInfo.icon) .setContentIntent(pendingIntent) .setOngoing(true) // 设置为持续通知,用户无法滑动删除 @@ -136,4 +144,16 @@ class AudioRecordingForegroundService : Service() { .setAutoCancel(false) // 点击通知后不自动取消 .build() } + + private fun getNativeLocalizedString(key: String, defaultValue: String): String { + return try { + val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) + val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue + val obj = JSONObject(json) + val value = obj.optString(key, defaultValue) + if (value.isBlank()) defaultValue else value + } catch (_: Exception) { + defaultValue + } + } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 0bda0f3e1..d1dad6aaa 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -25,6 +25,7 @@ import androidx.media.MediaBrowserServiceCompat import androidx.media.session.MediaButtonReceiver import android.util.Log import android.view.KeyEvent +import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.atomic.AtomicBoolean @@ -36,6 +37,8 @@ class MusicService : MediaBrowserServiceCompat() { private const val CHANNEL_ID = "music_media_service_channel" private const val NOTIFICATION_ID = 1002 private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION" + private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" + private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" private val isRunning = AtomicBoolean(false) private var currentIdInternal: Long? = null @@ -87,8 +90,10 @@ class MusicService : MediaBrowserServiceCompat() { fun updateMediaSessionMetadata(song: Map) { instance?.let { service -> try { - val title = song["title"] as? String ??: "未知标题" - val artist = song["artist"] as? String ??: "未知歌手" + val title = song["title"] as? String + ?: service.getNativeLocalizedString("unknownSong", "未知歌曲") + val artist = song["artist"] as? String + ?: service.getNativeLocalizedString("unknownArtist", "未知歌手") val coverUrl = song["coverUrl"] as? String ??: "" currentIdInternal = (song["id"] as? Number)?.toLong() ?: 0L Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") @@ -147,7 +152,9 @@ class MusicService : MediaBrowserServiceCompat() { instance = this isRunning.set(true) setupMediaSession() - startForeground(NOTIFICATION_ID, createNotification("等待播放","Voitrans","")) + val waitingTitle = getNativeLocalizedString("native.notification.music.waiting", "等待播放") + val appName = getNativeLocalizedString("native.notification.appName", "Voitrans") + startForeground(NOTIFICATION_ID, createNotification(waitingTitle, appName, "")) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager // 注册广播监听器 @@ -386,6 +393,12 @@ private fun updateProgress() { PendingIntent.FLAG_UPDATE_CURRENT } ) + + val prevLabel = getNativeLocalizedString("native.notification.music.previous", "上一首") + val nextLabel = getNativeLocalizedString("native.notification.music.next", "下一首") + val playLabel = getNativeLocalizedString("native.notification.music.play", "播放") + val pauseLabel = getNativeLocalizedString("native.notification.music.pause", "暂停") + val builder = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(title) .setContentText(artist) @@ -396,13 +409,13 @@ private fun updateProgress() { .setMediaSession(mediaSession.sessionToken) .setShowActionsInCompactView(0, 1, 2) ) - .addAction(R.drawable.ic_prev, "上一首", prevIntent) + .addAction(R.drawable.ic_prev, prevLabel, prevIntent) .addAction( if (currentStatus) R.drawable.ic_pause else R.drawable.ic_play, - if (currentStatus) "暂停" else "播放", + if (currentStatus) pauseLabel else playLabel, playPauseIntent ) - .addAction(R.drawable.ic_next, "下一首", nextIntent) + .addAction(R.drawable.ic_next, nextLabel, nextIntent) .setOngoing(false) .setDeleteIntent(deletePendingIntent) // 设置通知被滑动删除时的行为 // 异步加载封面图 @@ -430,6 +443,18 @@ private fun updateProgress() { return builder.build() } + + private fun getNativeLocalizedString(key: String, defaultValue: String): String { + return try { + val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) + val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue + val obj = JSONObject(json) + val value = obj.optString(key, defaultValue) + if (value.isBlank()) defaultValue else value + } catch (_: Exception) { + defaultValue + } + } fun mediaButtonPendingIntent(action: Long): PendingIntent { val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode()) val intent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt index f3152ff3e..c172e3a0c 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt @@ -17,6 +17,7 @@ import com.yunqiinnovation.qq_music.R import com.yunqiinnovation.qq_music.openid.OpenIDHelper import com.yunqiinnovation.qq_music.util.QPlayBindHelper import com.tencent.qqmusic.third.api.contract.* +import org.json.JSONObject /** * Created by clydeazhang on 2021/5/11 6:43 PM. @@ -28,6 +29,8 @@ class PlayerService : Service() { private const val TAG = "PlayerService" private const val KEY_ENTER_FOREGROUND = "KEY_ENTER_FOREGROUND" private const val CHANNEL_ID = "default" + private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" + private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" } private val qPlayBindHelper = QPlayBindHelper(this) @@ -103,16 +106,33 @@ class PlayerService : Service() { val enterForeground = intent?.getBooleanExtra(KEY_ENTER_FOREGROUND, false) ?: false Log.d(TAG, "onStartCommand, enter foreground=$enterForeground") if (enterForeground) { + val title = getNativeLocalizedString("native.notification.appName", "Voitrans") + val content = getNativeLocalizedString( + "native.notification.qqMusic.playing", + "Playing music" + ) val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_launcher_background) - .setContentTitle("demo标题") - .setContentText("demo正在播放音乐") + .setContentTitle(title) + .setContentText(content) .build() startForeground(1, notification) } return super.onStartCommand(intent, flags, startId) } + private fun getNativeLocalizedString(key: String, defaultValue: String): String { + return try { + val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) + val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue + val obj = JSONObject(json) + val value = obj.optString(key, defaultValue) + if (value.isBlank()) defaultValue else value + } catch (_: Exception) { + defaultValue + } + } + override fun onDestroy() { super.onDestroy() try { @@ -380,4 +400,4 @@ fun Bundle.toPrintableString(): String { sb.append("}") return sb.toString() -} \ No newline at end of file +} From 40df5f0f0028e4547db82818ee585302e4da259b Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 5 Feb 2026 20:24:04 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(i18n):=20=E6=B7=BB=E5=8A=A0=E5=AE=89?= =?UTF-8?q?=E5=8D=93=E5=89=8D=E5=8F=B0=E6=9C=8D=E5=8A=A1=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=9A=84=E5=A4=9A=E8=AF=AD=E8=A8=80=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为支持安卓前台服务的通知本地化,在多个语言文件中新增翻译键值。 同时在对应的安卓服务文件中添加注释,说明前台通知的触发条件。 --- lib/core/translations/language/ar_sa.dart | 15 ++++++++++ lib/core/translations/language/de_de.dart | 15 ++++++++++ lib/core/translations/language/fr_fr.dart | 16 ++++++++++ lib/core/translations/language/ja_jp.dart | 30 +++++++++++++++---- lib/core/translations/language/ko_kr.dart | 15 ++++++++++ lib/core/translations/language/ru_ru.dart | 15 ++++++++++ lib/core/translations/language/zh_hk.dart | 13 ++++++++ lib/core/translations/language/zh_tw.dart | 13 ++++++++ .../agent_service/BleMediaService.kt | 4 +++ .../amap_service/NavigationService.kt | 7 +++++ .../tools/AudioRecordingForegroundService.kt | 4 +++ .../music_service/MusicService.kt | 5 ++++ 12 files changed, 146 insertions(+), 6 deletions(-) diff --git a/lib/core/translations/language/ar_sa.dart b/lib/core/translations/language/ar_sa.dart index cadfa48dc..4699dee38 100644 --- a/lib/core/translations/language/ar_sa.dart +++ b/lib/core/translations/language/ar_sa.dart @@ -1064,6 +1064,21 @@ const Map arSA = { 'storagePermissionPermanentDenial': 'تم اكتشاف أنك رفضت إذن التخزين بشكل دائم. للمتابعة في استخدام هذه الميزة، يرجى الانتقال إلى إعدادات التطبيق لتمكين إذن التخزين يدويًا.', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', // VOITRANS + 'native.notification.navigation.title': 'التنقل جارٍ', // 导航进行中 + 'native.notification.navigation.keepRunning': + 'التنقل جارٍ، يرجى إبقاء التطبيق قيد التشغيل', // 正在导航中,请保持应用运行 + 'native.notification.bleAgent.running': 'المساعد الصوتي قيد التشغيل', // 语音助手正在运行 + 'native.notification.audioRecording.recording': + 'يتم التسجيل، اضغط للعودة إلى التطبيق', // 正在录音,点击返回应用 + 'native.notification.music.waiting': 'في انتظار التشغيل', // 等待播放 + 'native.notification.music.previous': 'السابق', // 上一首 + 'native.notification.music.next': 'التالي', // 下一首 + 'native.notification.music.play': 'تشغيل', // 播放 + 'native.notification.music.pause': 'إيقاف مؤقت', // 暂停 + 'native.notification.qqMusic.playing': 'يتم تشغيل الموسيقى', // 正在播放音乐 + //permission_util.dart 'permissionUtilNeed': 'يحتاج ', 'permissionUtilPermission': ' إذن', diff --git a/lib/core/translations/language/de_de.dart b/lib/core/translations/language/de_de.dart index 309b3b72b..dabb17783 100644 --- a/lib/core/translations/language/de_de.dart +++ b/lib/core/translations/language/de_de.dart @@ -1101,6 +1101,21 @@ const Map deDE = { 'storagePermissionPermanentDenial': 'Speicherberechtigung wurde dauerhaft verweigert. Um diese Funktion weiterhin nutzen zu können, gehen Sie bitte zu den App-Einstellungen, um die Speicherberechtigung manuell zu aktivieren.', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', // VOITRANS + 'native.notification.navigation.title': 'Navigation läuft', // 导航进行中 + 'native.notification.navigation.keepRunning': + 'Navigation läuft, App geöffnet lassen', // 正在导航中,请保持应用运行 + 'native.notification.bleAgent.running': 'Sprachassistent läuft', // 语音助手正在运行 + 'native.notification.audioRecording.recording': + 'Audioaufnahme läuft, tippen um zur App zurückzukehren', // 正在录音,点击返回应用 + 'native.notification.music.waiting': 'Warte auf Wiedergabe', // 等待播放 + 'native.notification.music.previous': 'Vorheriger Titel', // 上一首 + 'native.notification.music.next': 'Nächster Titel', // 下一首 + 'native.notification.music.play': 'Wiedergabe', // 播放 + 'native.notification.music.pause': 'Pause', // 暂停 + 'native.notification.qqMusic.playing': 'Musik wird wiedergegeben', // 正在播放音乐 + //permission_util.dart 'permissionUtilNeed': 'Benötige ', 'permissionUtilPermission': ' Berechtigung', diff --git a/lib/core/translations/language/fr_fr.dart b/lib/core/translations/language/fr_fr.dart index a1eda3df3..b6a26d25f 100644 --- a/lib/core/translations/language/fr_fr.dart +++ b/lib/core/translations/language/fr_fr.dart @@ -1088,6 +1088,22 @@ const Map frFR = { 'storagePermissionPermanentDenial': 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', // VOITRANS + 'native.notification.navigation.title': 'Navigation en cours', // 导航进行中 + 'native.notification.navigation.keepRunning': + 'Navigation en cours, gardez l’application active', // 正在导航中,请保持应用运行 + 'native.notification.bleAgent.running': + 'Assistant vocal en cours d’exécution', // 语音助手正在运行 + 'native.notification.audioRecording.recording': + 'Enregistrement audio, appuyez pour revenir à l’application', // 正在录音,点击返回应用 + 'native.notification.music.waiting': 'En attente de lecture', // 等待播放 + 'native.notification.music.previous': 'Précédent', // 上一首 + 'native.notification.music.next': 'Suivant', // 下一首 + 'native.notification.music.play': 'Lecture', // 播放 + 'native.notification.music.pause': 'Pause', // 暂停 + 'native.notification.qqMusic.playing': 'Musique en cours de lecture', // 正在播放音乐 + //permission_util.dart 'permissionUtilNeed': 'Need ', 'permissionUtilPermission': ' Permission', diff --git a/lib/core/translations/language/ja_jp.dart b/lib/core/translations/language/ja_jp.dart index d34d1fe17..ea62a1e32 100644 --- a/lib/core/translations/language/ja_jp.dart +++ b/lib/core/translations/language/ja_jp.dart @@ -439,11 +439,15 @@ const Map jaJP = { 'meetingRecordStopFailed': '録音を停止できませんでした。もう一度お試しください。', // 结束录音失败,请重试 'meetingRecordSaveFailed': '録音を保存できませんでした。もう一度お試しください。', // 保存录音失败,请重试 'meetingRecordRecordingNotificationTitle': '録音中', // 录音进行中 - 'meetingRecordRecordingNotificationBody': 'バックグラウンドで録音しています。タップしてアプリに戻ります。', // 正在后台录音,点击返回应用 + 'meetingRecordRecordingNotificationBody': + 'バックグラウンドで録音しています。タップしてアプリに戻ります。', // 正在后台录音,点击返回应用 'overlayPermissionTitle': '他のアプリの上に表示', // 悬浮窗权限 - 'overlayPermissionTranslationDesc': '通話中または音声/動画翻訳中に翻訳結果を表示するため、他のアプリの上に表示する権限が必要です。これにより、他のアプリを使用中でもリアルタイム翻訳を確認できます。', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。 - 'overlayPermissionRecordingDesc': '録音中に録音フローティングバー(タイマー/一時停止/再開)を表示するため、他のアプリの上に表示する権限が必要です。', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。 - 'overlayPermissionOpenSettingsFailed': 'システム設定を自動で開けませんでした。設定を開き、手動で「他のアプリの上に表示」を有効にしてください。', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。 + 'overlayPermissionTranslationDesc': + '通話中または音声/動画翻訳中に翻訳結果を表示するため、他のアプリの上に表示する権限が必要です。これにより、他のアプリを使用中でもリアルタイム翻訳を確認できます。', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。 + 'overlayPermissionRecordingDesc': + '録音中に録音フローティングバー(タイマー/一時停止/再開)を表示するため、他のアプリの上に表示する権限が必要です。', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。 + 'overlayPermissionOpenSettingsFailed': + 'システム設定を自動で開けませんでした。設定を開き、手動で「他のアプリの上に表示」を有効にしてください。', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。 'transcribing': 'Transcribing...', 'summarizing': 'Summarizing...', 'analyzing': 'Analyzing...', @@ -618,8 +622,7 @@ const Map jaJP = { // Meeting模块 - 发言者相关 'applyToAllParagraphs': 'この話者のすべての段落に適用', - 'summaryWillBeUpdatedAutomatically': - '要約はテンプレートに基づいて自動的に更新されます', + 'summaryWillBeUpdatedAutomatically': '要約はテンプレートに基づいて自動的に更新されます', 'nameCurrentSpeaker': 'Name Current Speaker', 'recentlyUsedNames': 'Recently Used Names', 'applyToCurrentParagraph': 'Apply to Current Paragraph', @@ -1032,6 +1035,21 @@ const Map jaJP = { 'storagePermissionPermanentDenial': 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', // VOITRANS + 'native.notification.navigation.title': 'ナビゲーション中', // 导航进行中 + 'native.notification.navigation.keepRunning': + 'ナビゲーション中です。アプリを起動したままにしてください', // 正在导航中,请保持应用运行 + 'native.notification.bleAgent.running': '音声アシスタントが実行中です', // 语音助手正在运行 + 'native.notification.audioRecording.recording': + '録音中です。タップしてアプリに戻る', // 正在录音,点击返回应用 + 'native.notification.music.waiting': '再生待機中', // 等待播放 + 'native.notification.music.previous': '前の曲', // 上一首 + 'native.notification.music.next': '次の曲', // 下一首 + 'native.notification.music.play': '再生', // 播放 + 'native.notification.music.pause': '一時停止', // 暂停 + 'native.notification.qqMusic.playing': '音楽を再生中', // 正在播放音乐 + //permission_util.dart 'permissionUtilNeed': 'Need ', 'permissionUtilPermission': ' Permission', diff --git a/lib/core/translations/language/ko_kr.dart b/lib/core/translations/language/ko_kr.dart index 82c25d615..3b2dd7431 100644 --- a/lib/core/translations/language/ko_kr.dart +++ b/lib/core/translations/language/ko_kr.dart @@ -1027,6 +1027,21 @@ const Map koKR = { 'storagePermissionPermanentDenial': 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', // VOITRANS + 'native.notification.navigation.title': '내비게이션 진행 중', // 导航进行中 + 'native.notification.navigation.keepRunning': + '내비게이션 중입니다. 앱을 실행 상태로 유지하세요', // 正在导航中,请保持应用运行 + 'native.notification.bleAgent.running': '음성 비서가 실행 중입니다', // 语音助手正在运行 + 'native.notification.audioRecording.recording': + '녹음 중, 탭하여 앱으로 돌아가기', // 正在录音,点击返回应用 + 'native.notification.music.waiting': '재생 대기 중', // 等待播放 + 'native.notification.music.previous': '이전 곡', // 上一首 + 'native.notification.music.next': '다음 곡', // 下一首 + 'native.notification.music.play': '재생', // 播放 + 'native.notification.music.pause': '일시정지', // 暂停 + 'native.notification.qqMusic.playing': '음악 재생 중', // 正在播放音乐 + //permission_util.dart 'permissionUtilNeed': 'Need ', 'permissionUtilPermission': ' Permission', diff --git a/lib/core/translations/language/ru_ru.dart b/lib/core/translations/language/ru_ru.dart index eccc6e122..13b6be6c8 100644 --- a/lib/core/translations/language/ru_ru.dart +++ b/lib/core/translations/language/ru_ru.dart @@ -1093,6 +1093,21 @@ const Map ruRU = { 'storagePermissionPermanentDenial': '检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', // VOITRANS + 'native.notification.navigation.title': 'Навигация запущена', // 导航进行中 + 'native.notification.navigation.keepRunning': + 'Идёт навигация, держите приложение запущенным', // 正在导航中,请保持应用运行 + 'native.notification.bleAgent.running': 'Голосовой помощник работает', // 语音助手正在运行 + 'native.notification.audioRecording.recording': + 'Идёт запись, нажмите чтобы вернуться в приложение', // 正在录音,点击返回应用 + 'native.notification.music.waiting': 'Ожидание воспроизведения', // 等待播放 + 'native.notification.music.previous': 'Предыдущая', // 上一首 + 'native.notification.music.next': 'Следующая', // 下一首 + 'native.notification.music.play': 'Воспроизвести', // 播放 + 'native.notification.music.pause': 'Пауза', // 暂停 + 'native.notification.qqMusic.playing': 'Воспроизведение музыки', // 正在播放音乐 + //permission_util.dart 'permissionUtilNeed': 'Need ', 'permissionUtilPermission': ' Permission', diff --git a/lib/core/translations/language/zh_hk.dart b/lib/core/translations/language/zh_hk.dart index 3629577cf..e56869de0 100644 --- a/lib/core/translations/language/zh_hk.dart +++ b/lib/core/translations/language/zh_hk.dart @@ -980,6 +980,19 @@ const Map zhHK = { 'storagePermissionPermanentDenial': '檢測到您已永久拒絕儲存權限。如需繼續使用此功能,請前往應用設定中手動開啟儲存權限。', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', + 'native.notification.navigation.title': '導航進行中', + 'native.notification.navigation.keepRunning': '正在導航中,請保持應用運行', + 'native.notification.bleAgent.running': '語音助手正在運行', + 'native.notification.audioRecording.recording': '正在錄音,點擊返回應用', + 'native.notification.music.waiting': '等待播放', + 'native.notification.music.previous': '上一首', + 'native.notification.music.next': '下一首', + 'native.notification.music.play': '播放', + 'native.notification.music.pause': '暫停', + 'native.notification.qqMusic.playing': '正在播放音樂', + //permission_util.dart 'permissionUtilNeed': '需要', 'permissionUtilPermission': '權限', diff --git a/lib/core/translations/language/zh_tw.dart b/lib/core/translations/language/zh_tw.dart index 47967b8ab..27d0afa85 100644 --- a/lib/core/translations/language/zh_tw.dart +++ b/lib/core/translations/language/zh_tw.dart @@ -980,6 +980,19 @@ const Map zhTW = { 'storagePermissionPermanentDenial': '檢測到您已永久拒絕儲存權限。如需繼續使用此功能,請前往應用設定中手動開啟儲存權限。', + // 原生前台服务通知(Android) + 'native.notification.appName': 'VOITRANS', + 'native.notification.navigation.title': '導航進行中', + 'native.notification.navigation.keepRunning': '正在導航中,請保持應用運行', + 'native.notification.bleAgent.running': '語音助手正在運行', + 'native.notification.audioRecording.recording': '正在錄音,點擊返回應用', + 'native.notification.music.waiting': '等待播放', + 'native.notification.music.previous': '上一首', + 'native.notification.music.next': '下一首', + 'native.notification.music.play': '播放', + 'native.notification.music.pause': '暫停', + 'native.notification.qqMusic.playing': '正在播放音樂', + //permission_util.dart 'permissionUtilNeed': '需要', 'permissionUtilPermission': '權限', diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 2e9c91893..d2111244b 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -415,6 +415,10 @@ private var companionManager: BleCompanionManager? = null // 设置服务状态 isRunning.set(true) + // 前台通知显示条件: + // - 只要 BleMediaService 被启动(Flutter 调用 AgentService.startAgentService),onCreate 会立刻执行 startForeground(); + // - 渠道使用 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在下拉通知栏里显示常驻通知; + // - Flutter 侧常见入口:PairingController 跳过配对时会调用 BleManager.startAgentService()。 startForeground(NOTIFICATION_ID, createNotification()) // 初始化BLE伴侣设备管理器 - 使用单例模式获取实例 companionManager = BleCompanionManager.getInstance(this) diff --git a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt index 6ec4d9e2e..d6490ac9e 100644 --- a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt +++ b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt @@ -61,6 +61,11 @@ class NavigationService: Service(),AMapNaviListener { override fun onCreate() { super.onCreate() instance = this + // 前台通知显示条件: + // - 只要该 Service 被启动(ContextCompat.startForegroundService / startForegroundService),onCreate 就会立刻调用 startForeground(); + // - 通知渠道使用 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在下拉通知栏里出现常驻通知; + // - Flutter 侧需要调用插件 com.yunqiinnovation.amap_service 的 "start"(会走 NavigationServiceHelper.startNavigation -> 启动该 Service), + // 调用 "open" 只会打开导航页面,不会启动该前台服务,因此不会有通知。 startForegroundNotification() // 注册广播监听器 @@ -150,6 +155,8 @@ class NavigationService: Service(),AMapNaviListener { .setOngoing(true) .build() + // 触发系统显示“前台服务常驻通知”的关键点:startForeground() + // 如果 Android 13+ 未授予通知权限 / 用户关闭了该渠道通知,可能导致通知不可见或启动前台服务失败(不同 ROM 行为可能不同)。 startForeground(NOTIFICATION_ID, notification) } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt index 77f3fbacb..46b9957a8 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt @@ -65,6 +65,10 @@ class AudioRecordingForegroundService : Service() { // 创建前台通知 val notification = createNotification() + // 前台通知显示条件: + // - 只要该 Service 被启动(AzureSpeechPlugin/AzureAsrHelper 在 enableRecord 时会启动它),就会在 onStartCommand 里立刻调用 startForeground(); + // - 渠道为 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在下拉通知栏显示常驻通知; + // - Android 13+ 如果未授予通知权限 / 用户关闭通知,可能出现通知不可见或启动前台服务失败(不同 ROM 行为可能不同)。 // 必须立即调用 startForeground 以满足 Android 8.0+ 的要求 startForeground(NOTIFICATION_ID, notification) diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index d1dad6aaa..dc57b1917 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -154,6 +154,11 @@ class MusicService : MediaBrowserServiceCompat() { setupMediaSession() val waitingTitle = getNativeLocalizedString("native.notification.music.waiting", "等待播放") val appName = getNativeLocalizedString("native.notification.appName", "Voitrans") + // 前台通知显示条件: + // - 只要 MusicService 被启动(Flutter 调用 com.yunqiinnovation.music_service 的 play/playlist 等会启动该服务), + // onCreate 会立刻调用 startForeground(),因此下拉通知栏会出现常驻通知(哪怕还没真正开始播放); + // - 渠道为 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在通知栏里常驻; + // - stop/close 会在 completeStop() 里 stopForeground(true)+stopSelf(),通知随之消失。 startForeground(NOTIFICATION_ID, createNotification(waitingTitle, appName, "")) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager From 372aac71e8d33bf647d49a55b34b77f6ebcb2691 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 5 Feb 2026 20:52:36 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor(android):=20=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E4=BB=A5=E8=BF=9B=E8=A1=8C=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 MCPClient 初始化时对 initializeToolsFunctions 的调用,改为注释状态。 此变更为临时调试措施,旨在隔离问题以定位音乐服务初始化过程中的潜在故障。 --- .../src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt index 08f05d3f1..644d90db5 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt @@ -54,7 +54,7 @@ class MCPClient(private val context: Context? = null,musicType: String = "AUTO", init { initializeSystemFunctions() - initializeToolsFunctions(searchUrl,token) + // initializeToolsFunctions(searchUrl,token) Log.d(TAG, "初始化音乐服务: $musicType") when (musicType) { "WY" -> initializeMusiceFunctions() //网易 From c6e76c5d5627db77ba0359661bcc5b81c49338bd Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 6 Feb 2026 11:39:01 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(meeting):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E4=BC=9A=E8=AE=AE=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加语言字段至数据库表,根据用户设备语言自动筛选对应模板 --- lib/data/services/db/database_helper.dart | 6 +- lib/data/services/db/sqflite_api.dart | 58 +- .../bottomSheet/generate_bottom_sheet.dart | 505 +++++++++--------- 3 files changed, 312 insertions(+), 257 deletions(-) diff --git a/lib/data/services/db/database_helper.dart b/lib/data/services/db/database_helper.dart index 34941efbe..8c3945a7a 100644 --- a/lib/data/services/db/database_helper.dart +++ b/lib/data/services/db/database_helper.dart @@ -94,7 +94,8 @@ class DatabaseHelper { batch.execute(''' CREATE TABLE IF NOT EXISTS meetingcategorytemplate ( id INTEGER PRIMARY KEY, - name TEXT + name TEXT, + language TEXT ) '''); // 商务会议助手模版 @@ -107,7 +108,8 @@ class DatabaseHelper { tag TEXT, desc TEXT, outline TEXT, - prompt TEXT + prompt TEXT, + language TEXT ) '''); batch.insert('user', { diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart index 8481cf800..b26ce9911 100644 --- a/lib/data/services/db/sqflite_api.dart +++ b/lib/data/services/db/sqflite_api.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import '/core/utils/logger.dart'; import 'database_helper.dart'; @@ -182,6 +183,42 @@ class SqfliteApi { }); } + static String _getMeetingTemplateLanguageCode() { + final locale = Get.locale ?? Get.deviceLocale; + if (locale == null) { + return 'en-US'; + } + final code = locale.languageCode.toLowerCase(); + switch (code) { + case 'zh': + return 'zh-CN'; + case 'en': + return 'en-US'; + case 'ja': + return 'ja-JP'; + case 'ko': + return 'ko-KR'; + case 'fr': + return 'fr-FR'; + case 'de': + return 'de-DE'; + case 'es': + return 'es-ES'; + case 'it': + return 'it-IT'; + case 'pt': + return 'pt-BR'; + case 'ru': + return 'ru-RU'; + case 'ar': + return 'ar-SA'; + case 'hi': + return 'hi-IN'; + default: + return 'en-US'; + } + } + // 获取商务会议助手Ask AI列表 static Future getMeetingAiList(int id) async { final db = await DatabaseHelper().database; @@ -223,6 +260,7 @@ class SqfliteApi { 'desc': item['description'], 'outline': item['outline'], 'prompt': item['template'], + 'language': item['language'], }; if (result != null) { result['templates'].add(template); @@ -230,6 +268,7 @@ class SqfliteApi { categoryList.add({ 'name': item['ttype'], 'templates': [template], + 'language': item['language'], }); } } @@ -252,6 +291,7 @@ class SqfliteApi { await txn.insert('meetingcategorytemplate', { 'id': categoryId, 'name': item['name'], + 'language': item['language'], }); for (var j = 0; j < item['templates'].length; j++) { Map template = item['templates'][j]; @@ -264,6 +304,7 @@ class SqfliteApi { 'desc': template['desc'], 'outline': template['outline'], 'prompt': template['prompt'], + 'language': template['language'], }); } } @@ -272,15 +313,22 @@ class SqfliteApi { // 获取商务会议助手所有分类模版列表 static Future getMeetingCategoryTemplateList() async { + final language = _getMeetingTemplateLanguageCode(); + Logger.info('language: $language'); final db = await DatabaseHelper().database; - List categorytemplateList = await db.query('meetingcategorytemplate'); + List categorytemplateList = await db.query( + 'meetingcategorytemplate', + where: '(language = ? OR language IS NULL OR language = "")', + whereArgs: [language], + ); + Logger.info('categorytemplateList: $categorytemplateList'); final dataList = categorytemplateList.map((item) => Map.from(item)).toList(); for (var element in dataList) { final List templateList = await db.query( 'meetingtemplate', limit: 4, - where: 'categoryid = ?', + where: 'categoryid = ? ', whereArgs: [element['id']], orderBy: 'id ${element['name'] == '自定义' ? 'DESC' : 'ASC'}', ); @@ -296,11 +344,13 @@ class SqfliteApi { // 获取商务会议助手模版列表 static Future getMeetingTemplateList(int id, {bool isCustom = false}) async { + final language = _getMeetingTemplateLanguageCode(); + Logger.info('language: $language'); final db = await DatabaseHelper().database; return await db.query( 'meetingtemplate', - where: 'categoryid = ?', - whereArgs: [id], + where: 'categoryid = ? AND language = ?', + whereArgs: [id, language], orderBy: 'id ${isCustom ? 'DESC' : 'ASC'}', ); } diff --git a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart index 62a127112..d1f66d982 100644 --- a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart @@ -93,285 +93,288 @@ class _GenerateBottomSheetState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - child: Row( - children: [ - Icon( - Icons.audio_file_outlined, - color: Colors.grey, - size: 32.w, - ), - 5.horizontalSpace, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _controller.meetingData.value.title, - style: TextStyle( - fontSize: 16.sp, - color: Colors.black87, + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Row( + children: [ + Icon( + Icons.audio_file_outlined, + color: Colors.grey, + size: 32.w, + ), + 5.horizontalSpace, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _controller.meetingData.value.title, + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + ), ), - ), - Text( - '${_controller.meetingData.value.seconds}s', - style: TextStyle( - fontSize: 12.sp, - color: Colors.grey, + Text( + '${_controller.meetingData.value.seconds}s', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, + ), ), - ), - ], - ), - ), - GestureDetector( - onTap: () => Get.back(), - child: Container( - width: 26.w, - height: 26.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(26.r), - ), - child: Icon( - Icons.close, - size: 18.w, - color: Colors.grey[400], - ), - ), - ) - ], - ), - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 12.w), - margin: EdgeInsets.only(bottom: 10.w), - child: Divider( - height: 1, - color: Colors.grey[300], - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - child: Row( - children: [ - Expanded( - child: Text( - 'selectSummaryTemplate'.tr, // 对应中文:选择总结模版 - style: TextStyle( - fontSize: 14.sp, - color: Colors.black87, + ], ), ), - ), - GestureDetector( - onTap: () async { - var result = await Get.to( - () => TemplateView( - _templateData, - _lastTemplateData['id'] ?? 0, + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), ), - preventDuplicates: false, - ); - if (result != null) { - _scrollController.jumpTo(0); - List list = List.from(_originalTemplate); - int index = list - .indexWhere((element) => element['id'] == result['id']); - if (index != -1) { - Map itemToMove = list.removeAt(index); - list.insert(0, itemToMove); - } else { - list.insert(0, result); - } - setState(() { - _templateData = result; - _templateList = list; - }); - } - if (_controller.editTemplate.isNotEmpty) { - Map? editMap = _templateList.firstWhere( - (element) => - element['id'] == _controller.editTemplate['id'], - orElse: () => null); - if (editMap != null) { - editMap['name'] = _controller.editTemplate['name']; - editMap['desc'] = _controller.editTemplate['desc']; - editMap['prompt'] = _controller.editTemplate['prompt']; - } - if (_controller.editTemplate['id'] == _templateData['id']) { - _templateData['name'] = _controller.editTemplate['name']; - _templateData['desc'] = _controller.editTemplate['desc']; - _templateData['prompt'] = - _controller.editTemplate['prompt']; - } - setState(() {}); - } - if (_controller.deleteTemplateId != 0) { - _templateList.removeWhere((element) => - element['id'] == _controller.deleteTemplateId); - if (_controller.deleteTemplateId == _templateData['id']) { - _templateData = _templateList[0]; - } - setState(() {}); - _storage.write("meeting_last_template_Data", {}); - _storage.write("meeting_template_list", _templateList); - } - }, - child: Row( - children: [ - Text( - 'viewAll'.tr, // 对应中文:查看全部 - style: TextStyle( - fontSize: 12.sp, - color: Colors.grey, - ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], ), - const Icon( - Icons.keyboard_arrow_right, - color: Colors.grey, - ) - ], - ), - ), - ], - ), - ), - Container( - height: 130.w, - margin: EdgeInsets.only(bottom: 16.w), - child: ListView.builder( - controller: _scrollController, - itemCount: _templateList.length, - scrollDirection: Axis.horizontal, - padding: EdgeInsets.symmetric(horizontal: 4.w), - itemBuilder: (BuildContext context, int index) { - Map data = _templateList[index]; - return _item(data); - }, + ), + ) + ], + ), ), - ), - if (!widget.isSummarizeAgain) Container( - margin: EdgeInsets.symmetric(horizontal: 12.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.r), + padding: EdgeInsets.symmetric(horizontal: 12.w), + margin: EdgeInsets.only(bottom: 10.w), + child: Divider( + height: 1, + color: Colors.grey[300], ), - child: Column( + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Row( children: [ - _row( - 'speakerIdentification'.tr, // 对应中文:区分说话人 - widget: Transform.scale( - scale: 0.8, - alignment: Alignment.centerRight, - child: Switch( - value: _isSpeaker, - onChanged: (bool value) { - setState(() { - _isSpeaker = value; - }); - }, + Expanded( + child: Text( + 'selectSummaryTemplate'.tr, // 对应中文:选择总结模版 + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, ), ), ), - _row( - 'recordingLanguage'.tr, // 对应中文:录音语言 - value: _formlanguageText, - onTap: () { - Get.bottomSheet( - RecordLanguageBottomSheet(_formlanguage), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), + GestureDetector( + onTap: () async { + var result = await Get.to( + () => TemplateView( + _templateData, + _lastTemplateData['id'] ?? 0, ), - ).then((value) { - if (value != null) { - setState(() { - _formlanguageText = value['name']; - _formlanguage = value['value']; - }); + preventDuplicates: false, + ); + if (result != null) { + _scrollController.jumpTo(0); + List list = List.from(_originalTemplate); + int index = list.indexWhere( + (element) => element['id'] == result['id']); + if (index != -1) { + Map itemToMove = list.removeAt(index); + list.insert(0, itemToMove); + } else { + list.insert(0, result); + } + setState(() { + _templateData = result; + _templateList = list; + }); + } + if (_controller.editTemplate.isNotEmpty) { + Map? editMap = _templateList.firstWhere( + (element) => + element['id'] == _controller.editTemplate['id'], + orElse: () => null); + if (editMap != null) { + editMap['name'] = _controller.editTemplate['name']; + editMap['desc'] = _controller.editTemplate['desc']; + editMap['prompt'] = _controller.editTemplate['prompt']; + } + if (_controller.editTemplate['id'] == + _templateData['id']) { + _templateData['name'] = + _controller.editTemplate['name']; + _templateData['desc'] = + _controller.editTemplate['desc']; + _templateData['prompt'] = + _controller.editTemplate['prompt']; + } + setState(() {}); + } + if (_controller.deleteTemplateId != 0) { + _templateList.removeWhere((element) => + element['id'] == _controller.deleteTemplateId); + if (_controller.deleteTemplateId == _templateData['id']) { + _templateData = _templateList[0]; } - }); + setState(() {}); + _storage.write("meeting_last_template_Data", {}); + _storage.write("meeting_template_list", _templateList); + } }, - ), - _row( - 'targetLanguage'.tr, // 对应中文:目标语言 - value: _tolanguageText, - isBorder: false, - onTap: () { - Get.bottomSheet( - RecordLanguageBottomSheet(_tolanguage), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), + child: Row( + children: [ + Text( + 'viewAll'.tr, // 对应中文:查看全部 + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, ), ), - ).then((value) { - if (value != null) { - setState(() { - _tolanguageText = value['name']; - _tolanguage = value['value']; - }); - } - }); - }, + const Icon( + Icons.keyboard_arrow_right, + color: Colors.grey, + ) + ], + ), ), ], ), ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), - child: GestureDetector( - onTap: () { - if (User.isLoggedIn() && User.instance.meetintegral <= 0) { - showRechargeDialog('meeting'); - return; - } - if (_templateData['id'] != _templateList[0]['id']) { - int index = _templateList - .indexWhere((v) => v['id'] == _templateData['id']); - if (index != -1) { - Map itemToMove = _templateList.removeAt(index); - _templateList.insert(0, itemToMove); - } - } - _storage.write("meeting_last_template_Data", _templateData); - _storage.write("meeting_template_list", _templateList); - Get.back(); - if (widget.isSummarizeAgain) { - _controller.refreshSummary(_templateData['id']); - } else { - _controller.summaryCreate( - _formlanguage, - _tolanguage, - _isSpeaker, - _templateData['id'], - ); - } - }, - child: Container( - height: 48.w, - alignment: Alignment.center, + Container( + height: 130.w, + margin: EdgeInsets.only(bottom: 16.w), + child: ListView.builder( + controller: _scrollController, + itemCount: _templateList.length, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 4.w), + itemBuilder: (BuildContext context, int index) { + Map data = _templateList[index]; + return _item(data); + }, + ), + ), + if (!widget.isSummarizeAgain) + Container( + margin: EdgeInsets.symmetric(horizontal: 12.w), decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(12.r), + color: Colors.white, + borderRadius: BorderRadius.circular(8.r), ), - child: Text( - 'meetingGenerateNow'.tr, // 对应中文:立即生成 - style: TextStyle( - fontSize: 16.sp, - color: Colors.white, + child: Column( + children: [ + _row( + 'speakerIdentification'.tr, // 对应中文:区分说话人 + widget: Transform.scale( + scale: 0.8, + alignment: Alignment.centerRight, + child: Switch( + value: _isSpeaker, + onChanged: (bool value) { + setState(() { + _isSpeaker = value; + }); + }, + ), + ), + ), + _row( + 'recordingLanguage'.tr, // 对应中文:录音语言 + value: _formlanguageText, + onTap: () { + Get.bottomSheet( + RecordLanguageBottomSheet(_formlanguage), + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ).then((value) { + if (value != null) { + setState(() { + _formlanguageText = value['name']; + _formlanguage = value['value']; + }); + } + }); + }, + ), + _row( + 'targetLanguage'.tr, // 对应中文:目标语言 + value: _tolanguageText, + isBorder: false, + onTap: () { + Get.bottomSheet( + RecordLanguageBottomSheet(_tolanguage), + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ).then((value) { + if (value != null) { + setState(() { + _tolanguageText = value['name']; + _tolanguage = value['value']; + }); + } + }); + }, + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + child: GestureDetector( + onTap: () { + if (User.isLoggedIn() && User.instance.meetintegral <= 0) { + showRechargeDialog('meeting'); + return; + } + if (_templateData['id'] != _templateList[0]['id']) { + int index = _templateList + .indexWhere((v) => v['id'] == _templateData['id']); + if (index != -1) { + Map itemToMove = _templateList.removeAt(index); + _templateList.insert(0, itemToMove); + } + } + _storage.write("meeting_last_template_Data", _templateData); + _storage.write("meeting_template_list", _templateList); + Get.back(); + if (widget.isSummarizeAgain) { + _controller.refreshSummary(_templateData['id']); + } else { + _controller.summaryCreate( + _formlanguage, + _tolanguage, + _isSpeaker, + _templateData['id'], + ); + } + }, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + 'meetingGenerateNow'.tr, // 对应中文:立即生成 + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), ), ), ), ), - ), ], ), );