diff --git a/.gitignore b/.gitignore index 51e5178e8..ac4aa4092 100644 --- a/.gitignore +++ b/.gitignore @@ -221,3 +221,4 @@ fastlane/test_output fastlane/readme.md .trae/rules/project_rules.md .augment/rules/imported/project_rules.md +.kiro/ diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 000000000..10e684870 --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectName":"trae_deep_voice_k3cu"} \ No newline at end of file diff --git a/assets/fonts/NotoSansSC-Regular.ttf b/assets/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 000000000..176f1134e Binary files /dev/null and b/assets/fonts/NotoSansSC-Regular.ttf differ diff --git a/assets/images/headphone_new3.png b/assets/images/headphone_new3.png new file mode 100644 index 000000000..6906348ce Binary files /dev/null and b/assets/images/headphone_new3.png differ diff --git a/assets/images/headphone_new4.png b/assets/images/headphone_new4.png new file mode 100644 index 000000000..7161868f8 Binary files /dev/null and b/assets/images/headphone_new4.png differ diff --git a/assets/images/mine-background.png b/assets/images/mine-background.png new file mode 100644 index 000000000..c12cdda23 Binary files /dev/null and b/assets/images/mine-background.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index b48f5642d..000000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,201 +0,0 @@ -PODS: - - amap_service (0.0.1): - - AMapNavi (~> 10.0.900) - - Flutter - - AMapFoundation (1.8.2) - - AMapNavi (10.0.900): - - AMapFoundation (>= 1.8.2) - - audio_waveforms (0.0.1): - - Flutter - - classic_bluetooth (0.0.1): - - Flutter - - FBAEMKit (18.0.0): - - FBSDKCoreKit_Basics (= 18.0.0) - - FBSDKCoreKit (18.0.0): - - FBAEMKit (= 18.0.0) - - FBSDKCoreKit_Basics (= 18.0.0) - - FBSDKCoreKit_Basics (18.0.0) - - FBSDKLoginKit (18.0.0): - - FBSDKCoreKit (= 18.0.0) - - Flutter (1.0.0) - - flutter_facebook_auth (7.1.2): - - FBSDKLoginKit (~> 18.0.0) - - Flutter - - flutter_image_compress_common (1.0.0): - - Flutter - - Mantle - - SDWebImage - - SDWebImageWebPCoder - - flutter_local_notifications (0.0.1): - - Flutter - - flutter_secure_storage (6.0.0): - - Flutter - - flutter_tts (0.0.1): - - Flutter - - fluwx (0.0.1): - - Flutter - - fluwx/pay (= 0.0.1) - - fluwx/pay (0.0.1): - - Flutter - - WechatOpenSDK-XCFramework (~> 2.0.4) - - haptic_feedback (0.5.1): - - Flutter - - keyboard_height_plugin (0.0.1): - - Flutter - - libwebp (1.5.0): - - libwebp/demux (= 1.5.0) - - libwebp/mux (= 1.5.0) - - libwebp/sharpyuv (= 1.5.0) - - libwebp/webp (= 1.5.0) - - libwebp/demux (1.5.0): - - libwebp/webp - - libwebp/mux (1.5.0): - - libwebp/demux - - libwebp/sharpyuv (1.5.0) - - libwebp/webp (1.5.0): - - libwebp/sharpyuv - - Mantle (2.2.0): - - Mantle/extobjc (= 2.2.0) - - Mantle/extobjc (2.2.0) - - ota (0.0.1): - - Flutter - - permission_handler_apple (9.3.0): - - Flutter - - QCloudCore (6.4.7): - - QCloudCore/Default (= 6.4.7) - - QCloudCore/Default (6.4.7): - - QCloudTrack/Beacon (= 6.4.7) - - QCloudCOSXML (6.4.7): - - QCloudCOSXML/Default (= 6.4.7) - - QCloudCOSXML/Default (6.4.7): - - QCloudCore (= 6.4.7) - - QCloudTrack/Beacon (6.4.7) - - record_darwin (1.0.0): - - Flutter - - SDWebImage (5.21.1): - - SDWebImage/Core (= 5.21.1) - - SDWebImage/Core (5.21.1) - - SDWebImageWebPCoder (0.14.6): - - libwebp (~> 1.0) - - SDWebImage/Core (~> 5.17) - - spotify_sdk (0.0.1): - - Flutter - - tencentcloud_cos_sdk_plugin (1.2.3): - - Flutter - - QCloudCOSXML (= 6.4.7) - - WechatOpenSDK-XCFramework (2.0.4) - - wifi_iot (0.0.1): - - Flutter - -DEPENDENCIES: - - amap_service (from `.symlinks/plugins/amap_service/ios`) - - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) - - classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`) - - Flutter (from `Flutter`) - - flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`) - - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) - - fluwx (from `.symlinks/plugins/fluwx/ios`) - - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`) - - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`) - - ota (from `.symlinks/plugins/ota/ios`) - - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - record_darwin (from `.symlinks/plugins/record_darwin/ios`) - - spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`) - - tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`) - - wifi_iot (from `.symlinks/plugins/wifi_iot/ios`) - -SPEC REPOS: - trunk: - - AMapFoundation - - AMapNavi - - FBAEMKit - - FBSDKCoreKit - - FBSDKCoreKit_Basics - - FBSDKLoginKit - - libwebp - - Mantle - - QCloudCore - - QCloudCOSXML - - QCloudTrack - - SDWebImage - - SDWebImageWebPCoder - - WechatOpenSDK-XCFramework - -EXTERNAL SOURCES: - amap_service: - :path: ".symlinks/plugins/amap_service/ios" - audio_waveforms: - :path: ".symlinks/plugins/audio_waveforms/ios" - classic_bluetooth: - :path: ".symlinks/plugins/classic_bluetooth/ios" - Flutter: - :path: Flutter - flutter_facebook_auth: - :path: ".symlinks/plugins/flutter_facebook_auth/ios" - flutter_image_compress_common: - :path: ".symlinks/plugins/flutter_image_compress_common/ios" - flutter_local_notifications: - :path: ".symlinks/plugins/flutter_local_notifications/ios" - flutter_secure_storage: - :path: ".symlinks/plugins/flutter_secure_storage/ios" - flutter_tts: - :path: ".symlinks/plugins/flutter_tts/ios" - fluwx: - :path: ".symlinks/plugins/fluwx/ios" - haptic_feedback: - :path: ".symlinks/plugins/haptic_feedback/ios" - keyboard_height_plugin: - :path: ".symlinks/plugins/keyboard_height_plugin/ios" - ota: - :path: ".symlinks/plugins/ota/ios" - permission_handler_apple: - :path: ".symlinks/plugins/permission_handler_apple/ios" - record_darwin: - :path: ".symlinks/plugins/record_darwin/ios" - spotify_sdk: - :path: ".symlinks/plugins/spotify_sdk/ios" - tencentcloud_cos_sdk_plugin: - :path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios" - wifi_iot: - :path: ".symlinks/plugins/wifi_iot/ios" - -SPEC CHECKSUMS: - amap_service: 5d40a69bd91031df3da423894b75a0236e26da96 - AMapFoundation: 9885c48fc3a78fdfb84a0299a2293e56ea3c9fec - AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd - audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf - classic_bluetooth: 29f392f1ab7c52862a9ef036c94f2713b02e0c50 - FBAEMKit: e34530df538b8eb8aeb53c35867715ba6c63ef0c - FBSDKCoreKit: d3f479a69127acebb1c6aad91c1a33907bcf6c2f - FBSDKCoreKit_Basics: 017b6dc2a1862024815a8229e75661e627ac1e29 - FBSDKLoginKit: 5875762d1fe09ddcb05d03365d4f5dc34413843d - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_facebook_auth: d97dc09216aebd99d7ac52360186a630f67bdf18 - flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 - flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 - flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 - flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833 - fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b - haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0 - keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 - libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - ota: 2256716620ec8ecfc2aea220679557f94b13915f - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223 - QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298 - QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3 - record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a - SDWebImage: f29024626962457f3470184232766516dee8dfea - SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 - spotify_sdk: 60c0a0c8d154f2c67f5598f2d652c3877e2b570a - tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b - WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f - wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc - -PODFILE CHECKSUM: e69e629768e642fde97676ea149350c61d57751e - -COCOAPODS: 1.16.2 diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 899e87492..73826786b 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,4 +1,4 @@ -import 'package:deep_voice/data/services/user_portrait.dart'; +import '../../../data/services/user_portrait.dart'; import '../../../data/services/location_manager.dart'; import '../../../data/services/music_manager.dart'; diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 48cd5df5f..afb5f6242 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -522,6 +522,47 @@ const Map enUS = { 'You have permanently denied {permission} permission, please go to app settings to enable it', 'goToSettings': 'Go to Settings', + // 权限申请 + 'bluetoothScan': 'Bluetooth Scan', + 'bluetoothConnect': 'Bluetooth Connect', + 'bluetoothScanExplanation': + 'The app needs Bluetooth scan permission to scan nearby Bluetooth devices.', + 'bluetoothConnectExplanation': + 'The app needs Bluetooth connect permission to connect to Bluetooth devices.', + 'locationForBluetoothExplanation': + 'The app needs location permission to scan Bluetooth devices.', + 'bluetoothScanPermanentDenial': + 'Bluetooth scan permission has been permanently denied, please go to settings to enable it manually.', + 'bluetoothConnectPermanentDenial': + 'Bluetooth connect permission has been permanently denied, please go to settings to enable it manually.', + 'notificationPermission': 'Notification', + 'locationPermission': 'Location', + 'notificationPermissionExplanation': + 'Notification permission is required to display music playback status.', + 'locationPermissionExplanation': + 'Location permission is required to get current location.', + 'notificationPermissionPermanentDenial': + 'You have permanently denied notification permission, please go to settings to enable it.', + 'locationPermissionPermanentDenial': + 'You have permanently denied location permission, please go to settings to enable it.', + //'microphonePermission': 'Microphone', + 'storagePermission': 'Storage', + 'microphonePermissionExplanation': + 'We need microphone permission to record audio content.', + 'storagePermissionExplanation': + 'We need storage permission so you can select and upload audio files.', + 'microphonePermissionPermanentDenial': + 'Microphone permission has been permanently denied. To continue using this feature, please go to app settings to manually enable microphone permission.', + 'storagePermissionPermanentDenial': + 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', + +//permission_util.dart + 'permissionUtilNeed': 'Need ', + 'permissionUtilPermission': ' Permission', + 'permissionUtilRestricted': ' Permission Restricted', + 'permissionUtilRestrictedMessage': + 'This feature is unavailable or restricted on the current device, please check device settings or system restrictions', + 'permissionUtilConfirm': 'OK', // 版本更新对话框 'newVersionFound': 'New Version Found', // 版本更新对话框标题 'remindLater': 'Remind Later', // 稍后提醒按钮 @@ -764,7 +805,8 @@ const Map enUS = { 'invalidDeviceAddress': 'Invalid device address', 'bleDeviceAssociationFailed': 'BLE device association failed', 'deviceAssociationFailedRetry': 'Device association failed, please retry', - 'bleConnectionTimeoutRetry': 'BLE connection timeout, please retry', + 'bleConnectionTimeoutRetry': + 'BLE connection timeout. Please restart the earphone and try again', 'blePairingStartupFailed': 'BLE pairing startup failed', 'bleConnectionFailed': 'BLE connection failed', 'deviceAuthorizationFailedCheckCode': @@ -851,4 +893,16 @@ const Map enUS = { "乌尔都语": "Urdu", "波斯语": "Persian", "斯瓦希里语": "Swahili", + + // Time related + "justNow": "Just now", + "minutesAgo": "@minutes minutes ago", + "yesterday": "Yesterday", + // "monday": "Monday", + // "tuesday": "Tuesday", + // "wednesday": "Wednesday", + // "thursday": "Thursday", + // "friday": "Friday", + // "saturday": "Saturday", + // "sunday": "Sunday", }; diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 177613276..23aab872a 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -482,6 +482,37 @@ const Map zhCN = { 'permanentDenialText': '您已永久拒绝{permission}权限,请前往应用设置开启', 'goToSettings': '去设置', + // 权限申请 + 'bluetoothScan': '蓝牙扫描', + 'bluetoothConnect': '蓝牙连接', + 'bluetoothScanExplanation': '为了扫描附近的蓝牙设备,应用需要获取蓝牙扫描权限。', + 'bluetoothConnectExplanation': '为了连接蓝牙设备,应用需要获取蓝牙连接权限。', + 'locationForBluetoothExplanation': '为了扫描蓝牙设备,应用需要获取位置信息权限。', + 'bluetoothScanPermanentDenial': '蓝牙扫描权限已被永久拒绝,请前往设置手动开启。', + 'bluetoothConnectPermanentDenial': '蓝牙连接权限已被永久拒绝,请前往设置手动开启。', + 'locationPermanentDenial': '位置信息权限已被永久拒绝,请前往设置手动开启。', + 'notificationPermission': '通知', + 'locationPermission': '定位', + 'notificationPermissionExplanation': '需要通知权限来显示音乐播放状态。', + 'locationPermissionExplanation': '需要定位权限来获取当前位置。', + 'notificationPermissionPermanentDenial': '您已永久拒绝通知权限,请前往设置开启权限。', + 'locationPermissionPermanentDenial': '您已永久拒绝定位权限,请前往设置开启权限。', +//'microphonePermission': '录音', + 'storagePermission': '存储', + 'microphonePermissionExplanation': '我们需要访问麦克风权限,以便您录制音频内容。', + 'storagePermissionExplanation': '我们需要访问存储权限,以便您可以选择并上传音频文件。', + 'microphonePermissionPermanentDenial': + '检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。', + 'storagePermissionPermanentDenial': + '检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。', + + //permission_util.dart + 'permissionUtilNeed': '需要', + 'permissionUtilPermission': '权限', + 'permissionUtilRestricted': '权限受限', + 'permissionUtilRestrictedMessage': '此功能在当前设备上不可用或受限,请检查设备设置或系统限制', + 'permissionUtilConfirm': '确定', + // 版本更新对话框 'newVersionFound': '发现新版本', // 版本更新对话框标题 'remindLater': '稍后提醒', // 稍后提醒按钮 @@ -710,7 +741,7 @@ const Map zhCN = { 'invalidDeviceAddress': '设备地址无效', 'bleDeviceAssociationFailed': 'BLE设备关联失败', 'deviceAssociationFailedRetry': '设备关联失败,请重试', - 'bleConnectionTimeoutRetry': 'BLE连接超时,请重试', + 'bleConnectionTimeoutRetry': 'BLE连接超时,请重启耳机后再试', 'blePairingStartupFailed': 'BLE配对启动失败', 'bleConnectionFailed': 'BLE连接失败', 'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码', @@ -793,4 +824,9 @@ const Map zhCN = { "乌尔都语": "乌尔都语", "波斯语": "波斯语", "斯瓦希里语": "斯瓦希里语", + + // 时间相关 + "justNow": "刚刚", + "minutesAgo": "@minutes分钟前", + "yesterday": "昨天", }; diff --git a/lib/core/utils/permission_util.dart b/lib/core/utils/permission_util.dart index 04ea36e25..2a88c5cd9 100644 --- a/lib/core/utils/permission_util.dart +++ b/lib/core/utils/permission_util.dart @@ -42,9 +42,14 @@ class PermissionUtil { required String permissionName, required String explanationText, required String permanentDenialText, - String settingsButtonText = '前往设置', - String cancelButtonText = '取消', + String? settingsButtonText, + String? cancelButtonText, }) async { + // 使用翻译键作为默认值 + final String finalSettingsButtonText = + settingsButtonText ?? 'goToSettings'.tr; + final String finalCancelButtonText = cancelButtonText ?? 'cancel'.tr; + try { // 检查初始权限状态 final PermissionStatus status = await permissionType.status; @@ -73,8 +78,8 @@ class PermissionUtil { // context: context, permissionName: permissionName, permanentDenialText: permanentDenialText, - settingsButtonText: settingsButtonText, - cancelButtonText: cancelButtonText, + settingsButtonText: finalSettingsButtonText, + cancelButtonText: finalCancelButtonText, ); // 移除解释横幅 _hideExplanationOverlay(); @@ -109,8 +114,8 @@ class PermissionUtil { // context: context, permissionName: permissionName, permanentDenialText: permanentDenialText, - settingsButtonText: settingsButtonText, - cancelButtonText: cancelButtonText, + settingsButtonText: finalSettingsButtonText, + cancelButtonText: finalCancelButtonText, ); _hideExplanationOverlay(); return result; @@ -171,7 +176,7 @@ class PermissionUtil { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '需要$permissionName权限', + 'permissionUtilNeed'.tr + permissionName + 'permissionUtilPermission'.tr, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -256,7 +261,7 @@ class PermissionUtil { bool? userChoice = await Get.dialog( AlertDialog( - title: Text('需要$permissionName权限'), + title: Text('permissionUtilNeed'.tr + permissionName + 'permissionUtilPermission'.tr), content: Text(permanentDenialText), actions: [ TextButton( @@ -301,8 +306,8 @@ class PermissionUtil { Logger.w('PermissionUtil', '显示受限权限对话框 ($permissionName)'); Get.dialog( AlertDialog( - title: Text('$permissionName权限受限'), - content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'), + title: Text(permissionName + 'permissionUtilRestricted'.tr), + content: Text('permissionUtilRestrictedMessage'.tr), actions: [ TextButton( onPressed: () { @@ -310,7 +315,7 @@ class PermissionUtil { // Navigator.pop(context); Get.back(); }, - child: const Text('确定'), + child: Text('permissionUtilConfirm'.tr), ), ], ), diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart index dfbbd8c70..7a91df50d 100644 --- a/lib/data/models/appconfig_model.dart +++ b/lib/data/models/appconfig_model.dart @@ -71,14 +71,24 @@ class DBMCPServer { @JsonSerializable() class DBProduct { - final String id; + final int id; final int factoryid; + final int devicetype; final String devicename; + final String scanuuid; + final String version; + final String updatedescription; + final String updatepackageaddress; DBProduct({ required this.id, required this.factoryid, + required this.devicetype, required this.devicename, + required this.scanuuid, + required this.version, + required this.updatedescription, + required this.updatepackageaddress, }); factory DBProduct.fromJson(Map json) => diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart index 3c6675929..c83d063fb 100644 --- a/lib/data/models/appconfig_model.g.dart +++ b/lib/data/models/appconfig_model.g.dart @@ -72,9 +72,14 @@ Map _$DBMCPServerToJson(DBMCPServer instance) => }; DBProduct _$DBProductFromJson(Map json) => DBProduct( - id: json['id'] as String, + id: json['id'] as int, factoryid: (json['factoryid'] as num).toInt(), devicename: json['devicename'] as String, + devicetype: json['devicetype'] as int, + scanuuid: json['scanuuid'] as String, + version: json['version'] as String, + updatedescription: json['updatedescription'] as String, + updatepackageaddress: json['updatepackageaddress'] as String, ); Map _$DBProductToJson(DBProduct instance) => { diff --git a/lib/data/models/channel_app_model.dart b/lib/data/models/channel_app_model.dart index fbf3b0bf2..7fc3dcabf 100644 --- a/lib/data/models/channel_app_model.dart +++ b/lib/data/models/channel_app_model.dart @@ -74,7 +74,7 @@ class AppInfo { required this.channel, required this.description, required this.version, - required this.tourists, + this.tourists = false, }); factory AppInfo.fromJson(Map json) => diff --git a/lib/data/models/channel_app_model.g.dart b/lib/data/models/channel_app_model.g.dart index 2c6e524f5..952f3fc41 100644 --- a/lib/data/models/channel_app_model.g.dart +++ b/lib/data/models/channel_app_model.g.dart @@ -37,7 +37,7 @@ AppInfo _$AppInfoFromJson(Map json) => AppInfo( channel: UserChannel.fromJson(json['channel']), description: json['description'] as String, version: json['version'] as String, - tourists: json['tourists'] as bool, + tourists: json['tourists'] as bool? ?? false, ); Map _$AppInfoToJson(AppInfo instance) => { diff --git a/lib/data/models/translation_usage_stats.dart b/lib/data/models/translation_usage_stats.dart new file mode 100644 index 000000000..6d73b1958 --- /dev/null +++ b/lib/data/models/translation_usage_stats.dart @@ -0,0 +1,450 @@ + + +/// 翻译模式统计数据 +class TranslationModeStats { + final int apiCalls; // API调用次数 + final int sourceCharacters; // 源文本字符数 + final int effectiveAudioDuration; // 有效音频时长 + final int ttsCharacters; // TTS字符数 + final double cost; // 成本 + + TranslationModeStats({ + this.apiCalls = 0, + this.sourceCharacters = 0, + this.effectiveAudioDuration = 0, + this.ttsCharacters = 0, + this.cost = 0.0, + }); + + factory TranslationModeStats.fromJson(Map json) { + return TranslationModeStats( + apiCalls: json['apiCalls'] ?? 0, + sourceCharacters: json['sourceCharacters'] ?? 0, + effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0, + ttsCharacters: json['ttsCharacters'] ?? 0, + cost: (json['cost'] ?? 0.0).toDouble(), + ); + } + + Map toJson() { + return { + 'apiCalls': apiCalls, + 'sourceCharacters': sourceCharacters, + 'effectiveAudioDuration': effectiveAudioDuration, + 'ttsCharacters': ttsCharacters, + 'cost': cost, + }; + } + + TranslationModeStats copyWith({ + int? apiCalls, + int? sourceCharacters, + int? effectiveAudioDuration, + int? ttsCharacters, + double? cost, + }) { + return TranslationModeStats( + apiCalls: apiCalls ?? this.apiCalls, + sourceCharacters: sourceCharacters ?? this.sourceCharacters, + effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration, + ttsCharacters: ttsCharacters ?? this.ttsCharacters, + cost: cost ?? this.cost, + ); + } +} + +/// 语言对统计数据 +class LanguagePairStats { + final String languagePair; // 语言对,如"zh-CN→en-US" + final int translationCount; // 翻译次数 + final int sourceCharacters; // 源文本字符数 + final double cost; // 成本 + + LanguagePairStats({ + required this.languagePair, + this.translationCount = 0, + this.sourceCharacters = 0, + this.cost = 0.0, + }); + + factory LanguagePairStats.fromJson(Map json) { + return LanguagePairStats( + languagePair: json['languagePair'] ?? '', + translationCount: json['translationCount'] ?? 0, + sourceCharacters: json['sourceCharacters'] ?? 0, + cost: (json['cost'] ?? 0.0).toDouble(), + ); + } + + Map toJson() { + return { + 'languagePair': languagePair, + 'translationCount': translationCount, + 'sourceCharacters': sourceCharacters, + 'cost': cost, + }; + } +} + +/// 小时统计数据 +class HourlyStats { + final int hour; // 小时(0-23) + final int apiCalls; // API调用次数 + final int sourceCharacters; // 源文本字符数 + final int effectiveAudioDuration; // 有效音频时长 + final double cost; // 成本 + + HourlyStats({ + required this.hour, + this.apiCalls = 0, + this.sourceCharacters = 0, + this.effectiveAudioDuration = 0, + this.cost = 0.0, + }); + + factory HourlyStats.fromJson(Map json) { + return HourlyStats( + hour: json['hour'] ?? 0, + apiCalls: json['apiCalls'] ?? 0, + sourceCharacters: json['sourceCharacters'] ?? 0, + effectiveAudioDuration: json['effectiveAudioDuration'] ?? 0, + cost: (json['cost'] ?? 0.0).toDouble(), + ); + } + + Map toJson() { + return { + 'hour': hour, + 'apiCalls': apiCalls, + 'sourceCharacters': sourceCharacters, + 'effectiveAudioDuration': effectiveAudioDuration, + 'cost': cost, + }; + } +} + +/// 翻译使用统计数据模型 - 优化版 +class TranslationUsageStats { + // 基础信息 + final String id; // 统计记录ID + final DateTime date; // 统计日期 + final String userId; // 用户ID + final String timeGranularity; // 时间粒度:realtime/hour/day/week/month + + // API调用统计 - 精确化 + final int translationApiCalls; // 翻译API调用次数 + final int asrApiCalls; // ASR语音识别调用次数 + final int ttsApiCalls; // TTS语音合成调用次数 + final int aiSummaryApiCalls; // AI摘要生成调用次数(已废弃但保留兼容性) + + // 精确计费参数统计 + final int sourceTextCharacters; // 源文本字符数(翻译API计费基础) + final int effectiveAudioDuration; // ASR有效语音时长(秒,ASR计费基础) + final int ttsTextCharacters; // TTS文本字符数(TTS计费基础) + final int totalSessionDuration; // 总会话时长(秒,用于分析) + + // 网络流量统计 + final int asrNetworkBytes; // ASR网络流量(字节) + final int ttsNetworkBytes; // TTS网络流量(字节) + final int translationNetworkBytes; // 翻译API网络流量(字节) + + // 多维度分类统计 + final int sessionCount; // 翻译会话数量 + final Map modeStats; // 按翻译模式分类统计 + final Map languagePairStats; // 按语言对分类统计 + final Map hourlyStats; // 按小时分类统计(仅日统计包含) + + // 成本相关 + final double estimatedCost; // 预估成本 + final String currency; // 货币单位 + final Map costBreakdown; // 成本分解(ASR/TTS/Translation) + + TranslationUsageStats({ + required this.id, + required this.date, + required this.userId, + this.timeGranularity = 'day', + this.translationApiCalls = 0, + this.asrApiCalls = 0, + this.ttsApiCalls = 0, + this.aiSummaryApiCalls = 0, + this.sourceTextCharacters = 0, + this.effectiveAudioDuration = 0, + this.ttsTextCharacters = 0, + this.totalSessionDuration = 0, + this.asrNetworkBytes = 0, + this.ttsNetworkBytes = 0, + this.translationNetworkBytes = 0, + this.sessionCount = 0, + this.modeStats = const {}, + this.languagePairStats = const {}, + this.hourlyStats = const {}, + this.estimatedCost = 0.0, + this.currency = 'CNY', + this.costBreakdown = const {}, + }); + + factory TranslationUsageStats.fromJson(Map json) { + // 处理多维度统计数据的反序列化 + final modeStatsMap = {}; + if (json['modeStats'] != null) { + final modeStatsJson = json['modeStats'] as Map; + modeStatsJson.forEach((key, value) { + modeStatsMap[key] = TranslationModeStats.fromJson(value); + }); + } + + final languagePairStatsMap = {}; + if (json['languagePairStats'] != null) { + final languagePairStatsJson = json['languagePairStats'] as Map; + languagePairStatsJson.forEach((key, value) { + languagePairStatsMap[key] = LanguagePairStats.fromJson(value); + }); + } + + final hourlyStatsMap = {}; + if (json['hourlyStats'] != null) { + final hourlyStatsJson = json['hourlyStats'] as Map; + hourlyStatsJson.forEach((key, value) { + hourlyStatsMap[key] = HourlyStats.fromJson(value); + }); + } + + return TranslationUsageStats( + id: json['id'] ?? '', + date: DateTime.parse(json['date']), + userId: json['userId'] ?? '', + timeGranularity: json['timeGranularity'] ?? 'day', + translationApiCalls: json['translationApiCalls'] ?? 0, + asrApiCalls: json['asrApiCalls'] ?? 0, + ttsApiCalls: json['ttsApiCalls'] ?? 0, + aiSummaryApiCalls: json['aiSummaryApiCalls'] ?? 0, + sourceTextCharacters: json['sourceTextCharacters'] ?? json['sourceCharacterCount'] ?? 0, // 兼容旧字段 + effectiveAudioDuration: json['effectiveAudioDuration'] ?? json['audioRecordingDuration'] ?? 0, // 兼容旧字段 + ttsTextCharacters: json['ttsTextCharacters'] ?? json['targetCharacterCount'] ?? 0, // 兼容旧字段 + totalSessionDuration: json['totalSessionDuration'] ?? 0, + asrNetworkBytes: json['asrNetworkBytes'] ?? 0, + ttsNetworkBytes: json['ttsNetworkBytes'] ?? 0, + translationNetworkBytes: json['translationNetworkBytes'] ?? 0, + sessionCount: json['sessionCount'] ?? 0, + modeStats: modeStatsMap, + languagePairStats: languagePairStatsMap, + hourlyStats: hourlyStatsMap, + estimatedCost: (json['estimatedCost'] ?? 0.0).toDouble(), + currency: json['currency'] ?? 'CNY', + costBreakdown: Map.from(json['costBreakdown'] ?? {}), + ); + } + + Map toJson() { + // 序列化多维度统计数据 + final modeStatsJson = {}; + modeStats.forEach((key, value) { + modeStatsJson[key] = value.toJson(); + }); + + final languagePairStatsJson = {}; + languagePairStats.forEach((key, value) { + languagePairStatsJson[key] = value.toJson(); + }); + + final hourlyStatsJson = {}; + hourlyStats.forEach((key, value) { + hourlyStatsJson[key] = value.toJson(); + }); + + return { + 'id': id, + 'date': date.toIso8601String(), + 'userId': userId, + 'timeGranularity': timeGranularity, + 'translationApiCalls': translationApiCalls, + 'asrApiCalls': asrApiCalls, + 'ttsApiCalls': ttsApiCalls, + 'aiSummaryApiCalls': aiSummaryApiCalls, + 'sourceTextCharacters': sourceTextCharacters, + 'effectiveAudioDuration': effectiveAudioDuration, + 'ttsTextCharacters': ttsTextCharacters, + 'totalSessionDuration': totalSessionDuration, + 'asrNetworkBytes': asrNetworkBytes, + 'ttsNetworkBytes': ttsNetworkBytes, + 'translationNetworkBytes': translationNetworkBytes, + 'sessionCount': sessionCount, + 'modeStats': modeStatsJson, + 'languagePairStats': languagePairStatsJson, + 'hourlyStats': hourlyStatsJson, + 'estimatedCost': estimatedCost, + 'currency': currency, + 'costBreakdown': costBreakdown, + // 保持向后兼容性 + 'sourceCharacterCount': sourceTextCharacters, + 'targetCharacterCount': ttsTextCharacters, + 'audioRecordingDuration': effectiveAudioDuration, + 'ttsPlaybackDuration': totalSessionDuration, + }; + } + + /// 创建空的统计记录 + factory TranslationUsageStats.empty(String userId) { + return TranslationUsageStats( + id: DateTime.now().millisecondsSinceEpoch.toString(), + date: DateTime.now(), + userId: userId, + ); + } + + /// 复制并更新统计数据 + TranslationUsageStats copyWith({ + String? id, + DateTime? date, + String? userId, + String? timeGranularity, + int? translationApiCalls, + int? asrApiCalls, + int? ttsApiCalls, + int? aiSummaryApiCalls, + int? sourceTextCharacters, + int? effectiveAudioDuration, + int? ttsTextCharacters, + int? totalSessionDuration, + int? asrNetworkBytes, + int? ttsNetworkBytes, + int? translationNetworkBytes, + int? sessionCount, + Map? modeStats, + Map? languagePairStats, + Map? hourlyStats, + double? estimatedCost, + String? currency, + Map? costBreakdown, + }) { + return TranslationUsageStats( + id: id ?? this.id, + date: date ?? this.date, + userId: userId ?? this.userId, + timeGranularity: timeGranularity ?? this.timeGranularity, + translationApiCalls: translationApiCalls ?? this.translationApiCalls, + asrApiCalls: asrApiCalls ?? this.asrApiCalls, + ttsApiCalls: ttsApiCalls ?? this.ttsApiCalls, + aiSummaryApiCalls: aiSummaryApiCalls ?? this.aiSummaryApiCalls, + sourceTextCharacters: sourceTextCharacters ?? this.sourceTextCharacters, + effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration, + ttsTextCharacters: ttsTextCharacters ?? this.ttsTextCharacters, + totalSessionDuration: totalSessionDuration ?? this.totalSessionDuration, + asrNetworkBytes: asrNetworkBytes ?? this.asrNetworkBytes, + ttsNetworkBytes: ttsNetworkBytes ?? this.ttsNetworkBytes, + translationNetworkBytes: translationNetworkBytes ?? this.translationNetworkBytes, + sessionCount: sessionCount ?? this.sessionCount, + modeStats: modeStats ?? this.modeStats, + languagePairStats: languagePairStats ?? this.languagePairStats, + hourlyStats: hourlyStats ?? this.hourlyStats, + estimatedCost: estimatedCost ?? this.estimatedCost, + currency: currency ?? this.currency, + costBreakdown: costBreakdown ?? this.costBreakdown, + ); + } + + /// 获取总API调用次数 + int get totalApiCalls => translationApiCalls + asrApiCalls + ttsApiCalls + aiSummaryApiCalls; + + /// 获取总字符数(精确计费基础) + int get totalCharacterCount => sourceTextCharacters + ttsTextCharacters; + + /// 获取总音频时长(包含有效时长和总会话时长) + int get totalAudioDuration => effectiveAudioDuration + totalSessionDuration; + + /// 获取总网络流量 + int get totalNetworkBytes => asrNetworkBytes + ttsNetworkBytes + translationNetworkBytes; + + /// 获取最常用的语言对 + String get mostUsedLanguagePair { + if (languagePairStats.isEmpty) return '无数据'; + return languagePairStats.entries + .reduce((a, b) => a.value.translationCount > b.value.translationCount ? a : b) + .key; + } + + /// 获取最常用的翻译模式 + String get mostUsedMode { + if (modeStats.isEmpty) return '无数据'; + return modeStats.entries + .reduce((a, b) => a.value.apiCalls > b.value.apiCalls ? a : b) + .key; + } + + // 向后兼容性getter + /// @deprecated 使用 sourceTextCharacters 替代 + int get sourceCharacterCount => sourceTextCharacters; + + /// @deprecated 使用 ttsTextCharacters 替代 + int get targetCharacterCount => ttsTextCharacters; + + /// @deprecated 使用 effectiveAudioDuration 替代 + int get audioRecordingDuration => effectiveAudioDuration; + + /// @deprecated 使用 totalSessionDuration 替代 + int get ttsPlaybackDuration => totalSessionDuration; + + /// @deprecated 使用 languagePairStats 替代 + Map get languagePairUsage { + final result = {}; + languagePairStats.forEach((key, value) { + result[key] = value.translationCount; + }); + return result; + } + + /// @deprecated 使用 modeStats 替代 + Map get modeUsage { + final result = {}; + modeStats.forEach((key, value) { + result[key] = value.apiCalls; + }); + return result; + } +} + +/// 实时统计数据模型(用于当前会话)- 优化版 +class RealtimeUsageStats { + // 精确计费参数 + int translationCount = 0; // 当前会话翻译次数 + int sourceTextCharacters = 0; // 当前会话源文本字符数(翻译计费基础) + int ttsTextCharacters = 0; // 当前会话TTS文本字符数(TTS计费基础) + int effectiveAudioDuration = 0; // 当前会话有效语音时长(ASR计费基础) + int totalSessionDuration = 0; // 当前会话总时长 + + // 会话信息 + String currentLanguagePair = ''; // 当前语言对 + String currentMode = ''; // 当前翻译模式 + DateTime sessionStartTime = DateTime.now(); // 会话开始时间 + + /// 重置统计数据 + void reset() { + translationCount = 0; + sourceTextCharacters = 0; + ttsTextCharacters = 0; + effectiveAudioDuration = 0; + totalSessionDuration = 0; + currentLanguagePair = ''; + currentMode = ''; + sessionStartTime = DateTime.now(); + } + + /// 获取会话持续时间(分钟) + int get sessionDurationMinutes { + return DateTime.now().difference(sessionStartTime).inMinutes; + } + + // 向后兼容性getter + /// @deprecated 使用 sourceTextCharacters 替代 + int get sourceCharacters => sourceTextCharacters; + + /// @deprecated 使用 ttsTextCharacters 替代 + int get targetCharacters => ttsTextCharacters; + + /// @deprecated 使用 effectiveAudioDuration 替代 + int get recordingDuration => effectiveAudioDuration; + + /// @deprecated 使用 totalSessionDuration 替代 + int get ttsPlayDuration => totalSessionDuration; +} diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index 04fa89610..e5efa62ec 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -23,6 +23,7 @@ class User { String phonemac; String password; int viplv = 0; //添加会员等级字段,0表示普通用户 + List devices; // 私有构造函数 @@ -43,7 +44,7 @@ class User { factory User.fromJson(Map json) { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 - final devicesData = json['devices'] as Map?; // 允许空值 + final devicesData = json['devices'] as List?; // 允许空值 //final token = json['data']['token'] as String? ?? ''; //final token = ''; // 更新单例实例 @@ -61,7 +62,7 @@ class User { phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 - devices: (devicesData as List?) + devices: devicesData ?.map((e) => UserDevice.fromJson(e as Map)) .toList() ?? [], // 处理设备列表 @@ -70,7 +71,7 @@ class User { factory User.fronTokenJson(Map json) { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 - final devicesData = json['devices'] as Map?; // 允许空值 + final devicesData = json['devices'] as List?; // 允许空值 print("数据获取fronTokenJson${userData} ${devicesData}"); // 更新单例实例 return _instance = User._( @@ -86,7 +87,7 @@ class User { password: (userData?['password'] as String?) ?? '', viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 devices: devicesData != null - ? (devicesData as List) + ? (devicesData) .map((e) => UserDevice.fromJson(e as Map)) .toList() : [], // 处理设备列表 diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart index 20e8298d7..7a0c85d65 100644 --- a/lib/data/services/db/sqflite_api.dart +++ b/lib/data/services/db/sqflite_api.dart @@ -6,27 +6,12 @@ import 'database_helper.dart'; class SqfliteApi { // 获取商务会议助手列表 - static Future getMeetingList(int offset) async { + static Future getMeetingList() async { final db = await DatabaseHelper().database; - List> dataList = []; - List meetingList = await db.query( + return await db.query( 'meeting', - limit: 10, - offset: offset, orderBy: 'creationtime DESC', ); - for (var element in meetingList) { - final List detailsList = await db.query( - 'meetingdetails', - where: 'meetingid = ?', - whereArgs: [element['id']], - ); - dataList.add({ - ...element, - 'taskid': detailsList.first['taskid'], - }); - } - return dataList; } // 获取商务会议助手详情 diff --git a/lib/data/services/location_manager.dart b/lib/data/services/location_manager.dart index 15a559ec4..412801df1 100644 --- a/lib/data/services/location_manager.dart +++ b/lib/data/services/location_manager.dart @@ -23,11 +23,11 @@ class LocationManager extends GetxService { final granted = await PermissionUtil.instance.requestPermission( // context: context, permissionType: Permission.locationAlways, - permissionName: '通知', - explanationText: '需要定位权限来获取当前位置。', - permanentDenialText: '您已永久定位通知权限,请前往设置开启权限。', - settingsButtonText: '去设置', - cancelButtonText: '取消', + permissionName: 'locationPermission'.tr, + explanationText: 'locationPermissionExplanation'.tr, + permanentDenialText: 'locationPermissionPermanentDenial'.tr, + settingsButtonText: 'goToSettings'.tr, + cancelButtonText: 'cancel'.tr, ); if (!granted) { diff --git a/lib/data/services/meeting/meeting_task_service.dart b/lib/data/services/meeting/meeting_task_service.dart index 225ad28cd..e4603a4ad 100644 --- a/lib/data/services/meeting/meeting_task_service.dart +++ b/lib/data/services/meeting/meeting_task_service.dart @@ -33,7 +33,6 @@ class MeetingTaskService extends GetxService { int meetingId = 0; RxInt tasktype = 1.obs; List textList = []; - String allText = ''; String summary = ''; String overview = ''; @@ -268,11 +267,10 @@ class MeetingTaskService extends GetxService { _storage.write("meeting_task_list", _taskList); _storage.write("meeting_task_finish_list", taskFinishList); } - allText = text; - _aiServiceMessage(id); + aiServiceMessage(id, text); } - void _aiServiceMessage(int id) { + void aiServiceMessage(int id, String text) { int awaitQuantity = 0; String summaryText = ''; String overviewText = ''; @@ -283,7 +281,7 @@ class MeetingTaskService extends GetxService { 'content': '您将扮演一名会议总结专家,拥有超强的信息归纳与细分能力。不仅能高效处理各类通用场景会议,精准提炼会议核心内容,为后续思维导图生成奠定坚实基础。若涉及人员,人员名称保留[]。并根据内容只输出对应语言。', }, - {'role': 'user', 'content': allText}, + {'role': 'user', 'content': text}, ], ).then((value) { awaitQuantity++; @@ -298,7 +296,7 @@ class MeetingTaskService extends GetxService { 'role': 'system', 'content': '您将扮演一名会议事件归纳专家,能准确抓住会议中事件的关键点,并每个事件只需一句话精简总结。', }, - {'role': 'user', 'content': allText}, + {'role': 'user', 'content': text}, ], ).then((value) { awaitQuantity++; diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index c184a9b78..e761e1a37 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -28,13 +28,13 @@ class MusicManager extends GetxService { Future requestNotificationPermission() async { final granted = await PermissionUtil.instance.requestPermission( - // context: context, + //context: context, permissionType: Permission.notification, - permissionName: '通知', - explanationText: '需要通知权限来显示音乐播放状态。', - permanentDenialText: '您已永久拒绝通知权限,请前往设置开启权限。', - settingsButtonText: '去设置', - cancelButtonText: '取消', + permissionName: 'notificationPermission'.tr, + explanationText: 'notificationPermissionExplanation'.tr, + permanentDenialText: 'notificationPermissionPermanentDenial'.tr, + settingsButtonText: 'goToSettings'.tr, + cancelButtonText: 'cancel'.tr, ); if (!granted) { diff --git a/lib/data/services/translation_usage_service.dart b/lib/data/services/translation_usage_service.dart new file mode 100644 index 000000000..90f3db799 --- /dev/null +++ b/lib/data/services/translation_usage_service.dart @@ -0,0 +1,519 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import '../models/translation_usage_stats.dart'; +import '../../core/utils/logger.dart'; + +/// 翻译使用量统计服务 +class TranslationUsageService extends GetxService { + static const String _storageKey = 'translation_usage_stats'; + static const String _dailyStatsKey = 'daily_translation_stats'; + static const String _costConfigKey = 'translation_cost_config'; + + final GetStorage _storage = GetStorage(); + + // 实时统计数据 + final Rx realtimeStats = RealtimeUsageStats().obs; + + // 当日统计数据 + final Rx todayStats = TranslationUsageStats.empty('').obs; + + // 成本配置 + final RxMap costConfig = {}.obs; + + // 预警阈值 + final RxDouble dailyCostThreshold = 10.0.obs; // 日成本预警阈值 + final RxDouble monthlyCostThreshold = 300.0.obs; // 月成本预警阈值 + + @override + void onInit() { + super.onInit(); + _initializeService(); + } + + /// 初始化服务 + void _initializeService() { + _loadCostConfig(); + _loadTodayStats(); + Logger.info('翻译使用量统计服务初始化完成'); + } + + /// 加载成本配置 + void _loadCostConfig() { + try { + final configData = _storage.read(_costConfigKey); + if (configData != null) { + costConfig.value = Map.from(configData); + } else { + // 设置默认成本配置(每千字符的成本,单位:元) + costConfig.value = { + 'translation_per_1k_chars': 0.05, // 翻译:每千字符0.05元 + 'asr_per_minute': 0.02, // ASR:每分钟0.02元 + 'tts_per_1k_chars': 0.03, // TTS:每千字符0.03元 + 'ai_summary_per_request': 0.01, // AI摘要:每次请求0.01元 + }; + _saveCostConfig(); + } + } catch (e) { + Logger.error('加载成本配置失败: $e'); + } + } + + /// 保存成本配置 + void _saveCostConfig() { + try { + _storage.write(_costConfigKey, costConfig.value); + } catch (e) { + Logger.error('保存成本配置失败: $e'); + } + } + + /// 加载当日统计数据 + void _loadTodayStats() { + try { + final today = DateTime.now(); + final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; + + final statsData = _storage.read('${_dailyStatsKey}_$todayKey'); + if (statsData != null) { + todayStats.value = TranslationUsageStats.fromJson(Map.from(statsData)); + } else { + todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); + } + } catch (e) { + Logger.error('加载当日统计数据失败: $e'); + todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); + } + } + + /// 获取当前用户ID + String getCurrentUserId() { + try { + final userInfo = _storage.read('user_info'); + if (userInfo != null && userInfo['user'] != null) { + return userInfo['user']['uid'] ?? 'anonymous'; + } + } catch (e) { + Logger.warning('获取用户ID失败: $e'); + } + return 'anonymous'; + } + + /// 记录翻译API调用 + void recordTranslationApiCall({ + required String sourceText, + required String targetText, + required String sourceLanguage, + required String targetLanguage, + required String mode, + }) { + try { + // 更新实时统计 + realtimeStats.value.translationCount++; + realtimeStats.value.sourceTextCharacters += sourceText.length; + realtimeStats.value.ttsTextCharacters += targetText.length; + realtimeStats.value.currentLanguagePair = '$sourceLanguage-$targetLanguage'; + realtimeStats.value.currentMode = mode; + realtimeStats.refresh(); + + // 更新当日统计 - 使用多维度统计 + final languagePair = '$sourceLanguage→$targetLanguage'; + + // 更新语言对统计 + final newLanguagePairStats = Map.from(todayStats.value.languagePairStats); + if (newLanguagePairStats.containsKey(languagePair)) { + final existing = newLanguagePairStats[languagePair]!; + newLanguagePairStats[languagePair] = LanguagePairStats( + languagePair: languagePair, + translationCount: existing.translationCount + 1, + sourceCharacters: existing.sourceCharacters + sourceText.length, + cost: existing.cost + _calculateTranslationCost(sourceText.length), + ); + } else { + newLanguagePairStats[languagePair] = LanguagePairStats( + languagePair: languagePair, + translationCount: 1, + sourceCharacters: sourceText.length, + cost: _calculateTranslationCost(sourceText.length), + ); + } + + // 更新模式统计 + final newModeStats = Map.from(todayStats.value.modeStats); + if (newModeStats.containsKey(mode)) { + final existing = newModeStats[mode]!; + newModeStats[mode] = existing.copyWith( + apiCalls: existing.apiCalls + 1, + sourceCharacters: existing.sourceCharacters + sourceText.length, + ttsCharacters: existing.ttsCharacters + targetText.length, + cost: existing.cost + _calculateTranslationCost(sourceText.length), + ); + } else { + newModeStats[mode] = TranslationModeStats( + apiCalls: 1, + sourceCharacters: sourceText.length, + ttsCharacters: targetText.length, + cost: _calculateTranslationCost(sourceText.length), + ); + } + + todayStats.value = todayStats.value.copyWith( + translationApiCalls: todayStats.value.translationApiCalls + 1, + sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length, + ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length, + languagePairStats: newLanguagePairStats, + modeStats: newModeStats, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + translationApiCalls: todayStats.value.translationApiCalls + 1, + sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length, + ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length, + )), + ); + + _saveTodayStats(); + _checkCostThreshold(); + + Logger.info('记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符'); + } catch (e) { + Logger.error('记录翻译API调用失败: $e'); + } + } + + /// 记录ASR API调用 - 优化版(精确计费参数) + void recordAsrApiCall({ + required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音) + required int totalDurationSeconds, // 总会话时长(包含静音) + required String language, + required String mode, // 翻译模式 + String? audioFormat, // 音频格式(可选) + int? sampleRate, // 采样率(可选) + }) { + try { + // 更新实时统计(使用有效语音时长作为主要计费依据) + realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds; + realtimeStats.value.totalSessionDuration += totalDurationSeconds; + realtimeStats.refresh(); + + // 更新模式统计 + final newModeStats = Map.from(todayStats.value.modeStats); + if (newModeStats.containsKey(mode)) { + final existing = newModeStats[mode]!; + newModeStats[mode] = existing.copyWith( + effectiveAudioDuration: existing.effectiveAudioDuration + effectiveDurationSeconds, + cost: existing.cost + _calculateAsrCost(effectiveDurationSeconds), + ); + } else { + newModeStats[mode] = TranslationModeStats( + effectiveAudioDuration: effectiveDurationSeconds, + cost: _calculateAsrCost(effectiveDurationSeconds), + ); + } + + // 更新当日统计 + todayStats.value = todayStats.value.copyWith( + asrApiCalls: todayStats.value.asrApiCalls + 1, + effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds, + totalSessionDuration: todayStats.value.totalSessionDuration + totalDurationSeconds, + modeStats: newModeStats, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + asrApiCalls: todayStats.value.asrApiCalls + 1, + effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds, + )), + ); + + _saveTodayStats(); + + Logger.info('ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode'); + } catch (e) { + Logger.error('记录ASR API调用失败: $e'); + } + } + + /// 记录TTS API调用 + void recordTtsApiCall({ + required String text, + required int durationSeconds, + required String language, + }) { + try { + // 更新实时统计 + realtimeStats.value.totalSessionDuration += durationSeconds; + realtimeStats.refresh(); + + // 更新当日统计(TTS按字符数计费,不是按时长) + todayStats.value = todayStats.value.copyWith( + ttsApiCalls: todayStats.value.ttsApiCalls + 1, + ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length, + totalSessionDuration: todayStats.value.totalSessionDuration + durationSeconds, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + ttsApiCalls: todayStats.value.ttsApiCalls + 1, + ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length, + )), + ); + + _saveTodayStats(); + + Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒'); + } catch (e) { + Logger.error('记录TTS API调用失败: $e'); + } + } + + /// 记录AI摘要API调用 + void recordAiSummaryApiCall() { + try { + // 更新当日统计 + todayStats.value = todayStats.value.copyWith( + aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1, + estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( + aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1, + )), + ); + + _saveTodayStats(); + + Logger.info('记录AI摘要API调用'); + } catch (e) { + Logger.error('记录AI摘要API调用失败: $e'); + } + } + + /// 开始新会话 + void startNewSession() { + realtimeStats.value.reset(); + + // 更新当日会话数 + todayStats.value = todayStats.value.copyWith( + sessionCount: todayStats.value.sessionCount + 1, + ); + + _saveTodayStats(); + Logger.info('开始新的翻译会话'); + } + + /// 计算预估成本 - 优化版(基于精确计费参数) + double _calculateEstimatedCost(TranslationUsageStats stats) { + double cost = 0.0; + + // 翻译成本:基于源文本字符数 + cost += (stats.sourceTextCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05); + + // ASR成本:基于有效音频时长 + cost += (stats.effectiveAudioDuration / 60) * (costConfig['asr_per_minute'] ?? 0.02); + + // TTS成本:基于TTS文本字符数 + cost += (stats.ttsTextCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03); + + // AI摘要成本(已废弃但保留兼容性) + cost += stats.aiSummaryApiCalls * (costConfig['ai_summary_per_request'] ?? 0.01); + + return cost; + } + + /// 计算翻译成本(单次调用) + double _calculateTranslationCost(int sourceCharacters) { + return (sourceCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05); + } + + /// 计算ASR成本(单次调用) + double _calculateAsrCost(int durationSeconds) { + return (durationSeconds / 60) * (costConfig['asr_per_minute'] ?? 0.02); + } + + /// 计算TTS成本(单次调用) + double _calculateTtsCost(int textCharacters) { + return (textCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03); + } + + /// 保存当日统计数据 + void _saveTodayStats() { + try { + final today = DateTime.now(); + final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; + _storage.write('${_dailyStatsKey}_$todayKey', todayStats.value.toJson()); + } catch (e) { + Logger.error('保存当日统计数据失败: $e'); + } + } + + /// 检查成本阈值 + void _checkCostThreshold() { + final todayCost = todayStats.value.estimatedCost; + + if (todayCost >= dailyCostThreshold.value) { + _showCostWarning('今日翻译成本已达到预警阈值:¥${todayCost.toStringAsFixed(2)}'); + } + } + + /// 显示成本预警 + void _showCostWarning(String message) { + Get.snackbar( + '成本预警', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), + colorText: Get.theme.colorScheme.error, + duration: const Duration(seconds: 5), + ); + } + + /// 获取历史统计数据 + Future> getHistoryStats({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final List historyStats = []; + + for (DateTime date = startDate; + date.isBefore(endDate) || date.isAtSameMomentAs(endDate); + date = date.add(const Duration(days: 1))) { + + final dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + final statsData = _storage.read('${_dailyStatsKey}_$dateKey'); + + if (statsData != null) { + historyStats.add(TranslationUsageStats.fromJson(Map.from(statsData))); + } + } + + return historyStats; + } catch (e) { + Logger.error('获取历史统计数据失败: $e'); + return []; + } + } + + /// 清理过期数据(保留最近90天) + void cleanupOldData() { + try { + final cutoffDate = DateTime.now().subtract(const Duration(days: 90)); + final allKeys = _storage.getKeys(); + + for (final key in allKeys) { + if (key.startsWith(_dailyStatsKey)) { + final dateStr = key.replaceFirst('${_dailyStatsKey}_', ''); + try { + final date = DateTime.parse(dateStr); + if (date.isBefore(cutoffDate)) { + _storage.remove(key); + Logger.info('清理过期统计数据: $key'); + } + } catch (e) { + // 忽略解析错误的键 + } + } + } + } catch (e) { + Logger.error('清理过期数据失败: $e'); + } + } + + /// 更新成本配置 + void updateCostConfig(Map newConfig) { + costConfig.value = newConfig; + _saveCostConfig(); + + // 重新计算当日成本 + todayStats.value = todayStats.value.copyWith( + estimatedCost: _calculateEstimatedCost(todayStats.value), + ); + _saveTodayStats(); + + Logger.info('更新成本配置完成'); + } + + /// 更新预警阈值 + void updateThresholds({ + double? dailyThreshold, + double? monthlyThreshold, + }) { + if (dailyThreshold != null) { + dailyCostThreshold.value = dailyThreshold; + } + if (monthlyThreshold != null) { + monthlyCostThreshold.value = monthlyThreshold; + } + + // 保存阈值配置 + _storage.write('daily_cost_threshold', dailyCostThreshold.value); + _storage.write('monthly_cost_threshold', monthlyCostThreshold.value); + + Logger.info('更新预警阈值完成'); + } + + /// 获取月度统计汇总 + Future getMonthlyStats(int year, int month) async { + try { + final startDate = DateTime(year, month, 1); + final endDate = DateTime(year, month + 1, 0); // 月末 + + final dailyStats = await getHistoryStats( + startDate: startDate, + endDate: endDate, + ); + + if (dailyStats.isEmpty) { + return TranslationUsageStats.empty(getCurrentUserId()); + } + + // 汇总月度数据 + int totalTranslationCalls = 0; + int totalAsrCalls = 0; + int totalTtsCalls = 0; + int totalAiSummaryCalls = 0; + int totalSourceChars = 0; + int totalTargetChars = 0; + int totalRecordingDuration = 0; + int totalTtsPlayDuration = 0; + int totalSessions = 0; + double totalCost = 0.0; + + final Map combinedLanguagePairUsage = {}; + final Map combinedModeUsage = {}; + + for (final stats in dailyStats) { + totalTranslationCalls += stats.translationApiCalls; + totalAsrCalls += stats.asrApiCalls; + totalTtsCalls += stats.ttsApiCalls; + totalAiSummaryCalls += stats.aiSummaryApiCalls; + totalSourceChars += stats.sourceCharacterCount; + totalTargetChars += stats.targetCharacterCount; + totalRecordingDuration += stats.audioRecordingDuration; + totalTtsPlayDuration += stats.ttsPlaybackDuration; + totalSessions += stats.sessionCount; + totalCost += stats.estimatedCost; + + // 合并语言对使用统计 + stats.languagePairUsage.forEach((key, value) { + combinedLanguagePairUsage[key] = (combinedLanguagePairUsage[key] ?? 0) + value; + }); + + // 合并模式使用统计 + stats.modeUsage.forEach((key, value) { + combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; + }); + } + + return TranslationUsageStats( + id: 'monthly_${year}_$month', + date: startDate, + userId: getCurrentUserId(), + timeGranularity: 'month', + translationApiCalls: totalTranslationCalls, + asrApiCalls: totalAsrCalls, + ttsApiCalls: totalTtsCalls, + aiSummaryApiCalls: totalAiSummaryCalls, + sourceTextCharacters: totalSourceChars, + effectiveAudioDuration: totalRecordingDuration, + ttsTextCharacters: totalTargetChars, + totalSessionDuration: totalTtsPlayDuration, + sessionCount: totalSessions, + estimatedCost: totalCost, + ); + } catch (e) { + Logger.error('获取月度统计失败: $e'); + return TranslationUsageStats.empty(getCurrentUserId()); + } + } +} diff --git a/lib/data/services/user_portrait.dart b/lib/data/services/user_portrait.dart index 08372b638..d4e521299 100644 --- a/lib/data/services/user_portrait.dart +++ b/lib/data/services/user_portrait.dart @@ -1,4 +1,4 @@ -import 'package:deep_voice/core/utils/logger.dart'; +import '/core/utils/logger.dart'; import 'package:get/get.dart'; import 'package:user_portrait/user_portrait.dart'; diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index 6d44c34a1..f2696b5b8 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -7,6 +7,7 @@ import 'package:logger/logger.dart'; import 'package:device_info_plus/device_info_plus.dart'; import '../models/channel_app_model.dart'; import 'network/api.dart'; +import 'dart:math' as math; /// 版本更新服务类 class VersionUpdateService extends GetxService { @@ -128,7 +129,7 @@ class VersionUpdateService extends GetxService { final response = await Api.getChannelApp({'channel': platformInfo.channel.value}); - print('获取渠道包信息:$response'); + _logger.d('获取渠道包信息:$response'); if (response == null || !response.containsKey('app')) { return VersionUpdateInfo( @@ -142,6 +143,7 @@ class VersionUpdateService extends GetxService { //final channelAppResponse = ChannelAppResponse.fromJson(response); // 直接从response中获取app信息 final appInfo = AppInfo.fromJson(response['app']); + _logger.d('获取到的最新版本信息: ${appInfo.toJson()}'); // 设置游客登录显示状态 isShowTourists.value = appInfo.tourists; @@ -170,8 +172,8 @@ class VersionUpdateService extends GetxService { return VersionUpdateInfo( hasUpdate: hasUpdate, - currentVersion: currentVersion, - latestVersion: appInfo.version, + currentVersion: currentVersion.split('+')[0], + latestVersion: appInfo.version.split('+')[0], appInfo: appInfo, updateType: updateType, ); @@ -204,7 +206,14 @@ class VersionUpdateService extends GetxService { // 比较主版本号(第一位) if (currentVersions.isNotEmpty && latestVersions.isNotEmpty) { - return currentVersions[0] != latestVersions[0]; + if (currentVersions[0] != latestVersions[0]) { + return true; + } + } + + // 检查是否有两个或更多的 + 号,如果有则强制更新 + if (latest.split('+').length > 2) { + return true; } return false; @@ -218,24 +227,25 @@ class VersionUpdateService extends GetxService { /// 返回true表示需要更新(远程版本更高) bool _compareVersions(String current, String latest) { // 拆分版本号与构建号 - final currentParts = current.split('+'); - final latestParts = latest.split('+'); + final currentParts = current.split('+')[0]; + final latestParts = latest.split('+')[0]; // 解析版本号 (major.minor.patch) - final currentVersions = currentParts[0].split('.').map(int.parse).toList(); - final latestVersions = latestParts[0].split('.').map(int.parse).toList(); + final currentVersions = currentParts.split('.').map(int.parse).toList(); + final latestVersions = latestParts.split('.').map(int.parse).toList(); + + // 使用较小的长度进行比较,避免索引越界 + final minLength = math.min(currentVersions.length, latestVersions.length); // 逐级比较版本号 - for (var i = 0; i < currentVersions.length; i++) { + for (var i = 0; i < minLength; i++) { if (latestVersions[i] > currentVersions[i]) return true; if (latestVersions[i] < currentVersions[i]) return false; } - // 版本号相同则比较构建号 - if (latestParts.length > 1 && currentParts.length > 1) { - return int.parse(latestParts[1]) > int.parse(currentParts[1]); - } - return false; + // 如果前面的版本号都相同,比较长度 + // 更长的版本号被认为是更新的版本(如 1.0.1 > 1.0) + return latestVersions.length > currentVersions.length; } /// 解析版本号字符串为数字数组 diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 08bdffa81..c29675888 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -152,6 +152,9 @@ class AgentController extends GetxController { _initMusicStatus(); //初始化音乐状态 _setupMediaSessionCallbacks(); //设置媒体会话回调 _setupTextInputListener(); //设置文本输入监听器 + + _configureSpeechRecognitionLanguages(); //配置语音识别语言 + startVoiceInput(); //开启语音输入 } @@ -465,7 +468,7 @@ class AgentController extends GetxController { case AgentServiceEventType.recognitionStarted: isListening.value = true; isTextInputMode.value = false; - print("订阅代理服务事件识别开始"); + Logger.i(TAG, '订阅代理服务事件识别开始'); // isTextInputMode.refresh(); // 只标记识别开始,不添加消息 Logger.i(TAG, '识别开始'); @@ -1010,10 +1013,54 @@ class AgentController extends GetxController { } } +//根据APP语言配置语音识别语言 + Future _configureSpeechRecognitionLanguages() async { + try { + // 1. 使用 GetX 获取 APP 当前语言环境 (Locale) + final appLocale = Get.locale; + List languagesToSupport = []; + + if (appLocale != null) { + final lang = appLocale.languageCode; // e.g., 'en' + final country = appLocale.countryCode; // e.g., 'US' + + // 2. 构造 Azure SDK 需要的格式 (e.g., 'en-US') + if (country != null && country.isNotEmpty) { + languagesToSupport.add('$lang-$country'); + } else { + languagesToSupport.add(lang); // fallback only language code + } + } + + // 3. 添加 en-US 作为备选 + if (!languagesToSupport.contains('en-US')) { + languagesToSupport.add('en-US'); + } + + // 4. 最终兜底 + if (languagesToSupport.isEmpty) { + languagesToSupport.add('en-US'); + } + + // 5. 调用原生设置 + Logger.i(TAG, '支持的语言: ${languagesToSupport.join(', ')}'); + AgentService.setSupportedLanguages(languagesToSupport).then((success) { + if (success) { + Logger.i(TAG, 'asr的语言已根据 APP 设置语言: ${languagesToSupport.join(', ')}'); + } else { + Logger.w(TAG, '根据 APP 设置asr语言失败'); + } + }); + } catch (e) { + Logger.e(TAG, "获取 APP 语言并设置失败: $e"); + AgentService.setSupportedLanguages(['zh-CN', 'en-US']); + } + } + // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; - print('开始语音输入'); // 开始语音输入 + Logger.i(TAG, '开始语音输入'); // 开始语音输入 final hasPermission = await PermissionUtil.instance.requestPermission( // context: Get.context!, permissionType: Permission.microphone, @@ -1309,7 +1356,7 @@ class AgentController extends GetxController { // 打电话模式不使用按住说话模式,而是持续语音输入 //isPushToTalkMode.value = false; // 开始语音识别 - print("开始打电话模式,开始语音输入"); + Logger.i(TAG, '开始打电话模式,开始语音输入'); await AgentService.startConversation(); //} } @@ -1446,11 +1493,12 @@ class AgentController extends GetxController { var status = await _musiceManager.getStatus(); isPlaying.value = status['isPlaying'] ?? false; currentId.value = status['id'] as int; - Logger.d('AgentController', - '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); - print("音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}"); + Logger.d( + TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); + Logger.d( + TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); } catch (e) { - Logger.e('AgentController', '获取音乐状态失败: $e'); + Logger.e(TAG, '获取音乐状态失败: $e'); } } @@ -1487,7 +1535,8 @@ class AgentController extends GetxController { var response = await Api.getMusicUrl({ "ids": [id] }); - print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据 + Logger.d( + TAG, 'getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据 final urldata = response['urls'][0] as Map; song["url"] = urldata['url'] ?? ''; // 修复:确保currentId接收int类型 diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index ed13c720a..3703ec3f4 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; @@ -61,40 +62,40 @@ class _AgentViewState extends State with WidgetsBindingObserver { backgroundColor: backgroundColor, leading: IconButton( icon: Icon(Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black87, size: 20), + color: isDarkMode ? Colors.white : Colors.black87, size: 20.sp), onPressed: () => Get.back(), ), titleSpacing: 0, title: Row( children: [ Container( - width: 40, - height: 40, + width: 40.w, + height: 40.w, decoration: BoxDecoration( color: primaryColor, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(20.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), - blurRadius: 3, + blurRadius: 3.w, offset: const Offset(0, 1), ), ], ), - child: const Icon( + child: Icon( Icons.smart_toy_outlined, color: Colors.white, - size: 22, + size: 22.sp, ), ), - const SizedBox(width: 12), + SizedBox(width: 12.w), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'XiaoYan'.tr, style: TextStyle( - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black87, ), @@ -102,18 +103,18 @@ class _AgentViewState extends State with WidgetsBindingObserver { Row( children: [ Container( - width: 6, - height: 6, + width: 6.w, + height: 6.w, decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), - const SizedBox(width: 4), + SizedBox(width: 4.w), Text( 'online'.tr, style: TextStyle( - fontSize: 12, + fontSize: 12.sp, color: Colors.green, fontWeight: FontWeight.w500, ), @@ -131,6 +132,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, + size: 24.sp, color: controller.isTtsEnabled.value ? primaryColor : isDarkMode @@ -142,9 +144,9 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 更多选项 Container( - width: 34, - height: 34, - margin: const EdgeInsets.only(right: 8), + width: 34.w, + height: 34.w, + margin: EdgeInsets.only(right: 8.w), decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], @@ -153,25 +155,28 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (!isDarkMode) BoxShadow( color: Colors.black.withOpacity(0.05), - blurRadius: 2, - spreadRadius: 0.5, + blurRadius: 2.w, + spreadRadius: 0.5.w, ), ], ), child: PopupMenuButton( icon: Icon(Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black87, size: 20), + color: isDarkMode ? Colors.white : Colors.black87, + size: 20.sp), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(12.w), ), color: isDarkMode ? Colors.grey[800] : Colors.white, itemBuilder: (context) => [ PopupMenuItem( child: Row( children: [ - Icon(Icons.delete_outline, color: primaryColor, size: 20), - const SizedBox(width: 8), - Text('clearChatHistory'.tr, style: TextStyle(fontSize: 14)), + Icon(Icons.delete_outline, + color: primaryColor, size: 20.sp), + SizedBox(width: 8.w), + Text('clearChatHistory'.tr, + style: TextStyle(fontSize: 14.sp)), ], ), onTap: () => Future.delayed( @@ -198,8 +203,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller: controller.scrollController, shrinkWrap: true, reverse: true, - padding: const EdgeInsets.symmetric( - vertical: 16, horizontal: 16), + padding: EdgeInsets.symmetric( + vertical: 16.h, horizontal: 16.w), itemCount: controller.messages.length + (controller.hasMoreMessages.value ? 1 : 0), itemBuilder: (context, index) { @@ -212,7 +217,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { final message = controller .messages[controller.messages.length - 1 - index]; return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: EdgeInsets.only(bottom: 8.h), child: GestureDetector( onLongPressStart: (LongPressStartDetails details) { @@ -237,21 +242,20 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 输入区域 Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: backgroundColor, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), - blurRadius: 8, + blurRadius: 8.w, offset: const Offset(0, -1), ), ], border: Border( top: BorderSide( color: Colors.grey.withOpacity(0.15), - width: 1, + width: 1.w, ), ), ), @@ -269,29 +273,29 @@ class _AgentViewState extends State with WidgetsBindingObserver { Stack( children: [ ClipRRect( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(12.w), child: Image.file( File(controller.selectedImagePath.value!), - height: 150, + height: 150.h, width: double.infinity, fit: BoxFit.cover, ), ), Positioned( - top: 8, - right: 8, + top: 8.h, + right: 8.w, child: GestureDetector( onTap: () => controller.clearImageInput(), child: Container( - padding: const EdgeInsets.all(4), + padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), - child: const Icon( + child: Icon( Icons.close, color: Colors.white, - size: 16, + size: 16.sp, ), ), ), @@ -300,34 +304,34 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), // 预设提示选项 - const SizedBox(height: 12), + SizedBox(height: 12.h), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildPromptChip( context: context, - label: 'summarizeImageContent'.tr,//总结图片内容 + label: 'summarizeImageContent'.tr, //总结图片内容 onTap: () => controller.summarizeImage(), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), _buildPromptChip( context: context, - label: 'extractImageText'.tr,//抽取图像文字 + label: 'extractImageText'.tr, //抽取图像文字 onTap: () => controller.extractTextFromImage(), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), _buildPromptChip( context: context, - label: 'translateImageText'.tr,//翻译图像文字 + label: 'translateImageText'.tr, //翻译图像文字 onTap: () => controller.translateImageText(), ), ], ), ), - const SizedBox(height: 12), + SizedBox(height: 12.h), ], ); } else { @@ -353,8 +357,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.toggleInputMode(); }, child: Container( - width: 44, - height: 44, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] @@ -368,13 +372,13 @@ class _AgentViewState extends State with WidgetsBindingObserver { color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 22, + size: 22.sp, ), ), ); }), - const SizedBox(width: 8), + SizedBox(width: 8.w), // 文本输入框或语音波形图 Expanded( @@ -385,23 +389,23 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (controller.isTextInputMode.value) { // 文本输入模式 - 限制最大高度并只向上扩展 return Container( - constraints: const BoxConstraints( - minHeight: 44, // 最小高度 - maxHeight: 120, // 最大高度限制,防止挤压按钮 + constraints: BoxConstraints( + minHeight: 44.h, + maxHeight: 120.h, ), - padding: const EdgeInsets.symmetric( - horizontal: 16), + padding: EdgeInsets.symmetric( + horizontal: 16.w), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(22), + borderRadius: + BorderRadius.circular(22.w), border: Border.all( color: isDarkMode - ? Colors.grey[ - 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] + ? Colors.grey[800]! : Colors.grey.withOpacity(0.1), - width: 1.0, + width: 1.w, ), ), child: Row( @@ -435,7 +439,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { } }, style: TextStyle( - fontSize: 15, + fontSize: 15.sp, color: isDarkMode ? Colors.white : Colors.grey[800], @@ -448,7 +452,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { .tr, // 输入消息... hintStyle: TextStyle( color: Colors.grey[500], - fontSize: 15, + fontSize: 15.sp, ), filled: false, border: InputBorder.none, @@ -458,14 +462,15 @@ class _AgentViewState extends State with WidgetsBindingObserver { color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!, - width: 1.0, + width: 1.w, ), ), contentPadding: EdgeInsets.symmetric( - vertical: isImageActive - ? 12 - : 8), // 调整垂直内边距 + vertical: + isImageActive + ? 12.h + : 8.h), ), onSubmitted: (_) => controller.sendMessage(), @@ -525,9 +530,9 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.isListening.value; return Container( - height: 50, - padding: const EdgeInsets.symmetric( - horizontal: 16), + height: 52.h, + padding: EdgeInsets.symmetric( + horizontal: 16.w), decoration: BoxDecoration( color: isPushToTalkActive ? (isDarkMode @@ -537,7 +542,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { ? Colors.grey[800] : Colors.grey[200]), borderRadius: - BorderRadius.circular(22), + BorderRadius.circular(22.w), border: Border.all( color: isPushToTalkActive ? Colors.blue @@ -546,8 +551,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { : Colors.grey .withOpacity(0.1)), width: isPushToTalkActive - ? 2.0 - : 1.0, + ? 2.w + : 1.w, ), ), child: Row( @@ -572,7 +577,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { isPushToTalkActive ? Icons.mic : Icons.mic_none, - size: 16, + size: 16.sp, color: isPushToTalkActive ? Colors.blue : Colors.grey[500], @@ -607,7 +612,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { }), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 Obx(() { @@ -627,7 +632,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (shouldShowExpand && hasText) Padding( padding: - const EdgeInsets.only(bottom: 40), + EdgeInsets.only(bottom: 40.h), child: GestureDetector( onTap: () { // 打开底部弹出编辑模式 @@ -646,21 +651,21 @@ class _AgentViewState extends State with WidgetsBindingObserver { }); }, child: Container( - width: 32, - height: 32, + width: 32.w, + height: 32.w, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[700] : Colors.grey[300], borderRadius: - BorderRadius.circular(16), + BorderRadius.circular(16.w), ), child: Icon( Icons.expand_less, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 20, + size: 20.sp, ), ), ), @@ -669,17 +674,17 @@ class _AgentViewState extends State with WidgetsBindingObserver { GestureDetector( onTap: () => controller.sendMessage(), child: Container( - width: 44, - height: 44, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: primaryColor, borderRadius: - BorderRadius.circular(22), + BorderRadius.circular(22.w), ), - child: const Icon( + child: Icon( Icons.send, color: Colors.white, - size: 22, + size: 22.sp, ), ), ), @@ -695,21 +700,21 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.togglePlusOptions(); }, child: Container( - width: 44, - height: 44, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: - BorderRadius.circular(22), + BorderRadius.circular(22.w), ), child: Icon( isExpanded ? Icons.close : Icons.add, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 22, + size: 22.sp, ), ), ); @@ -727,62 +732,77 @@ class _AgentViewState extends State with WidgetsBindingObserver { return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸 + height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸 child: isExpanded - ? Container( - margin: EdgeInsets.only(top: 8.h), - padding: EdgeInsets.symmetric( - horizontal: 16.w), // 添加左右边距 - child: Row( - mainAxisAlignment: MainAxisAlignment - .spaceEvenly, // 改为均匀分布 - children: [ - // 相机按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.camera_alt, - label: 'takePhoto'.tr, // 拍照 - onTap: () async { - //controller.collapsePlusOptions(); - await controller.takePhoto(); - }, - isDarkMode: isDarkMode, - ), - // 相册按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.photo_library, - label: 'photoAlbum'.tr, // 相册 - onTap: () async { - //controller.collapsePlusOptions(); - await controller.pickImage(); - }, - isDarkMode: isDarkMode, - ), - // 文件按钮(预留位置) - // _buildPlusOptionButton( - // context: context, - // icon: Icons.folder, - // label: '文件', - // onTap: () { - // //controller.collapsePlusOptions(); - // // TODO: 实现文件选择功能 - // }, - // isDarkMode: isDarkMode, - // ), - // 通话按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.phone, - label: 'makeCall'.tr, // 打电话 - onTap: () { - // 启动打电话模式 - controller.startPhoneCall(); - }, - isDarkMode: isDarkMode, + ? LayoutBuilder( + // 添加LayoutBuilder获取实际约束 + builder: (context, constraints) { + return SingleChildScrollView( + // 确保内容可滚动 + scrollDirection: + Axis.horizontal, // 水平滚动 + child: Container( + margin: EdgeInsets.only(top: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 16.w), + // 根据可用宽度调整容器宽度 + width: max(constraints.maxWidth, + 300.w), // 最小300宽 + child: Wrap( + // 使用Wrap替代Row,自动换行 + alignment: WrapAlignment.center, + spacing: 26.w, // 按钮水平间距 + runSpacing: 8.h, // 按钮垂直间距 + children: [ + // 相机按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.camera_alt, + label: 'takePhoto'.tr, // 拍照 + onTap: () async { + //controller.collapsePlusOptions(); + await controller.takePhoto(); + }, + isDarkMode: isDarkMode, + ), + // 相册按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.photo_library, + label: 'photoAlbum'.tr, // 相册 + onTap: () async { + //controller.collapsePlusOptions(); + await controller.pickImage(); + }, + isDarkMode: isDarkMode, + ), + // 文件按钮(预留位置) + // _buildPlusOptionButton( + // context: context, + // icon: Icons.folder, + // label: '文件', + // onTap: () { + // //controller.collapsePlusOptions(); + // // TODO: 实现文件选择功能 + // }, + // isDarkMode: isDarkMode, + // ), + // 通话按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.phone, + label: 'makeCall'.tr, // 打电话 + onTap: () { + // 启动打电话模式 + controller.startPhoneCall(); + }, + isDarkMode: isDarkMode, + ), + ], + ), ), - ], - ), + ); + }, ) : const SizedBox.shrink(), ); @@ -813,25 +833,24 @@ class _AgentViewState extends State with WidgetsBindingObserver { required String label, required VoidCallback onTap, }) { - final theme = Theme.of(context); - final primaryColor = theme.primaryColor; + final primaryColor = Theme.of(context).primaryColor; return GestureDetector( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), decoration: BoxDecoration( color: primaryColor.withOpacity(0.08), - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(16.w), border: Border.all( color: primaryColor.withOpacity(0.2), - width: 1, + width: 1.w, ), ), child: Text( label, style: TextStyle( - fontSize: 13, + fontSize: 13.sp, color: primaryColor, fontWeight: FontWeight.w500, ), @@ -865,10 +884,10 @@ class _AgentViewState extends State with WidgetsBindingObserver { height: 50.w, // 调整为44.w以适应布局 decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(12.r), // 调整为10.r + borderRadius: BorderRadius.circular(12.w), border: Border.all( color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, - width: 1, + width: 1.w, ), ), child: Icon( @@ -1085,8 +1104,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { BoxShadow( color: (isActive ? Colors.red : Colors.green) .withOpacity(0.3), - blurRadius: 12.r, - spreadRadius: 2.r, + blurRadius: 12.w, + spreadRadius: 2.w, ), ], ), @@ -1148,23 +1167,23 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (controller.isLoadingMore.value) { // 正在加载状态 return Container( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric(vertical: 16.h), child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - width: 16, - height: 16, + SizedBox( + width: 16.w, + height: 16.h, child: CircularProgressIndicator( - strokeWidth: 2, + strokeWidth: 2.w, ), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), Text( - 'loadMoreMessages'.tr, // 加载更多消息... - style: const TextStyle( - fontSize: 14, + 'loadMoreMessages'.tr, + style: TextStyle( + fontSize: 14.sp, color: Colors.grey, ), ), @@ -1175,23 +1194,23 @@ class _AgentViewState extends State with WidgetsBindingObserver { } else if (controller.isLoadingMoreError.value) { // 加载失败状态 return Container( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric(vertical: 16.h), child: Center( child: GestureDetector( onTap: () => controller.loadMoreMessages(), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( + Icon( Icons.refresh, - size: 16, + size: 16.sp, color: Colors.grey, ), - const SizedBox(width: 8), + SizedBox(width: 8.w), Text( - 'loadFailedClickRetry'.tr, // 加载失败,点击重试 - style: const TextStyle( - fontSize: 14, + 'loadFailedClickRetry'.tr, + style: TextStyle( + fontSize: 14.sp, color: Colors.grey, ), ), @@ -1203,12 +1222,12 @@ class _AgentViewState extends State with WidgetsBindingObserver { } else { // 默认状态(有更多消息但未加载) return Container( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: EdgeInsets.symmetric(vertical: 8.h), child: Center( child: Text( - 'swipeUpForMore'.tr, // 向上滑动查看更多 - style: const TextStyle( - fontSize: 12, + 'swipeUpForMore'.tr, + style: TextStyle( + fontSize: 12.sp, color: Colors.grey, ), ), diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 440dc1b0a..b8903cc5f 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -33,8 +33,6 @@ class MessageBubble extends GetView { @override Widget build(BuildContext context) { - final timeFormat = DateFormat('HH:mm'); - return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( @@ -91,7 +89,7 @@ class MessageBubble extends GetView { children: [ // 时间戳(左对齐或右对齐) Text( - timeFormat.format(timestamp), + _formatTimestamp(timestamp), style: TextStyle( fontSize: 10, color: Theme.of(context).brightness == Brightness.dark @@ -105,7 +103,7 @@ class MessageBubble extends GetView { Padding( padding: const EdgeInsets.only(left: 8), child: Text( - "generatedByAi".tr,// 由AI生成 + "generatedByAi".tr, // 由AI生成 style: TextStyle( fontSize: 8, color: Colors.blue[600], @@ -418,6 +416,77 @@ class MessageBubble extends GetView { Widget _buildDot(int milliseconds) { return _AnimatedDot(delayMilliseconds: milliseconds); } + + /// 智能格式化时间戳 + String _formatTimestamp(DateTime messageTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + final messageDate = + DateTime(messageTime.year, messageTime.month, messageTime.day); + + // 计算时间差 + final difference = now.difference(messageTime); + + // 1分钟内显示"刚刚" + if (difference.inMinutes < 1) { + return 'justNow'.tr; // 刚刚 + } + + // 1小时内显示"X分钟前" + if (difference.inHours < 1) { + return 'minutesAgo' + .trParams({'minutes': difference.inMinutes.toString()}); // X分钟前 + } + + // 今天的消息显示时间 + if (messageDate == today) { + return DateFormat('HH:mm').format(messageTime); + } + + // 昨天的消息显示"昨天 HH:mm" + if (messageDate == yesterday) { + return 'yesterday'.tr + + ' ' + + DateFormat('HH:mm').format(messageTime); // 昨天 HH:mm + } + + // 本周内的消息显示"星期X HH:mm" + if (difference.inDays < 7) { + final weekday = _getWeekdayName(messageTime.weekday); + return weekday + ' ' + DateFormat('HH:mm').format(messageTime); + } + + // 今年的消息显示"MM-dd HH:mm" + if (messageTime.year == now.year) { + return DateFormat('MM-dd HH:mm').format(messageTime); + } + + // 其他情况显示完整日期"yyyy-MM-dd HH:mm" + return DateFormat('yyyy-MM-dd HH:mm').format(messageTime); + } + + /// 获取星期名称 + String _getWeekdayName(int weekday) { + switch (weekday) { + case 1: + return 'monday'.tr; // 星期一 + case 2: + return 'tuesday'.tr; // 星期二 + case 3: + return 'wednesday'.tr; // 星期三 + case 4: + return 'thursday'.tr; // 星期四 + case 5: + return 'friday'.tr; // 星期五 + case 6: + return 'saturday'.tr; // 星期六 + case 7: + return 'sunday'.tr; // 星期日 + default: + return ''; + } + } } /// 动画点组件 diff --git a/lib/modules/agent/views/navigation_card.dart b/lib/modules/agent/views/navigation_card.dart index 2d2d0e2a4..0db554a6f 100644 --- a/lib/modules/agent/views/navigation_card.dart +++ b/lib/modules/agent/views/navigation_card.dart @@ -221,89 +221,108 @@ Widget navigationCard( // 底部:目的地信息和操作按钮 Container( padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h), - child: Stack( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 目的地和距离时间信息 - Padding( - padding: EdgeInsets.only(right: 70.w), // 为右下角按钮留出空间 - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 路径显示:当前位置 → 目的地 - Row( - children: [ - // 起点图标 - Icon( - Icons.my_location, - size: 16.sp, - color: Theme.of(context).primaryColor, - ), - // SizedBox(width: 6.w), - // // 当前位置文本 - // Text( - // 'currentLocation'.tr, // 当前位置 - // style: TextStyle( - // fontSize: 14.sp, - // fontWeight: FontWeight.w500, - // color: isDarkMode - // ? Colors.white70 - // : Colors.black54, - // ), - // ), - SizedBox(width: 8.w), - // 箭头图标 - Icon( - Icons.arrow_forward, - size: 16.sp, - color: - isDarkMode ? Colors.white54 : Colors.black45, - ), - SizedBox(width: 8.w), - // 目的地图标 - Icon( - Icons.location_on, - size: 16.sp, - color: Colors.red, - ), - SizedBox(width: 6.w), - // 目的地名称 - Expanded( - child: Text( - destination, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode - ? Colors.white - : Colors.black87, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + // 第一层:起点 → 目的地信息(独占整个宽度) + Row( + crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 + children: [ + // 起点图标 - 统一图标容器宽度,确保对齐 + SizedBox( + width: 16.sp, // 固定图标容器宽度 + child: Icon( + Icons.my_location, + size: 15.sp, // 统一图标大小 + color: Theme.of(context).primaryColor, + ), + ), + SizedBox(width: 6.w), // 统一间距 + // 当前位置文本 + Text( + 'currentLocation'.tr, // 当前位置 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: isDarkMode + ? Colors.white70 + : Colors.black54, + height: 1.3, // 设置行高,提升多行文本可读性 ), - SizedBox(height: 4.h), - // 距离和时间信息 - Column( + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 + ), + SizedBox(width: 8.w), + // 箭头图标 + Icon( + Icons.arrow_forward, + size: 15.sp, // 统一图标大小 + color: isDarkMode ? Colors.white54 : Colors.black45, + ), + SizedBox(width: 8.w), + // 目的地图标 + Icon( + Icons.location_on, + size: 15.sp, // 统一图标大小 + color: Colors.red, + ), + SizedBox(width: 6.w), // 统一间距 + // 目的地名称 - 使用Expanded确保自动换行 + Expanded( + child: Text( + destination, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode + ? Colors.white + : Colors.black87, + height: 1.2, // 设置行高,提升多行文本可读性 + ), + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 + ), + ), + ], + ), + + SizedBox(height: 4.h), // 第一层和第二层之间的间距 + + // 第二层:距离时间信息(左侧)+ 导航按钮(右侧) + Row( + crossAxisAlignment: CrossAxisAlignment.end, // 底部对齐 + children: [ + // 左侧:距离和时间信息 + Expanded( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 距离信息 Row( + crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 children: [ - Icon( - Icons.straighten, - size: 14.sp, - color: Theme.of(context).primaryColor, - ), - SizedBox(width: 4.w), - Text( - formatDistance( - modes[currentIndex]["distance"]), - style: TextStyle( - fontSize: 14.sp, + // 距离图标 - 统一图标容器宽度,确保与第一层对齐 + SizedBox( + width: 16.sp, // 与第一层图标容器宽度一致 + child: Icon( + Icons.straighten, + size: 15.sp, // 统一图标大小 color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 6.w), // 与第一层间距一致 + Flexible( + child: Text( + formatDistance( + modes[currentIndex]["distance"]), + style: TextStyle( + fontSize: 14.sp, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w500, + height: 1.3, // 设置行高,提升多行文本可读性 + ), + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), ), ], @@ -311,95 +330,106 @@ Widget navigationCard( SizedBox(height: 4.h), // 时间信息 Row( + crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 children: [ - Icon( - Icons.access_time, - size: 14.sp, - color: Theme.of(context).primaryColor, + // 时间图标 - 统一图标容器宽度,确保与第一层对齐 + SizedBox( + width: 16.sp, // 与第一层图标容器宽度一致 + child: Icon( + Icons.access_time, + size: 15.sp, // 统一图标大小 + color: Theme.of(context).primaryColor, + ), ), - SizedBox(width: 4.w), - Text( - '${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约 + SizedBox(width: 6.w), // 与第一层间距一致 + Flexible( + child: Text( + '${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约 + style: TextStyle( + fontSize: 14.sp, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w500, + height: 1.3, // 设置行高,提升多行文本可读性 + ), + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 + ), + ), + ], + ), + // 导航状态信息 + if (navigation != null) ...[ + SizedBox(height: 6.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: Theme.of(context) + .primaryColor + .withOpacity(0.1), + borderRadius: BorderRadius.circular(6.r), + ), + child: Text( + "${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入 style: TextStyle( - fontSize: 14.sp, + fontSize: 12.sp, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w500, + height: 1.3, // 设置行高,提升多行文本可读性 ), + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), - ], - ), - ], - ), - // 导航状态信息 - if (navigation != null) ...[ - SizedBox(height: 6.h), - Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: Theme.of(context) - .primaryColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular(6.r), - ), - child: Text( - "${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入 - style: TextStyle( - fontSize: 12.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, ), - ), - ), - ], - ], - ), - ), - // 开始导航按钮 - 绝对定位到右下角 - Positioned( - right: 0, - bottom: 0, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).primaryColor, - Theme.of(context).primaryColor.withOpacity(0.8), + ], ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, ), - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: - Theme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], ), - child: Material( - color: Colors.transparent, - child: InkWell( + + SizedBox(width: 16.w), // 左右内容之间的间距 + + // 右侧:开始导航按钮 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryColor, + Theme.of(context).primaryColor.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), borderRadius: BorderRadius.circular(12.r), - onTap: () { - controller.openNavigation(start, end, ""); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 12.h, + boxShadow: [ + BoxShadow( + color: Theme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), ), - child: Icon( - Icons.navigation, - color: Colors.white, - size: 20.sp, + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12.r), + onTap: () { + controller.openNavigation(start, end, ""); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + child: Icon( + Icons.navigation, + color: Colors.white, + size: 20.sp, + ), ), ), ), ), - ), + ], ), ], ), diff --git a/lib/modules/common/widgets/version_update_dialog.dart b/lib/modules/common/widgets/version_update_dialog.dart index d8375a604..8566067c9 100644 --- a/lib/modules/common/widgets/version_update_dialog.dart +++ b/lib/modules/common/widgets/version_update_dialog.dart @@ -151,7 +151,7 @@ class VersionUpdateDialog extends StatelessWidget { Expanded( child: ElevatedButton( onPressed: () async { - Get.back(); + //Get.back();//点击更新之后,弹窗不关闭,增强强制更新的有效性 await _handleUpdate(); onUpdate?.call(); }, diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index 067e4b6af..e03ed7d1d 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/lib/modules/explore/controllers/explore_controller.dart @@ -76,6 +76,17 @@ class ExploreController extends GetxController { targetRoute: Routes.translation, ), // AIFeature( + // title: 'conferenceAssistant'.tr, // 商务会议助手 + // description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 + // icon: Icons.people, + // startColor: const Color(0xFFab47bc), + // endColor: const Color(0xFFec407a), + // iconBgColor: const Color(0xFFd81b60).withOpacity(0.2), + // iconColor: const Color(0xFFd81b60), + // agentId: 'meeting_assistant', + // targetRoute: Routes.meeting, + // ), + // AIFeature( // title: 'aiGirlfriend'.tr, // 亲子陪伴 // description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天 // icon: Icons.favorite, @@ -99,6 +110,20 @@ class ExploreController extends GetxController { // ), ]; + final List aiFeaturesWide = [ + AIFeature( + title: 'conferenceAssistant'.tr, // 商务会议助手 + description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 + icon: Icons.people, + startColor: const Color(0xFFab47bc), + endColor: const Color(0xFFec407a), + iconBgColor: const Color(0xFFd81b60).withOpacity(0.2), + iconColor: const Color(0xFFd81b60), + agentId: 'meeting_assistant', + targetRoute: Routes.meeting, + ), + ]; + // 热门功能场景列表 final List functionScenes = [ FunctionScene( @@ -177,6 +202,36 @@ class ExploreController extends GetxController { Get.toNamed(feature.targetRoute); } else if (feature.targetRoute == Routes.agent) { Get.toNamed(feature.targetRoute); + } else if (feature.targetRoute == Routes.meeting) { + // 跳转到会议页面 + Get.toNamed(feature.targetRoute); + } + } + + // 点击AI功能(宽版本) + void onAIFeatureWideTap(int index) { + if (index < 0 || index >= aiFeaturesWide.length) return; + + final feature = aiFeaturesWide[index]; + + // 直接根据目标路由进行跳转 + if (feature.targetRoute == Routes.meeting) { + Get.toNamed(feature.targetRoute); + } else if (feature.targetRoute == Routes.chat) { + final agent = AgentProvider.getAgentById(feature.agentId); + if (agent != null) { + Get.toNamed(feature.targetRoute, arguments: { + 'agentId': agent.id, + 'agentName': agent.name, + 'agentVoice': agent.voice, + 'agentAvatar': agent.avatarUrl, + 'agentSubtitle': agent.description, + 'systemPrompt': agent.systemPrompt, + 'welcomeMessage': agent.welcomeMessage, + }); + } + } else if (feature.targetRoute.isNotEmpty) { + Get.toNamed(feature.targetRoute); } } @@ -208,6 +263,11 @@ class ExploreController extends GetxController { // 实际应用中,这里应该跳转到新功能页面 } + // 跳转到翻译统计页面 + void goToTranslationStats() { + Get.toNamed(Routes.translationStats); + } + @override void onClose() { searchController.dispose(); diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index e487c7fbb..60779feed 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -65,11 +65,11 @@ class ExploreView extends GetView { // AI功能列表 SizedBox(height: 24.h), - _buildAIFeaturesSection(isDarkMode), + _buildCustomAILayout(isDarkMode), // 热门功能场景 SizedBox(height: 24.h), - _buildFunctionScenesSection(isDarkMode), + //_buildFunctionScenesSection(isDarkMode), // 新功能推荐 SizedBox(height: 24.h), @@ -115,12 +115,12 @@ class ExploreView extends GetView { ), child: IconButton( icon: Icon( - Icons.more_vert, + Icons.analytics_outlined, size: 16.sp, color: isDarkMode ? Colors.white : Colors.grey[700], ), padding: EdgeInsets.zero, - onPressed: () {}, + onPressed: controller.goToTranslationStats, ), ), ], @@ -208,37 +208,65 @@ class ExploreView extends GetView { ), // 文本内容 - Padding( - padding: EdgeInsets.all(24.w), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 22.sp, - fontWeight: FontWeight.bold, - color: Colors.white, + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: EdgeInsets.all(24.w), + child: Column( + mainAxisSize: MainAxisSize.min, // 使用最小尺寸,避免占用过多空间 + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题文本 - 支持多行显示 + Text( + title, + style: TextStyle( + fontSize: 22.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + height: 1.2, // 设置行高,提升多行文本可读性 + ), + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), - ), - SizedBox(height: 4.h), - Text( - description, - style: TextStyle( - fontSize: 14.sp, - color: Colors.white.withOpacity(0.8), + SizedBox(height: 6.h), // 稍微增加间距,适应多行布局 + // 描述文本 - 支持多行显示 + Text( + description, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.8), + height: 1.3, // 设置行高,提升多行文本可读性 + ), + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), - ), - ], + ], + ), ), ), ], ); } - // AI功能部分 - Widget _buildAIFeaturesSection(bool isDarkMode) { + // 自定义AI功能布局:2x2网格,商务会议助手占据下方两个网格位置 + Widget _buildCustomAILayout(bool isDarkMode) { + // 找到AI全能助手 + final aiAssistantIndex = controller.aiFeatures.indexWhere( + (feature) => feature.agentId == 'personal_assistant', + ); + + // 找到同声传译 + final translationIndex = controller.aiFeatures.indexWhere( + (feature) => feature.agentId == 'simultaneous_interpreter', + ); + + // 找到商务会议助手(在aiFeaturesWide中) + final meetingFeature = controller.aiFeaturesWide.isNotEmpty + ? controller.aiFeaturesWide.first + : null; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -251,20 +279,56 @@ class ExploreView extends GetView { ), ), SizedBox(height: 16.h), - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16.w, - mainAxisSpacing: 16.h, - childAspectRatio: 1.0, - ), - itemCount: controller.aiFeatures.length, - itemBuilder: (context, index) { - final feature = controller.aiFeatures[index]; - return _buildAIFeatureItem(feature, index, isDarkMode); - }, + + // 2x2网格布局 + Column( + children: [ + // 上层:AI全能助手(左)和同声传译(右) + Row( + children: [ + // 左上角:AI全能助手 + if (aiAssistantIndex != -1) + Expanded( + child: AspectRatio( + aspectRatio: 1.0, // 正方形 + child: _buildAIFeatureItem( + controller.aiFeatures[aiAssistantIndex], + aiAssistantIndex, + isDarkMode, + ), + ), + ), + + SizedBox(width: 16.w), + + // 右上角:同声传译 + if (translationIndex != -1) + Expanded( + child: AspectRatio( + aspectRatio: 1.0, // 正方形 + child: _buildAIFeatureItem( + controller.aiFeatures[translationIndex], + translationIndex, + isDarkMode, + ), + ), + ), + ], + ), + + SizedBox(height: 16.h), + + // 下层:商务会议助手(横跨整行) + if (meetingFeature != null) + AspectRatio( + aspectRatio: 2.0, // 宽高比2:1,横跨两个网格位置 + child: _buildAIFeatureItemWideForMeeting( + meetingFeature, + 0, // 使用固定索引,因为这是aiFeaturesWide中的项目 + isDarkMode, + ), + ), + ], ), ], ); @@ -341,9 +405,202 @@ class ExploreView extends GetView { fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + } + +//整行布局 + Widget _buildAIFeaturesWideSection(bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text( + // 'aiFunction'.tr, // AI功能 + // style: TextStyle( + // fontSize: 18.sp, + // fontWeight: FontWeight.w600, + // color: isDarkMode ? Colors.white : Colors.grey[800], + // ), + // ), + // SizedBox(height: 16.h), + // 直接使用 Column 包裹宽版卡片 + Column( + children: controller.aiFeaturesWide.map((feature) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), // 卡片间距 + child: _buildAIFeatureItemWide( + feature, + controller.aiFeatures.indexOf(feature), + isDarkMode, + ), + ); + }).toList(), + ), + ], + ); + } + + // AI功能项(宽版本,用于横跨整行的AI全能助手) + Widget _buildAIFeatureItemWide( + AIFeature feature, int index, bool isDarkMode) { + return GestureDetector( + onTap: () => controller.onAIFeatureTap(index), + child: Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + feature.startColor.withOpacity(isDarkMode ? 0.3 : 0.3), + feature.endColor.withOpacity(isDarkMode ? 0.3 : 0.3), + ], + ), + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.grey.withOpacity(0.1), + blurRadius: 8.w, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // 图标圆圈 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: feature.iconColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Icon( + feature.icon, + size: 24.sp, + color: feature.iconColor, + ), + ), + SizedBox(width: 16.w), + // 文本内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 标题 + Text( + feature.title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.grey[800], + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), + SizedBox(height: 4.h), + // 描述 + Text( + feature.description, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + } + + // AI功能项(宽版本,专门用于商务会议助手) + Widget _buildAIFeatureItemWideForMeeting( + AIFeature feature, int index, bool isDarkMode) { + return GestureDetector( + onTap: () => controller.onAIFeatureWideTap(index), + child: Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + feature.startColor.withValues(alpha: isDarkMode ? 0.3 : 0.3), + feature.endColor.withValues(alpha: isDarkMode ? 0.3 : 0.3), + ], + ), + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // 图标圆圈 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: feature.iconColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Icon( + feature.icon, + size: 24.sp, + color: feature.iconColor, + ), + ), + SizedBox(width: 16.w), + // 文本内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 标题 + Text( + feature.title, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.grey[800], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + // 描述 + Text( + feature.description, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), ], ), ), @@ -358,15 +615,15 @@ class ExploreView extends GetView { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'popularFunction'.tr, // 热门功能场景 - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.grey[800], - ), - ), - SizedBox(height: 16.h), + // Text( + // 'popularFunction'.tr, // 热门功能场景 + // style: TextStyle( + // fontSize: 18.sp, + // fontWeight: FontWeight.w600, + // color: isDarkMode ? Colors.white : Colors.grey[800], + // ), + // ), + // SizedBox(height: 16.h), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -391,13 +648,15 @@ class ExploreView extends GetView { child: Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withValues(alpha: 0.05) + : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.grey.withOpacity(0.1), + ? Colors.black.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 41c57c10d..31db67e15 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -5,6 +5,9 @@ import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; +import '../../../data/services/version_update_service.dart'; +import '../../common/widgets/version_update_dialog.dart'; +import 'package:get_storage/get_storage.dart'; /// 连接状态枚举 enum ConnectionStatus { @@ -50,6 +53,14 @@ class HomeController extends GetxController { final _bluetoothManager = Get.find(); final _bleManager = Get.find(); + final _storage = GetStorage(); + + // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) + static bool _hasCheckedVersionInSession = false; + + // 版本检查相关常量 + static const String _lastVersionCheckKey = 'last_version_check_timestamp'; + static const int _versionCheckIntervalHours = 24; // 24小时间隔 @override void onInit() { super.onInit(); @@ -67,6 +78,12 @@ class HomeController extends GetxController { void _initWithCachedInfo() { _setupStateListeners(); _checkConnectionStatus(); + + // 延迟检查版本更新,避免影响页面加载 + // 混合检查机制:会话检查 + 24小时间隔检查 + Future.delayed(const Duration(seconds: 2), () { + _checkVersionUpdateIfNeeded(); + }); } void _setupStateListeners() { @@ -253,4 +270,104 @@ class HomeController extends GetxController { ? deviceInfo.value!.firmwareVersion : 'unknown'.tr; } + + /// 混合检查机制:判断是否需要进行版本检查 + /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 + void _checkVersionUpdateIfNeeded() { + try { + final now = DateTime.now(); + final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); + + bool shouldCheck = false; + String reason = ''; + + // 条件1:会话内未检查过 + if (!_hasCheckedVersionInSession) { + shouldCheck = true; + reason = '会话内首次检查'; + } + // 条件2:距离上次检查超过24小时 + else if (lastCheckTimestamp != null) { + final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); + final timeDifference = now.difference(lastCheckTime); + + if (timeDifference.inHours >= _versionCheckIntervalHours) { + shouldCheck = true; + reason = '距离上次检查已超过${_versionCheckIntervalHours}小时'; + } + } + // 条件3:从未检查过(首次使用应用) + else { + shouldCheck = true; + reason = '首次使用应用'; + } + + if (shouldCheck) { + Logger.i(_tag, '开始版本检查 - 原因: $reason'); + _performVersionCheck(); + } else { + Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); + } + } catch (e) { + Logger.e(_tag, '版本检查判断失败: $e'); + } + } + + /// 执行版本检查(在后台进行,不阻塞启动流程) + Future _performVersionCheck() async { + try { + final versionService = VersionUpdateService.to; + final updateInfo = await versionService.checkForUpdate(); + + if (updateInfo.hasUpdate) { + Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); + // 显示更新对话框 + await VersionUpdateDialog.show(updateInfo); + } else { + Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); + } + + // 更新检查记录 + _updateVersionCheckRecord(); + } catch (e) { + Logger.e(_tag, '版本检查失败: $e'); + // 即使检查失败,也更新记录,避免频繁重试 + _updateVersionCheckRecord(); + } + } + + /// 更新版本检查记录 + void _updateVersionCheckRecord() { + try { + // 更新时间戳 + final now = DateTime.now(); + _storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch); + + // 标记本次会话已检查过版本 + _hasCheckedVersionInSession = true; + + Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}'); + } catch (e) { + Logger.e(_tag, '更新版本检查记录失败: $e'); + } + } + + /// 重置版本检查标记(可用于强制重新检查) + /// 注意:这会清除会话标记,但不会清除时间戳记录 + static void resetVersionCheckFlag() { + _hasCheckedVersionInSession = false; + Logger.i('HomeCtrl', '版本检查会话标记已重置'); + } + + /// 完全重置版本检查状态(包括时间戳) + /// 用于测试或特殊情况下强制重新检查 + void forceResetVersionCheck() { + try { + _hasCheckedVersionInSession = false; + _storage.remove(_lastVersionCheckKey); + Logger.i(_tag, '版本检查状态已完全重置'); + } catch (e) { + Logger.e(_tag, '重置版本检查状态失败: $e'); + } + } } diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index c24af6607..9e30babf2 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -173,9 +173,9 @@ class HomeView extends GetView { borderRadius: BorderRadius.circular(16.r), child: Image.asset( isDarkMode - ? 'assets/images/headphone_dark.png' - : 'assets/images/headphone_light.png', - height: 180.h, + ? 'assets/images/headphone_new3.png' + : 'assets/images/headphone_new4.png', + height: 220.h, fit: BoxFit.contain, ), ), @@ -519,7 +519,6 @@ class HomeView extends GetView { Widget _buildDisconnectedInfo(bool isDarkMode) { return Container( padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 12.w), - alignment: Alignment.center, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[900] : Colors.grey[100], borderRadius: BorderRadius.circular(10.r), @@ -528,26 +527,37 @@ class HomeView extends GetView { width: 1, ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.bluetooth_disabled, - size: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[700], - ), - SizedBox(height: 12.h), - Text( - 'disconnectedDevice'.tr, // 未连接设备,点击下方按钮连接 - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[700], - // height: 1.4, - // fontWeight: FontWeight.w500, + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, // 整体居中对齐 + crossAxisAlignment: CrossAxisAlignment.start, // 顶部对齐,适应多行文本 + children: [ + // 图标容器,与文本第一行基线对齐 + Padding( + padding: EdgeInsets.only(top: 1.h), // 微调图标位置,与文本第一行基线对齐 + child: Icon( + Icons.bluetooth_disabled, + size: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[700], + ), ), - //textAlign: TextAlign.center, - ), - ], + SizedBox(width: 6.w), // 图标和文本间距 + // 文本容器,使用Flexible确保自动换行且保持居中效果 + Flexible( + child: Text( + 'disconnectedDevice'.tr, // 未连接设备,点击下方按钮连接 + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[700], + height: 1.4, // 启用行高,提升多行文本可读性 + ), + textAlign: TextAlign.center, // 文本居中对齐 + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 + ), + ), + ], + ), ), ); } diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index ab1c500ff..fcc7e2f62 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -210,12 +210,12 @@ class LoginController extends GetxController { //如果用户未接受隐私政策,显示弹窗 if (!_hasAcceptedPrivacy.value) { _showPrivacyDialog(); + } else { + _initDeviceInfo(); + _initAppInfo(); // 新增:初始化APP信息 + _checkGuestLoginConfig(); // 检查游客登录配置 } }); - - _initDeviceInfo(); - _initAppInfo(); // 新增:初始化APP信息 - _checkGuestLoginConfig(); // 检查游客登录配置 } @override @@ -229,7 +229,6 @@ class LoginController extends GetxController { Future _checkGuestLoginConfig() async { try { final versionService = VersionUpdateService.to; - await versionService.checkForUpdate(); // 先检查版本更新,这会同时设置 isShowTourists 的值 await versionService.checkForUpdate(); @@ -388,6 +387,9 @@ class LoginController extends GetxController { // 用户同意,关闭弹窗并继续 _hasAcceptedPrivacy.value = true; Get.back(); + _initDeviceInfo(); + _initAppInfo(); // 新增:初始化APP信息 + _checkGuestLoginConfig(); // 检查游客登录配置 }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0066CC), @@ -601,13 +603,13 @@ class LoginController extends GetxController { // 为了演示,我们只是模拟发送 if (isEmialContactController() || isPhoneContactController()) { _startCountdown(); - print(contactController.text); + Logger().d(contactController.text); var response = await Api.getCode({ "addr": selectedCountryCode + contactController.text, "vtype": loginType, }); if (response != null) { - print("请求后台数据验证码params=${response}"); + Logger().d("请求后台数据验证码params=$response"); //短信已发送 //EasyLoading.showInfo('codeSend'.tr); } @@ -632,7 +634,7 @@ class LoginController extends GetxController { // 更新验证码输入 void updateVerificationCode(int index, String value) { - print("updateVerificationCode=${index}${value}"); + Logger().d("updateVerificationCode=$index$value"); if (index >= 0 && index < 4) { var newList = [..._verificationCode]; newList[index] = value; @@ -665,7 +667,7 @@ class LoginController extends GetxController { bool isVerificationCodeComplete() { // 检查每个元素是否为单个字母或数字字符 final text = verificationCodeController.text; - print("数据获取Facebook${text.length}"); + Logger().d("数据获取Facebook${text.length}"); return text.length == 4 && RegExp(r'^[0-9]+$').hasMatch(text); } @@ -712,13 +714,6 @@ class LoginController extends GetxController { //验证隐私协议勾选状态 if (!_hasAcceptedPrivacy.value) { - // Get.snackbar( - // 'tip'.tr, - // 'pleaseAcceptPrivacyPolicy'.tr, - // snackPosition: SnackPosition.TOP, - // backgroundColor: Colors.red.withOpacity(0.1), - // colorText: Colors.red, - // ); _showPrivacyDialog(); return; } @@ -736,7 +731,7 @@ class LoginController extends GetxController { if (!isVerificationCodeComplete()) { Get.snackbar( 'tip'.tr, - 'pleaseEnterVerificationCode'.tr, + 'pleaseEnterCompleteCode'.tr, snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.1), colorText: Colors.red, @@ -783,14 +778,14 @@ class LoginController extends GetxController { } case 4: //Facebook登录 { - print("数据获取Facebook"); + Logger().d("数据获取Facebook"); final facebook = Facebook(); bool authResult = await facebook.signInWithFacebook(); if (authResult) { - print("数据获取_Facebook.token${facebook.token}"); - print("数据获取_Facebook.pictureurl${facebook.pictureurl}"); - print("数据获取_Facebook.pictureurl${facebook.email}"); + Logger().d("数据获取_Facebook.token${facebook.token}"); + Logger().d("数据获取_Facebook.pictureurl${facebook.pictureurl}"); + Logger().d("数据获取_Facebook.pictureurl${facebook.email}"); // SettingsController().userName = Facebook().name; // SettingsController().avatar = Facebook().pictureurl; // SettingsController().userId = Facebook().id; @@ -813,15 +808,15 @@ class LoginController extends GetxController { } case 3: //Google登录 { - print("数据获取_Google()"); + Logger().d("数据获取_Google()"); final google = Google(); bool authResult = await google.signInWithGoogle(); - print("数据获取_Google().token${authResult}"); + Logger().d("数据获取_Google().token${authResult}"); if (authResult) { - print("数据获取_Google.token${google.token}"); - print("数据获取_Google.pictureurl${google.pictureurl}"); - print("数据获取_Google.email${google.email}"); - print("数据获取_Google.email${google.displayName}"); + Logger().d("数据获取_Google.token${google.token}"); + Logger().d("数据获取_Google.pictureurl${google.pictureurl}"); + Logger().d("数据获取_Google.email${google.email}"); + Logger().d("数据获取_Google.email${google.displayName}"); // 验证成功,跳转到配对页面 // SettingsController().userName = Google().displayName!; // //SettingsController().avatar = Google().email; @@ -843,7 +838,7 @@ class LoginController extends GetxController { } case 2: //微信登录 { - print("数据获取_FlWX()"); + Logger().d("数据获取_FlWX()"); bool authResult = await FlWX().authBy( NormalAuth( @@ -856,7 +851,7 @@ class LoginController extends GetxController { if (authResult) { // 确保 FlWX() 返回的是同一个实例,并且 token 已经被设置 // 这里假设 FlWX().token 在 authBy 成功后被设置 - print("数据获取_FlWX().token${FlWX().token}"); + Logger().d("数据获取_FlWX().token${FlWX().token}"); // SettingsController().userName = FlWX().nickname; // SettingsController().avatar = FlWX().headimgurl; // SettingsController().userId = FlWX().openid; @@ -929,19 +924,19 @@ class LoginController extends GetxController { "avatar": params["avatar"], }); if (response != null) { - print("请求后台数据login回复数据=${response}"); + Logger().d("请求后台数据login回复数据=$response"); // final instance = await GetStorage(); await _storage.write('loginInfo', jsonEncode(response)); // 2. 将响应数据转换为User对象 try { - print('响应数据: ${response.toString()}'); // 调试输出原始数据 + Logger().d('响应数据: ${response.toString()}'); // 调试输出原始数据 if (response['token'].toString() != '') { // 新增:检查 user 字段是否存在且类型正确 // if (response.containsKey('data') && response['data'] is Map) { final user = User.fromJson(response); - print('用户初始化成功: ${response['token'].toString()}'); + Logger().d('用户初始化成功: ${response['token'].toString()}'); await _storage.write("logintoken", response['token'].toString()); // 存储到 SharedPreferences (仅存储用户数据) //final prefs = await GetStorage(); @@ -958,8 +953,8 @@ class LoginController extends GetxController { } } catch (e, stack) { // 新增:打印堆栈跟踪 - print('用户数据处理失败: $e'); - print('堆栈跟踪: $stack'); + Logger().d('用户数据处理失败: $e'); + Logger().d('堆栈跟踪: $stack'); Get.snackbar('错误', '用户数据解析失败,请检查网络'); } @@ -981,18 +976,18 @@ class LoginController extends GetxController { Future getAppConfig() async { try { - print('getAppConfig:进来了'); + Logger().d('getAppConfig:进来了'); //if (User.getname() != "") { final response = await Api.getappconfig(); - print('getAppConfig: $response'); + Logger().d('getAppConfig: $response'); if (response == null) return false; - print('getAppConfig:进来了'); + Logger().d('getAppConfig:进来了'); // 根据API设计调整 final resp = UserGetAppConfigResp.fromJson(response); AppConfig.initialize(resp); return true; } catch (e) { - print('getAppConfig 取失败: $e'); + Logger().d('getAppConfig 取失败: $e'); return false; } } diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 9df8ffb0f..543ba1827 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -39,13 +39,13 @@ class LoginView extends GetView { SizedBox(height: 32.h), _buildWelcomeText(isDarkMode), - // // 输入框和继续按钮 - // SizedBox(height: 24.h), - // _buildInputField(isDarkMode), - // SizedBox(height: 16.h), - // _buildVerificationInputField(isDarkMode), - // SizedBox(height: 16.h), - // _buildContinueButton(isDarkMode), + // 输入框和继续按钮 + SizedBox(height: 24.h), + _buildInputField(isDarkMode), + SizedBox(height: 16.h), + _buildVerificationInputField(isDarkMode), + SizedBox(height: 16.h), + _buildContinueButton(isDarkMode), // 社交媒体登录 SizedBox(height: 32.h), diff --git a/lib/modules/meeting/bindings/meeting_binding.dart b/lib/modules/meeting/bindings/meeting_binding.dart index 82c4c9f5d..0edd89e9b 100644 --- a/lib/modules/meeting/bindings/meeting_binding.dart +++ b/lib/modules/meeting/bindings/meeting_binding.dart @@ -1,10 +1,16 @@ import 'package:get/get.dart'; import '../controllers/meeting_controller.dart'; +import '../controllers/meeting_home_controller.dart'; +import '../controllers/meeting_mine_controller.dart'; +import '../../profile/controllers/profile_controller.dart'; class MeetingBinding extends Bindings { @override void dependencies() { // 注册控制器 Get.lazyPut(() => MeetingController()); + Get.lazyPut(() => MeetingHomeController()); + Get.lazyPut(() => MeetingMineController()); + Get.lazyPut(() => ProfileController()); } } diff --git a/lib/modules/meeting/controllers/meeting_connect_controller.dart b/lib/modules/meeting/controllers/meeting_connect_controller.dart index 9101c21f4..fd1642779 100644 --- a/lib/modules/meeting/controllers/meeting_connect_controller.dart +++ b/lib/modules/meeting/controllers/meeting_connect_controller.dart @@ -7,10 +7,10 @@ import 'package:path_provider/path_provider.dart'; import 'package:wifi_iot/wifi_iot.dart'; import '../../../core/utils/ftpconnect/ftpconnect.dart'; -import 'meeting_controller.dart'; +import 'meeting_home_controller.dart'; class MeetingConnectController extends GetxController { - final _meetingController = Get.find(); + final _meetingController = Get.find(); RxString stateStr = '搜索中...'.obs; RxBool isRescan = false.obs; diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index 777e551d7..a865d5362 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -1,402 +1,24 @@ -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; - -import '../../../core/utils/logger.dart'; -import '../../../core/utils/synchrodata.dart'; -import '../../../data/services/ble_manager.dart'; -import '../../../data/services/db/sqflite_api.dart'; -import '../../../core/utils/permission_util.dart'; -import 'package:share_plus/share_plus.dart'; - -import '../../../data/services/meeting/meeting_task_service.dart'; -import '../../../data/services/meeting/meeting_upload_service.dart'; -import '../../../routes/app_routes.dart'; +import '../views/meeting_home_view.dart'; +import '../views/meeting_mine_view.dart'; class MeetingController extends GetxController { - final meetingUploadService = Get.find(); - final meetingTaskService = Get.find(); - - final ScrollController listController = ScrollController(); - final AudioPlayer _audioPlayer = AudioPlayer(); + final GlobalKey scaffoldKey = GlobalKey(); - // 是否还有更多数据 - bool isGetMore = true; - bool isLoading = false; - int page = 1; - int addQuantity = 0; - List selected = []; + late PageController pageController; - final appDocDir = getApplicationDocumentsDirectory(); - List files = []; - TextEditingController titleController = TextEditingController(); // 录音标题 - String titleCSuffix = ""; // 录音标题后缀 - RxList dataList = [].obs; + final List pageList = [ + const MeetingHomeView(), + const MeetingMineView(), + ]; - // 蓝牙服务 - final bleManager = Get.find(); + RxInt pageIndex = 0.obs; @override void onInit() { super.onInit(); - listController.addListener(_onScroll); - _requestStoragePermission(); - _initGetDataList(); - addMeetingAudio(); - final Map args = Get.arguments ?? {}; - // 检查参数是否存在且包含有效路径 - if (args.isNotEmpty && - args.containsKey('filePath') && - args['filePath'] is String && - args['filePath'].isNotEmpty) { - final String path = args['filePath']; - addAudio(path); - } - } - - @override - void onClose() { - listController.dispose(); - titleController.dispose(); - _audioPlayer.dispose(); - super.onClose(); - } - - void onPopInvokedWithResult(didPop, result) { - Synchrodata.uploadOperationRecords(isSynchro: true); - } - - void _onScroll() { - if (isGetMore && - !isLoading && - listController.position.pixels >= - listController.position.maxScrollExtent - 100) { - isLoading = true; - page++; - _getDataList(); - } - } - - void _initGetDataList() async { - await Synchrodata.uploadOperationRecords(isSynchro: true); - _getDataList(); - } - - // 请求本地数据 - void _getDataList() async { - List list = await SqfliteApi.getMeetingList((page - 1) * 10 + addQuantity); - dataList.addAll(list); - addQuantity = 0; - if (list.length < 10) { - isGetMore = false; - } else { - isGetMore = true; - } - isLoading = false; - } - - // 请求存储权限 - void _requestStoragePermission() async { - PermissionUtil.instance - .requestPermission( - permissionType: Permission.storage, - permissionName: "存储", - explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", - permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", - ) - .then((granted) { - if (granted) { - Logger.d("Permission", "用户授予了存储权限"); - } else { - Logger.d("Permission", "用户未授予存储权限"); - } - }); - } - - void loadTranslatopus() async { - final dir = - Directory("/data/user/0/com.yunqiinnovation.deepsound/files/OpusAudio"); - - files = dir - .listSync() - .where((f) => f is File && (f.path.endsWith('.opus'))) - .toList(); - } - - void loadTranslatAudio() async { - try { - final appDir = await appDocDir; // 先 await 获取目录 - final dir = Directory("${appDir.path}/TranslatAudio"); - - if (!await dir.exists()) { - await dir.create(recursive: true); - files = []; - return; - } - - files = dir - .listSync() - .where((f) => - f is File && - (f.path.endsWith('.mp3') || - f.path.endsWith('.wav') || - f.path.endsWith('.aac') || - f.path.endsWith('.m4a') || - f.path.endsWith('.mp4') || - f.path.endsWith('.mov'))) - .toList(); - } catch (e) { - files = []; - } - } - - Future shareAllTranslatAudio() async { - final filePath = selected[0]; - try { - await Share.shareXFiles( - [XFile(filePath)], - text: '分享会议音频', - ); - - EasyLoading.showToast('分享完成!'); - } catch (e) { - EasyLoading.showToast('分享失败: ${e.toString()}'); - } - } - - void addMeetingAudio() async { - EasyLoading.show(status: '正在导入本地录音...'); - try { - int addUp = 0; - final appDir = await appDocDir; - - final sourceDirs = [ - Directory("${appDir.path}/MeetingAudio/"), - Directory("${appDir.path}/DestAudio/"), - ]; - - final destDir = Directory("${appDir.path}/LocalAudio/"); - if (!await destDir.exists()) { - await destDir.create(recursive: true); - } - - List allFiles = []; - - for (final dir in sourceDirs) { - if (await dir.exists()) { - final files = - await dir.list().where((entity) => entity is File).toList(); - allFiles.addAll(files); - } - } - - for (final fileEntity in allFiles) { - try { - final file = File(fileEntity.path); - final filePath = file.path; - - // 设置音频源获取时长 - - await _audioPlayer.setAudioSource(AudioSource.file(filePath)); - int seconds = _audioPlayer.duration?.inSeconds ?? 0; - - // 获取文件修改时间 - final fileStat = await file.stat(); - final creationTime = fileStat.changed.millisecondsSinceEpoch; - - // 准备新路径 - final fileName = filePath.split('/').last; - final newPath = "${destDir.path}/$fileName"; - - // 构建数据 - Map data = { - 'title': fileName, - 'type': 'LOCAL', - 'seconds': seconds, - 'filepath': newPath, // 存储新路径 - 'audiourl': '', - 'tasktype': 0, - 'creationtime': creationTime, - }; - - // 插入数据库 - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { - // 上传OSS - meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio'); - - // 更新UI数据 - dataList.insert(0, { - 'id': meetingId, - 'taskid': '', - ...data, - }); - - // 移动文件 - await file.copy(newPath); - await file.delete(); - - addQuantity++; - addUp++; - } - } catch (e) { - print("文件处理失败: ${fileEntity.path}, 错误: $e"); - } - } - if (addUp > 0) EasyLoading.showSuccess('成功导入本地 $addUp 条录音'); - } catch (e) { - EasyLoading.showError('导入失败: $e'); - } finally { - EasyLoading.dismiss(); - } - } - - void addTranslatAudio() async { - EasyLoading.show(status: '正在批量导入...'); - for (final filePath in selected) { - try { - await _audioPlayer.setAudioSource(AudioSource.file(filePath)); - final fileStat = await File(filePath).stat(); - final creationTime = fileStat.changed.millisecondsSinceEpoch; // 时间戳 - int seconds = _audioPlayer.duration?.inSeconds ?? 0; - final fileName = filePath.split('/').last; - Map data = { - 'title': fileName, - 'type': 'TRANSLAT', - 'seconds': seconds, - 'filepath': filePath, - 'audiourl': '', - 'tasktype': 0, - 'creationtime': creationTime, - }; - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { - meetingUploadService.addUpload(meetingId, filePath, 'TranslatAudio'); - - dataList.insert(0, { - 'id': meetingId, - 'taskid': '', - ...data, - }); - addQuantity++; - } - } catch (e) { - Logger.error('导入文件失败: $filePath, error: $e'); - } - } - EasyLoading.dismiss(); - } - - void addAudio([String? filePath]) async { - EasyLoading.show(status: '正在导入...'); - String fileName = ""; - bool isParamProvided = filePath != null; // 更清晰的变量名 - if (filePath != null) { - // 正确截取文件名(跨平台安全方式) - fileName = path.basename(filePath); - } else { - FilePickerResult? result = await FilePicker.platform.pickFiles( - withData: true, - ); - if (result != null) { - PlatformFile file = result.files.first; - filePath = await _saveFile(file); - fileName = file.name; - } else { - EasyLoading.dismiss(); - return; - } - } - - final meetingId = await insertMeetingAudio(fileName, filePath); - - if (isParamProvided) { - Get.toNamed(Routes.meetingDetails, arguments: { - 'id': meetingId, - }); - } - EasyLoading.dismiss(); - } - - Future insertMeetingAudio(String fileName, String filePath) async { - await _audioPlayer.setAudioSource(AudioSource.file(filePath)); - int seconds = _audioPlayer.duration?.inSeconds ?? 0; - int creationTime = DateTime.now().millisecondsSinceEpoch; - Map data = { - 'title': fileName, - 'type': 'IMPORT', - 'seconds': seconds, - 'filepath': filePath, - 'audiourl': '', - 'tasktype': 0, - 'creationtime': creationTime, - }; - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { - meetingUploadService.addUpload(meetingId, filePath, 'ExternalAudio'); - - dataList.insert(0, { - 'id': meetingId, - 'taskid': '', - ...data, - }); - addQuantity++; - } - return meetingId; - } - - Future _saveFile(PlatformFile file) async { - final directory = await getApplicationDocumentsDirectory(); - final directoryDir = Directory('${directory.path}/ExternalAudio'); - if (!await directoryDir.exists()) { - await directoryDir.create(recursive: true); - } - final String filePath = '${directory.path}/ExternalAudio/${file.name}'; - final File localFile = File(filePath); - await localFile.writeAsBytes(file.bytes!); - return filePath; - } - - // 修改标题 - void editMeetingTitle(int id) async { - if (titleController.text.isNotEmpty) { - String title = titleController.text + titleCSuffix; - SqfliteApi.editMeetingTitle(id, {'title': title}); - int index = dataList.indexWhere((item) => item['id'] == id); - if (index != -1) { - final dataMap = Map.from(dataList[index]); - dataMap['title'] = title; - dataList[index] = dataMap; - } - } - Get.back(); - } - - // 删除数据 - void deleteMeeting(int id, String filepath) async { - dataList.removeWhere((item) => item['id'] == id); - // 删除数据库中的记录 - SqfliteApi.deleteMeeting(id); - _deleteFile(filepath); - meetingUploadService.removeUpload(id, isDeleteTask: true); - meetingTaskService.removeTask(id, isDeleteTask: true); - addQuantity--; - } - - // 删除文件 - Future _deleteFile(String filepath) async { - File file = File(filepath); - bool fileExists = await file.exists(); - if (fileExists) { - await file.delete(); - } + pageController = PageController(initialPage: 0); } } diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index c3f3e75df..19a9db30f 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -25,6 +25,8 @@ class MeetingDetailsController extends GetxController late AnimationController lottieController; + late final Worker _tasktypeListener; + final _meetingUploadService = Get.find(); final _meetingTaskService = Get.find(); @@ -49,12 +51,15 @@ class MeetingDetailsController extends GetxController Rx meetingData = MeetingModel().obs; Rx meetingDetails = MeetingDetailsModel().obs; RxList textList = [].obs; - String allText = ''; RxBool isSpeakerText = false.obs; List editSpeakerTextList = []; RxInt editSpeakerTextId = 0.obs; String editSpeakerTextChange = ''; + int _everType = 99; + RxInt summaryNumberModifications = 0.obs; + RxInt personnelNumberModifications = 0.obs; + String markmapLibJs = ''; String d3Js = ''; String markmapViewJs = ''; @@ -81,10 +86,24 @@ class MeetingDetailsController extends GetxController _meetingTaskService.meetingId = id; _getDataDetails(); _loadJS(); + _tasktypeListener = ever(_meetingTaskService.tasktype, (value) { + meetingDetails.value.tasktype = value; + if (_everType <= value) { + if (value == 2) { + textList.value = _meetingTaskService.textList; + } else if (value == 3) { + meetingDetails.value.summary = _meetingTaskService.summary; + meetingDetails.value.overview = _meetingTaskService.overview; + summaryNumberModifications.value++; + } + } + meetingDetails.refresh(); + }); } @override void onClose() { + _tasktypeListener.dispose(); _currentDurationSubscription?.cancel(); _playerStateSubscription?.cancel(); tabController.dispose(); @@ -103,6 +122,7 @@ class MeetingDetailsController extends GetxController final data = await SqfliteApi.getMeetingDetails(id); meetingData.value = MeetingModel.fromJson(data); meetingDetails.value = MeetingDetailsModel.fromJson(data['details']); + _meetingTaskService.tasktype.value = meetingDetails.value.tasktype; if (meetingDetails.value.tasktype >= 3) { dragOffset.value = 0; isTop.value = true; @@ -227,6 +247,7 @@ class MeetingDetailsController extends GetxController void summaryCreate() async { EasyLoading.show(); + _everType = 2; String audiourl = meetingData.value.audiourl; String taskId = meetingDetails.value.taskid; await _meetingTaskService.addTask(id, audiourl, taskId); @@ -234,4 +255,18 @@ class MeetingDetailsController extends GetxController meetingDetails.refresh(); EasyLoading.dismiss(); } + + void refreshSummary() async { + String text = ''; + for (var element in textList) { + if (element['speaker'].isNotEmpty) { + text += '[${element['speaker']}]: '; + } + text += '${element['content']}\n'; + } + _everType = 3; + await SqfliteApi.editMeeting(id, {'tasktype': 2}); + _meetingTaskService.tasktype.value = 2; + _meetingTaskService.aiServiceMessage(id, text); + } } diff --git a/lib/modules/meeting/controllers/meeting_home_controller.dart b/lib/modules/meeting/controllers/meeting_home_controller.dart new file mode 100644 index 000000000..3c1150630 --- /dev/null +++ b/lib/modules/meeting/controllers/meeting_home_controller.dart @@ -0,0 +1,420 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../../../core/utils/logger.dart'; +import '../../../core/utils/synchrodata.dart'; +import '../../../data/services/ble_manager.dart'; +import '../../../data/services/db/sqflite_api.dart'; +import '../../../core/utils/permission_util.dart'; + +import 'package:share_plus/share_plus.dart'; + +import '../../../data/services/meeting/meeting_task_service.dart'; +import '../../../data/services/meeting/meeting_upload_service.dart'; +import '../../../routes/app_routes.dart'; +import 'meeting_controller.dart'; + +class MeetingHomeController extends GetxController { + final meetingController = Get.find(); + final meetingUploadService = Get.find(); + final meetingTaskService = Get.find(); + + final AudioPlayer _audioPlayer = AudioPlayer(); + + List selected = []; + + final appDocDir = getApplicationDocumentsDirectory(); + List files = []; + TextEditingController titleController = TextEditingController(); // 录音标题 + String titleCSuffix = ""; // 录音标题后缀 + + RxList originalDataList = [].obs; + RxList dataList = [].obs; + RxString filtrateType = ''.obs; + bool isAsc = false; + TextEditingController filtrateController = TextEditingController(); + + // 蓝牙服务 + final bleManager = Get.find(); + + @override + void onInit() { + super.onInit(); + _requestStoragePermission(); + _initGetDataList(); + addMeetingAudio(); + final Map args = Get.arguments ?? {}; + // 检查参数是否存在且包含有效路径 + if (args.isNotEmpty && + args.containsKey('filePath') && + args['filePath'] is String && + args['filePath'].isNotEmpty) { + final String path = args['filePath']; + addAudio(path); + } + } + + @override + void onClose() { + titleController.dispose(); + filtrateController.dispose(); + _audioPlayer.dispose(); + super.onClose(); + } + + void onPopInvokedWithResult(didPop, result) { + Synchrodata.uploadOperationRecords(isSynchro: true); + } + + void _initGetDataList() async { + await Synchrodata.uploadOperationRecords(isSynchro: true); + _getDataList(); + } + + // 请求本地数据 + void _getDataList() async { + List list = await SqfliteApi.getMeetingList(); + originalDataList.value = List.from(list); + dataList.value = List.from(list); + } + + // 请求存储权限 + void _requestStoragePermission() async { + PermissionUtil.instance + .requestPermission( + permissionType: Permission.storage, + permissionName: "存储", + explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", + permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", + ) + .then((granted) { + if (granted) { + Logger.d("Permission", "用户授予了存储权限"); + } else { + Logger.d("Permission", "用户未授予存储权限"); + } + }); + } + + void loadTranslatopus() async { + final dir = + Directory("/data/user/0/com.yunqiinnovation.deepsound/files/OpusAudio"); + + files = dir + .listSync() + .where((f) => f is File && (f.path.endsWith('.opus'))) + .toList(); + } + + void loadTranslatAudio() async { + try { + final appDir = await appDocDir; // 先 await 获取目录 + final dir = Directory("${appDir.path}/TranslatAudio"); + + if (!await dir.exists()) { + await dir.create(recursive: true); + files = []; + return; + } + + files = dir + .listSync() + .where((f) => + f is File && + (f.path.endsWith('.mp3') || + f.path.endsWith('.wav') || + f.path.endsWith('.aac') || + f.path.endsWith('.m4a') || + f.path.endsWith('.mp4') || + f.path.endsWith('.mov'))) + .toList(); + } catch (e) { + files = []; + } + } + + Future shareAllTranslatAudio() async { + final filePath = selected[0]; + try { + await Share.shareXFiles( + [XFile(filePath)], + text: '分享会议音频', + ); + + EasyLoading.showToast('分享完成!'); + } catch (e) { + EasyLoading.showToast('分享失败: ${e.toString()}'); + } + } + + void addMeetingAudio() async { + EasyLoading.show(status: '正在导入本地录音...'); + try { + int addUp = 0; + final appDir = await appDocDir; + + final sourceDirs = [ + Directory("${appDir.path}/MeetingAudio/"), + Directory("${appDir.path}/DestAudio/"), + ]; + + final destDir = Directory("${appDir.path}/LocalAudio/"); + if (!await destDir.exists()) { + await destDir.create(recursive: true); + } + + List allFiles = []; + + for (final dir in sourceDirs) { + if (await dir.exists()) { + final files = + await dir.list().where((entity) => entity is File).toList(); + allFiles.addAll(files); + } + } + + for (final fileEntity in allFiles) { + try { + final file = File(fileEntity.path); + final filePath = file.path; + + // 设置音频源获取时长 + + await _audioPlayer.setAudioSource(AudioSource.file(filePath)); + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + + // 获取文件修改时间 + final fileStat = await file.stat(); + final creationTime = fileStat.changed.millisecondsSinceEpoch; + + // 准备新路径 + final fileName = filePath.split('/').last; + final newPath = "${destDir.path}/$fileName"; + + // 构建数据 + Map data = { + 'title': fileName, + 'type': 'LOCAL', + 'seconds': seconds, + 'filepath': newPath, // 存储新路径 + 'audiourl': '', + 'tasktype': 0, + 'creationtime': creationTime, + }; + + // 插入数据库 + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + // 上传OSS + meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio'); + + originalDataList.insert(0, { + 'id': meetingId, + ...data, + }); + // 更新UI数据 + if (isAsc) { + dataList.add({ + 'id': meetingId, + ...data, + }); + } else { + dataList.insert(0, { + 'id': meetingId, + ...data, + }); + } + + // 移动文件 + await file.copy(newPath); + await file.delete(); + + addUp++; + } + } catch (e) { + print("文件处理失败: ${fileEntity.path}, 错误: $e"); + } + } + if (addUp > 0) EasyLoading.showSuccess('成功导入本地 $addUp 条录音'); + } catch (e) { + EasyLoading.showError('导入失败: $e'); + } finally { + EasyLoading.dismiss(); + } + } + + void addTranslatAudio() async { + EasyLoading.show(status: '正在批量导入...'); + for (final filePath in selected) { + try { + await _audioPlayer.setAudioSource(AudioSource.file(filePath)); + final fileStat = await File(filePath).stat(); + final creationTime = fileStat.changed.millisecondsSinceEpoch; // 时间戳 + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + final fileName = filePath.split('/').last; + Map data = { + 'title': fileName, + 'type': 'TRANSLAT', + 'seconds': seconds, + 'filepath': filePath, + 'audiourl': '', + 'tasktype': 0, + 'creationtime': creationTime, + }; + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + meetingUploadService.addUpload(meetingId, filePath, 'TranslatAudio'); + + originalDataList.insert(0, { + 'id': meetingId, + ...data, + }); + if (isAsc) { + dataList.add({ + 'id': meetingId, + ...data, + }); + } else { + dataList.insert(0, { + 'id': meetingId, + ...data, + }); + } + } + } catch (e) { + Logger.error('导入文件失败: $filePath, error: $e'); + } + } + EasyLoading.dismiss(); + } + + void addAudio([String? filePath]) async { + EasyLoading.show(status: '正在导入...'); + String fileName = ""; + bool isParamProvided = filePath != null; // 更清晰的变量名 + if (filePath != null) { + // 正确截取文件名(跨平台安全方式) + fileName = path.basename(filePath); + } else { + FilePickerResult? result = await FilePicker.platform.pickFiles( + withData: true, + type: FileType.audio, + ); + if (result != null) { + PlatformFile file = result.files.first; + filePath = await _saveFile(file); + fileName = file.name; + } else { + EasyLoading.dismiss(); + return; + } + } + + final meetingId = await insertMeetingAudio(fileName, filePath); + + if (isParamProvided) { + Get.toNamed(Routes.meetingDetails, arguments: { + 'id': meetingId, + }); + } + EasyLoading.dismiss(); + } + + Future insertMeetingAudio(String fileName, String filePath) async { + await _audioPlayer.setAudioSource(AudioSource.file(filePath)); + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + int creationTime = DateTime.now().millisecondsSinceEpoch; + Map data = { + 'title': fileName, + 'type': 'IMPORT', + 'seconds': seconds, + 'filepath': filePath, + 'audiourl': '', + 'tasktype': 0, + 'creationtime': creationTime, + }; + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + meetingUploadService.addUpload(meetingId, filePath, 'ExternalAudio'); + originalDataList.insert(0, { + 'id': meetingId, + ...data, + }); + if (isAsc) { + dataList.add({ + 'id': meetingId, + ...data, + }); + } else { + dataList.insert(0, { + 'id': meetingId, + ...data, + }); + } + } + return meetingId; + } + + Future _saveFile(PlatformFile file) async { + final directory = await getApplicationDocumentsDirectory(); + final directoryDir = Directory('${directory.path}/ExternalAudio'); + if (!await directoryDir.exists()) { + await directoryDir.create(recursive: true); + } + final String filePath = '${directory.path}/ExternalAudio/${file.name}'; + final File localFile = File(filePath); + await localFile.writeAsBytes(file.bytes!); + return filePath; + } + + // 修改标题 + void editMeetingTitle(int id) async { + if (titleController.text.isNotEmpty) { + String title = titleController.text + titleCSuffix; + SqfliteApi.editMeetingTitle(id, {'title': title}); + int originalIndex = + originalDataList.indexWhere((item) => item['id'] == id); + int index = dataList.indexWhere((item) => item['id'] == id); + if (originalIndex != -1) { + final dataMap = + Map.from(originalDataList[originalIndex]); + dataMap['title'] = title; + originalDataList[originalIndex] = dataMap; + } + if (index != -1) { + final dataMap = Map.from(dataList[index]); + dataMap['title'] = title; + dataList[index] = dataMap; + } + } + Get.back(); + } + + // 删除数据 + void deleteMeeting(int id, String filepath) async { + originalDataList.removeWhere((item) => item['id'] == id); + dataList.removeWhere((item) => item['id'] == id); + // 删除数据库中的记录 + SqfliteApi.deleteMeeting(id); + _deleteFile(filepath); + meetingUploadService.removeUpload(id, isDeleteTask: true); + meetingTaskService.removeTask(id, isDeleteTask: true); + } + + // 删除文件 + Future _deleteFile(String filepath) async { + File file = File(filepath); + bool fileExists = await file.exists(); + if (fileExists) { + await file.delete(); + } + } +} diff --git a/lib/modules/meeting/controllers/meeting_mine_controller.dart b/lib/modules/meeting/controllers/meeting_mine_controller.dart new file mode 100644 index 000000000..72f06ab37 --- /dev/null +++ b/lib/modules/meeting/controllers/meeting_mine_controller.dart @@ -0,0 +1,121 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../data/services/db/sqflite_api.dart'; +import '../../profile/controllers/profile_controller.dart'; +import 'meeting_controller.dart'; +import 'meeting_home_controller.dart'; + +class MeetingMineController extends GetxController { + final _meetingController = Get.find(); + final _homeController = Get.find(); + final profileCtrl = Get.find(); + + final ScrollController scrollController = ScrollController(); + + late final Worker _pageIndexListener; + late final Worker _originalDataListener; + + bool isStatistics = true; + Map statistics = {}; + + RxInt days = 0.obs; + RxInt numberFiles = 0.obs; + RxDouble hourage = 0.0.obs; + RxInt continuousDays = 0.obs; + RxInt maxNumberFiles = 0.obs; + RxDouble maxHourage = 0.0.obs; + + int averageNumberFiles = 0; + + @override + void onInit() { + super.onInit(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + final now = DateTime.now(); + if (now.month > 6) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + } + }); + _getStatistics(); + _pageIndexListener = ever(_meetingController.pageIndex, (value) { + if (isStatistics && value == 2) { + _getStatistics(); + } + }); + _originalDataListener = ever(_homeController.originalDataList, (value) { + isStatistics = true; + }); + } + + @override + void onClose() { + _pageIndexListener.dispose(); + _originalDataListener.dispose(); + scrollController.dispose(); + super.onClose(); + } + + void _getStatistics() async { + if (!isStatistics) return; + List dataList = await SqfliteApi.getMeetingList(); + numberFiles.value = dataList.length; + statistics = {}; + + for (var element in dataList) { + DateTime creationtime = + DateTime.fromMillisecondsSinceEpoch(element['creationtime']); + int dayOfYear = DateUtil.getDayOfYear(creationtime); + + if (!statistics.containsKey(dayOfYear)) { + statistics[dayOfYear] = { + 'creationtime': creationtime, + 'count': 0, + 'seconds': 0, + }; + } + statistics[dayOfYear]['count']++; + statistics[dayOfYear]['seconds'] += element['seconds']; + } + + int currentD = 1; + int maxD = 1; + int previous = 0; + int h = 0; + int maxH = 0; + int maxN = 0; + int minN = 0; + statistics.forEach((key, value) { + h += value['seconds'] as int; + if (key + 1 == previous) { + currentD++; + if (currentD > maxD) { + maxD = currentD; + } + } else { + currentD = 1; + } + previous = key; + if (value['count'] > maxN) { + maxN = value['count']; + } + if (value['count'] < minN || minN == 0) { + minN = value['count']; + } + if (value['seconds'] > maxH) { + maxH = value['seconds']; + } + }); + + days.value = statistics.length; + hourage.value = double.parse((h / 60 / 60).toStringAsFixed(2)); + continuousDays.value = maxD; + maxNumberFiles.value = maxN; + maxHourage.value = double.parse((maxH / 60 / 60).toStringAsFixed(2)); + averageNumberFiles = (maxN + minN) ~/ 2; + isStatistics = false; + } +} diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 3ebec2940..19ada35c7 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -103,9 +103,9 @@ class MeetingRecordController extends GetxController try { final bool granted = await PermissionUtil.instance.requestPermission( permissionType: Permission.microphone, - permissionName: "录音", - explanationText: "我们需要访问麦克风权限,以便您录制音频内容。", - permanentDenialText: "检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。", + permissionName: 'microphonePermission'.tr, + explanationText: 'microphonePermissionExplanation'.tr, + permanentDenialText: 'microphonePermissionPermanentDenial'.tr, ); if (granted) { @@ -138,7 +138,7 @@ class MeetingRecordController extends GetxController switch (audioType.value) { case 0: fileName.value = "现场录音"; - _audioSourceType = false; + _audioSourceType = true; break; case 1: fileName.value = "音、视频录音"; @@ -271,7 +271,7 @@ class MeetingRecordController extends GetxController void _startHardwareServices() { switch (audioType.value) { case 0: - // _bleManager.openEncoder(); + _bleManager.openEncoder(); break; case 1: _bleManager.openDecoder(); diff --git a/lib/modules/meeting/controllers/mind_map_controller.dart b/lib/modules/meeting/controllers/mind_map_controller.dart index d30244f29..b35c53e3e 100644 --- a/lib/modules/meeting/controllers/mind_map_controller.dart +++ b/lib/modules/meeting/controllers/mind_map_controller.dart @@ -18,6 +18,8 @@ class MindMapController extends GetxController { final double _width = 1.sw - 25.w; bool isWebViewReady = false; + late final Worker _summaryNumberListener; + @override void onInit() { super.onInit(); @@ -108,6 +110,12 @@ class MindMapController extends GetxController { const toolbarElement = document.getElementById('toolbar'); toolbarElement.append(el); + window.updateMarkmap = async function(value) { + const { root } = transformer.transform(value); + await mm.setData(root); + await mm.fit(); + } + window.captureImage = async function() { const markmapElement = document.getElementById('markmap'); markmapElement.style.height = `${_width}px`; @@ -144,6 +152,17 @@ class MindMapController extends GetxController { ) ..setBackgroundColor(Colors.transparent) ..loadHtmlString(html); + _summaryNumberListener = + ever(_meetingDetailsController.summaryNumberModifications, (value) { + webViewController.runJavaScript( + 'updateMarkmap(`${_meetingDetailsController.meetingDetails.value.summary}`)'); + }); + } + + @override + void onClose() { + _summaryNumberListener.dispose(); + super.onClose(); } void _messageReceived(message) async { @@ -151,7 +170,7 @@ class MindMapController extends GetxController { final String base64Str = dataUrl.split(',').last; final Uint8List bytes = base64.decode(base64Str); final String fileName = - '${_meetingDetailsController.meetingData.value.title} mindmap.png'; + '${_meetingDetailsController.meetingData.value.title}_mindmap.png'; final tempDir = await getTemporaryDirectory(); final File file = File('${tempDir.path}/$fileName'); await file.writeAsBytes(bytes); diff --git a/lib/modules/meeting/model/meeting_model.dart b/lib/modules/meeting/model/meeting_model.dart index 3e2fd0f40..186237cb5 100644 --- a/lib/modules/meeting/model/meeting_model.dart +++ b/lib/modules/meeting/model/meeting_model.dart @@ -7,7 +7,6 @@ class MeetingModel { late int seconds; late String filepath; late String audiourl; - late String taskid; late int tasktype; late int creationtime; @@ -18,7 +17,6 @@ class MeetingModel { ..seconds = map["seconds"] ..filepath = map["filepath"] ..audiourl = map["audiourl"] - ..taskid = map["taskid"] ..tasktype = map["tasktype"] ..creationtime = map["creationtime"]; @@ -30,7 +28,6 @@ class MeetingModel { data['seconds'] = seconds; data['filepath'] = filepath; data['audiourl'] = audiourl; - data['taskid'] = taskid; data['tasktype'] = tasktype; data['creationtime'] = creationtime; return data; diff --git a/lib/modules/meeting/utils/meeting_ui_utils.dart b/lib/modules/meeting/utils/meeting_ui_utils.dart new file mode 100644 index 000000000..26ccb05a0 --- /dev/null +++ b/lib/modules/meeting/utils/meeting_ui_utils.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +/// 会议模块UI工具类 +/// 提供统一的颜色和样式方法,确保UI风格一致性 +class MeetingUIUtils { + /// 获取背景颜色 + static Color getBackgroundColor(bool isDarkMode) { + return isDarkMode ? Colors.black : Colors.grey[50]!; + } + + /// 获取卡片背景颜色 + static Color getCardColor(bool isDarkMode) { + return isDarkMode ? const Color(0xFF1A1A1A) : Colors.white; + } + + /// 获取半透明白色 + static Color getTranslucentWhite(double alpha) { + return Colors.white.withValues(alpha: alpha); + } + + /// 获取半透明黑色 + static Color getTranslucentBlack(double alpha) { + return Colors.black.withValues(alpha: alpha); + } + + /// 获取主文本颜色 + static Color getTextColor(bool isDarkMode) { + return isDarkMode ? Colors.white : Colors.grey[800]!; + } + + /// 获取次要文本颜色 + static Color getSecondaryTextColor(bool isDarkMode) { + return isDarkMode ? Colors.grey[400]! : Colors.grey[600]!; + } + + /// 获取卡片阴影 + static List getCardShadow(bool isDarkMode) { + return [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ]; + } + + /// 获取按钮颜色 + static Color getButtonColor(bool isDarkMode) { + return isDarkMode ? const Color(0xFF0066CC) : Colors.blue; + } + + /// 获取带透明度的颜色 + static Color getColorWithAlpha(Color color, double alpha) { + return color.withValues(alpha: alpha); + } + + /// 获取禁用状态的颜色 + static Color getDisabledColor(Color color, bool isEnabled) { + return isEnabled ? color : color.withValues(alpha: 0.5); + } +} \ No newline at end of file diff --git a/lib/modules/meeting/views/bottomSheet/ai_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/ai_bottom_sheet.dart index 06977d83f..ecf83a2e7 100644 --- a/lib/modules/meeting/views/bottomSheet/ai_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/ai_bottom_sheet.dart @@ -292,8 +292,8 @@ class _AIBottomSheetState extends State { begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ - const Color(0xFFab47bc).withOpacity(0.1), // 薰衣草紫 - const Color(0xFFec407a).withOpacity(0.2), // 品红 + const Color(0xFFab47bc).withValues(alpha: 0.1), // 薰衣草紫 + const Color(0xFFec407a).withValues(alpha: 0.2), // 品红 ], ), ), @@ -320,7 +320,6 @@ class _AIBottomSheetState extends State { isDense: true, fillColor: Colors.transparent, border: InputBorder.none, - contentPadding: EdgeInsets.zero, ), onSubmitted: _sendMessage, ), diff --git a/lib/modules/meeting/views/bottomSheet/edit_title_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/edit_title_bottom_sheet.dart index dc964d23e..eab7f98a3 100644 --- a/lib/modules/meeting/views/bottomSheet/edit_title_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/edit_title_bottom_sheet.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import '../../controllers/meeting_controller.dart'; +import '../../controllers/meeting_home_controller.dart'; -class EditTitleBottomSheet extends GetView { +class EditTitleBottomSheet extends GetView { final int id; const EditTitleBottomSheet(this.id, {super.key}); @@ -53,7 +53,6 @@ class EditTitleBottomSheet extends GetView { color: Colors.grey[300], ), Container( - height: 48.w, margin: EdgeInsets.symmetric(vertical: 20.w), alignment: Alignment.center, decoration: BoxDecoration( @@ -61,7 +60,7 @@ class EditTitleBottomSheet extends GetView { borderRadius: BorderRadius.circular(12.w), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -79,9 +78,10 @@ class EditTitleBottomSheet extends GetView { color: Colors.black54, fontSize: 16.sp, ), + isDense: true, fillColor: Colors.transparent, border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 10.w), + contentPadding: EdgeInsets.all(10.w), ), ), ), diff --git a/lib/modules/meeting/views/bottomSheet/filtrate_drawer.dart b/lib/modules/meeting/views/bottomSheet/filtrate_drawer.dart index bb843c478..b9b8df66f 100644 --- a/lib/modules/meeting/views/bottomSheet/filtrate_drawer.dart +++ b/lib/modules/meeting/views/bottomSheet/filtrate_drawer.dart @@ -1,74 +1,240 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; -class FiltrateDrawer extends StatelessWidget { +import '../../controllers/meeting_controller.dart'; +import '../../controllers/meeting_home_controller.dart'; + +class FiltrateDrawer extends StatefulWidget { const FiltrateDrawer({super.key}); + @override + State createState() => _FiltrateDrawerState(); +} + +class _FiltrateDrawerState extends State { + final _controller = Get.find(); + final _homeController = Get.find(); + @override Widget build(BuildContext context) { return SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(20.w), - child: TextField( - decoration: InputDecoration( - hintText: '在所有文件中搜索', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.w), - borderSide: BorderSide.none, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 10.w), + margin: EdgeInsets.symmetric(vertical: 15.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12.w), + ), + child: Row( + children: [ + Icon( + Icons.search, + color: Colors.black87, + size: 20.w, + ), + Expanded( + child: TextField( + controller: _homeController.filtrateController, + style: TextStyle( + color: Colors.black87, + fontSize: 14.sp, + ), + decoration: InputDecoration( + hintText: '在所有文件中搜索', + hintStyle: TextStyle( + color: Colors.black54, + fontSize: 14.sp, + ), + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 10.w), + ), + onSubmitted: (String value) { + _controller.scaffoldKey.currentState?.closeDrawer(); + _homeController.dataList.value = + _homeController.originalDataList.where((i) { + final titleLower = i['title'].trim().toLowerCase(); + final queryLower = value.trim().toLowerCase(); + bool isType = true; + if (_homeController.filtrateType.value.isNotEmpty) { + isType = + _homeController.filtrateType.value == i['type']; + } + return isType && titleLower.contains(queryLower); + }).toList(); + if (_homeController.isAsc) { + _homeController.dataList.value = + _homeController.dataList.reversed.toList(); + } + }, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () { + _filtrateDataList(''); + }, + behavior: HitTestBehavior.translucent, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), + decoration: _homeController.filtrateType.value == '' + ? BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ) + : null, + child: Row( + children: [ + Icon( + Icons.folder, + color: Colors.red, + size: 20.w, + ), + 6.horizontalSpace, + Text( + '全部文件(${_homeController.originalDataList.length})', + style: TextStyle( + color: Colors.black87, + fontSize: 14.sp, + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 15.w), + child: Divider( + height: 1, + color: Colors.grey[300], + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: Text( + '来自', + style: TextStyle( + color: Colors.grey, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ...['IMPORT', 'TRANSLAT', 'LOCAL'].map( + (name) => GestureDetector( + onTap: () { + _filtrateDataList(name); + }, + behavior: HitTestBehavior.translucent, + child: Container( + width: double.infinity, + padding: + EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), + margin: EdgeInsets.symmetric(vertical: 2.w), + decoration: _homeController.filtrateType.value == name + ? BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ) + : null, + child: Text( + name, + style: TextStyle( + color: Colors.black87, + fontSize: 14.sp, + ), + ), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Text( + '排序方式', + style: TextStyle( + color: Colors.grey, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + GestureDetector( + onTap: () { + _controller.scaffoldKey.currentState?.closeDrawer(); + _homeController.isAsc = !_homeController.isAsc; + _homeController.dataList.value = + _homeController.dataList.reversed.toList(); + }, + behavior: HitTestBehavior.translucent, + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + margin: EdgeInsets.symmetric(vertical: 2.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '创建时间', + style: TextStyle( + color: Colors.black87, + fontSize: 14.sp, + ), + ), + Icon( + _homeController.isAsc + ? Icons.keyboard_double_arrow_up + : Icons.keyboard_double_arrow_down, + color: Colors.grey, + size: 20.w, + ), + ], ), - filled: true, - fillColor: Colors.grey[100], - contentPadding: EdgeInsets.zero, ), ), - ), - ListTile( - leading: Icon(Icons.folder, color: Colors.red), - title: - Text('全部文件(4)', style: TextStyle(fontWeight: FontWeight.bold)), - onTap: () {}, - ), - ListTile( - leading: Icon(Icons.category, color: Colors.blue), - title: Text('未分类(4)'), - onTap: () {}, - ), - ListTile( - leading: Icon(Icons.delete, color: Colors.teal), - title: Text('回收站'), - onTap: () {}, - ), - Divider(), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w), - child: Text('文件夹', style: TextStyle(color: Colors.grey)), - ), - ...['NOTE', 'CALL', 'NotePin', 'Import', 'Desktop'] - .map((name) => ListTile( - title: Text(name), - onTap: () {}, - )), - Divider(), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w), - child: Text('排序方式', style: TextStyle(color: Colors.grey)), - ), - ListTile( - title: Text('创建时间'), - trailing: Icon(Icons.arrow_upward), - onTap: () {}, - ), - ListTile( - title: Text('修改时间'), - trailing: Icon(Icons.arrow_upward), - onTap: () {}, - ), - ], + ], + ), ), ); } + + void _filtrateDataList(String type) { + _controller.scaffoldKey.currentState?.closeDrawer(); + _homeController.filtrateType.value = type; + _homeController.filtrateController.text = ''; + if (type.isNotEmpty) { + _homeController.dataList.value = _homeController.originalDataList + .where((i) => i['type'] == type) + .toList(); + } else { + _homeController.dataList.value = + List.from(_homeController.originalDataList); + } + if (_homeController.isAsc) { + _homeController.dataList.value = + _homeController.dataList.reversed.toList(); + } + } } diff --git a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart index 436cfe930..b5b5ce256 100644 --- a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart @@ -6,7 +6,7 @@ import 'package:get_storage/get_storage.dart'; import '../../controllers/meeting_details_controller.dart'; import 'record_language_bottom_sheet.dart'; -import '../template_view.dart'; +import '../template/template_view.dart'; class GenerateBottomSheet extends StatefulWidget { final bool isSummarizeAgain; @@ -283,11 +283,10 @@ class _GenerateBottomSheetState extends State { _storage.write("meeting_template_list", _templateList); Get.back(); if (widget.isSummarizeAgain) { - _controller.meetingDetails.value.tasktype = 2; - _controller.meetingDetails.value.summary = ''; - _controller.meetingDetails.refresh(); + _controller.refreshSummary(); + } else { + _controller.summaryCreate(); } - _controller.summaryCreate(); }, child: Container( height: 48.w, diff --git a/lib/modules/meeting/views/bottomSheet/import_audio_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/import_audio_bottom_sheet.dart index 1b94778f2..f62cf7944 100644 --- a/lib/modules/meeting/views/bottomSheet/import_audio_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/import_audio_bottom_sheet.dart @@ -3,13 +3,14 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../../../routes/app_routes.dart'; -import '../../controllers/meeting_controller.dart'; +import '../../controllers/meeting_home_controller.dart'; -class ImportAudioBottomSheet extends GetView { +class ImportAudioBottomSheet extends GetView { const ImportAudioBottomSheet({super.key}); @override Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: Column( @@ -24,7 +25,7 @@ class ImportAudioBottomSheet extends GetView { '添加音频', style: TextStyle( fontSize: 16.sp, - color: Colors.black87, + color: isDarkMode ? Colors.white : Colors.black87, fontWeight: FontWeight.bold, ), ), @@ -34,7 +35,7 @@ class ImportAudioBottomSheet extends GetView { width: 26.w, height: 26.w, decoration: BoxDecoration( - color: Colors.white, + color: isDarkMode ? Colors.grey[800] : Colors.white, borderRadius: BorderRadius.circular(26.r), ), child: Icon( @@ -49,7 +50,7 @@ class ImportAudioBottomSheet extends GetView { ), Divider( height: 1, - color: Colors.grey[300], + color: isDarkMode ? Colors.grey[700] : Colors.grey[300], ), SizedBox(height: 16.w), _importAudioItem(Icons.mic, '现场录音', onTap: () async { @@ -95,7 +96,7 @@ class ImportAudioBottomSheet extends GetView { '支持导入单文件时长不超过5小时的音频', style: TextStyle( fontSize: 14.sp, - color: Colors.grey[600], + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], ), ), ), @@ -110,14 +111,19 @@ class ImportAudioBottomSheet extends GetView { String title, { VoidCallback? onTap, }) { - return GestureDetector( + return Builder( + builder: (context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return GestureDetector( onTap: onTap, child: Container( height: 48.w, padding: EdgeInsets.symmetric(horizontal: 12.w), margin: EdgeInsets.symmetric(vertical: 4.w), decoration: BoxDecoration( - color: Colors.grey[50]!.withOpacity(onTap != null ? 1 : 0.5), + color: isDarkMode + ? Colors.grey[800]!.withValues(alpha: onTap != null ? 1 : 0.5) + : Colors.grey[50]!.withValues(alpha: onTap != null ? 1 : 0.5), borderRadius: BorderRadius.circular(12.w), ), child: Row( @@ -132,18 +138,22 @@ class ImportAudioBottomSheet extends GetView { title, style: TextStyle( fontSize: 14.sp, - color: Colors.black87.withOpacity(onTap != null ? 1 : 0.5), + color: isDarkMode + ? Colors.white.withValues(alpha: onTap != null ? 1 : 0.5) + : Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5), ), ), ), Icon( Icons.arrow_forward_ios, size: 16.w, - color: Colors.grey.withOpacity(onTap != null ? 1 : 0.5), + color: Colors.grey.withValues(alpha: onTap != null ? 1 : 0.5), ), ], ), ), ); + }, + ); } } diff --git a/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart index e2fd34257..a02fff599 100644 --- a/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import '../../controllers/meeting_controller.dart'; +import '../../controllers/meeting_home_controller.dart'; import '../../controllers/meeting_details_controller.dart'; import 'generate_bottom_sheet.dart'; @@ -14,7 +14,7 @@ class MoreBottomSheet extends StatefulWidget { } class _MoreBottomSheetState extends State { - final _controller = Get.find(); + final _controller = Get.find(); final _detailsController = Get.find(); @override @@ -64,7 +64,7 @@ class _MoreBottomSheetState extends State { return InkWell( onTap: onTap, child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w), margin: EdgeInsets.only(bottom: 10.w), decoration: BoxDecoration( color: onTap != null ? Colors.white : Colors.grey[200], @@ -72,9 +72,12 @@ class _MoreBottomSheetState extends State { ), child: Row( children: [ - Icon( - icon, - color: Colors.black87, + Opacity( + opacity: onTap != null ? 1.0 : 0.5, + child: Icon( + icon, + color: Colors.black87, + ), ), 10.horizontalSpace, Expanded( @@ -82,7 +85,7 @@ class _MoreBottomSheetState extends State { title, style: TextStyle( fontSize: 14.sp, - color: Colors.black87, + color: Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5), ), ), ), diff --git a/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart new file mode 100644 index 000000000..d4db457b4 --- /dev/null +++ b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get_navigation/src/extension_navigation.dart'; +import 'package:get/state_manager.dart'; + +import '../../controllers/meeting_home_controller.dart'; +import '../../utils/meeting_ui_utils.dart'; + +class NavigationBarBottomSheet extends GetView { + const NavigationBarBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildItem( + context, // 传递 context 参数 + '开始录音', + Icons.mic_rounded, + Colors.red, + ), + SizedBox(height: 15.w), + _buildItem( + context, // 传递 context 参数 + '导入音频', + Icons.file_upload_outlined, + Colors.deepPurple, + onTap: () { + Get.back(); + controller.addAudio(); + }, + ), + SizedBox(height: 24.w), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 45.w, + height: 45.w, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(50.w), + ), + child: const Icon(Icons.close, color: Colors.white), + ), + ), + SizedBox(height: 24.w), + ], + ), + ); + } + + Widget _buildItem( + BuildContext context, // 添加 context 参数 + String title, + IconData icon, + Color color, { + Function()? onTap, + }) { + final isDarkMode = + Theme.of(context).brightness == Brightness.dark; // 在方法内部获取 isDarkMode + return GestureDetector( + onTap: onTap, + child: Container( + width: 220.w, + height: 60.w, + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[800]!.withValues(alpha: onTap != null ? 1 : 0.5) + : Colors.white.withValues(alpha: onTap != null ? 1 : 0.5), + borderRadius: BorderRadius.circular(60.w), + ), + child: Row( + children: [ + Container( + width: 46.w, + height: 46.w, + margin: EdgeInsets.only(right: 5.w), + decoration: BoxDecoration( + color: color.withValues(alpha: onTap != null ? 0.3 : 0.2), + borderRadius: BorderRadius.circular(60.w), + ), + child: Icon( + icon, + color: color.withValues(alpha: onTap != null ? 1 : 0.5), + ), + ), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 15.sp, + color: isDarkMode + ? Colors.white.withValues(alpha: onTap != null ? 1 : 0.5) + : Colors.black87 + .withValues(alpha: onTap != null ? 1 : 0.5), + fontWeight: FontWeight.bold, + ), + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: Colors.grey.withOpacity(onTap != null ? 1 : 0.5), + ), + 5.horizontalSpace, + ], + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/bottomSheet/second_level_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/second_level_bottom_sheet.dart new file mode 100644 index 000000000..023056133 --- /dev/null +++ b/lib/modules/meeting/views/bottomSheet/second_level_bottom_sheet.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class SecondLevelBottomSheet extends StatefulWidget { + final String title; + final String subtitle; + final Function(int index) onTap; + final List children; + + const SecondLevelBottomSheet( + this.title, this.subtitle, this.onTap, this.children, + {super.key}); + + @override + State createState() => _SecondLevelBottomSheetState(); +} + +class _SecondLevelBottomSheetState extends State { + int _selectedIndex = 0; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + 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], + ), + ), + ) + ], + ), + ), + Divider( + height: 1, + color: Colors.grey[300], + ), + Padding( + padding: EdgeInsetsGeometry.only( + top: 20.w, + bottom: 10.w, + left: 12.w, + ), + child: Text( + widget.subtitle, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + ), + ), + Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + children: List.generate(widget.children.length, (int index) { + Map item = widget.children[index]; + return InkWell( + onTap: () { + setState(() { + _selectedIndex = index; + }); + }, + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w), + decoration: BoxDecoration( + border: item['isBorder'] + ? Border( + bottom: BorderSide( + width: 0.5, + color: Colors.grey[300]!, + ), + ) + : null, + ), + child: Row( + children: [ + Icon( + item['icon'], + color: Colors.black87, + ), + 10.horizontalSpace, + Expanded( + child: Text( + item['title'], + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + if (_selectedIndex == index) + const Icon( + Icons.check, + color: Colors.black87, + ), + ], + ), + ), + ); + }), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20.w), + child: GestureDetector( + onTap: () { + Get.back(); + widget.onTap(_selectedIndex); + }, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '导出', + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart index 0936a5636..896e5bcf8 100644 --- a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart @@ -7,6 +7,7 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:htmltopdfwidgets/htmltopdfwidgets.dart' as pdf; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -14,6 +15,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import '../../../../core/utils/synchrodata.dart'; import '../../controllers/meeting_details_controller.dart'; import '../../controllers/mind_map_controller.dart'; +import 'second_level_bottom_sheet.dart'; class ShareBottomSheet extends StatefulWidget { const ShareBottomSheet({super.key}); @@ -141,7 +143,7 @@ class _ShareBottomSheetState extends State { Get.back(); final tempDir = await getTemporaryDirectory(); final file = File( - '${tempDir.path}/${_controller.meetingData.value.title} speaker.txt'); + '${tempDir.path}/${_controller.meetingData.value.title}_speaker.txt'); String allText = _getAllText(); await file.writeAsString(allText); _share(file.path, '分享会议转写'); @@ -151,12 +153,46 @@ class _ShareBottomSheetState extends State { onTap: _controller.meetingDetails.value.tasktype >= 3 ? () async { Get.back(); - final tempDir = await getTemporaryDirectory(); - final file = File( - '${tempDir.path}/${_controller.meetingData.value.title} summary.txt'); - await file.writeAsString( - _controller.meetingDetails.value.summary); - _share(file.path, '分享会议总结'); + Get.bottomSheet( + SecondLevelBottomSheet( + '导出总结', + '导出为', + (int index) async { + if (index == 0) { + // 导出TXT + final tempDir = + await getTemporaryDirectory(); + final file = File( + '${tempDir.path}/${_controller.meetingData.value.title}_summary.txt'); + await file.writeAsString(_controller + .meetingDetails.value.summary); + _share(file.path, '分享会议总结'); + } else { + // 导出PDF + _exportMarkdownPdf(); + } + }, + const [ + { + 'icon': Icons.text_snippet, + 'title': 'TXT', + 'isBorder': true, + }, + { + 'icon': Icons.picture_as_pdf, + 'title': 'PDF', + 'isBorder': false, + }, + ], + ), + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); } : null), _shareItem( @@ -218,12 +254,13 @@ class _ShareBottomSheetState extends State { return InkWell( onTap: onTap, child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w), decoration: BoxDecoration( color: onTap != null ? null : Colors.grey[200], border: isBorder ? Border( bottom: BorderSide( + width: 0.5, color: Colors.grey[300]!, ), ) @@ -231,9 +268,12 @@ class _ShareBottomSheetState extends State { ), child: Row( children: [ - Icon( - icon, - color: Colors.black87, + Opacity( + opacity: onTap != null ? 1.0 : 0.5, + child: Icon( + icon, + color: Colors.black87, + ), ), 10.horizontalSpace, Expanded( @@ -241,7 +281,7 @@ class _ShareBottomSheetState extends State { title, style: TextStyle( fontSize: 14.sp, - color: Colors.black87, + color: Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5), ), ), ), @@ -251,7 +291,7 @@ class _ShareBottomSheetState extends State { ); } - void _share(String filepath, String title) async { + Future _share(String filepath, String title) async { final result = await SharePlus.instance.share(ShareParams( text: title, files: [XFile(filepath)], @@ -268,7 +308,7 @@ class _ShareBottomSheetState extends State { (value) => Get.snackbar( 'success'.tr, // 成功 '复制成功', - backgroundColor: Colors.blue.withOpacity(0.8), + backgroundColor: Colors.blue.withValues(alpha: 0.8), colorText: Colors.white, ), ); @@ -297,4 +337,26 @@ class _ShareBottomSheetState extends State { await file.writeAsBytes(byteData.buffer.asUint8List()); return XFile(file.path); } + + Future _exportMarkdownPdf() async { + EasyLoading.show(status: '正在导出PDF...'); + final tempDir = await getTemporaryDirectory(); + final file = File( + '${tempDir.path}/${_controller.meetingData.value.title}_summary.pdf'); + final List markdownWidgets = + await pdf.HTMLToPdf().convertMarkdown( + _controller.meetingDetails.value.summary, + fontFallback: [ + pdf.Font.ttf( + await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf')), + ], + ); + final markdownPdf = pdf.Document(); + markdownPdf.addPage(pdf.MultiPage( + build: (context) => markdownWidgets, + )); + await file.writeAsBytes(await markdownPdf.save()); + await _share(file.path, '分享会议总结'); + EasyLoading.dismiss(); + } } diff --git a/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart index 0134569d5..07ba347b0 100644 --- a/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart @@ -576,10 +576,12 @@ class _SpeakerAllBottomSheetState extends State { 'personnel': personnel, }); _controller.meetingDetails.value.summary = summaryText; + _controller.summaryNumberModifications.value++; } else { SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); } _controller.meetingDetails.value.personnel = personnel; + _controller.personnelNumberModifications.value++; _controller.meetingDetails.refresh(); _storage.write("speaker_list", _historySpeakerList); Get.back(); diff --git a/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart index 9f9e02f68..3c7e69dad 100644 --- a/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart @@ -382,6 +382,7 @@ class _SpeakerBottomSheetState extends State { 'personnel': personnel, }); _controller.meetingDetails.value.summary = summaryText; + _controller.summaryNumberModifications.value++; } else { SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); } diff --git a/lib/modules/meeting/views/bottomSheet/statistics_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/statistics_bottom_sheet.dart new file mode 100644 index 000000000..eed48af63 --- /dev/null +++ b/lib/modules/meeting/views/bottomSheet/statistics_bottom_sheet.dart @@ -0,0 +1,383 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../../../profile/controllers/profile_controller.dart'; +import '../../controllers/meeting_mine_controller.dart'; + +class StatisticsBottomSheet extends StatefulWidget { + const StatisticsBottomSheet({super.key}); + + @override + State createState() => _StatisticsBottomSheetState(); +} + +class _StatisticsBottomSheetState extends State { + final _mineController = Get.find(); + final _profileCtrl = Get.find(); + + final GlobalKey _posterKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + GestureDetector( + onTap: () => Get.back(), + behavior: HitTestBehavior.translucent, + child: SizedBox( + width: 1.sw, + height: 1.sh, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RepaintBoundary( + key: _posterKey, + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 15.w, vertical: 20.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.w), + image: const DecorationImage( + alignment: Alignment.topCenter, + image: AssetImage('assets/images/mine-background.png'), + fit: BoxFit.fitWidth, + ), + ), + child: Column( + children: [ + Container( + width: 50.w, + height: 50.w, + margin: EdgeInsets.only(bottom: 10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.w), + ), + child: CircleAvatar( + radius: 25.w, + backgroundImage: _profileCtrl + .localAvatar.value.isNotEmpty + ? FileImage(File(_profileCtrl.localAvatar.value)) + : (_profileCtrl.userInfo.value.avatar.isNotEmpty + ? NetworkImage( + _profileCtrl.userInfo.value.avatar) + : null) as ImageProvider?, + backgroundColor: Colors.grey[200], + child: (_profileCtrl.localAvatar.value.isEmpty && + _profileCtrl.userInfo.value.avatar.isEmpty) + ? Icon( + Icons.person, + size: 25.r, + color: Colors.grey[600], + ) + : null, + ), + ), + Text( + _profileCtrl.userInfo.value.name, + style: TextStyle( + color: Colors.black87, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20.w), + child: Wrap( + spacing: 5.w, + runSpacing: 5.w, + children: [ + _item( + '天', + '${_mineController.days.value}', + Colors.amber, + ), + _item( + '文件', + '${_mineController.numberFiles.value}', + Colors.blueGrey, + ), + _item( + '小时', + '${_mineController.hourage.value}', + Colors.cyan, + ), + _item( + '连续录音天数', + '${_mineController.continuousDays.value}', + Colors.deepOrangeAccent, + ), + _item( + '单日最多录音', + '${_mineController.maxNumberFiles.value}', + Colors.teal, + ), + _item( + '单日最长录音', + '${_mineController.maxHourage.value}', + Colors.pink, + ), + ], + ), + ), + Row( + children: [ + Text( + '过去12个月的热力图', + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + const Spacer(), + Text( + '较冷', + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + 5.horizontalSpace, + Container( + width: 10.w, + height: 10.w, + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(2.w), + ), + ), + Container( + width: 10.w, + height: 10.w, + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + color: Colors.red[100], + borderRadius: BorderRadius.circular(2.w), + ), + ), + Container( + width: 10.w, + height: 10.w, + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(2.w), + ), + ), + 5.horizontalSpace, + Text( + '较热', + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + ], + ), + Container( + height: 50.w, + alignment: Alignment.center, + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 52, + mainAxisSpacing: 1.w, + crossAxisSpacing: 1.w, + ), + itemCount: 364, + itemBuilder: (BuildContext context, int index) { + final col = index % 52; + final row = index ~/ 52; + final vIndex = row + col * 7; + + int type = 0; + if (_mineController.statistics + .containsKey(vIndex + 1)) { + if (_mineController.statistics[vIndex + 1] + ['count'] <= + _mineController.averageNumberFiles) { + type = 1; + } else { + type = 2; + } + } + return Container( + color: type == 0 + ? Colors.grey[200] + : type == 1 + ? Colors.red[100] + : Colors.red, + ); + }, + ), + ), + DefaultTextStyle( + style: TextStyle( + color: Colors.grey, + fontSize: 10.sp, + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text('Jan'), + Text('Feb'), + Text('Mar'), + Text('Apr'), + Text('May'), + Text('Jun'), + Text('Jul'), + Text('Aug'), + Text('Sep'), + Text('Oct'), + Text('Nov'), + Text('Dec'), + ], + ), + ), + ], + ), + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + _button(Icons.share, '分享图片', Colors.deepOrange, () async { + EasyLoading.show(); + Uint8List pngBytes = await _generate(); + final tempDir = await getTemporaryDirectory(); + final String fileName = + 'share_${DateTime.now().millisecondsSinceEpoch}.png'; + final file = File('${tempDir.path}/$fileName'); + await file.writeAsBytes(pngBytes); + final result = await SharePlus.instance.share(ShareParams( + text: '分享 DEAPSOUND', + files: [XFile(file.path)], + )); + if (result.status == ShareResultStatus.success) { + Get.back(); + EasyLoading.showToast('分享完成!'); + } + EasyLoading.dismiss(); + }), + 10.horizontalSpace, + _button(Icons.download, '保存图片', Colors.cyan, () async { + EasyLoading.show(); + Uint8List pngBytes = await _generate(); + final result = await ImageGallerySaverPlus.saveImage( + pngBytes, + quality: 100, + ); + if (result['isSuccess']) { + Get.back(); + EasyLoading.showToast('保存成功'); + } else { + EasyLoading.showToast('保存失败'); + } + EasyLoading.dismiss(); + }), + ], + ), + ], + ), + ), + ], + ); + } + + Widget _item(String unit, String amount, Color color) { + return Container( + width: 105.w, + height: 70.w, + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8.w), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + unit, + style: TextStyle( + color: Colors.grey, + fontSize: 12.sp, + ), + ), + Text( + amount, + style: TextStyle( + color: Colors.black87, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + Widget _button(IconData icon, String title, Color color, Function() onTap) { + return Expanded( + child: GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.w), + ), + child: Container( + height: 60.w, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 25.w, + ), + 5.horizontalSpace, + Text( + title, + style: TextStyle( + color: Colors.black87, + fontSize: 16.sp, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Future _generate() async { + RenderRepaintBoundary boundary = + _posterKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; + ui.Image image = await boundary.toImage(pixelRatio: 6.0); + ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); + Uint8List pngBytes = byteData!.buffer.asUint8List(); + return pngBytes; + } +} diff --git a/lib/modules/meeting/views/meeting_connect_view.dart b/lib/modules/meeting/views/meeting_connect_view.dart index 82c3e3d97..e56d0ab1f 100644 --- a/lib/modules/meeting/views/meeting_connect_view.dart +++ b/lib/modules/meeting/views/meeting_connect_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/meeting_connect_controller.dart'; +import '../utils/meeting_ui_utils.dart'; class MeetingConnectView extends GetView { const MeetingConnectView({super.key}); @@ -12,7 +13,7 @@ class MeetingConnectView extends GetView { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode), appBar: _buildAppBar(isDarkMode), body: _buildBody(isDarkMode), ); @@ -105,34 +106,39 @@ class MeetingConnectView extends GetView { } Widget _button(String text, IconData icon, {Function()? onTap}) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 150.w, - height: 42.w, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(42.w), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - color: Colors.white, - size: 20.sp, + return Builder( + builder: (context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return GestureDetector( + onTap: onTap, + child: Container( + width: 150.w, + height: 42.w, + decoration: BoxDecoration( + color: MeetingUIUtils.getButtonColor(isDarkMode), + borderRadius: BorderRadius.circular(42.w), ), - 5.horizontalSpace, - Text( - text, - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: Colors.white, + size: 20.sp, + ), + 5.horizontalSpace, + Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ], ), - ], - ), - ), + ), + ); + }, ); } } diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index dd73a4bba..0d8ae4769 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -9,6 +9,7 @@ import 'package:lottie/lottie.dart'; import '../../../core/theme/app_colors.dart'; import '../controllers/meeting_details_controller.dart'; +import '../utils/meeting_ui_utils.dart'; import 'tabs/mind_map_tab.dart'; import 'bottomSheet/more_bottom_sheet.dart'; import 'tabs/overview_tab.dart'; @@ -25,215 +26,226 @@ class MeetingDetailsView extends GetView { return PopScope( onPopInvokedWithResult: controller.popInvokedWithResult, - child: Obx( - () => Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(context, isDarkMode), - body: _buildBody(isDarkMode), - ), + child: Scaffold( + backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode), + appBar: _buildAppBar(context, isDarkMode), + body: _buildBody(isDarkMode), ), ); } // 构建应用栏 PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) { - return AppBar( - backgroundColor: - controller.isTop.value ? Colors.grey[50] : Colors.grey[200], - surfaceTintColor: Colors.transparent, - elevation: 0.5, - leading: controller.isSpeakerText.value - ? GestureDetector( - onTap: () { - controller.isSpeakerText.value = false; - controller.editSpeakerTextId.value = 0; - controller.editSpeakerTextList = []; - }, - child: Center( - child: Text( - 'cancel'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 16.sp, + return PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Obx( + () => AppBar( + backgroundColor: + isDarkMode + ? (controller.isTop.value ? Colors.black : Colors.grey[900]) + : (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]), + surfaceTintColor: Colors.transparent, + elevation: 0.5, + leading: controller.isSpeakerText.value + ? GestureDetector( + onTap: () { + controller.isSpeakerText.value = false; + controller.editSpeakerTextId.value = 0; + controller.editSpeakerTextList = []; + }, + child: Center( + child: Text( + 'cancel'.tr, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + ), + ), + ) + : IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, ), + onPressed: () => Get.back(), ), - ), - ) - : IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - title: Opacity( - opacity: controller.isTop.value ? 1.0 : 0.0, - child: Container( - width: 170.w, - padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 3.w), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(30.r), - ), - child: Row( - children: [ - GestureDetector( - onTap: () { - if (controller.meetingData.value.filepath.isEmpty) { - controller.downloadAudio(); - } else { - if (controller.isPlay.value) { - controller.playerController.pausePlayer(); - } else { - controller.playerController.startPlayer(); - } - } - }, - child: Obx( - () => !controller.isLoading.value && - controller.meetingData.value.filepath.isEmpty - ? Obx( - () => Stack( - alignment: Alignment.center, - children: [ - if (controller.isDownload.value) - SizedBox( - width: 26.w, - height: 26.w, - child: CircularProgressIndicator( - value: controller.downloadProgress.value, - valueColor: - const AlwaysStoppedAnimation( - Colors.red), - strokeWidth: 2.w, - ), + title: Obx( + () => Opacity( + opacity: + (1 - (controller.dragOffset.value / 100.w)).clamp(0.0, 1.0), + child: Container( + width: 170.w, + padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 3.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(30.r), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + if (controller.meetingData.value.filepath.isEmpty) { + controller.downloadAudio(); + } else { + if (controller.isPlay.value) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } + } + }, + child: Obx( + () => !controller.isLoading.value && + controller.meetingData.value.filepath.isEmpty + ? Obx( + () => Stack( + alignment: Alignment.center, + children: [ + if (controller.isDownload.value) + SizedBox( + width: 26.w, + height: 26.w, + child: CircularProgressIndicator( + value: + controller.downloadProgress.value, + valueColor: + const AlwaysStoppedAnimation< + Color>(Colors.red), + strokeWidth: 2.w, + ), + ), + Icon( + Icons.download_for_offline, + color: Colors.black, + size: 30.w, + ), + ], ), - Icon( - Icons.download_for_offline, + ) + : Icon( + controller.isPlay.value + ? Icons.pause_circle + : Icons.play_circle, color: Colors.black, size: 30.w, ), - ], + ), + ), + 4.horizontalSpace, + GestureDetector( + onTap: () { + if (controller.isSpeakerText.value) return; + controller.dragOffset.value = 210.w; + controller.isTop.value = false; + }, + behavior: HitTestBehavior.translucent, + child: SizedBox( + width: 130.w, + height: 30.w, + child: FittedBox( + fit: BoxFit.scaleDown, + child: AudioFileWaveforms( + size: Size(350.w, 30.w), + playerController: controller.playerController, + waveformData: List.generate(88, (int index) { + return 0.01 + Random().nextDouble() * 0.09; + }), + waveformType: WaveformType.fitWidth, + enableSeekGesture: false, + playerWaveStyle: PlayerWaveStyle( + spacing: 4.w, + waveThickness: 2.w, + fixedWaveColor: Colors.grey[400]!, + liveWaveColor: Colors.red, + seekLineColor: Colors.red, + scaleFactor: 200, + ), ), - ) - : Icon( - controller.isPlay.value - ? Icons.pause_circle - : Icons.play_circle, - color: Colors.black, - size: 30.w, ), - ), - ), - 4.horizontalSpace, - GestureDetector( - onTap: () { - if (controller.isSpeakerText.value) return; - controller.dragOffset.value = 210.w; - controller.isTop.value = false; - }, - child: SizedBox( - width: 130.w, - child: FittedBox( - fit: BoxFit.scaleDown, - child: AudioFileWaveforms( - size: Size(350.w, 30.w), - playerController: controller.playerController, - waveformData: List.generate(88, (int index) { - return 0.01 + Random().nextDouble() * 0.09; - }), - waveformType: WaveformType.fitWidth, - enableSeekGesture: false, - playerWaveStyle: PlayerWaveStyle( - spacing: 4.w, - waveThickness: 2.w, - fixedWaveColor: Colors.grey[400]!, - liveWaveColor: Colors.red, - seekLineColor: Colors.red, - scaleFactor: 200, ), ), - ), - ), - ), - ], - ), - ), - ), - centerTitle: true, - actions: [ - if (controller.isSpeakerText.value) - GestureDetector( - onTap: controller.editSpeakerText, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 6.w, - ), - child: Text( - 'save'.tr, - style: TextStyle( - color: isDarkMode ? AppColors.primary : Colors.blue, - fontSize: 16.sp, + ], ), ), ), ), - if (!controller.isSpeakerText.value) - IconButton( - icon: Icon( - Icons.ios_share, - color: isDarkMode ? Colors.white : Colors.black, - size: 22.sp, - ), - onPressed: () { - Get.bottomSheet( - const ShareBottomSheet(), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), + centerTitle: true, + actions: [ + if (controller.isSpeakerText.value) + GestureDetector( + onTap: controller.editSpeakerText, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 6.w, + ), + child: Text( + 'save'.tr, + style: TextStyle( + color: isDarkMode ? AppColors.primary : Colors.blue, + fontSize: 16.sp, + ), ), ), - ); - }, - ), - if (!controller.isSpeakerText.value) - IconButton( - icon: Container( - width: 22.w, - height: 22.w, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20.r), - border: Border.all( + ), + if (!controller.isSpeakerText.value) + IconButton( + icon: Icon( + Icons.ios_share, color: isDarkMode ? Colors.white : Colors.black, - width: 2, + size: 22.sp, ), + onPressed: () { + Get.bottomSheet( + const ShareBottomSheet(), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, ), - child: Icon( - Icons.more_horiz, - color: isDarkMode ? Colors.white : Colors.black, - size: 16.sp, - ), - ), - onPressed: () { - Get.bottomSheet( - const MoreBottomSheet(), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), + if (!controller.isSpeakerText.value) + IconButton( + icon: Container( + width: 22.w, + height: 22.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: isDarkMode ? Colors.white : Colors.black, + width: 2, + ), + ), + child: Icon( + Icons.more_horiz, + color: isDarkMode ? Colors.white : Colors.black, + size: 16.sp, ), ), - ); - }, - ), - ], + onPressed: () { + Get.bottomSheet( + const MoreBottomSheet(), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + ), + ], + ), + ), ); } @@ -308,6 +320,7 @@ class MeetingDetailsView extends GetView { child: TabBarView( controller: controller.tabController, physics: const NeverScrollableScrollPhysics(), + clipBehavior: Clip.none, children: [ Obx( () => controller.isLoading.value @@ -350,199 +363,208 @@ class MeetingDetailsView extends GetView { } Widget _meetingWait(String title) { - return Stack( - alignment: Alignment.topCenter, - children: [ - Lottie.asset( - 'assets/lottie/meeting_wait.json', - ), - Positioned( - top: 320.w, - child: Text( - title, - style: TextStyle( - fontSize: 18.sp, - color: Colors.black87, - fontWeight: FontWeight.w500, + return Builder( + builder: (context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return Stack( + alignment: Alignment.topCenter, + children: [ + Lottie.asset( + 'assets/lottie/meeting_wait.json', ), - ), - ), - ], + Positioned( + top: 320.w, + child: Text( + title, + style: TextStyle( + fontSize: 18.sp, + color: isDarkMode ? Colors.white : Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + }, ); } Widget _soundWave(bool isDarkMode) { return Obx( - () => SizedBox( - height: controller.dragOffset.value < 0 - ? 0 - : controller.dragOffset.value > 210.w - ? 210.w - : controller.dragOffset.value, - child: Stack( - children: [ - Positioned( - bottom: 0, - child: Container( - width: 1.sw, - height: 200.w, - margin: EdgeInsets.only(bottom: 10.w), - decoration: BoxDecoration( - color: Colors.grey[200]!, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(20.r), + () => Opacity( + opacity: (controller.dragOffset.value / 210.w).clamp(0.0, 1.0), + child: SizedBox( + height: controller.dragOffset.value < 0 + ? 0 + : controller.dragOffset.value > 210.w + ? 210.w + : controller.dragOffset.value, + child: Stack( + children: [ + Positioned( + bottom: 0, + child: Container( + width: 1.sw, + height: 200.w, + margin: EdgeInsets.only(bottom: 10.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900]! : Colors.grey[200]!, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(20.r), + ), ), - ), - child: Column( - children: [ - Container( - margin: EdgeInsets.only(bottom: 5.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.r), - ), - child: AudioFileWaveforms( - size: Size(350.w, 120.w), - playerController: controller.playerController, - waveformData: List.generate(88, (int index) { - return 0.01 + Random().nextDouble() * 0.09; - }), - waveformType: WaveformType.fitWidth, - playerWaveStyle: PlayerWaveStyle( - spacing: 4.w, - waveThickness: 2.w, - fixedWaveColor: Colors.grey[300]!, - liveWaveColor: Colors.red, - seekLineColor: Colors.red, - scaleFactor: 600, + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(8.r), + ), + child: AudioFileWaveforms( + size: Size(350.w, 120.w), + playerController: controller.playerController, + waveformData: List.generate(88, (int index) { + return 0.01 + Random().nextDouble() * 0.09; + }), + waveformType: WaveformType.fitWidth, + playerWaveStyle: PlayerWaveStyle( + spacing: 4.w, + waveThickness: 2.w, + fixedWaveColor: Colors.grey[300]!, + liveWaveColor: Colors.red, + seekLineColor: Colors.red, + scaleFactor: 600, + ), ), ), - ), - SizedBox( - width: 350.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () => Text( - DateUtil.formatDateMs( - controller.currentDuration.value, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[500], - fontSize: 12.sp, + SizedBox( + width: 350.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () => Text( + DateUtil.formatDateMs( + controller.currentDuration.value, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + fontSize: 12.sp, + ), ), ), - ), - Obx( - () => Text( - DateUtil.formatDateMs( - controller.durationMax.value, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[500], - fontSize: 12.sp, + Obx( + () => Text( + DateUtil.formatDateMs( + controller.durationMax.value, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + fontSize: 12.sp, + ), ), ), - ), - ], + ], + ), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - int progress = - controller.currentDuration.value - 10000; - if (progress < 0) { - progress = 0; - } - controller.playerController.seekTo(progress); - }, - child: Icon( - Icons.replay_10, - color: Colors.black, - size: 30.w, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + int progress = + controller.currentDuration.value - 10000; + if (progress < 0) { + progress = 0; + } + controller.playerController.seekTo(progress); + }, + child: Icon( + Icons.replay_10, + color: Colors.black, + size: 30.w, + ), ), - ), - GestureDetector( - onTap: () { - if (controller.meetingData.value.filepath.isEmpty) { - controller.downloadAudio(); - } else { - if (controller.isPlay.value) { - controller.playerController.pausePlayer(); + GestureDetector( + onTap: () { + if (controller + .meetingData.value.filepath.isEmpty) { + controller.downloadAudio(); } else { - controller.playerController.startPlayer(); + if (controller.isPlay.value) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } } - } - }, - child: Obx( - () => !controller.isLoading.value && - controller - .meetingData.value.filepath.isEmpty - ? Obx( - () => Stack( - alignment: Alignment.center, - children: [ - if (controller.isDownload.value) - SizedBox( - width: 42.w, - height: 42.w, - child: CircularProgressIndicator( - value: controller - .downloadProgress.value, - valueColor: - const AlwaysStoppedAnimation< - Color>(Colors.red), - strokeWidth: 3.w, + }, + child: Obx( + () => !controller.isLoading.value && + controller + .meetingData.value.filepath.isEmpty + ? Obx( + () => Stack( + alignment: Alignment.center, + children: [ + if (controller.isDownload.value) + SizedBox( + width: 42.w, + height: 42.w, + child: CircularProgressIndicator( + value: controller + .downloadProgress.value, + valueColor: + const AlwaysStoppedAnimation< + Color>(Colors.red), + strokeWidth: 3.w, + ), ), + Icon( + Icons.download_for_offline, + color: Colors.black, + size: 48.w, ), - Icon( - Icons.download_for_offline, - color: Colors.black, - size: 48.w, - ), - ], + ], + ), + ) + : Icon( + controller.isPlay.value + ? Icons.pause_circle + : Icons.play_circle, + color: Colors.black, + size: 48.w, ), - ) - : Icon( - controller.isPlay.value - ? Icons.pause_circle - : Icons.play_circle, - color: Colors.black, - size: 48.w, - ), + ), ), - ), - GestureDetector( - onTap: () { - int progress = - controller.currentDuration.value + 10000; - controller.playerController.seekTo(progress); - }, - child: Icon( - Icons.forward_10, - color: Colors.black, - size: 30.w, + GestureDetector( + onTap: () { + int progress = + controller.currentDuration.value + 10000; + controller.playerController.seekTo(progress); + }, + child: Icon( + Icons.forward_10, + color: Colors.black, + size: 30.w, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/modules/meeting/views/meeting_home_view.dart b/lib/modules/meeting/views/meeting_home_view.dart new file mode 100644 index 000000000..712078790 --- /dev/null +++ b/lib/modules/meeting/views/meeting_home_view.dart @@ -0,0 +1,474 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:haptic_feedback/haptic_feedback.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../../../routes/app_routes.dart'; +import '../controllers/meeting_home_controller.dart'; +import '../model/meeting_model.dart'; +import '../utils/meeting_ui_utils.dart'; +import 'bottomSheet/edit_title_bottom_sheet.dart'; +import 'bottomSheet/import_audio_bottom_sheet.dart'; + +class MeetingHomeView extends StatefulWidget { + const MeetingHomeView({super.key}); + + @override + State createState() => _MeetingHomeViewState(); +} + +class _MeetingHomeViewState extends State + with AutomaticKeepAliveClientMixin { + final _controller = Get.put(MeetingHomeController()); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return PopScope( + onPopInvokedWithResult: _controller.onPopInvokedWithResult, + child: Scaffold( + backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode), + appBar: _buildAppBar(isDarkMode), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildFiltrate(isDarkMode), + _buildConnect(isDarkMode), + ], + ), + Expanded( + child: _buildBody(context, isDarkMode), + ), + ], + ), + floatingActionButton: FloatingActionButton( + heroTag: "addAudio", + onPressed: () { + Get.bottomSheet( + const ImportAudioBottomSheet(), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + child: const Icon(Icons.add), + ), + ), + ); + } + + Widget _buildFiltrate(bool isDarkMode) { + return GestureDetector( + onTap: () { + _controller.meetingController.scaffoldKey.currentState?.openDrawer(); + }, + behavior: HitTestBehavior.translucent, + child: Row( + children: [ + 12.horizontalSpace, + Container( + width: 32.w, + height: 32.w, + margin: EdgeInsets.symmetric( + vertical: 10.w, + ), + decoration: BoxDecoration( + color: isDarkMode + ? MeetingUIUtils.getTranslucentWhite(0.05) + : MeetingUIUtils.getCardColor(isDarkMode), + borderRadius: BorderRadius.circular(12.w), + boxShadow: MeetingUIUtils.getCardShadow(isDarkMode), + ), + child: Icon( + Icons.format_list_bulleted, + color: isDarkMode ? Colors.white : Colors.black87, + size: 20.sp, + ), + ), + 5.horizontalSpace, + Obx( + () => Text( + '${_controller.filtrateType.value.isNotEmpty ? _controller.filtrateType : '全部文件'}(${_controller.dataList.length})', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } + + Widget _buildConnect(bool isDarkMode) { + return GestureDetector( + onTap: () { + Get.toNamed(Routes.meetingConnect); + }, + child: Container( + padding: EdgeInsets.all(5.w), + margin: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 10.w, + ), + decoration: BoxDecoration( + color: isDarkMode + ? MeetingUIUtils.getTranslucentWhite(0.05) + : MeetingUIUtils.getCardColor(isDarkMode), + borderRadius: BorderRadius.circular(50.w), + boxShadow: MeetingUIUtils.getCardShadow(isDarkMode), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: MeetingUIUtils.getButtonColor(isDarkMode), + borderRadius: BorderRadius.circular(50.w), + ), + child: Icon( + Icons.add, + color: Colors.white, + size: 20.sp, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.w), + child: Text( + '连接', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 14.sp, + ), + ), + ) + ], + ), + ), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'conferenceAssistant'.tr, // 商务会议助手 + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(BuildContext context, bool isDarkMode) { + return Obx( + () => ListView.builder( + itemCount: _controller.dataList.length, + padding: EdgeInsets.fromLTRB(12.w, 5.w, 12.w, 90.w), + itemBuilder: (BuildContext context, int index) { + MeetingModel data = + MeetingModel.fromJson(_controller.dataList[index]); + return _item(context, isDarkMode, data); + }, + ), + ); + } + + Widget _item(BuildContext context, bool isDarkMode, MeetingModel data) { + return GestureDetector( + onTap: () { + Get.toNamed(Routes.meetingDetails, arguments: { + 'id': data.id, + }); + }, + onLongPressStart: (LongPressStartDetails details) { + _onLongPressItem(context, isDarkMode, details, data); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode + ? MeetingUIUtils.getTranslucentWhite(0.05) + : MeetingUIUtils.getCardColor(isDarkMode), + borderRadius: BorderRadius.circular(12.w), + boxShadow: MeetingUIUtils.getCardShadow(isDarkMode), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.title, + style: TextStyle( + color: MeetingUIUtils.getTextColor(isDarkMode), + fontSize: 18.sp, + ), + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Row( + children: [ + Icon( + Icons.calendar_month, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 2.horizontalSpace, + Text( + DateUtil.formatDateMs( + data.creationtime, + format: DateFormats.y_mo_d_h_m, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + 20.horizontalSpace, + Icon( + Icons.access_time, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 2.horizontalSpace, + Text( + _formatDateSeconds(data.seconds), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + ], + ), + ), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), + margin: EdgeInsets.only(top: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + borderRadius: BorderRadius.circular(4.w), + ), + child: Text( + data.type, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontSize: 10.sp, + ), + ), + ), + const Spacer(), + Obx(() { + Map uploadData = + _controller.meetingUploadService.uploadList.firstWhere( + (element) => element['id'] == data.id, + orElse: () => {}, + ); + return uploadData.isNotEmpty + ? Stack( + alignment: Alignment.center, + children: [ + if (uploadData['id'] == + _controller + .meetingUploadService.meetingId.value) + Obx( + () => SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + value: _controller.meetingUploadService + .uploadProgress.value, + valueColor: + const AlwaysStoppedAnimation( + Colors.red), + strokeWidth: 1.w, + ), + ), + ), + SizedBox( + width: 20.w, + height: 20.w, + child: Icon( + uploadData['audiourl'].isNotEmpty + ? Icons.cloud_done + : Icons.cloud_upload, + color: Colors.blue, + size: 16.w, + ), + ), + ], + ) + : const SizedBox(); + }), + Obx(() { + Map taskData = + _controller.meetingTaskService.taskFinishList.firstWhere( + (element) => element['id'] == data.id, + orElse: () => {}, + ); + return taskData.isNotEmpty + ? Padding( + padding: EdgeInsets.only(top: 5.w), + child: Text( + taskData['tasktype'] == 1 + ? '正在转写...' + : taskData['tasktype'] == 2 + ? '正在总结...' + : '总结完成', + style: TextStyle( + color: Colors.blue, + fontSize: 10.sp, + ), + ), + ) + : const SizedBox(); + }), + ], + ), + ], + ), + ), + ); + } + + void _onLongPressItem(BuildContext context, bool isDarkMode, + LongPressStartDetails details, MeetingModel data) { + 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: [ + _popupMenuEntry( + Icon( + Icons.edit_calendar, + size: 20.w, + color: isDarkMode ? Colors.white : Colors.black87, + ), + 'edit'.tr, // 编辑 + isDarkMode ? Colors.white : Colors.black87, + () { + final dotIndex = data.title.lastIndexOf('.'); + _controller.titleController.text = + dotIndex != -1 ? data.title.substring(0, dotIndex) : data.title; + _controller.titleCSuffix = + data.title.substring(dotIndex, data.title.length); + Get.bottomSheet( + EditTitleBottomSheet(data.id), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + ), + _popupMenuEntry( + Icon( + Icons.delete_outline, + size: 20.w, + color: isDarkMode ? Colors.red[700] : Colors.red, + ), + 'delete'.tr, // 删除 + isDarkMode ? Colors.red[700]! : Colors.red, + () { + _controller.deleteMeeting(data.id, data.filepath); + }, + ), + ], + ); + } + + PopupMenuEntry _popupMenuEntry( + Icon icon, + String title, + Color color, + Function() onTap, + ) { + return PopupMenuItem( + onTap: onTap, + child: SizedBox( + width: 140.w, + child: Row( + children: [ + icon, + 8.horizontalSpace, + Text( + title, + style: TextStyle( + color: color, + fontSize: 14.sp, + ), + ), + ], + ), + ), + ); + } + + String _formatDateSeconds(int seconds) { + if (seconds < 60) { + return '${seconds}s'; + } else if (seconds < 3600) { + int minutes = seconds ~/ 60; + int remainingSeconds = seconds % 60; + return '${minutes}m ${remainingSeconds}s'; + } else { + int hours = seconds ~/ 3600; + int remainingMinutes = (seconds % 3600) ~/ 60; + int remainingSeconds = seconds % 60; + return '${hours}h ${remainingMinutes}m ${remainingSeconds}s'; + } + } +} diff --git a/lib/modules/meeting/views/meeting_mine_view.dart b/lib/modules/meeting/views/meeting_mine_view.dart new file mode 100644 index 000000000..de5e9e955 --- /dev/null +++ b/lib/modules/meeting/views/meeting_mine_view.dart @@ -0,0 +1,439 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../../routes/app_routes.dart'; +import '../../profile/views/unified_avatar.dart'; + +import '../controllers/meeting_mine_controller.dart'; +import '../utils/meeting_ui_utils.dart'; +import 'bottomSheet/statistics_bottom_sheet.dart'; + +class MeetingMineView extends StatefulWidget { + const MeetingMineView({super.key}); + + @override + State createState() => _MeetingMineViewState(); +} + +class _MeetingMineViewState extends State + with AutomaticKeepAliveClientMixin { + final _controller = Get.put(MeetingMineController()); + + @override + bool get wantKeepAlive => true; + + // 添加 isDarkMode 作为类的 getter + bool get isDarkMode => Theme.of(context).brightness == Brightness.dark; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode), + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + alignment: Alignment.topCenter, + image: AssetImage('assets/images/mine-background.png'), + fit: BoxFit.fitWidth, + ), + ), + child: SafeArea( + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 12.w), + children: [ + _head(), + _statistics(), + _member(), + _box([ + _item( + Icons.article_outlined, + '转写记录', + onTap: () {}, + ), + ]), + _box([ + _item( + Icons.settings_outlined, + '设置', + onTap: () {}, + ), + _item( + Icons.layers_outlined, + '模版社区', + onTap: () {}, + ), + _item( + Icons.sync_alt_outlined, + '集成', + onTap: () {}, + ), + ]), + _box([ + _item( + Icons.cloud_upload_outlined, + 'DEAPSOUND PRIVATE CLOUD', + description: '开启', + onTap: () {}, + ), + ]), + _box([ + _item( + Icons.language_outlined, + 'WEB', + description: 'web.deapsound.com', + onTap: () {}, + ), + _item( + Icons.settings_remote_outlined, + '我的DEAPSOUND设备', + onTap: () {}, + ), + ]), + _box([ + _item( + Icons.help_outline_rounded, + '帮助与反馈', + onTap: () {}, + ), + _item( + Icons.info_outline_rounded, + '关于DEAPSOUND', + description: 'v1.0.0', + onTap: () {}, + ), + ]), + ], + ), + ), + ), + ); + } + + Widget _head() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 20.w), + child: GestureDetector( + onTap: () => Get.toNamed(Routes.profileMeeting), // 跳转到个人信息页面 + child: Row( + children: [ + // 使用统一头像组件,避免闪烁问题 + Container( + margin: EdgeInsets.only(right: 10.w), + child: UnifiedAvatar( + radius: 25.w, + backgroundColor: Colors.grey[200], + iconColor: Colors.grey[600], + iconSize: 25.r, + isDarkMode: isDarkMode, + ), + ), + Expanded( + child: Obx( + () => Text( + _controller.profileCtrl.userInfo.value.name, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: Colors.grey[700], + ), + ], + ), + ), + ); + } + + Widget _statistics() { + return GestureDetector( + onTap: () { + Get.bottomSheet( + const StatisticsBottomSheet(), + isScrollControlled: true, + ); + }, + child: Container( + padding: EdgeInsets.all(8.w), + margin: EdgeInsets.only(bottom: 15.w), + decoration: BoxDecoration( + color: MeetingUIUtils.getCardColor(isDarkMode), + borderRadius: BorderRadius.circular(16.w), + boxShadow: MeetingUIUtils.getCardShadow(isDarkMode), + ), + child: Obx( + () => Column( + children: [ + Row( + children: [ + _statisticsItem( + '${_controller.days.value}', + '天', + Colors.lime, + ), + _statisticsItem( + '${_controller.numberFiles.value}', + '文件', + Colors.lightGreen, + ), + _statisticsItem( + '${_controller.hourage.value}', + '小时', + Colors.blueGrey, + ), + ], + ), + Container( + height: 80.w, + padding: EdgeInsets.symmetric(horizontal: 2.w), + margin: EdgeInsets.only(top: 8.w, bottom: 2.w), + child: GridView.builder( + controller: _controller.scrollController, + scrollDirection: Axis.horizontal, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + mainAxisSpacing: 2.w, + crossAxisSpacing: 2.w, + ), + itemCount: 364, + itemBuilder: (BuildContext context, int index) { + int type = 0; + if (_controller.statistics.containsKey(index + 1)) { + if (_controller.statistics[index + 1]['count'] <= + _controller.averageNumberFiles) { + type = 1; + } else { + type = 2; + } + } + return Container( + decoration: BoxDecoration( + color: type == 0 + ? Colors.grey[200] + : type == 1 + ? Colors.red[100] + : Colors.red, + borderRadius: BorderRadius.circular(2.w), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _statisticsItem(String amount, String unit, Color color) { + return Expanded( + child: Container( + height: 30.w, + margin: EdgeInsets.symmetric(horizontal: 2.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + amount, + style: TextStyle( + height: 0.1, + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + 2.horizontalSpace, + Text( + unit, + style: TextStyle( + height: 0.1, + color: Colors.grey, + fontSize: 10.sp, + ), + ), + ], + ), + ), + ); + } + + Widget _member() { + return Container( + margin: EdgeInsets.only(bottom: 15.w), + decoration: BoxDecoration( + color: MeetingUIUtils.getCardColor(isDarkMode), + borderRadius: BorderRadius.circular(16.w), + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'DEAPSOUND AI会员', + style: TextStyle( + color: MeetingUIUtils.getTextColor(isDarkMode), + fontSize: 16.sp, + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: Colors.grey, + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(16.w), + ), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.auto_awesome, + color: Colors.amberAccent, + size: 25.sp, + ), + 5.horizontalSpace, + Text( + '解锁 Starter 会员', + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.only(top: 15.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '绑定 DEAPSOUND 设备可激活 Starter 会员', + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + ), + ), + GestureDetector( + onTap: () {}, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 6.w, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.w), + gradient: const LinearGradient( + colors: [Colors.blue, Colors.pink], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Text( + '立即解锁', + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _box(List children) { + return Container( + padding: EdgeInsets.symmetric(vertical: 5.w), + margin: EdgeInsets.only(bottom: 15.w), + decoration: BoxDecoration( + color: MeetingUIUtils.getCardColor(isDarkMode), + borderRadius: BorderRadius.circular(16.w), + boxShadow: MeetingUIUtils.getCardShadow(isDarkMode), + ), + child: Column( + children: children, + ), + ); + } + + Widget _item( + IconData icon, + String title, { + String? description, + Function()? onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 12.w), + child: Row( + children: [ + Icon( + icon, + size: 24.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey, + ), + 8.horizontalSpace, + Expanded( + child: Text( + title, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + ), + ), + if (description != null) + Padding( + padding: EdgeInsets.only(right: 5.w), + child: Text( + description, + style: TextStyle( + color: Colors.grey, + fontSize: 16.sp, + ), + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey, + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index 0f333450e..045a92721 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -1,444 +1,120 @@ -import 'package:common_utils/common_utils.dart'; +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'package:haptic_feedback/haptic_feedback.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../routes/app_routes.dart'; import '../controllers/meeting_controller.dart'; -import '../model/meeting_model.dart'; -import 'bottomSheet/edit_title_bottom_sheet.dart'; +import '../utils/meeting_ui_utils.dart'; import 'bottomSheet/filtrate_drawer.dart'; -import 'bottomSheet/import_audio_bottom_sheet.dart'; +import 'bottomSheet/navigation_bar_bottom_sheet.dart'; class MeetingView extends GetView { const MeetingView({super.key}); @override Widget build(BuildContext context) { - final GlobalKey scaffoldKey = GlobalKey(); final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return PopScope( - onPopInvokedWithResult: controller.onPopInvokedWithResult, - child: Scaffold( - key: scaffoldKey, - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - drawer: Drawer( - width: 260.w, - child: const FiltrateDrawer(), - ), - drawerEnableOpenDragGesture: false, - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon( - Icons.folder_open, - color: isDarkMode ? Colors.white : Colors.black, - ), - onPressed: () { - scaffoldKey.currentState?.openDrawer(); - }, + + return Scaffold( + key: controller.scaffoldKey, + backgroundColor: MeetingUIUtils.getBackgroundColor(isDarkMode), + body: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: controller.pageController, + children: controller.pageList, + ), + drawer: Drawer( + width: 260.w, + backgroundColor: isDarkMode ? const Color(0xFF1A1A1A) : Colors.grey[50], + child: const FiltrateDrawer(), + ), + drawerEnableOpenDragGesture: false, + bottomNavigationBar: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Obx( + () => BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.folder_outlined), + activeIcon: Icon(Icons.folder), + label: '文件', ), - _buildConnect(isDarkMode), - ], - ), - Expanded( - child: _buildBody(context, isDarkMode), - ), - ], - ), - floatingActionButton: FloatingActionButton( - heroTag: "addAudio", - onPressed: () { - Get.bottomSheet( - const ImportAudioBottomSheet(), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), + BottomNavigationBarItem( + icon: SizedBox(), + label: '', ), - ), - ); - }, - child: const Icon(Icons.add), - ), - ), - ); - } - - Widget _buildConnect(bool isDarkMode) { - return GestureDetector( - onTap: () { - Get.toNamed(Routes.meetingConnect); - }, - child: Container( - padding: EdgeInsets.all(5.w), - margin: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 10.w, - ), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(50.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(50.w), - ), - child: Icon( - Icons.add, - color: Colors.white, - size: 20.sp, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 5.w), - child: Text( - '连接', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 14.sp, + BottomNavigationBarItem( + icon: Icon(Icons.person_outline), + activeIcon: Icon(Icons.person), + label: '我的', ), - ), - ) - ], - ), - ), - ); - } - - // 构建应用栏 - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - surfaceTintColor: Colors.transparent, - elevation: 0.5, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - title: Text( - 'conferenceAssistant'.tr, // 商务会议助手 - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - ); - } - - // 构建主体内容 - Widget _buildBody(BuildContext context, bool isDarkMode) { - return Obx( - () => ListView.builder( - controller: controller.listController, - itemCount: controller.dataList.length, - padding: EdgeInsets.fromLTRB(12.w, 5.w, 12.w, 90.w), - itemBuilder: (BuildContext context, int index) { - MeetingModel data = MeetingModel.fromJson(controller.dataList[index]); - return _item(context, isDarkMode, data); - }, - ), - ); - } - - Widget _item(BuildContext context, bool isDarkMode, MeetingModel data) { - return GestureDetector( - onTap: () { - Get.toNamed(Routes.meetingDetails, arguments: { - 'id': data.id, - }); - }, - onLongPressStart: (LongPressStartDetails details) { - _onLongPressItem(context, isDarkMode, details, data); - }, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), - margin: EdgeInsets.symmetric(vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - data.title, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 18.sp, - ), - overflow: TextOverflow.ellipsis, + ], + backgroundColor: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, + currentIndex: controller.pageIndex.value, + unselectedItemColor: Colors.grey, + selectedItemColor: isDarkMode ? Colors.white : Colors.blue, + unselectedFontSize: 10.sp, + selectedFontSize: 10.sp, + onTap: (int index) { + if (index == 1 || index == controller.pageIndex.value) return; + if (index == 0) { + controller.pageIndex.value = 0; + controller.pageController.jumpToPage(0); + } else { + controller.pageIndex.value = 2; + controller.pageController.jumpToPage(1); + } + }, ), - Padding( - padding: EdgeInsets.symmetric(vertical: 5.w), - child: Row( - children: [ - Icon( - Icons.calendar_month, - size: 14.w, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - ), - 2.horizontalSpace, - Text( - DateUtil.formatDateMs( - data.creationtime, - format: DateFormats.y_mo_d_h_m, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, - ), - ), - 20.horizontalSpace, - Icon( - Icons.access_time, - size: 14.w, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + Positioned( + top: -18.w, + child: GestureDetector( + onTap: () { + Get.bottomSheet( + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: const NavigationBarBottomSheet(), ), - 2.horizontalSpace, - Text( - _formatDateSeconds(data.seconds), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, + isScrollControlled: true, + ); + }, + child: Container( + width: 60.w, + height: 60.w, + padding: EdgeInsets.all(5.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, + borderRadius: BorderRadius.circular(60.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), + blurRadius: 8, + offset: const Offset(0, 2), ), - ), - ], - ), - ), - Row( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), - margin: EdgeInsets.only(top: 5.w), + ], + ), + child: Container( + alignment: Alignment.center, decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[100], - borderRadius: BorderRadius.circular(4.w), + color: MeetingUIUtils.getButtonColor(isDarkMode), + borderRadius: BorderRadius.circular(60.w), ), - child: Text( - data.type, - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - fontSize: 10.sp, - ), + child: Icon( + Icons.add, + color: Colors.white, + size: 30.w, ), ), - const Spacer(), - Obx(() { - Map uploadData = - controller.meetingUploadService.uploadList.firstWhere( - (element) => element['id'] == data.id, - orElse: () => {}, - ); - return uploadData.isNotEmpty - ? Stack( - alignment: Alignment.center, - children: [ - if (uploadData['id'] == - controller.meetingUploadService.meetingId.value) - Obx( - () => SizedBox( - width: 20.w, - height: 20.w, - child: CircularProgressIndicator( - value: controller.meetingUploadService - .uploadProgress.value, - valueColor: - const AlwaysStoppedAnimation( - Colors.red), - strokeWidth: 1.w, - ), - ), - ), - SizedBox( - width: 20.w, - height: 20.w, - child: Icon( - uploadData['audiourl'].isNotEmpty - ? Icons.cloud_done - : Icons.cloud_upload, - color: Colors.blue, - size: 16.w, - ), - ), - ], - ) - : const SizedBox(); - }), - Obx(() { - Map taskData = - controller.meetingTaskService.taskFinishList.firstWhere( - (element) => element['id'] == data.id, - orElse: () => {}, - ); - return taskData.isNotEmpty - ? Padding( - padding: EdgeInsets.only(top: 5.w), - child: Text( - taskData['tasktype'] == 1 - ? '正在转写...' - : taskData['tasktype'] == 2 - ? '正在总结...' - : '总结完成', - style: TextStyle( - color: Colors.blue, - fontSize: 10.sp, - ), - ), - ) - : const SizedBox(); - }), - ], - ), - ], - ), - ), - ); - } - - void _onLongPressItem(BuildContext context, bool isDarkMode, - LongPressStartDetails details, MeetingModel data) { - 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: [ - _popupMenuEntry( - Icon( - Icons.edit_calendar, - size: 20.w, - color: isDarkMode ? Colors.white : Colors.black87, - ), - 'edit'.tr, // 编辑 - isDarkMode ? Colors.white : Colors.black87, - () { - final dotIndex = data.title.lastIndexOf('.'); - controller.titleController.text = - dotIndex != -1 ? data.title.substring(0, dotIndex) : data.title; - controller.titleCSuffix = - data.title.substring(dotIndex, data.title.length); - Get.bottomSheet( - EditTitleBottomSheet(data.id), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - ); - }, - ), - _popupMenuEntry( - Icon( - Icons.delete_outline, - size: 20.w, - color: isDarkMode ? Colors.red[700] : Colors.red, - ), - 'delete'.tr, // 删除 - isDarkMode ? Colors.red[700]! : Colors.red, - () { - controller.deleteMeeting(data.id, data.filepath); - }, - ), - ], - ); - } - - PopupMenuEntry _popupMenuEntry( - Icon icon, - String title, - Color color, - Function() onTap, - ) { - return PopupMenuItem( - onTap: onTap, - child: SizedBox( - width: 140.w, - child: Row( - children: [ - icon, - 8.horizontalSpace, - Text( - title, - style: TextStyle( - color: color, - fontSize: 14.sp, ), ), - ], - ), + ), + ], ), ); } - - String _formatDateSeconds(int seconds) { - if (seconds < 60) { - return '${seconds}s'; - } else if (seconds < 3600) { - int minutes = seconds ~/ 60; - int remainingSeconds = seconds % 60; - return '${minutes}m ${remainingSeconds}s'; - } else { - int hours = seconds ~/ 3600; - int remainingMinutes = (seconds % 3600) ~/ 60; - int remainingSeconds = seconds % 60; - return '${hours}h ${remainingMinutes}m ${remainingSeconds}s'; - } - } } diff --git a/lib/modules/meeting/views/tabs/overview_tab.dart b/lib/modules/meeting/views/tabs/overview_tab.dart index 68c441135..ca5f016bb 100644 --- a/lib/modules/meeting/views/tabs/overview_tab.dart +++ b/lib/modules/meeting/views/tabs/overview_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../controllers/meeting_details_controller.dart'; +import '../../utils/meeting_ui_utils.dart'; import '../bottomSheet/ai_bottom_sheet.dart'; import '../bottomSheet/generate_bottom_sheet.dart'; @@ -29,6 +30,7 @@ class _OverviewTabState extends State { @override Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Stack( children: [ _dataList.isNotEmpty @@ -43,7 +45,7 @@ class _OverviewTabState extends State { format: DateFormats.y_mo_d_h_m, ), style: TextStyle( - color: Colors.black54, + color: isDarkMode ? Colors.grey[400] : Colors.black54, fontSize: 12.sp, ), ), @@ -55,6 +57,7 @@ class _OverviewTabState extends State { style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, ), ), ), @@ -62,7 +65,7 @@ class _OverviewTabState extends State { padding: EdgeInsets.symmetric(vertical: 10.w), child: Divider( height: 1, - color: Colors.grey[300], + color: isDarkMode ? Colors.grey[700] : Colors.grey[300], ), ), Padding( @@ -70,7 +73,7 @@ class _OverviewTabState extends State { child: Text( '分析出${_dataList.length}个关键事件。', style: TextStyle( - color: Colors.grey, + color: isDarkMode ? Colors.grey[400] : Colors.grey, fontSize: 12.sp, ), ), @@ -103,7 +106,7 @@ class _OverviewTabState extends State { Get.bottomSheet( const AIBottomSheet(), isScrollControlled: true, - backgroundColor: Colors.white, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -122,7 +125,7 @@ class _OverviewTabState extends State { Get.bottomSheet( const GenerateBottomSheet(), isScrollControlled: true, - backgroundColor: Colors.grey[100], + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -141,22 +144,21 @@ class _OverviewTabState extends State { } Widget _item(String data) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Container( padding: EdgeInsets.all(10.w), margin: EdgeInsets.symmetric(vertical: 5.w), decoration: BoxDecoration( - color: Colors.white, + color: MeetingUIUtils.getCardColor(isDarkMode), borderRadius: BorderRadius.circular(12.w), - boxShadow: const [ - BoxShadow( - color: Color(0x1A3045C8), - offset: Offset(0.0, 0.0), - blurRadius: 10, - spreadRadius: 0.0, - ), - ], + boxShadow: MeetingUIUtils.getCardShadow(isDarkMode), + ), + child: Text( + data, + style: TextStyle( + color: MeetingUIUtils.getTextColor(isDarkMode), + ), ), - child: Text(data), ); } @@ -194,6 +196,7 @@ class _OverviewTabState extends State { } Widget _empty() { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Padding( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 30.w), child: Column( @@ -202,14 +205,14 @@ class _OverviewTabState extends State { padding: EdgeInsets.only(bottom: 10.w), child: Icon( Icons.summarize, - color: Colors.grey, + color: isDarkMode ? Colors.grey[400] : Colors.grey, size: 40.sp, ), ), Text( '概览', style: TextStyle( - color: Colors.black87, + color: MeetingUIUtils.getTextColor(isDarkMode), fontSize: 18.sp, fontWeight: FontWeight.bold, ), @@ -217,7 +220,7 @@ class _OverviewTabState extends State { Text( 'AI分析识别录音中的关键事件,让你选择重要事件进行AI生成。', style: TextStyle( - color: Colors.grey, + color: MeetingUIUtils.getSecondaryTextColor(isDarkMode), fontSize: 14.sp, ), textAlign: TextAlign.center, diff --git a/lib/modules/meeting/views/tabs/speech_tab.dart b/lib/modules/meeting/views/tabs/speech_tab.dart index 37bd4373e..d0f2fa9ea 100644 --- a/lib/modules/meeting/views/tabs/speech_tab.dart +++ b/lib/modules/meeting/views/tabs/speech_tab.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:lottie/lottie.dart'; import '../../controllers/meeting_details_controller.dart'; +import '../../utils/meeting_ui_utils.dart'; import '../bottomSheet/ai_bottom_sheet.dart'; import '../bottomSheet/generate_bottom_sheet.dart'; import '../bottomSheet/speaker_all_bottom_sheet.dart'; @@ -15,6 +16,7 @@ class SpeechTab extends GetView { @override Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Stack( children: [ Obx( @@ -25,7 +27,7 @@ class SpeechTab extends GetView { Map item = controller.textList[index]; TextStyle textStyle = TextStyle( fontSize: 15.sp, - color: Colors.black87, + color: MeetingUIUtils.getTextColor(isDarkMode), letterSpacing: 0, height: 1.4, ); @@ -56,7 +58,7 @@ class SpeechTab extends GetView { Get.bottomSheet( SpeakerBottomSheet(item['id'], item['speaker']), isScrollControlled: true, - backgroundColor: Colors.grey[100], + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -82,7 +84,7 @@ class SpeechTab extends GetView { isUtc: true, ), style: TextStyle( - color: Colors.grey, + color: MeetingUIUtils.getSecondaryTextColor(isDarkMode), fontSize: 13.sp, ), ), @@ -134,7 +136,6 @@ class SpeechTab extends GetView { () => Container( padding: EdgeInsets.symmetric( horizontal: 14.w, - vertical: 5.w, ), decoration: BoxDecoration( color: @@ -173,11 +174,12 @@ class SpeechTab extends GetView { style: textStyle, minLines: 1, maxLines: 100, - decoration: const InputDecoration( + decoration: InputDecoration( isDense: true, fillColor: Colors.transparent, border: InputBorder.none, - contentPadding: EdgeInsets.zero, + contentPadding: + EdgeInsets.symmetric(vertical: 5.w), ), onChanged: (String value) { controller.editSpeakerTextChange = value; @@ -227,7 +229,7 @@ class SpeechTab extends GetView { item['content'], style: TextStyle( fontSize: 15.sp, - color: isActive ? Colors.red : Colors.black87, + color: isActive ? Colors.red : MeetingUIUtils.getTextColor(isDarkMode), letterSpacing: 0, height: 1.4, ), @@ -270,7 +272,7 @@ class SpeechTab extends GetView { Get.bottomSheet( const SpeakerAllBottomSheet(), isScrollControlled: true, - backgroundColor: Colors.grey[100], + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -288,7 +290,7 @@ class SpeechTab extends GetView { Get.bottomSheet( const AIBottomSheet(), isScrollControlled: true, - backgroundColor: Colors.white, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -307,7 +309,7 @@ class SpeechTab extends GetView { Get.bottomSheet( const GenerateBottomSheet(), isScrollControlled: true, - backgroundColor: Colors.grey[100], + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), diff --git a/lib/modules/meeting/views/tabs/summary_tab.dart b/lib/modules/meeting/views/tabs/summary_tab.dart index 2cba7aac5..bd24e8c4f 100644 --- a/lib/modules/meeting/views/tabs/summary_tab.dart +++ b/lib/modules/meeting/views/tabs/summary_tab.dart @@ -1,8 +1,9 @@ import 'package:common_utils/common_utils.dart'; +import 'package:fleather/fleather.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:parchment/codecs.dart'; import '../../../../data/services/db/sqflite_api.dart'; import '../../controllers/meeting_details_controller.dart'; @@ -19,6 +20,9 @@ class SummaryTab extends StatefulWidget { class _SummaryTabState extends State { final _controller = Get.find(); + + late final Worker _personnelListener; + final String _addressPrefix = '地点:'; String _addressText = ''; final String _personnelPrefix = '与会人员:'; @@ -26,9 +30,30 @@ class _SummaryTabState extends State { final TextEditingController _addressController = TextEditingController(); final TextEditingController _personnelController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + final GlobalKey _editorKey = GlobalKey(); + FleatherController? _fleatherController; + + bool _hasFocus = false; + @override void initState() { super.initState(); + _initController(); + _addressController.addListener(_handleAddressChanged); + _personnelController.addListener(_handlePersonnelChanged); + _focusNode.addListener(() { + setState(() { + _hasFocus = _focusNode.hasFocus; + }); + }); + _personnelListener = + ever(_controller.personnelNumberModifications, (value) { + _initController(); + }); + } + + void _initController() { _addressController.text = _addressPrefix + (_controller.meetingDetails.value.address.isNotEmpty ? _controller.meetingDetails.value.address @@ -37,8 +62,11 @@ class _SummaryTabState extends State { (_controller.meetingDetails.value.personnel.isNotEmpty ? _controller.meetingDetails.value.personnel : '[输入与会人员]'); - _addressController.addListener(_handleAddressChanged); - _personnelController.addListener(_handlePersonnelChanged); + final markdown = const ParchmentMarkdownCodec() + .decode(_controller.meetingDetails.value.summary); + setState(() { + _fleatherController = FleatherController(document: markdown); + }); } void _handleAddressChanged() { @@ -101,8 +129,25 @@ class _SummaryTabState extends State { } } + void _saveSummary() async { + final markdown = + const ParchmentMarkdownCodec().encode(_fleatherController!.document); + if (markdown != _controller.meetingDetails.value.summary) { + SqfliteApi.editMeeting( + _controller.id, + {'summary': markdown}, + ); + _controller.meetingDetails.value.summary = markdown; + _controller.summaryNumberModifications.value++; + } + } + @override void dispose() { + _personnelListener.dispose(); + _saveSummary(); + _fleatherController?.dispose(); + _focusNode.dispose(); _addressController.dispose(); _personnelController.dispose(); super.dispose(); @@ -119,6 +164,7 @@ class _SummaryTabState extends State { ); return Stack( + clipBehavior: Clip.none, children: [ SingleChildScrollView( padding: EdgeInsets.only(bottom: 80.w), @@ -202,7 +248,7 @@ class _SummaryTabState extends State { left: BorderSide( width: 2, color: isDarkMode - ? Colors.white.withOpacity(0.1) + ? Colors.white.withValues(alpha: 0.1) : Colors.grey[300]!, ), ), @@ -278,67 +324,51 @@ class _SummaryTabState extends State { ), Padding( padding: EdgeInsets.only(top: 10.w), - child: MarkdownBody( - data: _controller.meetingDetails.value.summary, + child: FleatherEditor( + controller: _fleatherController!, + focusNode: _focusNode, + editorKey: _editorKey, ), ), ], ), ), ), - Obx( - () => Positioned( - bottom: 20.w, - left: 0, - right: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_controller.meetingDetails.value.tasktype >= 3) - _button( - Icons.supervisor_account, - '编辑发言者', - () { - Get.bottomSheet( - const SpeakerAllBottomSheet(), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - ); - }, - ), - if (_controller.meetingDetails.value.tasktype >= 3) - _button( - Icons.auto_awesome, - 'Ask AI', - () { - Get.bottomSheet( - const AIBottomSheet(), - isScrollControlled: true, - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), + if (!_hasFocus) + Obx( + () => Positioned( + bottom: 20.w, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_controller.meetingDetails.value.tasktype >= 3) + _button( + Icons.supervisor_account, + '编辑发言者', + () { + Get.bottomSheet( + const SpeakerAllBottomSheet(), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), ), - ), - ); - }, - ), - if (_controller.meetingDetails.value.tasktype == 0) - SizedBox( - width: 150.w, - child: _button( + ); + }, + ), + if (_controller.meetingDetails.value.tasktype >= 3) + _button( Icons.auto_awesome, - '生成', + 'Ask AI', () { Get.bottomSheet( - const GenerateBottomSheet(), + const AIBottomSheet(), isScrollControlled: true, - backgroundColor: Colors.grey[100], + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -347,11 +377,45 @@ class _SummaryTabState extends State { ); }, ), - ), - ], + if (_controller.meetingDetails.value.tasktype == 0) + SizedBox( + width: 150.w, + child: _button( + Icons.auto_awesome, + '生成', + () { + Get.bottomSheet( + const GenerateBottomSheet(), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + if (_hasFocus) + Positioned( + bottom: 0, + left: -12.w, + right: -12.w, + child: Container( + height: 40, + color: isDarkMode ? Colors.grey[800] : Colors.white, + alignment: Alignment.center, + child: FleatherToolbar.basic( + controller: _fleatherController!, + editorKey: _editorKey, + ), ), ), - ), ], ); } diff --git a/lib/modules/meeting/views/template_all_view.dart b/lib/modules/meeting/views/template/template_all_view.dart similarity index 99% rename from lib/modules/meeting/views/template_all_view.dart rename to lib/modules/meeting/views/template/template_all_view.dart index 95a644e89..7100e20b6 100644 --- a/lib/modules/meeting/views/template_all_view.dart +++ b/lib/modules/meeting/views/template/template_all_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import '../../../data/services/db/sqflite_api.dart'; +import '../../../../data/services/db/sqflite_api.dart'; class TemplateAllView extends StatefulWidget { final String title; diff --git a/lib/modules/meeting/views/template_view.dart b/lib/modules/meeting/views/template/template_view.dart similarity index 100% rename from lib/modules/meeting/views/template_view.dart rename to lib/modules/meeting/views/template/template_view.dart diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 0df0f9d40..907dd85f8 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -1,4 +1,6 @@ import 'package:classic_bluetooth/classic_bluetooth.dart'; +import '/routes/app_routes.dart'; +import 'package:logger/web.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:path/path.dart'; @@ -121,7 +123,17 @@ class OtaUpgradeController extends GetxController { // 实现更新逻辑... } catch (e) { errorMessage.value = e.toString(); - Get.snackbar('更新失败', e.toString()); + Get.snackbar( + '更新失败', + e.toString(), + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } } @@ -166,7 +178,7 @@ class OtaUpgradeController extends GetxController { // 从第三个 `.` 开始分割 final firstPart = firmwareParts.sublist(0, 3).join('.'); final secondPart = firmwareParts.sublist(3).join('.'); - print("firstPart${firstPart},secondPart=${secondPart}"); + Logger().d("firstPart${firstPart},secondPart=${secondPart}"); // 判断两部分是否一致 if (firstPart != secondPart) { throw "固件版本格式错误:${deviceInfo.firmwareVersion}"; @@ -174,19 +186,13 @@ class OtaUpgradeController extends GetxController { // 正常流程 currentVersion.value = firstPart; - print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); + Logger().d('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); final connectedDevices = await ClassicBluetooth.getConnectedDevices(); if (connectedDevices.isNotEmpty) { _address = connectedDevices.first.address; } - await _bleManager.setEnterOta(true); - - await Future.delayed(const Duration(seconds: 2)); - - jlota.init(); - // API请求 final response = await Api.getproduct({ "id": deviceInfo.firmwareProductId, @@ -198,20 +204,35 @@ class OtaUpgradeController extends GetxController { } final resp = OtaConfigResp.fromJson(response); - print('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); + Logger().d('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); latestVersion.value = resp.systemversion!; updatedescription.value = resp.updatedescription!; updatepackageaddress = resp.updatepackageaddress!; // 版本检查 - isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!); + bool isUpdate = _isNewVersionAvailable(resp.systemversion!); + + if (isUpdate) { + await _bleManager.setEnterOta(true); + await Future.delayed(const Duration(seconds: 2)); + jlota.init(); + } + + isUpdateAvailable.value = isUpdate; } catch (e) { // 统一错误处理 errorMessage.value = e.toString(); - Get.snackbar('错误', e.toString(), - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white); + Get.snackbar( + '错误', + e.toString(), + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); // 重要:重新抛出异常以便上层处理(可选) } finally { isFetchingDeviceInfo.value = false; @@ -235,7 +256,7 @@ class OtaUpgradeController extends GetxController { } return false; } catch (e) { - print('版本比较错误: $e'); + Logger().d('版本比较错误: $e'); return false; } } @@ -246,15 +267,35 @@ class OtaUpgradeController extends GetxController { if (latestVersion.value != "") { _showUpdateDialog(latestVersion.value); } else { - Get.snackbar('错误', '获取最新版本信息失败'); + Get.snackbar( + '错误', + '获取最新版本信息失败', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[800], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } } catch (e) { - Get.snackbar('错误', '获取版本信息失败'); + Get.snackbar( + '错误', + '获取版本信息失败', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[800], + colorText: Colors.white, + icon: const Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } } void _showUpdateDialog(String latest) async { - print('显示更新对话框'); + Logger().d('显示更新对话框'); Get.dialog( AlertDialog( title: Text('发现新版本 $latest'), @@ -262,7 +303,7 @@ class OtaUpgradeController extends GetxController { actions: [ TextButton( onPressed: () async { - print('取消按钮被点击'); + Logger().d('取消按钮被点击'); Get.back(); Future.delayed(Duration(milliseconds: 300)); @@ -271,7 +312,7 @@ class OtaUpgradeController extends GetxController { ), TextButton( onPressed: () async { - print('更新按钮被点击'); + Logger().d('更新按钮被点击'); Get.back(); Future.delayed(Duration(milliseconds: 300)); @@ -339,7 +380,17 @@ class OtaUpgradeController extends GetxController { } } } catch (e) { - Get.snackbar('错误', '检查本地文件失败: $e'); + Get.snackbar( + '错误', + '检查本地文件失败: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); return; } @@ -361,12 +412,22 @@ class OtaUpgradeController extends GetxController { } } } catch (e) { - Get.snackbar('错误', '删除旧文件失败: $e'); + Get.snackbar( + '错误', + '删除旧文件失败: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); //isUpgrade.value = false; return; } - print('文件保存路径: $savePath'); + Logger().d('文件保存路径: $savePath'); detailedStatus.value = '开始下载固件包...'; final options = Options( @@ -392,7 +453,17 @@ class OtaUpgradeController extends GetxController { // Get.snackbar('下载完成', '文件已保存至: $savePath'); detailedStatus.value = '下载完成,准备升级...'; } else { - Get.snackbar('错误', '文件未找到,请检查写入权限'); + Get.snackbar( + '错误', + '文件未找到,请检查写入权限', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); detailedStatus.value = '下载失败'; showDetailedStatus.value = false; return; @@ -401,15 +472,45 @@ class OtaUpgradeController extends GetxController { startUpgrading(savePath); return; } on DioException catch (e) { - Get.snackbar('下载失败', '错误: ${e.message}'); + Get.snackbar( + '下载失败', + '错误: ${e.message}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } on FileSystemException catch (e) { - Get.snackbar('存储失败', '无法写入文件: ${e.message}'); + Get.snackbar( + '存储失败', + '无法写入文件: ${e.message}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } catch (e) { isDownloading.value = false; detailedStatus.value = '下载失败,请检查网络连接'; showDetailedStatus.value = false; - Get.snackbar('下载失败', '请检查网络连接: $e'); - print('下载失败: $e'); + Get.snackbar( + '下载失败', + '请检查网络连接: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); + Logger().d('下载失败: $e'); } } @@ -440,8 +541,12 @@ class OtaUpgradeController extends GetxController { '电量检查失败', '无法获取设备信息,请检查设备连接', snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange, + backgroundColor: Colors.orange[600], colorText: Colors.white, + icon: Icon( + Icons.warning_amber, + color: Colors.white, + ), ); return false; } @@ -458,14 +563,36 @@ class OtaUpgradeController extends GetxController { Get.dialog( AlertDialog( - title: Text('电量不足'), - content: Text(message), + title: Text( + '电量不足', + style: TextStyle( + color: Colors.orange[700], + fontWeight: FontWeight.w600, + ), + ), + content: Text( + message, + style: TextStyle( + color: Colors.orange[600], + ), + ), actions: [ TextButton( onPressed: () => Get.back(), - child: Text('确定'), + style: TextButton.styleFrom( + foregroundColor: Colors.orange[700], + ), + child: const Text('确定'), ), ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.orange.withValues(alpha: 0.3), + width: 1, + ), + ), + backgroundColor: Colors.orange[50], ), ); return false; @@ -477,8 +604,12 @@ class OtaUpgradeController extends GetxController { '电量检查失败', '检查设备电量时发生错误:$e', snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, + backgroundColor: Colors.orange[600], colorText: Colors.white, + icon: const Icon( + Icons.warning_amber, + color: Colors.white, + ), ); return false; } @@ -505,15 +636,83 @@ class OtaUpgradeController extends GetxController { isUpgrade.value = false; detailedStatus.value = '升级成功完成'; showDetailedStatus.value = false; - Get.snackbar('升级成功', '请退出重新连接设备'); - _getCurrentVersion(); + + // 升级成功,更新版本信息显示 + currentVersion.value = latestVersion.value; + isUpdateAvailable.value = false; + + // 显示升级成功对话框,引导用户重新配对 + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon( + Icons.check_circle, + color: Colors.green[600], + size: 28, + ), + SizedBox(width: 12), + Text( + '升级成功', + style: TextStyle( + color: Colors.green[700], + fontWeight: FontWeight.w600, + ), + ), + ], + ), + content: Text( + '固件升级已完成!\n\n设备需要重新配对才能正常使用,现在跳转到配对页面吗?', + style: TextStyle( + fontSize: 16, + height: 1.4, + ), + ), + actions: [ + TextButton( + onPressed: () { + Get.back(); // 关闭对话框 + }, + child: Text( + '稍后配对', + style: TextStyle(color: Colors.grey[600]), + ), + ), + ElevatedButton( + onPressed: () { + Get.back(); // 关闭对话框 + Get.toNamed(Routes.pairing); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[600], + foregroundColor: Colors.white, + ), + child: Text('立即配对'), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + barrierDismissible: false, // 防止用户意外关闭 + ); } if (event.event == 'onError') { type.value = UPGRADE_FAILED; isUpgrade.value = false; detailedStatus.value = '升级失败,错误码:${event.code}'; showDetailedStatus.value = false; - Get.snackbar('升级失败', '请检查设备连接或升级包,错误码:${event.code}'); + Get.snackbar( + '升级失败', + '请检查设备连接或升级包,错误码:${event.code}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[600], + colorText: Colors.white, + icon: Icon( + Icons.error_outline, + color: Colors.white, + ), + ); } if (event.event == 'NOCONNECTION') { type.value = CONNECTION_DISCONNECTION; diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 927606ad1..819ca1d39 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -76,6 +76,9 @@ class PairingController extends GetxController { bool isShowSkipButton = false; //是否显示跳过按钮 + final availableDevices = [].obs; // 可用设备列表 + final selectedDevice = Rx(null); // 用户选中的设备 + // ========== 监听器管理 ========== final List _workers = []; Timer? _bleTimeoutTimer; @@ -385,13 +388,22 @@ class PairingController extends GetxController { void _checkCurrentStatus() async { try { // 1. 检查蓝牙状态 - if (_bluetoothManager.bluetoothState == 'off') { + final results = await Future.wait([ + _checkBluetoothState(), // 检查蓝牙状态 + _checkPermissions(), // 检查权限 + _bluetoothManager.refreshDeviceConnection(), // 刷新设备连接 + ]); + + final isBluetoothOk = results[0] as bool; + final arePermissionsOk = results[1] as bool; + + if (!isBluetoothOk) { _setError(ErrorType.bluetoothOff, 'pleaseEnableBluetooth'.tr); return; } // 2. 检查权限 - if (!await _checkPermissions()) { + if (!arePermissionsOk) { _setError(ErrorType.noPermission, 'pleaseGrantNecessaryPermissions'.tr); return; } @@ -416,6 +428,11 @@ class PairingController extends GetxController { } } + /// 新增一个辅助方法来检查蓝牙状态 + Future _checkBluetoothState() async { + return _bluetoothManager.bluetoothState != 'off'; + } + /// 检查权限 Future _checkPermissions() async { // Android权限检查 @@ -553,10 +570,10 @@ class PairingController extends GetxController { try { // 设置超时定时器 _bleTimeoutTimer?.cancel(); - _bleTimeoutTimer = Timer(const Duration(seconds: 30), () { + _bleTimeoutTimer = Timer(const Duration(seconds: 20), () { if (_currentState.value == PairingState.waitingBle) { _setError(ErrorType.bleTimeout, - 'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重试 + 'bleConnectionTimeoutRetry'.tr); //BLE连接超时,请重启耳机后再试 } }); @@ -587,18 +604,29 @@ class PairingController extends GetxController { } } - //自动绑定设备 0 未合作商 1 合作商 2,已绑定 + //自动绑定设备 0 未合作商 1 合作商 9经典蓝牙已绑定 10,ble蓝牙已绑定 Future authbindingDevice(BluetoothDevice device) async { - // if (User.instance.earphoneauthcode != "" && - // User.instance.earphonemac == device.address) { - // // 已经绑定 - // return 9; - // } - // 检查设备名称是否包含 DeepSound 或 DeapSound final deviceName = device.name; final names = AppConfig.allproductNames(); + // 检查是否已有绑定的设备 + print("device.address${device.address}"); + final existingDevice = User.instance.devices.firstWhere( + (d) => d.devicetype == 1 && d.devicemac == device.address, + orElse: () => UserDevice( + id: -1, + uid: '', + devicetype: -1, + devicename: '', + devicemac: '', + license: ''), + ); + if (existingDevice.id != -1) { + // 已经绑定 + return 9; + } + print( '获取到 deviceName: $deviceName address: ${device.address} names:$names'); if (names.contains(deviceName)) { @@ -649,14 +677,26 @@ class PairingController extends GetxController { _bleManager.stopScan(); deviceCode.value = code; _verifiedDeviceAddress = device.address; - // 检查是否已经绑定 - // if (User.instance.earphonelicense != "" && - // User.instance.earphonemac == device.address) { - // // 已经绑定 - // return 10; - // } else { - return 1; - // } + //检查是否已经绑定 + final existingDevice = User.instance.devices.firstWhere( + (d) => + d.devicetype == 2 && + d.license != "" && + d.devicemac == device.address, + orElse: () => UserDevice( + id: -1, + uid: '', + devicetype: -1, + devicename: '', + devicemac: '', + license: ''), + ); + if (existingDevice.id != -1) { + // 已经绑定 + return 10; + } else { + return 1; + } } } else { break; @@ -667,10 +707,11 @@ class PairingController extends GetxController { } await Future.delayed(Duration(seconds: 1)); } - return 0; - } else { - return 0; + + return 1; } + return 0; + //可以跳转1 } // 新增方法:从hex数据中提取MAC地址并与当前设备MAC对比 @@ -794,7 +835,7 @@ class PairingController extends GetxController { /// 导航到主页 void _navigateToHome() { - Future.delayed(const Duration(seconds: 1), () async { + Future.delayed(const Duration(milliseconds: 500), () async { // 启动AgentService服务 try { Logger.i(_tag, '启动AgentService服务'); @@ -905,4 +946,13 @@ class PairingController extends GetxController { Logger.e(_tag, '清除设备关联失败: $e'); } } + + // 用户选择设备后调用 + void onDeviceSelected(BluetoothDevice device) { + Logger.i(_tag, '用户选择了设备: ${device.name} (${device.address})'); + selectedDevice.value = device; + _currentClassicDevice = device; + _transition(PairingState.verifying); + _verifyDevice(device); + } } diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 1f2bf9b31..a059ab6a8 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -3,9 +3,68 @@ import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/pairing_controller.dart'; -class PairingView extends GetView { +class PairingView extends StatefulWidget { const PairingView({Key? key}) : super(key: key); + @override + State createState() => _PairingViewState(); +} + +class _PairingViewState extends State + with TickerProviderStateMixin { + late AnimationController _pulseController; + late AnimationController _rotationController; + late Animation _pulseAnimation; + late Animation _rotationAnimation; + + PairingController get controller => Get.find(); + + @override + void initState() { + super.initState(); + + // 脉搏动画控制器 + _pulseController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + // 旋转动画控制器 + _rotationController = AnimationController( + duration: const Duration(milliseconds: 2000), + vsync: this, + ); + + // 脉搏动画 + _pulseAnimation = Tween( + begin: 0.8, + end: 1.2, + ).animate(CurvedAnimation( + parent: _pulseController, + curve: Curves.easeInOut, + )); + + // 旋转动画 + _rotationAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _rotationController, + curve: Curves.linear, + )); + + // 开始动画 + _pulseController.repeat(reverse: true); + _rotationController.repeat(); + } + + @override + void dispose() { + _pulseController.dispose(); + _rotationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; @@ -30,7 +89,7 @@ class PairingView extends GetView { padding: EdgeInsets.symmetric(horizontal: 24.w), // 添加SingleChildScrollView以防止溢出 child: SingleChildScrollView( - physics: BouncingScrollPhysics(), + physics: const BouncingScrollPhysics(), child: ConstrainedBox( constraints: BoxConstraints( minHeight: @@ -98,8 +157,8 @@ class PairingView extends GetView { final state = controller.currentState; final isLoading = controller.isLoading.value; // 根据状态决定动画颜色 - Color primaryColor = Color(0xFF0066CC); - Color secondaryColor = Color(0xFF5E5CE6); + Color primaryColor = const Color(0xFF0066CC); + Color secondaryColor = const Color(0xFF5E5CE6); // 错误状态使用红色 if (state == PairingState.error) { @@ -114,7 +173,7 @@ class PairingView extends GetView { // 待机状态的特殊处理 if (state == PairingState.idle || state == PairingState.waitingClassic) { - return Container( + return SizedBox( width: 240.w, height: 240.w, child: Stack( @@ -129,8 +188,8 @@ class PairingView extends GetView { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - primaryColor.withOpacity(0.1), - secondaryColor.withOpacity(0.1), + primaryColor.withValues(alpha: 0.1), + secondaryColor.withValues(alpha: 0.1), ], ), shape: BoxShape.circle, @@ -159,51 +218,25 @@ class PairingView extends GetView { ); } - return Container( + return SizedBox( width: 240.w, height: 240.w, child: Stack( alignment: Alignment.center, children: [ - // 脉搏背景 - 只在加载时显示动画 - if (isLoading) - TweenAnimationBuilder( - tween: Tween(begin: 0.8, end: 1.1), - duration: const Duration(seconds: 2), - curve: Curves.easeInOut, - builder: (context, value, child) { - return Container( - width: 200.w * value, - height: 200.w * value, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - primaryColor.withOpacity(0.3), - secondaryColor.withOpacity(0.3), - ], - ), - shape: BoxShape.circle, - ), - ); - }, - onEnd: () { - // 重新开始动画 - }, - ), + // 连续脉搏动画 - 只在加载时显示 + if (isLoading) _buildPulseAnimation(primaryColor, secondaryColor), + + // 特殊的安全连接动画 - 只在建立安全连接时显示 + if (state == PairingState.waitingBle) + _buildSecureConnectionAnimation(primaryColor, secondaryColor), + + // 设备关联动画 - 只在关联设备时显示 + if (state == PairingState.associating) + _buildAssociatingAnimation(primaryColor, secondaryColor), // 旋转边框 - 只在加载时显示 - if (isLoading) - SizedBox( - width: 100.w, - height: 100.w, - child: CircularProgressIndicator( - strokeWidth: 4.w, - valueColor: AlwaysStoppedAnimation(primaryColor), - backgroundColor: Colors.grey.withOpacity(0.2), - ), - ), + if (isLoading) _buildRotatingBorder(primaryColor), // 设备图标 Container( @@ -229,6 +262,128 @@ class PairingView extends GetView { }); } + // 构建连续脉搏动画 + Widget _buildPulseAnimation(Color primaryColor, Color secondaryColor) { + return AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final value = _pulseAnimation.value; + final opacity = 0.4 - (_pulseController.value * 0.3); // 0.4 到 0.1 的透明度变化 + + return Container( + width: 200.w * value, + height: 200.w * value, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + primaryColor.withValues(alpha: opacity), + secondaryColor.withValues(alpha: opacity), + ], + ), + shape: BoxShape.circle, + ), + ); + }, + ); + } + + // 构建旋转边框动画 + Widget _buildRotatingBorder(Color primaryColor) { + return AnimatedBuilder( + animation: _rotationAnimation, + builder: (context, child) { + return Transform.rotate( + angle: _rotationAnimation.value * 2 * 3.14159, // 完整旋转 + child: SizedBox( + width: 140.w, + height: 140.w, + child: CircularProgressIndicator( + strokeWidth: 3.w, + valueColor: AlwaysStoppedAnimation(primaryColor), + backgroundColor: Colors.grey.withValues(alpha: 0.2), + value: 0.7, // 显示70%的圆弧 + ), + ), + ); + }, + ); + } + + // 构建安全连接特殊动画 - 多层波纹效果 + Widget _buildSecureConnectionAnimation(Color primaryColor, Color secondaryColor) { + return Stack( + alignment: Alignment.center, + children: [ + // 第一层波纹 + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final value = _pulseAnimation.value; + final opacity = 0.3 - (_pulseController.value * 0.2); + + return Container( + width: 180.w * value, + height: 180.w * value, + decoration: BoxDecoration( + border: Border.all( + color: primaryColor.withValues(alpha: opacity), + width: 2.w, + ), + shape: BoxShape.circle, + ), + ); + }, + ), + + // 第二层波纹 - 延迟动画 + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final delayedValue = (_pulseController.value + 0.3) % 1.0; + final value = 0.8 + (delayedValue * 0.4); + final opacity = 0.25 - (delayedValue * 0.15); + + return Container( + width: 160.w * value, + height: 160.w * value, + decoration: BoxDecoration( + border: Border.all( + color: secondaryColor.withValues(alpha: opacity), + width: 1.5.w, + ), + shape: BoxShape.circle, + ), + ); + }, + ), + + // 第三层波纹 - 更大延迟 + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + final delayedValue = (_pulseController.value + 0.6) % 1.0; + final value = 0.8 + (delayedValue * 0.4); + final opacity = 0.2 - (delayedValue * 0.1); + + return Container( + width: 140.w * value, + height: 140.w * value, + decoration: BoxDecoration( + border: Border.all( + color: primaryColor.withValues(alpha: opacity), + width: 1.w, + ), + shape: BoxShape.circle, + ), + ); + }, + ), + ], + ); + } + // 根据状态获取图标 IconData _getIconForState(PairingState state) { switch (state) { @@ -352,7 +507,7 @@ class PairingView extends GetView { // 已连接设备信息 Widget _buildConnectedDevice(device, bool isDarkMode) { - return Container( + return SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -443,7 +598,7 @@ class PairingView extends GetView { final showSkipButton = controller.showSkipButton.value; final showAutoCodeInput = controller.showAutoCodeInput.value; final showSkipBindingButton = controller.showSkipBindingButton.value; - return Container( + return SizedBox( width: double.infinity, child: Column( children: [ @@ -452,7 +607,7 @@ class PairingView extends GetView { ElevatedButton( onPressed: () => controller.onConnectTap(), style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF0066CC), + backgroundColor: const Color(0xFF0066CC), foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 16.h), shape: RoundedRectangleBorder( @@ -572,7 +727,7 @@ class PairingView extends GetView { TextButton( onPressed: () => controller.onSkipTap(), style: TextButton.styleFrom( - foregroundColor: Color(0xFF0066CC), + foregroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), minimumSize: Size(double.infinity, 48.h), ), @@ -592,7 +747,7 @@ class PairingView extends GetView { TextButton( onPressed: () => controller.onSkipBindingTap(), style: TextButton.styleFrom( - foregroundColor: Color(0xFF0066CC), + foregroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), minimumSize: Size(double.infinity, 48.h), ), @@ -611,7 +766,7 @@ class PairingView extends GetView { TextButton( onPressed: () => controller.onRetryTap(), style: TextButton.styleFrom( - foregroundColor: Color(0xFF0066CC), + foregroundColor: const Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), minimumSize: Size(double.infinity, 48.h), ), @@ -629,4 +784,89 @@ class PairingView extends GetView { ); }); } + + // 构建设备关联动画 - 连接线条效果 + Widget _buildAssociatingAnimation(Color primaryColor, Color secondaryColor) { + return AnimatedBuilder( + animation: _rotationAnimation, + builder: (context, child) { + return Stack( + alignment: Alignment.center, + children: [ + // 旋转的连接线条 + Transform.rotate( + angle: _rotationAnimation.value * 2 * 3.14159, + child: Container( + width: 160.w, + height: 160.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: primaryColor.withValues(alpha: 0.6), + width: 2.w, + ), + ), + child: Stack( + children: [ + // 连接点1 + Positioned( + top: 10.w, + left: 80.w - 4.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + // 连接点2 + Positioned( + bottom: 10.w, + left: 80.w - 4.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: secondaryColor, + shape: BoxShape.circle, + ), + ), + ), + // 连接点3 + Positioned( + top: 80.w - 4.w, + left: 10.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + // 连接点4 + Positioned( + top: 80.w - 4.w, + right: 10.w, + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: secondaryColor, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ), + ), + ], + ); + }, + ); + } } diff --git a/lib/modules/pairing/views/pairing_view_new.dart b/lib/modules/pairing/views/pairing_view_new.dart index acb400810..b4a2f94d2 100644 --- a/lib/modules/pairing/views/pairing_view_new.dart +++ b/lib/modules/pairing/views/pairing_view_new.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/pairing_controller.dart'; -import '../../../core/themes/app_theme.dart'; /// 配对视图 - 简化版本 class PairingView extends GetView { @@ -94,9 +93,9 @@ class PairingView extends GetView { Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), + color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12.r), - border: Border.all(color: Colors.red.withOpacity(0.3)), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), ), child: Row( children: [ @@ -184,7 +183,7 @@ class PairingView extends GetView { width: 100.w, height: 100.w, decoration: BoxDecoration( - color: iconColor.withOpacity(0.1), + color: iconColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index 260b4ef73..a26ec587e 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -1,4 +1,4 @@ -import 'package:deep_voice/data/services/user_portrait.dart'; +import '/data/services/user_portrait.dart'; import 'package:dio/dio.dart' as dio; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -31,6 +31,7 @@ class ProfileController extends GetxController { final RxBool hasEdits = false.obs; // 是否有编辑 final RxBool isUpdating = false.obs; // 是否正在更新用户信息 final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态) + final RxBool isAvatarReady = false.obs; // 头像数据是否已准备好,用于防止闪烁 //音乐播放管理 final _userportraitManager = Get.find(); // 用于乐观更新的原始状态备份 @@ -91,7 +92,7 @@ class ProfileController extends GetxController { currentLanguage.value = languageCode; } } catch (e) { - print('Failed to load language preference: $e'); + Logger().e('Failed to load language preference: $e'); } } @@ -101,7 +102,7 @@ class ProfileController extends GetxController { await _storage.write('languageCode', locale.languageCode); await _storage.write('countryCode', locale.countryCode); } catch (e) { - print('Failed to save language preference: $e'); + Logger().e('Failed to save language preference: $e'); } } @@ -191,6 +192,15 @@ class ProfileController extends GetxController { // } // } + // 强制回滚未保存的更改(在页面退出时调用) + void forceRollbackIfUnsaved() { + if (hasEdits.value) { + Logger().d('检测到未保存的编辑,执行强制回滚'); + _rollbackToOriginalState(); // 复用现有的回滚方法 + } + isEditingNickname.value = false; + } + // 更新用户信息到后端 Future _syncWithBackend() async { try { @@ -289,19 +299,23 @@ class ProfileController extends GetxController { } } - void loadUserInfo() { + void loadUserInfo() async { final userJson = _storage.read(userKey); if (userJson != null) { userInfo.value = User.instance; Logger().d('从本地加载用户信息: $userJson'); - // 加载本地头像路径并验证文件有效性 + // 重置头像准备状态 + isAvatarReady.value = false; + + // 同步加载本地头像路径并验证文件有效性 - 修复闪烁问题 final savedLocalAvatar = _storage.read('local_avatar'); if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) { final avatarPath = savedLocalAvatar.toString(); - // 验证文件是否存在 + // 同步验证文件是否存在,避免异步导致的闪烁 final file = File(avatarPath); - file.exists().then((exists) { + try { + final exists = await file.exists(); if (exists) { localAvatar.value = avatarPath; Logger().d('加载本地头像路径: ${localAvatar.value}'); @@ -311,18 +325,26 @@ class ProfileController extends GetxController { _storage.remove('local_avatar'); Logger().w('本地头像文件不存在,已清除路径: $avatarPath'); } - }); + } catch (e) { + // 文件访问异常,清除无效路径 + localAvatar.value = ''; + _storage.remove('local_avatar'); + Logger().e('验证本地头像文件时发生异常: $e'); + } } else { // 确保本地头像路径为空 localAvatar.value = ''; Logger().d('没有本地头像路径'); } + // 头像数据准备完成 + isAvatarReady.value = true; + // 保存原始状态用于回滚 _saveOriginalState(); - // 如果有远程头像地址,下载并保存到本地 - if (userInfo.value.avatar.isNotEmpty) { + // 如果有远程头像地址且本地头像不存在,才下载并保存到本地 + if (userInfo.value.avatar.isNotEmpty && localAvatar.value.isEmpty) { Logger().d('远程头像地址: ${userInfo.value.avatar}'); _downloadAndSaveAvatar(userInfo.value.avatar); } @@ -333,11 +355,12 @@ class ProfileController extends GetxController { } // 确保本地状态与服务器状态的一致性 - void _ensureStateConsistency() { - // 检查本地头像文件是否存在 + void _ensureStateConsistency() async { + // 同步检查本地头像文件是否存在 - 修复闪烁问题 if (localAvatar.value.isNotEmpty) { final file = File(localAvatar.value); - file.exists().then((exists) { + try { + final exists = await file.exists(); if (!exists) { Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}'); localAvatar.value = ''; @@ -348,7 +371,11 @@ class ProfileController extends GetxController { _downloadAndSaveAvatar(userInfo.value.avatar); } } - }); + } catch (e) { + Logger().e('检查本地头像文件一致性时发生异常: $e'); + localAvatar.value = ''; + _storage.remove('local_avatar'); + } } } @@ -577,15 +604,6 @@ class ProfileController extends GetxController { _saveOriginalState(); await _syncLocalStorage(); - // 同步更新 SettingsController - if (Get.isRegistered()) { - final settingsController = Get.find(); - settingsController.userInfo.value.name = userInfo.value.name; - settingsController.userInfo.value.avatar = - '${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}'; - settingsController.userInfo.refresh(); - } - // 重置编辑状态 hasEdits.value = false; diff --git a/lib/modules/profile/views/profile_meeting_view.dart b/lib/modules/profile/views/profile_meeting_view.dart new file mode 100644 index 000000000..dcda97711 --- /dev/null +++ b/lib/modules/profile/views/profile_meeting_view.dart @@ -0,0 +1,488 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../controllers/profile_controller.dart'; +import 'unified_avatar.dart'; + +class ProfileMeetingView extends GetView { + ProfileMeetingView({super.key}); + + // 昵称编辑控制器 - 与 profile_view.dart 保持一致 + final TextEditingController nameController = TextEditingController(); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ); + } + + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + // 初始化 nameController 的文本内容 + nameController.text = controller.userInfo.value.name; + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black87, + size: 20.w, + ), + onPressed: () { + controller.forceRollbackIfUnsaved(); + Get.back(); + }, + ), + title: Text( + '账号和资料', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + centerTitle: true, + actions: [ + Obx(() => Visibility( + visible: controller.hasEdits.value, + child: controller.isUpdating.value + ? Padding( + padding: EdgeInsets.all(16.w), + child: SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue, + ), + ), + ), + ) + : TextButton( + onPressed: () async { + // 保存用户名和头像 + await controller + .updateUserNameAndAvatar(nameController.text); + // 如果保存成功(没有编辑状态),延迟返回上一级页面让用户看到成功提示 + if (!controller.hasEdits.value) { + // 延迟跳转,让用户看到成功提示 + Future.delayed(const Duration(milliseconds: 500), () { + if (Get.isRegistered()) { + Get.back(); + } + }); + } + }, + child: Text( + 'save'.tr, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.blue[300] : Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), + )) + ], + ); + } + + Widget _buildBody(bool isDarkMode) { + return WillPopScope( + onWillPop: () async { + controller.forceRollbackIfUnsaved(); + return true; + }, + child: SafeArea( + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 16.w), + children: [ + SizedBox(height: 4.h), + _buildAvatarSection(isDarkMode), + SizedBox(height: 16.h), + _buildNicknameItem(isDarkMode), + Divider( + height: 0.5.h, + color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), + _buildLoginMethodItem(isDarkMode), + Divider( + height: 0.5.h, + color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), + _buildManageLoginItem(isDarkMode), + Divider( + height: 0.5.h, + color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), + SizedBox(height: 32.h), + _buildLogoutButton(isDarkMode), + SizedBox(height: 16.h), + _buildDeleteAccountButton(isDarkMode), + SizedBox(height: 8.h), + _buildWarningText(), + SizedBox(height: 32.h), + ], + ), + )); + } + + Widget _buildAvatarSection(bool isDarkMode) { + return Center( + child: Column( + children: [ + // 使用统一头像组件,避免闪烁问题 + GestureDetector( + onTap: () => controller.updateUserAvatar(), + child: Stack( + children: [ + UnifiedAvatar( + radius: 40.r, + backgroundColor: + isDarkMode ? Colors.grey[800] : Colors.grey[200], + iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600], + iconSize: 30.r, + isDarkMode: isDarkMode, + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 22.w, + height: 22.w, + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[600] : Colors.grey[350], + shape: BoxShape.circle, + border: Border.all( + color: isDarkMode ? Colors.black : Colors.white, + width: 0.w, + ), + ), + child: Icon( + Icons.camera_alt, + size: 14.w, + color: Colors.black, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + // 昵称设置项(双模式内联编辑)- 与 profile_view.dart 完全相同的逻辑 + Widget _buildNicknameItem(bool isDarkMode) { + return Obx(() { + // 根据编辑状态显示不同的UI + if (controller.isEditingNickname.value) { + return _buildNicknameEditingMode(isDarkMode, nameController); + } else { + return _buildNicknameDisplayMode(isDarkMode, nameController); + } + }); + } + + // 构建昵称显示模式 - 适配 profile_meeting_view 的 ListTile 样式 + Widget _buildNicknameDisplayMode( + bool isDarkMode, TextEditingController nameController) { + return ListTile( + onTap: () { + // 开始内联编辑 + nameController.text = controller.userInfo.value.name; + controller.startEditingNickname(); + }, + // 禁用水波纹效果 + splashColor: Colors.transparent, + title: Text( + 'nickname'.tr, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx(() => Text( + controller.userInfo.value.name.isNotEmpty + ? controller.userInfo.value.name + : '未设置昵称', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.white : Colors.grey[600], + ), + )), + SizedBox(width: 8.w), + Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ], + ), + ); + } + + // 构建昵称编辑模式 - 适配 profile_meeting_view 的 ListTile 样式 + Widget _buildNicknameEditingMode( + bool isDarkMode, TextEditingController nameController) { + return ListTile( + title: Text( + 'nickname'.tr, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 编辑输入框 - 替换原来的昵称文本显示区域 + Container( + constraints: BoxConstraints( + minWidth: 80.w, // 确保有足够的编辑空间 + maxWidth: 120.w, // 限制最大宽度保持布局稳定 + ), + child: TextField( + controller: nameController, + autofocus: true, + textAlign: TextAlign.right, // 右对齐,与原昵称显示保持一致 + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.white : Colors.grey[600], + ), + decoration: InputDecoration( + hintText: '输入昵称', + hintStyle: TextStyle( + color: isDarkMode ? Colors.grey[500] : Colors.grey[400], + fontSize: 16.sp, + ), + border: InputBorder.none, // 移除边框,保持简洁 + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, // 移除内边距 + isDense: true, + ), + onSubmitted: (value) { + // 按回车确认编辑 + controller.finishEditingNickname(value); + }, + onTapOutside: (event) { + // 点击外部区域确认编辑 + controller.finishEditingNickname(nameController.text); + }, + ), + ), + SizedBox(width: 8.w), + // 保持原有的箭头图标位置,但在编辑模式下可以考虑隐藏或改变 + Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ], + ), + ); + } + + Widget _buildLoginMethodItem(bool isDarkMode) { + return ListTile( + title: Text( + '登录方式', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + trailing: Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ); + } + + Widget _buildManageLoginItem(bool isDarkMode) { + return ListTile( + title: Text( + '管理我的登录', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + trailing: Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ); + } + + Widget _buildLogoutButton(bool isDarkMode) { + return SizedBox( + width: double.infinity, + height: 48.h, + child: OutlinedButton( + onPressed: () => _showLogoutDialog(isDarkMode), + style: OutlinedButton.styleFrom( + backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200], + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + child: Text( + '退出登录', + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + ), + ); + } + + Widget _buildDeleteAccountButton(bool isDarkMode) { + return SizedBox( + width: double.infinity, + height: 48.h, + child: OutlinedButton( + onPressed: () => _showDeleteAccountDialog(isDarkMode), + style: OutlinedButton.styleFrom( + side: BorderSide.none, + backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + child: Text( + '删除账号', + style: TextStyle( + fontSize: 16.sp, + color: Colors.red[600], + ), + ), + ), + ); + } + + Widget _buildWarningText() { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: '警告:', + style: TextStyle( + fontSize: 12.sp, // 稍微大一点的字号 + color: Colors.red[800], // 更深的红色 + //fontStyle: FontStyle.italic, // 斜体 + ), + ), + TextSpan( + text: '您的账号信息和所有文件将被永久删除。', + style: TextStyle( + fontSize: 11.sp, + color: Colors.grey[600], + ), + ), + ], + ), + textAlign: TextAlign.center, + ); + } + +//退出登录提示窗 + void _showLogoutDialog(bool isDarkMode) { + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + '退出登录', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + '确定要退出当前账号吗?\n请确认所有转写任务已完成。', + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + controller.signOut(); + }, + child: Text( + 'confirm'.tr, + style: const TextStyle( + color: Colors.red, + ), + ), + ), + ], + ), + ); + } + + void _showDeleteAccountDialog(bool isDarkMode) { + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + '删除账号', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + '警告:您的账号信息和所有文件将被永久删除。', + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + controller.deleteAccount(); + }, + child: Text( + 'confirm'.tr, + style: const TextStyle( + color: Colors.red, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index dc61e98b4..6f18f6d9f 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -7,6 +7,7 @@ import '../../../routes/app_routes.dart'; import 'dart:io'; import 'dart:ui'; import '../../common/webview_page.dart'; +import 'unified_avatar.dart'; // ProfileView: 用户设置页面 // class ProfileView extends GetView { @@ -157,7 +158,10 @@ class EditProfileView extends GetView { size: 20.sp, color: isDarkMode ? Colors.white : Colors.black, ), - onPressed: () => Get.back(), + onPressed: () { + controller.forceRollbackIfUnsaved(); + Get.back(); + }, ), // title: Text( // 'editProfile'.tr, @@ -213,15 +217,23 @@ class EditProfileView extends GetView { )) ], ), - body: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildEditProfileSection(context, isDarkMode, nameController), - SizedBox(height: 16.h), - _buildAccountSettings(isDarkMode), - SizedBox(height: 16.h), - ]))); + // ignore: deprecated_member_use + body: WillPopScope( + onWillPop: () async { + ProfileController.to.forceRollbackIfUnsaved(); + return true; + }, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildEditProfileSection( + context, isDarkMode, nameController), + SizedBox(height: 16.h), + _buildAccountSettings(isDarkMode), + SizedBox(height: 16.h), + ])))); } // 新增:构建编辑资料区域的方法 @@ -326,20 +338,14 @@ class EditProfileView extends GetView { // --- 右侧内容保持不变 --- Row( children: [ - Obx(() => CircleAvatar( - radius: 26.r, // 调整头像大小以适应行高 - backgroundImage: controller.localAvatar.value.isNotEmpty - ? FileImage(File(controller.localAvatar.value)) - : (controller.userInfo.value.avatar.isNotEmpty - ? NetworkImage(controller.userInfo.value.avatar) - : null) as ImageProvider?, - child: (controller.localAvatar.value.isEmpty && - controller.userInfo.value.avatar.isEmpty) - ? Icon(Icons.person, - size: 20.r, color: Colors.grey) // 默认图标 - : null, - backgroundColor: Colors.grey[200], // 添加背景色防止透明图片问题 - )), + // 使用统一头像组件,避免闪烁问题 + UnifiedAvatar( + radius: 26.r, + backgroundColor: Colors.grey[200], + iconColor: Colors.grey, + iconSize: 20.r, + isDarkMode: isDarkMode, + ), SizedBox(width: 8.w), Container( width: 32.w, diff --git a/lib/modules/profile/views/unified_avatar.dart b/lib/modules/profile/views/unified_avatar.dart new file mode 100644 index 000000000..d25a8a8f4 --- /dev/null +++ b/lib/modules/profile/views/unified_avatar.dart @@ -0,0 +1,137 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../controllers/profile_controller.dart'; + +/// 统一头像显示组件 +/// 确保所有页面的头像显示逻辑一致,避免闪烁问题 +class UnifiedAvatar extends StatelessWidget { + final double radius; + final Color? backgroundColor; + final Color? iconColor; + final double? iconSize; + final bool isDarkMode; + final VoidCallback? onTap; + + const UnifiedAvatar({ + super.key, + required this.radius, + this.backgroundColor, + this.iconColor, + this.iconSize, + this.isDarkMode = false, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final ProfileController profileCtrl = Get.find(); + + return GestureDetector( + onTap: onTap, + child: Obx(() { + // 等待头像数据准备完成,避免闪烁 + if (!profileCtrl.isAvatarReady.value) { + return _buildLoadingAvatar(); + } + + return _buildAvatar(profileCtrl); + }), + ); + } + + /// 构建加载中的头像占位符 + Widget _buildLoadingAvatar() { + return CircleAvatar( + radius: radius, + backgroundColor: + backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), + child: Icon( + Icons.person, + size: iconSize ?? (radius * 0.6), + color: iconColor ?? (isDarkMode ? Colors.grey[400] : Colors.grey[600]), + ), + ); + } + + /// 构建实际头像 + Widget _buildAvatar(ProfileController profileCtrl) { + ImageProvider? imageProvider; + + // 统一头像显示优先级:本地头像 > 网络头像 > 默认图标 + if (profileCtrl.localAvatar.value.isNotEmpty) { + imageProvider = FileImage(File(profileCtrl.localAvatar.value)); + } else if (profileCtrl.userInfo.value.avatar.isNotEmpty) { + imageProvider = NetworkImage(profileCtrl.userInfo.value.avatar); + } + + return CircleAvatar( + radius: radius, + backgroundImage: imageProvider, + backgroundColor: + backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), + child: (profileCtrl.localAvatar.value.isEmpty && + profileCtrl.userInfo.value.avatar.isEmpty) + ? Icon( + Icons.person, + size: iconSize ?? (radius * 0.6), + color: iconColor ?? + (isDarkMode ? Colors.grey[400] : Colors.grey[600]), + ) + : null, + ); + } +} + +/// 带容器装饰的统一头像组件 +class UnifiedAvatarWithContainer extends StatelessWidget { + final double width; + final double height; + final double radius; + final Color? backgroundColor; + final Color? iconColor; + final double? iconSize; + final bool isDarkMode; + final VoidCallback? onTap; + final EdgeInsetsGeometry? margin; + final List? boxShadow; + + const UnifiedAvatarWithContainer({ + super.key, + required this.width, + required this.height, + required this.radius, + this.backgroundColor, + this.iconColor, + this.iconSize, + this.isDarkMode = false, + this.onTap, + this.margin, + this.boxShadow, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + margin: margin, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + boxShadow: boxShadow, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: UnifiedAvatar( + radius: radius / 2, + backgroundColor: backgroundColor, + iconColor: iconColor, + iconSize: iconSize, + isDarkMode: isDarkMode, + onTap: onTap, + ), + ), + ); + } +} diff --git a/lib/modules/settings/bindings/settings_binding.dart b/lib/modules/settings/bindings/settings_binding.dart index f8ff3eb08..dae35e08d 100644 --- a/lib/modules/settings/bindings/settings_binding.dart +++ b/lib/modules/settings/bindings/settings_binding.dart @@ -1,11 +1,11 @@ import 'package:get/get.dart'; +import '../../profile/controllers/profile_controller.dart'; import '../controllers/settings_controller.dart'; class SettingsBinding extends Bindings { @override void dependencies() { - Get.lazyPut( - () => SettingsController(), - ); + Get.lazyPut(() => SettingsController()); + Get.lazyPut(() => ProfileController()); } -} \ No newline at end of file +} diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 6df73311d..5b92b5474 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -121,10 +121,8 @@ class SettingsController extends GetxController { RxString timbreName = 'defaultTimbre'.obs; //用户信息 - static const String userKey = 'user_info'; - //final _storage=GetStorage(); - //final ImagePicker _picker = ImagePicker(); - final Rx userInfo = User.instance.obs; //使用单例实例 + // static const String userKey = 'user_info'; + // final Rx userInfo = User.instance.obs; //使用单例实例 // 添加版本信息变量 final appVersion = ''.obs; @@ -197,14 +195,6 @@ class SettingsController extends GetxController { ]; // 检查单个权限状态 - Future checkPermission(Permission permission) async { - return await permission.status; - } - - // 请求单个权限 - Future requestPermission(Permission permission) async { - return await permission.request(); - } // 打开应用设置 void openSettings() { @@ -244,7 +234,7 @@ class SettingsController extends GetxController { _loadCurrentSpeechServiceType(); //获取用户信息 - loadUserInfo(); + // loadUserInfo(); // 获取应用版本信息 getAppVersion(); @@ -264,71 +254,71 @@ class SettingsController extends GetxController { } } - void loadUserInfo() { - final userJson = _storage.read(userKey); - Logger().d('加载用户信息: $userJson'); - - if (userJson != null) { - userInfo.value = User.instance; - Logger().d('用户信息已加载: ${userInfo.value.toJson()}'); - - if (userInfo.value.avatar.isNotEmpty) { - Logger().d('开始下载头像'); - _downloadAndSaveAvatar(userInfo.value.avatar); - } else { - Logger().d('用户没有头像'); - } - } else { - Logger().d('没有找到用户信息'); - } - } + // void loadUserInfo() { + // final userJson = _storage.read(userKey); + // Logger().d('加载用户信息: $userJson'); + + // if (userJson != null) { + // userInfo.value = User.instance; + // Logger().d('用户信息已加载: ${userInfo.value.toJson()}'); + + // if (userInfo.value.avatar.isNotEmpty) { + // Logger().d('开始下载头像'); + // _downloadAndSaveAvatar(userInfo.value.avatar); + // } else { + // Logger().d('用户没有头像'); + // } + // } else { + // Logger().d('没有找到用户信息'); + // } + // } - Future saveUserInfo() async { - await _storage.write(userKey, userInfo.value.toJson()); - } + // Future saveUserInfo() async { + // await _storage.write(userKey, userInfo.value.toJson()); + // } // 下载并保存远程头像 - Future _downloadAndSaveAvatar(String url) async { - try { - Logger().d('开始下载头像: $url'); - - final dio = Dio(BaseOptions( - headers: { - 'accept': '*/*', - 'Authorization': _storage.read("logintoken"), - }, - )); - - final response = await dio.get( - url, - options: Options(responseType: ResponseType.bytes), - ); - - Logger().d('下载状态: ${response.statusCode}'); - - if (response.statusCode == 200) { - // 保存到应用文档目录 - final appDir = await getApplicationSupportDirectory(); - final fileName = - '/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final localPath = '${appDir.path}/$fileName'; - -// 确保目录存在 - final file = File(localPath); - await file.parent.create(recursive: true); - - await file.writeAsBytes(response.data); - - // 更新本地头像路径 - //localAvatar.value = localPath; - await _storage.write('local_avatar', localPath); - Logger().d('头像已下载到本地: $localPath'); - //Get.snackbar('头像', '头像已下载到本地'); - } - } catch (e) { - Logger().e('下载头像失败: $e'); - } - } +// Future _downloadAndSaveAvatar(String url) async { +// try { +// Logger().d('开始下载头像: $url'); + +// final dio = Dio(BaseOptions( +// headers: { +// 'accept': '*/*', +// 'Authorization': _storage.read("logintoken"), +// }, +// )); + +// final response = await dio.get( +// url, +// options: Options(responseType: ResponseType.bytes), +// ); + +// Logger().d('下载状态: ${response.statusCode}'); + +// if (response.statusCode == 200) { +// // 保存到应用文档目录 +// final appDir = await getApplicationSupportDirectory(); +// final fileName = +// '/UserAvatar/avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; +// final localPath = '${appDir.path}/$fileName'; + +// // 确保目录存在 +// final file = File(localPath); +// await file.parent.create(recursive: true); + +// await file.writeAsBytes(response.data); + +// // 更新本地头像路径 +// //localAvatar.value = localPath; +// await _storage.write('local_avatar', localPath); +// Logger().d('头像已下载到本地: $localPath'); +// //Get.snackbar('头像', '头像已下载到本地'); +// } +// } catch (e) { +// Logger().e('下载头像失败: $e'); +// } +// } // Future updateUserAvatar() async { // final XFile? image = await _picker.pickImage( @@ -348,16 +338,16 @@ class SettingsController extends GetxController { // } // } - Future updateUserName(String name) async { - if (name.trim().isNotEmpty) { - userInfo.update((user) { - if (user != null) { - user.name = name.trim(); - } - }); - await saveUserInfo(); - } - } + // Future updateUserName(String name) async { + // if (name.trim().isNotEmpty) { + // userInfo.update((user) { + // if (user != null) { + // user.name = name.trim(); + // } + // }); + // await saveUserInfo(); + // } + // } // Future updateDeviceName(String deviceName) async { // userInfo.update((user) { @@ -514,8 +504,9 @@ class SettingsController extends GetxController { // 显示已是最新版本的提示 Get.snackbar( 'checkUpdate'.tr, - 'currentLatestVersion' //当前已是最新版本 - .trParams({'version': updateInfo.currentVersion}), + 'currentLatestVersion' + .tr //当前已是最新版本 + .replaceAll('{version}', updateInfo.currentVersion), snackPosition: SnackPosition.TOP, backgroundColor: Colors.green.withOpacity(0.8), colorText: Colors.white, diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index a7d241f80..2c5b7849e 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -3,13 +3,15 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:ui'; +import 'dart:io'; import '../../../core/theme/app_colors.dart'; import '../controllers/settings_controller.dart'; import '../../../core/widgets/common_bottom_nav.dart'; import '../../../routes/app_routes.dart'; //import '../../../data/models/user_Info.dart'; -import 'dart:io'; import '../../common/webview_page.dart'; +import '../../../modules/profile/controllers/profile_controller.dart'; +import '../../profile/views/unified_avatar.dart'; class SettingsView extends GetView { const SettingsView({Key? key}) : super(key: key); @@ -60,6 +62,8 @@ class SettingsView extends GetView { // 用户信息卡片 Widget _buildUserInfoCard(bool isDarkMode) { + final ProfileController profileCtrl = + Get.find(); // 从GetX中获取ProfileController实例 return GestureDetector( onTap: () => Get.toNamed(Routes.editProfile), child: Container( @@ -95,77 +99,24 @@ class SettingsView extends GetView { : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Row( children: [ - Obx(() => GestureDetector( - //onTap: ()=>controller.updateUserAvatar(), - child: Container( - //用户头像部分 - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: ClipRRect( - // 裁剪圆形头像 - borderRadius: BorderRadius.circular(30.r), - child: controller.userInfo.value.avatar.isEmpty - ? Container( - decoration: BoxDecoration( - color: Colors.blue[600], - borderRadius: BorderRadius.circular(30.r), - ), - child: Center( - child: Text( - controller - .userInfo.value.name.isNotEmpty - ? controller.userInfo.value.name[0] - .toUpperCase() - : '?', - style: TextStyle( - color: Colors.white, - fontSize: 24.sp, - fontWeight: FontWeight.bold, - ), - ), - ), - ) - : Image.network( - controller.userInfo.value.avatar, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - // 当图片加载失败时显示用户名首字母 - return Container( - decoration: BoxDecoration( - color: Colors.blue[600], - borderRadius: - BorderRadius.circular(30.r), - ), - child: Center( - child: Text( - controller.userInfo.value.name - .isNotEmpty - ? controller - .userInfo.value.name[0] - .toUpperCase() - : '?', - style: TextStyle( - color: Colors.white, - fontSize: 24.sp, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - }, - )), - ), - )), + // 使用统一头像组件,避免闪烁问题 + UnifiedAvatarWithContainer( + width: 60.w, + height: 60.w, + radius: 30.r, + backgroundColor: + isDarkMode ? Colors.grey[800] : Colors.grey[200], + iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600], + iconSize: 30.r, + isDarkMode: isDarkMode, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), SizedBox(width: 16.w), Expanded( // 占据剩余空间 @@ -175,7 +126,7 @@ class SettingsView extends GetView { children: [ Text( //用户名 - controller.userInfo.value.name, + profileCtrl.userInfo.value.name, style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, @@ -200,7 +151,7 @@ class SettingsView extends GetView { 2: 'proUser', //'专业用户' }; final membershipString = membershipMap[ - controller.userInfo.value.viplv] ?? + profileCtrl.userInfo.value.viplv] ?? 'Unknown'; return Text( membershipString.tr, // 会员等级 @@ -228,7 +179,7 @@ class SettingsView extends GetView { ), SizedBox(height: 4.h), Text( - 'ID:${controller.userInfo.value.uid}', + 'ID:${profileCtrl.userInfo.value.uid}', style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.black54, @@ -456,6 +407,8 @@ class SettingsView extends GetView { ? Colors.white.withOpacity(0.1) : Colors.grey[200]), + + // 用户反馈 _buildSimpleNavigationSetting( title: 'feedback'.tr, // 用户反馈 @@ -715,8 +668,8 @@ class SettingsView extends GetView { ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + // 左侧应用信息区域 - 使用固定宽度 Row( children: [ Container( @@ -770,36 +723,48 @@ class SettingsView extends GetView { ), ], ), - InkWell( - onTap: () { - // 调用检查更新方法 - controller.checkForUpdate(); - }, - child: Container( - constraints: BoxConstraints( - minWidth: 100.w, - maxWidth: 180.w, - ), - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 8.h, - ), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.blue[900]!.withOpacity(0.3) - : Colors.blue[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Text( - 'checkUpdates'.tr, // 检查更新 - style: TextStyle( - fontSize: 12.sp, - color: - isDarkMode ? Colors.blue[300] : Colors.blue[600], + + // 间距 + SizedBox(width: 16.w), + + // 右侧检查更新按钮 - 使用Expanded确保充分利用剩余空间 + Expanded( + child: InkWell( + onTap: () { + // 调用检查更新方法 + controller.checkForUpdate(); + }, + child: Container( + constraints: BoxConstraints( + minHeight: 36.h, // 设置最小高度而不是宽度约束 + ), + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 8.h, + ), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.blue[900]!.withOpacity(0.3) + : Colors.blue[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, // 如果文本过长,自动缩小字体 + child: Text( + 'checkUpdates'.tr, // 检查更新 + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode + ? Colors.blue[300] + : Colors.blue[600], + height: 1.2, // 稍微减少行高,适应FittedBox + ), + textAlign: TextAlign.center, + maxLines: 2, // 最多显示2行,配合FittedBox使用 + ), + ), ), - overflow: TextOverflow.ellipsis, // 添加省略号 - maxLines: 1, // 限制为单行 - textAlign: TextAlign.center, // 保持居中 ), ), ), @@ -848,7 +813,7 @@ class SettingsView extends GetView { //------------测试使用标记 start---------------// SizedBox(height: 4.h), Text( - '${Platform.operatingSystem}-test-0707', + '${Platform.operatingSystem}-test-0721', style: TextStyle( fontSize: 12.sp, color: const Color.fromARGB(255, 143, 94, 94), @@ -905,8 +870,10 @@ class SettingsView extends GetView { fontSize: 16.sp, fontWeight: FontWeight.w500, color: isDarkMode ? Colors.white : Colors.black87, + height: 1.2, // 设置行高,提升多行文本可读性 ), - overflow: TextOverflow.ellipsis, + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), if (subtitle != null) SizedBox(height: 2.h), if (subtitle != null) @@ -915,8 +882,10 @@ class SettingsView extends GetView { style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.black54, + height: 1.3, // 设置行高,提升多行文本可读性 ), - overflow: TextOverflow.ellipsis, + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), ], ), @@ -980,8 +949,10 @@ class SettingsView extends GetView { fontSize: 16.sp, fontWeight: FontWeight.w500, color: isDarkMode ? Colors.white : Colors.black87, + height: 1.2, // 设置行高,提升多行文本可读性 ), - overflow: TextOverflow.ellipsis, + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), if (subtitle != null) SizedBox(height: 2.h), if (subtitle != null) @@ -991,8 +962,10 @@ class SettingsView extends GetView { fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.black54, + height: 1.3, // 设置行高,提升多行文本可读性 ), - overflow: TextOverflow.ellipsis, + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), ], ), @@ -1085,8 +1058,10 @@ class SettingsView extends GetView { fontWeight: FontWeight.w500, color: titleColor ?? (isDarkMode ? Colors.white : Colors.black87), + height: 1.2, // 设置行高,提升多行文本可读性 ), - overflow: TextOverflow.ellipsis, + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), if (subtitle != null) SizedBox(height: 2.h), if (subtitle != null) @@ -1098,8 +1073,10 @@ class SettingsView extends GetView { (isDarkMode ? Colors.white70 : Colors.black54), + height: 1.3, // 设置行高,提升多行文本可读性 ), - overflow: TextOverflow.ellipsis, + softWrap: true, // 启用软换行 + overflow: TextOverflow.visible, // 确保文本完全显示 ), ], ), diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 4f9dc5a3c..76b7ff177 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -76,7 +76,7 @@ class SplashController extends GetxController { // await _initDatabase(); // 检查版本更新(在后台进行,不阻塞启动流程) - _checkVersionUpdate(); + //_checkVersionUpdate(); _isResourceLoaded.value = true; _checkNavigateCondition(); @@ -159,21 +159,21 @@ class SplashController extends GetxController { } /// 检查版本更新(后台执行,不阻塞启动流程) - void _checkVersionUpdate() { - // 延迟执行版本检查,避免影响启动速度 - Timer(const Duration(seconds: 1), () async { - try { - final versionService = VersionUpdateService.to; - final updateInfo = await versionService.checkForUpdate(); - - if (updateInfo.hasUpdate) { - // 显示更新对话框 - await VersionUpdateDialog.show(updateInfo); - } - } catch (e) { - print('版本检查失败: $e'); - // 版本检查失败不影响用户使用,静默处理 - } - }); - } + // void _checkVersionUpdate() { + // // 延迟执行版本检查,避免影响启动速度 + // Timer(const Duration(seconds: 1), () async { + // try { + // final versionService = VersionUpdateService.to; + // final updateInfo = await versionService.checkForUpdate(); + + // if (updateInfo.hasUpdate) { + // // 显示更新对话框 + // await VersionUpdateDialog.show(updateInfo); + // } + // } catch (e) { + // print('版本检查失败: $e'); + // // 版本检查失败不影响用户使用,静默处理 + // } + // }); + // } } diff --git a/lib/modules/translation/bindings/translation_stats_binding.dart b/lib/modules/translation/bindings/translation_stats_binding.dart new file mode 100644 index 000000000..031cdc0ea --- /dev/null +++ b/lib/modules/translation/bindings/translation_stats_binding.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; +import '../controllers/translation_stats_controller.dart'; +import '../../../data/services/translation_usage_service.dart'; + +class TranslationStatsBinding implements Bindings { + @override + void dependencies() { + // 确保服务已注册 + if (!Get.isRegistered()) { + Get.put(TranslationUsageService(), permanent: true); + } + + // 注册控制器 + Get.lazyPut( + () => TranslationStatsController(), + ); + } +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 5c87f7a92..a58ddb678 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -14,8 +14,9 @@ import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; -import '../../../data/models/events/voice_interaction_event.dart'; + import '../../../data/services/asr_service.dart'; +import '../../../data/services/translation_usage_service.dart'; import '../../../data/services/ble_manager.dart'; // 切换音频输出设备的枚举 @@ -39,6 +40,15 @@ class TranslationController extends GetxController { final bleManager = Get.find(); //音乐播放管理 final _musiceManager = Get.find(); + // 统计服务 + late final TranslationUsageService _usageService; + + // ASR统计相关 - 精确时长统计 + DateTime? _asrSessionStartTime; // ASR会话开始时间 + DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间) + Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长 + bool _isAsrActive = false; // ASR是否处于活跃状态 + // 音频输入源 bool _audioSourceType = false; //是否开启播放 @@ -121,15 +131,16 @@ class TranslationController extends GetxController { // 监听识别结果 if (activeSpeaker.value != 0) { isPlayback = false; - if (id == 1) { - await _asrService.restoreOriginalAudioState(); - } else { - await _asrService.disableBluetoothAudio(); - } await _asrService.startContinuousRecognition(_audioSourceType); + // 开始ASR活跃时长跟踪(面对面模式) + _startAsrActiveTracking(); + lastActiveSpeaker.value = id; } else { + // 停止ASR活跃时长跟踪(面对面模式) + _stopAsrActiveTracking(); + if (fTFTranslationResult != '' && !isPlayback) { isPlayback = true; print("松开走了着"); @@ -178,6 +189,9 @@ class TranslationController extends GetxController { super.onInit(); //_setupEventListeners(); + // 初始化统计服务 + _initUsageService(); + // 加载上次保存的语言设置 _loadSavedLanguages(); @@ -204,6 +218,20 @@ class TranslationController extends GetxController { // 参考Agent模块的实现模式 } + // 初始化统计服务 + void _initUsageService() { + try { + // 确保统计服务已注册 + if (!Get.isRegistered()) { + Get.put(TranslationUsageService(), permanent: true); + } + _usageService = Get.find(); + Logger.info('翻译统计服务初始化完成'); + } catch (e) { + Logger.error('翻译统计服务初始化失败: $e'); + } + } + // 加载保存的语言设置 void _loadSavedLanguages() { final savedSourceLanguage = _storage.read(_sourceLanguageKey); @@ -230,9 +258,9 @@ class TranslationController extends GetxController { try { final bool granted = await PermissionUtil.instance.requestPermission( permissionType: Permission.microphone, - permissionName: "录音", - explanationText: "我们需要访问麦克风权限,以便您录制音频内容。", - permanentDenialText: "检测到您已永久拒绝录音权限。如需继续使用此功能,请前往应用设置中手动开启麦克风权限。", + permissionName: 'microphonePermission'.tr, + explanationText: 'microphonePermissionExplanation'.tr, + permanentDenialText: 'microphonePermissionPermanentDenial'.tr, ); if (granted) { @@ -254,9 +282,9 @@ class TranslationController extends GetxController { try { final bool granted = await PermissionUtil.instance.requestPermission( permissionType: Permission.storage, - permissionName: "存储", - explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", - permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", + permissionName: 'storagePermission'.tr, + explanationText: 'storagePermissionExplanation'.tr, + permanentDenialText: 'storagePermissionPermanentDenial'.tr, ); if (granted) { @@ -423,6 +451,17 @@ class TranslationController extends GetxController { } currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); + + // 开始新的翻译会话统计 + try { + _usageService.startNewSession(); + _asrSessionStartTime = DateTime.now(); // 记录ASR会话开始时间 + _asrActiveDuration = Duration.zero; // 重置ASR活跃时长 + _isAsrActive = false; // 重置ASR活跃状态 + } catch (e) { + Logger.error('开始新会话统计失败: $e'); + } + // 开始连续语音识别 if (currentMode.value == "audioVideo") { bleManager.openDecoder(); @@ -446,8 +485,9 @@ class TranslationController extends GetxController { _asrService.stopContinuousRecognition(); } else { await _ttsService.setAudioOutputDevice(0); - final recognitionStream = - await _asrService.startContinuousRecognition(_audioSourceType); + await _asrService.startContinuousRecognition(_audioSourceType); + // 开始ASR活跃时长计时 + _startAsrActiveTracking(); } isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { @@ -486,7 +526,10 @@ class TranslationController extends GetxController { try { await _asrService.stopContinuousRecognition(); - await _asrService.restoreOriginalAudioState(); + + // 停止ASR活跃时长计时 + _stopAsrActiveTracking(); + isRecognizing.value = false; // 如果当前有中间结果,将其作为最终结果处理 @@ -564,6 +607,8 @@ class TranslationController extends GetxController { Future handleFinalResult(String text) async { if (text.isEmpty || translationHistory.isEmpty) return; + // ASR识别到最终结果,表示用户完成了一次有效的语音输入 + // 更新当前项 if (translationHistory.last.isIntermediate) { translationHistory.last.sourceText = text; @@ -666,6 +711,33 @@ class TranslationController extends GetxController { // 【改进】只有在最终结果时才保存,避免中间态时的频繁保存 if (isFinal) { saveTranslationHistory(); + + // 记录翻译API调用统计 - 优化版(精确计费参数) + try { + final sourceLanguageName = _languageManager + .getChineseNameByAsrCode(detectedSourceLanguageCode) ?? + '未知'; + final targetLanguageName = _languageManager + .getChineseNameByAsrCode(detectedTargetLanguageCode) ?? + '未知'; + + // 使用精确的计费参数:只统计源文本字符数(翻译API的实际计费基础) + _usageService.recordTranslationApiCall( + sourceText: sourceText, // 只有源文本用于翻译API计费 + targetText: translationResult, // 目标文本用于TTS计费 + sourceLanguage: sourceLanguageName, + targetLanguage: targetLanguageName, + mode: currentMode.value, + ); + + Logger.info( + '翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName'); + + // 记录ASR统计(在翻译完成时统计ASR使用情况) + _recordAsrUsageStats(sourceLanguageName); + } catch (e) { + Logger.error('记录翻译统计失败: $e'); + } } // 更新后滚动到底部 @@ -711,9 +783,32 @@ class TranslationController extends GetxController { final voiceName = _languageManager .getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural'; - print("voiceName=$voiceName,text$text"); + Logger.info("voiceName=$voiceName,text=$text"); + + final startTime = DateTime.now(); await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(text); + final endTime = DateTime.now(); + + // 记录TTS API调用统计 - 优化版(基于文本字符数计费) + try { + final durationSeconds = endTime.difference(startTime).inSeconds; + final languageName = _languageManager + .getChineseNameByAsrCode(detectedTargetLanguageCode) ?? + '未知'; + + // TTS的主要计费依据是文本字符数,播放时长仅用于分析 + _usageService.recordTtsApiCall( + text: text, // 文本字符数是TTS计费的主要依据 + durationSeconds: durationSeconds, // 播放时长用于用户体验分析 + language: languageName, + ); + + Logger.info( + 'TTS统计: 文本${text.length}字符, 播放${durationSeconds}秒, 语言$languageName'); + } catch (e) { + Logger.error('记录TTS统计失败: $e'); + } } catch (e) { Logger.error('播放翻译失败: ${e.toString()}'); } @@ -730,6 +825,8 @@ class TranslationController extends GetxController { if (isRecognizing.value) { await stopRecognition(); } + // 确保停止ASR活跃时长跟踪 + _stopAsrActiveTracking(); stopRecording(); _translationDebounceTimer?.cancel(); await _ttsService.stop(); @@ -1216,6 +1313,55 @@ class TranslationController extends GetxController { void scrollToBottom({bool animate = true}) { _scrollToBottom(animate: animate); } + + /// 开始ASR活跃时长跟踪 + void _startAsrActiveTracking() { + if (!_isAsrActive) { + _asrActiveStartTime = DateTime.now(); + _isAsrActive = true; + Logger.info('开始ASR活跃时长跟踪'); + } + } + + /// 停止ASR活跃时长跟踪 + void _stopAsrActiveTracking() { + if (_isAsrActive && _asrActiveStartTime != null) { + final activeDuration = DateTime.now().difference(_asrActiveStartTime!); + _asrActiveDuration += activeDuration; + _isAsrActive = false; + _asrActiveStartTime = null; + Logger.info('停止ASR活跃时长跟踪,本次活跃时长: ${activeDuration.inSeconds}秒'); + } + } + + /// 记录ASR使用统计 - 基于精确时长跟踪 + void _recordAsrUsageStats(String language) { + try { + if (_asrSessionStartTime == null) return; + + // 确保停止当前的活跃时长跟踪 + _stopAsrActiveTracking(); + + final now = DateTime.now(); + final totalDurationSeconds = + now.difference(_asrSessionStartTime!).inSeconds; + final effectiveDurationSeconds = _asrActiveDuration.inSeconds; + + _usageService.recordAsrApiCall( + effectiveDurationSeconds: effectiveDurationSeconds, + totalDurationSeconds: totalDurationSeconds, + language: language, + mode: currentMode.value, + audioFormat: 'pcm', + sampleRate: 16000, + ); + + Logger.info( + 'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language'); + } catch (e) { + Logger.error('记录ASR统计失败: $e'); + } + } } // 翻译项模型 diff --git a/lib/modules/translation/controllers/translation_cost_controller.dart b/lib/modules/translation/controllers/translation_cost_controller.dart new file mode 100644 index 000000000..75b7fccff --- /dev/null +++ b/lib/modules/translation/controllers/translation_cost_controller.dart @@ -0,0 +1,258 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import '../../../data/services/translation_usage_service.dart'; +import '../../../core/utils/logger.dart'; + +/// 翻译成本配置控制器 +class TranslationCostController extends GetxController { + // 依赖注入 + final TranslationUsageService _usageService = Get.find(); + + // 成本配置 + final RxMap costConfig = {}.obs; + final RxDouble dailyCostThreshold = 10.0.obs; + final RxDouble monthlyCostThreshold = 300.0.obs; + + // 表单控制器 + final translationCostController = TextEditingController(); + final asrCostController = TextEditingController(); + final ttsCostController = TextEditingController(); + final aiSummaryCostController = TextEditingController(); + final dailyThresholdController = TextEditingController(); + final monthlyThresholdController = TextEditingController(); + + // 状态变量 + final isLoading = false.obs; + final isSaving = false.obs; + + @override + void onInit() { + super.onInit(); + _loadCurrentConfig(); + } + + @override + void onClose() { + // 释放控制器 + translationCostController.dispose(); + asrCostController.dispose(); + ttsCostController.dispose(); + aiSummaryCostController.dispose(); + dailyThresholdController.dispose(); + monthlyThresholdController.dispose(); + super.onClose(); + } + + /// 加载当前配置 + void _loadCurrentConfig() { + isLoading.value = true; + + try { + // 从服务获取当前配置 + costConfig.value = Map.from(_usageService.costConfig); + dailyCostThreshold.value = _usageService.dailyCostThreshold.value; + monthlyCostThreshold.value = _usageService.monthlyCostThreshold.value; + + // 更新表单控制器 + translationCostController.text = (costConfig['translation_per_1k_chars'] ?? 0.05).toString(); + asrCostController.text = (costConfig['asr_per_minute'] ?? 0.02).toString(); + ttsCostController.text = (costConfig['tts_per_1k_chars'] ?? 0.03).toString(); + aiSummaryCostController.text = (costConfig['ai_summary_per_request'] ?? 0.01).toString(); + dailyThresholdController.text = dailyCostThreshold.value.toString(); + monthlyThresholdController.text = monthlyCostThreshold.value.toString(); + + Logger.info('成本配置加载完成'); + } catch (e) { + Logger.error('加载成本配置失败: $e'); + _showErrorSnackbar('加载配置失败'); + } finally { + isLoading.value = false; + } + } + + /// 保存配置 + Future saveConfig() async { + if (isSaving.value) return; + + isSaving.value = true; + + try { + // 验证输入 + if (!_validateInputs()) { + return; + } + + // 更新成本配置 + final newCostConfig = { + 'translation_per_1k_chars': double.parse(translationCostController.text), + 'asr_per_minute': double.parse(asrCostController.text), + 'tts_per_1k_chars': double.parse(ttsCostController.text), + 'ai_summary_per_request': double.parse(aiSummaryCostController.text), + }; + + final newDailyThreshold = double.parse(dailyThresholdController.text); + final newMonthlyThreshold = double.parse(monthlyThresholdController.text); + + // 保存到服务 + _usageService.updateCostConfig(newCostConfig); + _usageService.updateThresholds( + dailyThreshold: newDailyThreshold, + monthlyThreshold: newMonthlyThreshold, + ); + + // 更新本地状态 + costConfig.value = newCostConfig; + dailyCostThreshold.value = newDailyThreshold; + monthlyCostThreshold.value = newMonthlyThreshold; + + _showSuccessSnackbar('配置保存成功'); + Logger.info('成本配置保存成功'); + + } catch (e) { + Logger.error('保存成本配置失败: $e'); + _showErrorSnackbar('保存配置失败'); + } finally { + isSaving.value = false; + } + } + + /// 重置为默认配置 + void resetToDefault() { + translationCostController.text = '0.05'; + asrCostController.text = '0.02'; + ttsCostController.text = '0.03'; + aiSummaryCostController.text = '0.01'; + dailyThresholdController.text = '10.0'; + monthlyThresholdController.text = '300.0'; + + _showInfoSnackbar('已重置为默认配置,请点击保存生效'); + } + + /// 验证输入 + bool _validateInputs() { + try { + final translationCost = double.parse(translationCostController.text); + final asrCost = double.parse(asrCostController.text); + final ttsCost = double.parse(ttsCostController.text); + final aiSummaryCost = double.parse(aiSummaryCostController.text); + final dailyThreshold = double.parse(dailyThresholdController.text); + final monthlyThreshold = double.parse(monthlyThresholdController.text); + + if (translationCost < 0 || asrCost < 0 || ttsCost < 0 || aiSummaryCost < 0) { + _showErrorSnackbar('成本不能为负数'); + return false; + } + + if (dailyThreshold < 0 || monthlyThreshold < 0) { + _showErrorSnackbar('预警阈值不能为负数'); + return false; + } + + if (dailyThreshold > monthlyThreshold) { + _showErrorSnackbar('日预警阈值不能大于月预警阈值'); + return false; + } + + return true; + } catch (e) { + _showErrorSnackbar('请输入有效的数字'); + return false; + } + } + + /// 计算预估月成本 + double calculateEstimatedMonthlyCost() { + try { + final dailyStats = _usageService.todayStats.value; + if (dailyStats.estimatedCost == 0) return 0.0; + + // 简单按30天计算 + return dailyStats.estimatedCost * 30; + } catch (e) { + Logger.error('计算预估月成本失败: $e'); + return 0.0; + } + } + + /// 获取成本节约建议 + List getCostSavingTips() { + final tips = []; + + try { + final dailyStats = _usageService.todayStats.value; + + // 基于使用情况给出建议 + if (dailyStats.translationApiCalls > 100) { + tips.add('考虑批量翻译以减少API调用次数'); + } + + if (dailyStats.ttsApiCalls > 50) { + tips.add('可以关闭自动语音播放以节省TTS成本'); + } + + if (dailyStats.aiSummaryApiCalls > 10) { + tips.add('AI摘要功能按需使用,避免频繁调用'); + } + + if (dailyStats.effectiveAudioDuration > 3600) { // 超过1小时 + tips.add('长时间录音会增加ASR成本,建议分段录音'); + } + + if (tips.isEmpty) { + tips.add('您的使用习惯很好,继续保持!'); + } + + } catch (e) { + Logger.error('获取成本节约建议失败: $e'); + tips.add('暂无建议'); + } + + return tips; + } + + /// 显示成功提示 + void _showSuccessSnackbar(String message) { + Get.snackbar( + '成功', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + colorText: Get.theme.colorScheme.primary, + duration: const Duration(seconds: 3), + ); + } + + /// 显示错误提示 + void _showErrorSnackbar(String message) { + Get.snackbar( + '错误', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), + colorText: Get.theme.colorScheme.error, + duration: const Duration(seconds: 3), + ); + } + + /// 显示信息提示 + void _showInfoSnackbar(String message) { + Get.snackbar( + '提示', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.surface.withOpacity(0.9), + colorText: Get.theme.colorScheme.onSurface, + duration: const Duration(seconds: 3), + ); + } + + /// 格式化成本显示 + String formatCost(double cost) { + return '¥${cost.toStringAsFixed(2)}'; + } + + /// 格式化百分比显示 + String formatPercentage(double percentage) { + return '${percentage.toStringAsFixed(1)}%'; + } +} diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index a33bf50ac..12a586aab 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -6,11 +6,13 @@ import '../controllers/translation_history_controller.dart'; import 'package:get_storage/get_storage.dart'; import 'dart:async'; import '../../../core/utils/logger.dart'; +import '../../../data/services/translation_usage_service.dart'; class TranslationDetailController extends GetxController { // 依赖注入 final VolcanoAIService _aiService = Get.find(); final GetStorage _storage = GetStorage(); + late final TranslationUsageService _usageService; // 存储键前缀 static const String _summaryKeyPrefix = 'translation_summary_'; @@ -33,13 +35,23 @@ class TranslationDetailController extends GetxController { @override void onInit() { super.onInit(); - + + // 初始化统计服务 + try { + if (!Get.isRegistered()) { + Get.put(TranslationUsageService(), permanent: true); + } + _usageService = Get.find(); + } catch (e) { + Logger.error('初始化统计服务失败: $e'); + } + // 从路由参数中获取数据 if (Get.arguments != null) { currentItem = Get.arguments['item']; sessionItems = Get.arguments['sessionItems']; historyController = Get.arguments['historyController']; - + // 异步加载数据,避免阻塞主线程 _asyncInitialize(); } else { @@ -170,8 +182,15 @@ class TranslationDetailController extends GetxController { if (result.isNotEmpty) { summaryResult.value = result; saveSummary(result); + + // 记录AI摘要API调用统计 + try { + _usageService.recordAiSummaryApiCall(); + } catch (e) { + Logger.error('记录AI摘要统计失败: $e'); + } } - + return result; } catch (e) { final errorMessage = '生成摘要失败: ${e.toString()}'; diff --git a/lib/modules/translation/controllers/translation_stats_controller.dart b/lib/modules/translation/controllers/translation_stats_controller.dart new file mode 100644 index 000000000..1e103b372 --- /dev/null +++ b/lib/modules/translation/controllers/translation_stats_controller.dart @@ -0,0 +1,358 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import '../../../data/services/translation_usage_service.dart'; +import '../../../data/models/translation_usage_stats.dart'; +import '../../../core/utils/logger.dart'; + +/// 翻译统计页面控制器 +class TranslationStatsController extends GetxController { + // 依赖注入 + final TranslationUsageService _usageService = + Get.find(); + + // 状态变量 + final isLoading = false.obs; + final selectedTabIndex = 0.obs; // 0=今日, 1=本周, 2=本月, 3=历史 + final selectedTimeRange = '今日'.obs; + + // 统计数据 + final Rx todayStats = + TranslationUsageStats.empty('').obs; + final Rx weekStats = + TranslationUsageStats.empty('').obs; + final Rx monthStats = + TranslationUsageStats.empty('').obs; + final RxList historyStats = + [].obs; + + // 实时统计数据 + RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value; + + // 成本配置 + final RxMap costConfig = {}.obs; + final RxDouble dailyCostThreshold = 10.0.obs; + final RxDouble monthlyCostThreshold = 300.0.obs; + + // 图表数据 + final RxList> chartData = >[].obs; + + @override + void onInit() { + super.onInit(); + _initializeData(); + } + + /// 初始化数据 + void _initializeData() { + // 监听使用服务的数据变化 + ever(_usageService.todayStats, (stats) { + todayStats.value = stats; + _updateChartData(); + }); + + ever(_usageService.costConfig, (config) { + costConfig.value = config; + }); + + ever(_usageService.dailyCostThreshold, (threshold) { + dailyCostThreshold.value = threshold; + }); + + ever(_usageService.monthlyCostThreshold, (threshold) { + monthlyCostThreshold.value = threshold; + }); + + // 初始加载数据 + loadAllStats(); + } + + /// 加载所有统计数据 + Future loadAllStats() async { + isLoading.value = true; + + try { + // 加载今日数据 + todayStats.value = _usageService.todayStats.value; + + // 加载本周数据 + await _loadWeekStats(); + + // 加载本月数据 + await _loadMonthStats(); + + // 加载历史数据(最近7天) + await _loadHistoryStats(); + + // 更新图表数据 + _updateChartData(); + + Logger.info('统计数据加载完成'); + } catch (e) { + Logger.error('加载统计数据失败: $e'); + Get.snackbar( + '加载失败', + '统计数据加载失败,请稍后重试', + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), + colorText: Get.theme.colorScheme.error, + ); + } finally { + isLoading.value = false; + } + } + + /// 加载本周统计数据 + Future _loadWeekStats() async { + try { + final now = DateTime.now(); + final weekStart = now.subtract(Duration(days: now.weekday - 1)); + final weekEnd = weekStart.add(const Duration(days: 6)); + + final dailyStats = await _usageService.getHistoryStats( + startDate: weekStart, + endDate: weekEnd, + ); + + weekStats.value = _aggregateStats(dailyStats, 'week'); + } catch (e) { + Logger.error('加载本周统计失败: $e'); + } + } + + /// 加载本月统计数据 + Future _loadMonthStats() async { + try { + final now = DateTime.now(); + monthStats.value = + await _usageService.getMonthlyStats(now.year, now.month); + } catch (e) { + Logger.error('加载本月统计失败: $e'); + } + } + + /// 加载历史统计数据 + Future _loadHistoryStats() async { + try { + final endDate = DateTime.now(); + final startDate = endDate.subtract(const Duration(days: 7)); + + historyStats.value = await _usageService.getHistoryStats( + startDate: startDate, + endDate: endDate, + ); + + // 按日期排序 + historyStats.sort((a, b) => b.date.compareTo(a.date)); + } catch (e) { + Logger.error('加载历史统计失败: $e'); + } + } + + /// 聚合统计数据 + TranslationUsageStats _aggregateStats( + List statsList, String period) { + if (statsList.isEmpty) { + return TranslationUsageStats.empty(_usageService.getCurrentUserId()); + } + + int totalTranslationCalls = 0; + int totalAsrCalls = 0; + int totalTtsCalls = 0; + int totalAiSummaryCalls = 0; + int totalSourceChars = 0; + int totalTargetChars = 0; + int totalRecordingDuration = 0; + int totalTtsPlayDuration = 0; + int totalSessions = 0; + double totalCost = 0.0; + + final Map combinedLanguagePairUsage = {}; + final Map combinedModeUsage = {}; + + for (final stats in statsList) { + totalTranslationCalls += stats.translationApiCalls; + totalAsrCalls += stats.asrApiCalls; + totalTtsCalls += stats.ttsApiCalls; + totalAiSummaryCalls += stats.aiSummaryApiCalls; + totalSourceChars += stats.sourceTextCharacters; + totalTargetChars += stats.ttsTextCharacters; + totalRecordingDuration += stats.effectiveAudioDuration; + totalTtsPlayDuration += stats.totalSessionDuration; + totalSessions += stats.sessionCount; + totalCost += stats.estimatedCost; + + // 合并语言对使用统计(使用兼容性getter) + stats.languagePairUsage.forEach((key, value) { + combinedLanguagePairUsage[key] = + (combinedLanguagePairUsage[key] ?? 0) + value; + }); + + // 合并模式使用统计(使用兼容性getter) + stats.modeUsage.forEach((key, value) { + combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; + }); + } + + return TranslationUsageStats( + id: '${period}_${DateTime.now().millisecondsSinceEpoch}', + date: DateTime.now(), + userId: 'current_user', + timeGranularity: period, + translationApiCalls: totalTranslationCalls, + asrApiCalls: totalAsrCalls, + ttsApiCalls: totalTtsCalls, + aiSummaryApiCalls: totalAiSummaryCalls, + sourceTextCharacters: totalSourceChars, + ttsTextCharacters: totalTargetChars, + effectiveAudioDuration: totalRecordingDuration, + totalSessionDuration: totalTtsPlayDuration, + sessionCount: totalSessions, + estimatedCost: totalCost, + ); + } + + /// 更新图表数据 + void _updateChartData() { + final List> data = []; + + // 根据选中的时间范围生成图表数据 + switch (selectedTabIndex.value) { + case 0: // 今日 + data.add({ + 'label': '今日', + 'apiCalls': todayStats.value.totalApiCalls, + 'cost': todayStats.value.estimatedCost, + 'characters': todayStats.value.totalCharacterCount, + }); + break; + case 1: // 本周 + data.add({ + 'label': '本周', + 'apiCalls': weekStats.value.totalApiCalls, + 'cost': weekStats.value.estimatedCost, + 'characters': weekStats.value.totalCharacterCount, + }); + break; + case 2: // 本月 + data.add({ + 'label': '本月', + 'apiCalls': monthStats.value.totalApiCalls, + 'cost': monthStats.value.estimatedCost, + 'characters': monthStats.value.totalCharacterCount, + }); + break; + case 3: // 历史 + for (final stats in historyStats.take(7)) { + data.add({ + 'label': '${stats.date.month}/${stats.date.day}', + 'apiCalls': stats.totalApiCalls, + 'cost': stats.estimatedCost, + 'characters': stats.totalCharacterCount, + }); + } + break; + } + + chartData.value = data; + } + + /// 切换时间范围 + void switchTimeRange(int index) { + selectedTabIndex.value = index; + + switch (index) { + case 0: + selectedTimeRange.value = '今日'; + break; + case 1: + selectedTimeRange.value = '本周'; + break; + case 2: + selectedTimeRange.value = '本月'; + break; + case 3: + selectedTimeRange.value = '历史'; + break; + } + + _updateChartData(); + } + + /// 获取当前显示的统计数据 + TranslationUsageStats get currentStats { + switch (selectedTabIndex.value) { + case 0: + return todayStats.value; + case 1: + return weekStats.value; + case 2: + return monthStats.value; + default: + return todayStats.value; + } + } + + /// 刷新数据 + Future refreshData() async { + await loadAllStats(); + } + + /// 清理过期数据 + void cleanupOldData() { + _usageService.cleanupOldData(); + Get.snackbar( + '清理完成', + '已清理90天前的过期数据', + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + colorText: Get.theme.colorScheme.primary, + ); + } + + /// 导出统计数据 + Future exportStats() async { + try { + // TODO: 实现数据导出功能 + Get.snackbar( + '导出功能', + '数据导出功能正在开发中', + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + colorText: Get.theme.colorScheme.primary, + ); + } catch (e) { + Logger.error('导出统计数据失败: $e'); + } + } + + /// 格式化时长显示 + String formatDuration(int seconds) { + if (seconds < 60) { + return '${seconds}秒'; + } else if (seconds < 3600) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + return '${minutes}分${remainingSeconds}秒'; + } else { + final hours = seconds ~/ 3600; + final minutes = (seconds % 3600) ~/ 60; + return '${hours}小时${minutes}分'; + } + } + + /// 格式化成本显示 + String formatCost(double cost) { + return '¥${cost.toStringAsFixed(2)}'; + } + + /// 格式化数字显示 + String formatNumber(int number) { + if (number < 1000) { + return number.toString(); + } else if (number < 1000000) { + return '${(number / 1000).toStringAsFixed(1)}K'; + } else { + return '${(number / 1000000).toStringAsFixed(1)}M'; + } + } +} diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index afe3e668d..629348420 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -110,7 +110,7 @@ class TranslationDetailView extends GetView { isDarkMode: isDarkMode, )), ), - //屏蔽“记录摘要”,需要是开启 + //屏蔽“记录摘要”,需要时开启 // SizedBox(width: 12.w), // Expanded( // child: Obx(() => _buildTabButton( diff --git a/lib/modules/translation/views/translation_stats_view.dart b/lib/modules/translation/views/translation_stats_view.dart new file mode 100644 index 000000000..22773ddcb --- /dev/null +++ b/lib/modules/translation/views/translation_stats_view.dart @@ -0,0 +1,757 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../controllers/translation_stats_controller.dart'; +import '../../../core/theme/app_colors.dart'; + +/// 翻译统计页面 +class TranslationStatsView extends GetView { + const TranslationStatsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: Obx(() => controller.isLoading.value + ? _buildLoadingWidget() + : _buildBody(isDarkMode)), + ); + } + + /// 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, + elevation: 0.5, + title: Text( + '翻译统计', // translationStats + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + actions: [ + // 刷新按钮 + IconButton( + icon: Icon( + Icons.refresh, + color: isDarkMode ? Colors.white : Colors.black, + ), + onPressed: controller.refreshData, + ), + // 更多操作菜单 + PopupMenuButton( + icon: Icon( + Icons.more_vert, + color: isDarkMode ? Colors.white : Colors.black, + ), + onSelected: (value) { + switch (value) { + case 'cleanup': + controller.cleanupOldData(); + break; + case 'export': + controller.exportStats(); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'cleanup', + child: Text('清理过期数据'), + ), + const PopupMenuItem( + value: 'export', + child: Text('导出数据'), + ), + ], + ), + ], + ); + } + + /// 构建加载组件 + Widget _buildLoadingWidget() { + return const Center( + child: CircularProgressIndicator(), + ); + } + + /// 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Column( + children: [ + // 时间范围选择器 + _buildTimeRangeSelector(isDarkMode), + // 统计内容 + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + // 实时统计卡片 + if (controller.selectedTabIndex.value == 0) + _buildRealtimeStatsCard(isDarkMode), + SizedBox(height: 16.h), + // 概览统计卡片 + _buildOverviewCard(isDarkMode), + SizedBox(height: 16.h), + // API调用统计 + _buildApiCallsCard(isDarkMode), + SizedBox(height: 16.h), + // 数据量统计 + _buildDataVolumeCard(isDarkMode), + SizedBox(height: 16.h), + // 成本统计 + _buildCostCard(isDarkMode), + SizedBox(height: 16.h), + // 使用行为统计 + _buildUsageBehaviorCard(isDarkMode), + ], + ), + ), + ), + ], + ); + } + + /// 构建时间范围选择器 + Widget _buildTimeRangeSelector(bool isDarkMode) { + return Container( + margin: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Obx(() => Row( + children: [ + _buildTimeRangeTab('今日', 0, isDarkMode), + _buildTimeRangeTab('本周', 1, isDarkMode), + _buildTimeRangeTab('本月', 2, isDarkMode), + _buildTimeRangeTab('历史', 3, isDarkMode), + ], + )), + ); + } + + /// 构建时间范围标签 + Widget _buildTimeRangeTab(String title, int index, bool isDarkMode) { + final isSelected = controller.selectedTabIndex.value == index; + + return Expanded( + child: GestureDetector( + onTap: () => controller.switchTimeRange(index), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12.h), + decoration: BoxDecoration( + color: isSelected + ? (isDarkMode ? AppColors.primary : AppColors.primary) + : Colors.transparent, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + color: isSelected + ? Colors.white + : (isDarkMode ? Colors.white70 : Colors.black54), + ), + ), + ), + ), + ); + } + + /// 构建实时统计卡片 + Widget _buildRealtimeStatsCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Row( + children: [ + Icon( + Icons.access_time, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '当前会话统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '翻译次数', + controller.realtimeStats.translationCount.toString(), + Icons.translate, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '会话时长', + '${controller.realtimeStats.sessionDurationMinutes}分钟', + Icons.timer, + isDarkMode, + ), + ), + ], + )), + SizedBox(height: 12.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '字符数', + controller.formatNumber( + controller.realtimeStats.sourceCharacters + + controller.realtimeStats.targetCharacters, + ), + Icons.text_fields, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '录音时长', + controller.formatDuration(controller.realtimeStats.recordingDuration), + Icons.mic, + isDarkMode, + ), + ), + ], + )), + ], + ), + ); + } + + /// 构建概览卡片 + Widget _buildOverviewCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Row( + children: [ + Icon( + Icons.dashboard, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Obx(() => Text( + '${controller.selectedTimeRange.value}概览', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + )), + ], + ), + SizedBox(height: 16.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '总调用次数', + controller.formatNumber(controller.currentStats.totalApiCalls), + Icons.api, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '预估成本', + controller.formatCost(controller.currentStats.estimatedCost), + Icons.attach_money, + isDarkMode, + ), + ), + ], + )), + SizedBox(height: 12.h), + Obx(() => Row( + children: [ + Expanded( + child: _buildStatItem( + '会话数量', + controller.currentStats.sessionCount.toString(), + Icons.chat, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '总字符数', + controller.formatNumber(controller.currentStats.totalCharacterCount), + Icons.text_snippet, + isDarkMode, + ), + ), + ], + )), + ], + ), + ); + } + + /// 构建统计项 + Widget _buildStatItem(String label, String value, IconData icon, bool isDarkMode) { + return Column( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 24.sp, + ), + SizedBox(height: 8.h), + Text( + value, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ], + ); + } + + /// 构建API调用统计卡片 + Widget _buildApiCallsCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Row( + children: [ + Icon( + Icons.api, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + 'API调用统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + _buildApiCallRow('翻译API', controller.currentStats.translationApiCalls, Icons.translate, isDarkMode), + SizedBox(height: 12.h), + _buildApiCallRow('语音识别', controller.currentStats.asrApiCalls, Icons.mic, isDarkMode), + SizedBox(height: 12.h), + _buildApiCallRow('语音合成', controller.currentStats.ttsApiCalls, Icons.volume_up, isDarkMode), + SizedBox(height: 12.h), + _buildApiCallRow('AI摘要', controller.currentStats.aiSummaryApiCalls, Icons.summarize, isDarkMode), + ], + )), + ], + ), + ); + } + + /// 构建API调用行 + Widget _buildApiCallRow(String label, int count, IconData icon, bool isDarkMode) { + return Row( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 18.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + controller.formatNumber(count), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ); + } + + /// 构建数据量统计卡片 + Widget _buildDataVolumeCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Row( + children: [ + Icon( + Icons.data_usage, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '数据量统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + Row( + children: [ + Expanded( + child: _buildStatItem( + '源文本字符', + controller.formatNumber(controller.currentStats.sourceTextCharacters), + Icons.text_fields, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + 'TTS文本字符', + controller.formatNumber(controller.currentStats.ttsTextCharacters), + Icons.translate, + isDarkMode, + ), + ), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: _buildStatItem( + '有效音频时长', + controller.formatDuration(controller.currentStats.effectiveAudioDuration), + Icons.mic, + isDarkMode, + ), + ), + Expanded( + child: _buildStatItem( + '总会话时长', + controller.formatDuration(controller.currentStats.totalSessionDuration), + Icons.volume_up, + isDarkMode, + ), + ), + ], + ), + ], + )), + ], + ), + ); + } + + /// 构建成本统计卡片 + Widget _buildCostCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Row( + children: [ + Icon( + Icons.attach_money, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '成本统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + // 当前成本 + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${controller.selectedTimeRange.value}预估成本', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + Text( + controller.formatCost(controller.currentStats.estimatedCost), + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ], + ), + ), + SizedBox(height: 12.h), + // 预警阈值 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '日成本预警阈值', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + Text( + controller.formatCost(controller.dailyCostThreshold.value), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ], + ), + SizedBox(height: 4.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '月成本预警阈值', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + Text( + controller.formatCost(controller.monthlyCostThreshold.value), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ], + ), + ], + )), + ], + ), + ); + } + + /// 构建使用行为统计卡片 + Widget _buildUsageBehaviorCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : 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: [ + Row( + children: [ + Icon( + Icons.analytics, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '使用行为统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() => Column( + children: [ + // 最常用语言对 + _buildUsageBehaviorRow( + '最常用语言对', + controller.currentStats.mostUsedLanguagePair, + Icons.language, + isDarkMode, + ), + SizedBox(height: 12.h), + // 最常用翻译模式 + _buildUsageBehaviorRow( + '最常用模式', + controller.currentStats.mostUsedMode, + Icons.settings, + isDarkMode, + ), + ], + )), + ], + ), + ); + } + + /// 构建使用行为行 + Widget _buildUsageBehaviorRow(String label, String value, IconData icon, bool isDarkMode) { + return Row( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 18.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ); + } +} diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 6c4a1cec4..d69278376 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -664,32 +664,35 @@ class TranslationView extends GetView { void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { Get.dialog( Dialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24.r), + borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 ), insetPadding: EdgeInsets.symmetric(horizontal: 20.w), - backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, - child: Container( - width: 0.9.sw, - padding: EdgeInsets.only(top: 32.h, bottom: 16.h), + child: Padding( + // 使用Padding替代Container的padding + padding: EdgeInsets.all(12.w), // 统一内边距 child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 标题区域 Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.symmetric(horizontal: 8.w), child: Text( isSource - ? 'selectSourceLanguage'.tr // 选择源语言 - : 'selectTargetLanguage'.tr, // 选择目标语言 + ? 'selectSourceLanguage'.tr + : 'selectTargetLanguage'.tr, // 保留源语言/目标语言区分 style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black, + fontSize: 18.sp, // 调整字号 + fontWeight: FontWeight.w600, // 使用数字字重 + color: isDarkMode ? Colors.white : Colors.black87, // 优化文本颜色 ), ), ), SizedBox(height: 16.h), + + // 语言列表区域 Container( constraints: BoxConstraints(maxHeight: 0.5.sh), child: SingleChildScrollView( @@ -697,52 +700,105 @@ class TranslationView extends GetView { mainAxisSize: MainAxisSize.min, children: controller.supportedLanguages.keys.map((language) { - return ListTile( - title: Text( - '${language}'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, + return Obx(() { + final currentLanguage = isSource + ? controller.sourceLanguage.value + : controller.targetLanguage.value; + final isSelected = currentLanguage == language; + + return GestureDetector( + // 改用手势检测器实现点击 + onTap: () { + if (isSource) { + controller.setSourceLanguage(language); + } else { + controller.setTargetLanguage(language); + } + Get.back(); + }, + child: Container( + margin: EdgeInsets.only(bottom: 10.h), // 添加项间距 + padding: EdgeInsets.all(10.w), // 统一内边距 + decoration: BoxDecoration( + color: isSelected // 选中状态背景色 + ? (isDarkMode + ? const Color(0xFF6200EE).withOpacity(0.2) + : Colors.blue.withOpacity(0.1)) + : (isDarkMode // 未选中状态背景色 + ? Colors.white.withOpacity(0.05) + : Colors.grey[200]!), + borderRadius: BorderRadius.circular(12.r), // 项圆角 + border: Border.all( + // 选中状态边框 + color: isSelected + ? (isDarkMode + ? const Color(0xFF6200EE) + : Colors.blue) + : (isDarkMode // 未选中状态背景色 + ? Colors.white.withOpacity(0.05) + : Colors.grey[200]!), + width: 1, + ), + ), + child: Row( + children: [ + // 语言名称 + Expanded( + child: Text( + language.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode + ? Colors.white + : Colors.black87, + overflow: TextOverflow.ellipsis, + ), + ), + ), + // 选中指示器 + Container( + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: isSelected // 动态颜色 + ? (isDarkMode + ? const Color(0xFF6200EE) + : Colors.blue) + : (isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[300]), + shape: BoxShape.circle, // 圆形样式 + ), + child: isSelected + ? Icon( + // 选中时显示对勾 + Icons.check, + size: 16.w, + color: Colors.white, + ) + : null, + ), + ], + ), ), - ), - trailing: Obx(() { - final currentLanguage = isSource - ? controller.sourceLanguage.value - : controller.targetLanguage.value; - return currentLanguage == language - ? Icon( - Icons.check_circle, - color: isDarkMode - ? AppColors.primary - : Colors.blue, - ) - : const SizedBox.shrink(); - }), - onTap: () { - if (isSource) { - controller.setSourceLanguage(language); - } else { - controller.setTargetLanguage(language); - } - Get.back(); - }, - ); + ); + }); }).toList(), ), ), ), SizedBox(height: 16.h), + + // 取消按钮 Align( alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.only(right: 24.w), - child: TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? AppColors.primary : Colors.blue, - fontSize: 16.sp, - ), + child: TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, + style: TextStyle( + color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue, ), ), ), @@ -974,7 +1030,7 @@ class TranslationView extends GetView { // 显示删除成功提示 Get.snackbar( - '删除成功', // deleteSuccess + 'success'.tr, // deleteSuccess '已删除该翻译记录', // deleteTranslationSuccess snackPosition: SnackPosition.BOTTOM, backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], @@ -984,7 +1040,7 @@ class TranslationView extends GetView { } catch (e) { // 显示删除失败提示 Get.snackbar( - '删除失败', // deleteFailed + 'error'.tr, // deleteFailed '删除翻译记录时发生错误: ${e.toString()}', // deleteTranslationError snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red[400], diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 48efd4fe9..724205b30 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -21,6 +21,7 @@ import '../modules/meeting/views/meeting_view.dart'; import '../modules/opus_test/bindings/opus_test_binding.dart'; import '../modules/opus_test/views/opus_test_view.dart'; import '../modules/profile/views/profile_view.dart'; +import '../modules/profile/views/profile_meeting_view.dart'; import '../modules/profile/bindings/profile_binding.dart'; // import '../modules/chat/bindings/chat_binding.dart'; //import '../modules/chat/views/chat_view.dart'; @@ -30,6 +31,8 @@ import '../modules/timbre/views/timbre_view.dart'; import '../modules/translation/views/translation_view.dart'; import '../modules/translation/bindings/translation_binding.dart'; import '../modules/translation/views/translation_history_view.dart'; +import '../modules/translation/views/translation_stats_view.dart'; +import '../modules/translation/bindings/translation_stats_binding.dart'; import '../modules/translation/bindings/translation_list_binding.dart'; import '../modules/translation/views/translation_list_view.dart'; import '../modules/translation/bindings/translation_history_binding.dart'; @@ -129,6 +132,13 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.profileMeeting, + page: () => ProfileMeetingView(), + binding: ProfileBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), GetPage( name: Routes.translation, page: () => const TranslationView(), @@ -143,6 +153,13 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.translationStats, + page: () => const TranslationStatsView(), + binding: TranslationStatsBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), GetPage( name: Routes.translationList, page: () => const TranslationListView(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index e955f025e..6bd4aab32 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,6 +1,6 @@ abstract class Routes { // 认证相关 - static const splash = '/'; + static const splash = '/splash'; static const login = '/login'; static const pairing = '/pairing'; @@ -11,6 +11,7 @@ abstract class Routes { static const chat = '/chat'; static const profile = '/profile'; static const editProfile = '/edit_profile'; + static const profileMeeting = '/profile_meeting'; // 个人信息页面(从会议模块跳转) // 设备管理 static const OTA_UPGRADE = '/ota_upgrade'; // OTA升级 @@ -27,6 +28,7 @@ abstract class Routes { // 语言功能 static const translation = '/translation'; // 实时翻译 static const translationHistory = '/translation/history'; // 翻译历史 + static const translationStats = '/translation/stats'; // 翻译统计 static const speechDemo = '/speech_demo'; // 语音演示 static const translationList = '/translation/List'; // 翻译历史 static const ftftranslation = '/ftftranslation'; // 面对面翻译 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 37ebe489e..1437d4982 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -52,6 +52,9 @@ interface AgentServiceListener { object AgentService : CoroutineScope { private val TAG = "AgentService" + private var azureSpeechKey: String = "" + private var azureSpeechRegion: String = "" + // 协程相关 private val job = SupervisorJob() override val coroutineContext: CoroutineContext @@ -179,6 +182,9 @@ object AgentService : CoroutineScope { LocationService.initialize(appContext) // 设置系统提示词 val customSystemPrompt = config["systemPrompt"] as? String + //保存配置 + this.azureSpeechKey = config["azureSpeechKey"]?.toString() ?: "" + this.azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: "" if (!customSystemPrompt.isNullOrBlank()) { systemPrompt = customSystemPrompt } else { @@ -188,9 +194,15 @@ object AgentService : CoroutineScope { azureAsrHelper = AzureAsrHelper(context).apply { // 使用协程处理异步初始化 CoroutineScope(Dispatchers.IO).launch { + //获取语音识别的语种 + val supportedLanguagesConfig = config["supportedLanguages"] as? List + val supportedLanguages = supportedLanguagesConfig?.toTypedArray() + ?: arrayOf("zh-CN", "en-US") // 默认支持多语言 + val asrInitSuccess = initialize( subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", region = config["azureSpeechRegion"]?.toString() ?: "", + supportedLanguages = supportedLanguages, audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL else AzureAsrHelper.AudioSourceType.MICROPHONE ) @@ -1091,6 +1103,58 @@ object AgentService : CoroutineScope { } } + /** + * 设置支持的语音识别语言 + */ + fun setSupportedLanguages(languages: List): Boolean { + if (!isInitialized) { + Log.e(TAG, "服务未初始化") + sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) + return false + } + + if (languages.isEmpty()) { + Log.e(TAG, "语言列表不能为空") + sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空")) + return false + } + + return try { + Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}") + + // 重新初始化ASR服务以应用新的语言设置 + val success = azureAsrHelper?.initialize( + subscriptionKey = this.azureSpeechKey, + region = this.azureSpeechRegion, + supportedLanguages = languages.toTypedArray(), + audioSourceType = audioSourceType + ) ?: false + + if (success) { + Log.i(TAG, "语言设置更新成功") + sendEvent("languages_updated", mapOf( + "languages" to languages, + "status" to "success" + )) + } else { + Log.e(TAG, "语言设置更新失败") + sendEvent("error", mapOf( + "code" to "LANGUAGE_UPDATE_FAILED", + "message" to "语言设置更新失败" + )) + } + + success + } catch (e: Exception) { + Log.e(TAG, "设置支持语言异常: ${e.message}") + sendEvent("error", mapOf( + "code" to "LANGUAGE_UPDATE_ERROR", + "message" to e.message.toString() + )) + false + } + } + /** * 保存聊天消息 */ diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 1a5d06561..8229f4863 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -201,6 +201,22 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } } + "setSupportedLanguages" -> { + val languages = call.argument>("languages") + if (languages.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "语言列表不能为空", null) + return + } + + try { + val isSuccess = AgentService.setSupportedLanguages(languages) + result.success(isSuccess) + } catch (e: Exception) { + Log.e(TAG, "设置支持语言失败", e) + result.error("SET_LANGUAGES_ERROR", "设置支持语言失败: ${e.message}", null) + } + } + "stopAiSteam" -> { try { AgentService.stopAiStream() diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index e37032b85..753dd87c4 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -27,6 +27,7 @@ class AgentServiceImpl: NSObject { private var azureSpeechKey: String = "" private var azureSpeechRegion: String = "" + private var config: [String: Any] = [:] private var isSpeaking: Bool = false @@ -136,6 +137,8 @@ class AgentServiceImpl: NSObject { if isInitialized { return true } // os_log("initialize: config=%{public}@", log: logger, type: .info, config) + self.config = config + if let openaiApiKey = config["openaiApiKey"] as? String { self.apiKey = openaiApiKey } else { @@ -199,11 +202,14 @@ class AgentServiceImpl: NSObject { private func initializeAzureSpeech() -> Bool { azureTtsHelper?.addListener(self) + + // 获取支持的语言列表,支持从配置中读取 + let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN", "en-US"] guard let asrSuccess = azureAsrHelper?.initialize( subscriptionKey: azureSpeechKey, region: azureSpeechRegion, - supportedLanguages: ["zh-CN"], + supportedLanguages: supportedLanguages, audioSourceType: .microphone ), asrSuccess else { sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") @@ -844,6 +850,54 @@ private func processNavigationCard(_ cardNavigation: [String: Any]) { return "通过test插件集成MacPaw OpenAI SDK v0.4.3 - 集成成功" } + /** + * 设置支持的语音识别语言 + * + * @param languages 支持的语言列表,如 ["zh-CN", "en-US"] + * @return 是否设置成功 + */ + func setSupportedLanguages(_ languages: [String]) -> Bool { + os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) + + if !isInitialized { + os_log("服务未初始化,无法设置语言", log: logger, type: .error) + sendError("服务未初始化", code: "NOT_INITIALIZED") + return false + } + + if languages.isEmpty { + os_log("语言列表为空,使用默认语言", log: logger, type: .info) + return false + } + + // 如果正在识别,需要先停止 + let wasRecognizing = isRecognizing + if wasRecognizing { + _ = stopRecognition() + } + + // 重新初始化ASR引擎,使用新的语言设置 + guard let success = azureAsrHelper?.initialize( + subscriptionKey: azureSpeechKey, + region: azureSpeechRegion, + supportedLanguages: languages, + audioSourceType: .microphone + ), success else { + os_log("重新初始化ASR引擎失败", log: logger, type: .error) + sendError("重新初始化ASR引擎失败", code: "ASR_INIT_ERROR") + return false + } + + os_log("成功设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) + + // 如果之前正在识别,重新开始 + if wasRecognizing { + _ = startRecognition() + } + + return true + } + func dispose() -> Bool { print("ai释放资源") if isRecognizing { diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift index e3ad6b106..1e6be6288 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift @@ -101,6 +101,16 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { let success = impl.dispose() result(success) + case "setSupportedLanguages": + guard let arguments = call.arguments as? [String: Any], + let languages = arguments["languages"] as? [String] else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "语言列表不能为空", details: nil)) + return + } + + let success = impl.setSupportedLanguages(languages) + result(success) + default: result(FlutterMethodNotImplemented) } diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 664b3faad..ede6b56dc 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -332,6 +332,21 @@ class AgentService { } } + /// 设置支持的语音识别语言 + /// + /// [languages] 支持的语言列表,如 ['zh-CN', 'en-US'] + /// 返回是否设置成功 + static Future setSupportedLanguages(List languages) async { + try { + final bool result = await _channel.invokeMethod('setSupportedLanguages', { + 'languages': languages, + }); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details); + } + } + /// 停止语音合成 static Future stopAiSteam() async { try { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 36891f2b8..35fa385de 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -87,7 +87,7 @@ class AzureAsrHelper(private val context: Context) { fun initialize( subscriptionKey: String, region: String, - supportedLanguages: Array = arrayOf("zh-CN"), + supportedLanguages: Array = arrayOf("zh-CN", "en-US"), audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE ): Boolean { try { @@ -171,7 +171,7 @@ class AzureAsrHelper(private val context: Context) { } setupMicrophoneStream() - recognizer?.close() + recognizer?.close() recognizer = null //创建识别器 recognizer = if (isAutoDetectLanguage) { diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 190e5eb4f..0426e6409 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -106,6 +106,15 @@ object BleService { // 初始化状态 private var isInitialized = false + private val replyTimeoutHandler = Handler(Looper.getMainLooper()) + private val replyTimeoutRunnable = Runnable { + if (!isReply) { + Log.w(TAG, "命令响应超时,自动重置 isReply 为 true") + isReply = true + processNextCommand() + } + } + /** * 初始化BLE服务 * @param appContext 应用上下文 @@ -348,17 +357,34 @@ object BleService { } return null; } - fun getDeviceInfos(deviceName: String): List>? { + fun getDeviceInfos(deviceName: String): List> { // 更新缓存 val matchedResults = scanResults.filter { it.device.name == deviceName } Log.d(TAG, "找到 ${matchedResults.size} 个名称为 $deviceName 的设备") - - if (matchedResults.isEmpty()) { - return emptyList() + + val ret = mutableListOf>() + + // 如果找到匹配设备名的结果,返回匹配设备的信息 + if (matchedResults.isNotEmpty()) { + for (result in matchedResults) { + val info = printDeviceInfo(result) + if (info != null) { + ret.add(info) + } + } + } else { + // 如果没找到匹配设备名的结果,返回所有扫描到的设备信息 + for (result in scanResults) { + val info = printDeviceInfo(result) + if (info != null) { + ret.add(info) + } + } } - return null; + + return ret } /** * 打印设备信息和服务UUID @@ -386,7 +412,7 @@ object BleService { // 解析服务数据 scanRecord.serviceData?.forEach { (uuid, data) -> val dataHex = data.joinToString("") { String.format("%02X", it) } - Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") + //Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") } } return info @@ -482,6 +508,8 @@ object BleService { } catch (e: Exception) { Log.e(TAG, "关闭GATT连接异常: ${e.message}", e) } + isReply = true; + commandQueue.clear() bluetoothGatt = null notifyChar = null writeChar = null @@ -531,6 +559,7 @@ object BleService { newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) disconnectGatt() + // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING) // g.close() // otaManager?.release(); @@ -763,19 +792,10 @@ object BleService { } }" ) - isReply = true; - cmdReplyType = cmdType; - if (!commandQueue.isEmpty()) { - val nextCmd = commandQueue.poll() - writeChar!!.value = nextCmd - var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) - if (isSeccess == true) { - Log.d( - TAG, - "收到设备响应:" - ) - } - } + isReply = true + cmdReplyType = cmdType + replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) // 移除超时回调 + processNextCommand() // 根据命令类型处理不同的响应 when (cmdType) { BleConst.CMD_GET_VERSION -> { @@ -1396,13 +1416,16 @@ object BleService { // 加入队列并触发发送 if (isReply) { - var isSeccess = bluetoothGatt?.writeCharacteristic(writeChar) - if (isSeccess == true) { - isReply = false; - cmdType; + val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar) + if (isSuccess == true) { + isReply = false + replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 return true } else + { + commandQueue.poll() return false + } } else { commandQueue.offer(cmdPacket) } @@ -1414,6 +1437,19 @@ object BleService { } } + private fun processNextCommand() { + if (!commandQueue.isEmpty()) { + val nextCmd = commandQueue.poll() + writeChar!!.value = nextCmd + val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar) + if (isSuccess == true) { + isReply = false + replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 + Log.d(TAG, "发送队列中的下一个命令") + } + } + } + /** * 计算CRC8/MAXIM校验值 */ @@ -1599,4 +1635,3 @@ class RecordingFile(private val context: Context) { } } } - \ No newline at end of file diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift index 790d615cc..b0c76fa81 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift @@ -25,10 +25,10 @@ class BleConst { static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") /** 扫描服务UUID - 用于后台扫描过滤 */ - static let SERVICE_UUID = CBUUID(string: "FD7B") + static let SERVICE_UUID = CBUUID(string: "abc0") /** 扫描服务UUID1 - 用于后台扫描过滤 */ - static let SERVICE_UUID1 = CBUUID(string: "FF11") + static let SERVICE_UUID1 = CBUUID(string: "fd7b") /** BLE扫描Action */ static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 3f85aaa1e..6fbf53154 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -233,11 +233,12 @@ private var cmdReplyType: UInt8 = 0 // manager.scanForPeripherals(withServices: nil, options: [ // CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据 // ]) - //let serviceUUIDs = [BleConst.SERVICE_UUID, BleConst.SERVICE_UUID1] - manager.scanForPeripherals(withServices: nil, options: [ - CBCentralManagerScanOptionAllowDuplicatesKey: true + let serviceUUIDs = [BleConst.SERVICE_UUID, BleConst.SERVICE_UUID1] + + manager.scanForPeripherals(withServices: serviceUUIDs, options: [ + CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据 ]) - os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout) + os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout) // 设置扫描超时 scanTimer?.invalidate() diff --git a/pubspec.yaml b/pubspec.yaml index f8b2f463c..9ee828450 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,10 +23,9 @@ dependencies: audio_session: ^0.1.18 flutter_markdown: ^0.7.7+1 markdown: ^7.1.1 - appflowy_editor: - git: - url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: main + fleather: ^1.24.0+1 + parchment: ^1.24.0 + htmltopdfwidgets: ^1.1.0 wifi_iot: ^0.3.19+2 permission_handler: ^10.4.5 web_socket_channel: ^2.4.0 @@ -68,7 +67,7 @@ dependencies: flutter_facebook_auth: ^7.1.1 flutter_image_compress: ^2.4.0 common_utils: ^2.1.0 - # image_cropper: ^9.0.0 + image_gallery_saver_plus: ^4.0.1 dio: ^5.8.0+1 sqflite: ^2.4.2 path: ^1.9.1 @@ -124,15 +123,19 @@ flutter: shaders: - shaders/mic_flow.frag assets: - - assets/images/headphone_light.png - - assets/images/headphone_dark.png + # - assets/images/headphone_light.png + # - assets/images/headphone_dark.png - assets/images/robot_light.png - assets/images/glasses_light.png - assets/images/agents/ - assets/images/explore.png - assets/images/explore1.png - assets/images/explore2.jpg + - assets/images/mine-background.png + - assets/fonts/NotoSansSC-Regular.ttf - assets/lottie/ - assets/js/ + - assets/images/headphone_new4.png + - assets/images/headphone_new3.png - .env - .mcp.json