Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
f1cea09dc9
  1. 30
      .metadata
  2. 12
      android/app/build.gradle.kts
  3. 55
      android/app/src/main/AndroidManifest.xml
  4. 1
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  5. 8
      android/app/src/main/res/xml/network_security_config.xml
  6. 2
      android/gradle/wrapper/gradle-wrapper.properties
  7. 84
      flutter_01.log
  8. 76
      ios/Podfile.lock
  9. 18
      ios/Runner.xcodeproj/project.pbxproj
  10. 4
      ios/Runner/Info.plist
  11. 18
      lib/core/bindings/initial_binding.dart
  12. 4
      lib/core/routes/app_pages.dart
  13. 139
      lib/data/services/notification_service.dart
  14. 167
      lib/data/services/voice_recognition_service.dart
  15. 384
      lib/data/services/volcano_asr_service.dart
  16. 33
      lib/main.dart
  17. 7
      lib/modules/chat/bindings/chat_binding.dart
  18. 581
      lib/modules/chat/controllers/chat_controller.dart
  19. 128
      lib/modules/chat/controllers/voice_input_controller.dart
  20. 455
      lib/modules/chat/views/chat_view.dart
  21. 130
      lib/modules/chat/widgets/voice_input_panel.dart
  22. 2
      lib/modules/explore/views/explore_view.dart
  23. 4
      linux/flutter/generated_plugin_registrant.cc
  24. 1
      linux/flutter/generated_plugins.cmake
  25. 18
      macos/Podfile
  26. 20
      macos/Podfile.lock
  27. 8
      macos/Runner.xcodeproj/project.pbxproj
  28. 23
      macos/Runner/AppDelegate.swift
  29. 2
      macos/Runner/DebugProfile.entitlements
  30. 2
      macos/Runner/Info.plist
  31. 2
      macos/Runner/Release.entitlements
  32. 4
      pubspec.yaml
  33. 3
      windows/flutter/generated_plugin_registrant.cc
  34. 1
      windows/flutter/generated_plugins.cmake

30
.metadata

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

12
android/app/build.gradle.kts

@ -13,6 +13,7 @@ android {
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
// Enable core library desugaring
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
} }
@ -25,11 +26,10 @@ android {
applicationId = "com.example.deep_voice" applicationId = "com.example.deep_voice"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = 23 // Updated to meet record_android plugin requirements
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
multiDexEnabled = true
} }
buildTypes { buildTypes {
@ -41,10 +41,10 @@ android {
} }
} }
flutter {
source = "../.."
}
dependencies { dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
} }
flutter {
source = "../.."
}

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

@ -1,39 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 添加必要的权限 -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<application <application
android:label="deep_voice" android:label="deep_voice"
android:name="android.app.Application" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher">
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
<!-- 添加前台服务 --> <!-- Audio Service -->
<service <service android:name="com.ryanheise.audioservice.AudioService"
android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback" android:foregroundServiceType="mediaPlayback"
android:exported="false"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<!-- 广播接收器用于接收媒体按钮事件 --> <receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
@ -48,9 +38,7 @@
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize">
android:showWhenLocked="true"
android:turnScreenOn="true">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
@ -69,23 +57,6 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- 添加通知图标 -->
<meta-data
android:name="com.dexterous.flutterlocalnotifications.default_notification_icon"
android:resource="@mipmap/ic_launcher" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

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

@ -1,6 +1,5 @@
package com.example.deep_voice package com.example.deep_voice
import io.flutter.embedding.android.FlutterActivity
import com.ryanheise.audioservice.AudioServiceActivity import com.ryanheise.audioservice.AudioServiceActivity
class MainActivity: AudioServiceActivity() class MainActivity: AudioServiceActivity()

8
android/app/src/main/res/xml/network_security_config.xml

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

2
android/gradle/wrapper/gradle-wrapper.properties

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

84
flutter_01.log

@ -0,0 +1,84 @@
Flutter crash report.
Please report a bug at https://github.com/flutter/flutter/issues.
## command
flutter run --machine --start-paused -d 3063725814005V5 --devtools-server-address http://127.0.0.1:9100/ --target /Users/wolfplus/Developer/deep_voice/lib/main.dart
## exception
RPCError: setIsolatePauseMode: (112) Service has disappeared
```
#0 new _OutstandingRequest (package:vm_service/src/vm_service.dart:265:34)
#1 VmService._call.<anonymous closure> (package:vm_service/src/vm_service.dart:1921:25)
#2 VmService._call (package:vm_service/src/vm_service.dart:1933:8)
#3 VmService.setIsolatePauseMode (package:vm_service/src/vm_service.dart:1685:7)
#4 HotRunner._restartFromSources.<anonymous closure> (package:flutter_tools/src/run_hot.dart:662:43)
#5 _rootRunUnary (dart:async/zone.dart:1538:47)
#6 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:932:45)
#7 Future._propagateToListeners (dart:async/future_impl.dart:961:13)
#8 Future._completeWithValue (dart:async/future_impl.dart:712:5)
#9 Future._asyncCompleteWithValue.<anonymous closure> (dart:async/future_impl.dart:792:7)
#10 _rootRun (dart:async/zone.dart:1525:13)
#11 _CustomZone.run (dart:async/zone.dart:1422:19)
#12 _CustomZone.runGuarded (dart:async/zone.dart:1321:7)
#13 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1362:23)
#14 _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#15 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#16 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:128:13)
#17 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:195:5)
```
## flutter doctor
```
[!] Flutter (Channel stable, 3.29.0, on macOS 15.3.1 24D70 darwin-arm64, locale en-US) [179ms]
• Flutter version 3.29.0 on channel stable at /Users/wolfplus/Developer/SDKs/flutter
! The flutter binary is not on your path. Consider adding /Users/wolfplus/Developer/SDKs/flutter/bin to your path.
! The dart binary is not on your path. Consider adding /Users/wolfplus/Developer/SDKs/flutter/bin to your path.
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 35c388afb5 (2 weeks ago), 2025-02-10 12:48:41 -0800
• Engine revision f73bfc4522
• Dart version 3.7.0
• DevTools version 2.42.2
• If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.1) [1,851ms]
• Android SDK at /Users/wolfplus/Library/Android/sdk
• Platform android-35, build-tools 35.0.1
• ANDROID_HOME = /Users/wolfplus/Library/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.4+-12422083-b607.1)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 16.2) [663ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16C5032a
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [2ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2024.2) [2ms]
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.4+-12422083-b607.1)
[✓] Connected device (5 available) [5.6s]
• V2049A (mobile) • 3063725814005V5 • android-arm64 • Android 14 (API 34)
• Chris’s iPhone16 (wireless) (mobile) • 00008140-0006402C3CF3001C • ios • iOS 18.2.1 22C161
• macOS (desktop) • macos • darwin-arm64 • macOS 15.3.1 24D70 darwin-arm64
• Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 15.3.1 24D70 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 133.0.6943.127
[✓] Network resources [550ms]
• All expected network resources are available.
! Doctor found issues in 1 category.
```

76
ios/Podfile.lock

@ -1,23 +1,87 @@
PODS: PODS:
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1):
- Flutter
- azure_speech_recognition_null_safety (0.0.1):
- Flutter
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.29.0)
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- shared_preferences_foundation (0.0.1): - flutter_local_notifications (0.0.1):
- Flutter
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- MicrosoftCognitiveServicesSpeech-iOS (1.29.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- record_darwin (1.0.0):
- Flutter
- sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- azure_speech_recognition_null_safety (from `.symlinks/plugins/azure_speech_recognition_null_safety/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
SPEC REPOS:
trunk:
- MicrosoftCognitiveServicesSpeech-iOS
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_service:
:path: ".symlinks/plugins/audio_service/darwin"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
azure_speech_recognition_null_safety:
:path: ".symlinks/plugins/azure_speech_recognition_null_safety/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
shared_preferences_foundation: flutter_local_notifications:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/flutter_local_notifications/ios"
just_audio:
:path: ".symlinks/plugins/just_audio/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
record_darwin:
:path: ".symlinks/plugins/record_darwin/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
azure_speech_recognition_null_safety: aed1d7ededc73a1d0b7a342b1f4803ea166e2747
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
MicrosoftCognitiveServicesSpeech-iOS: 0de20a8ebb6bbd87decaeaa9a707dd40830106a6
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 PODFILE CHECKSUM: 359b0564325fb2f8842a19a47bd7e63cbdcd9c04
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

18
ios/Runner.xcodeproj/project.pbxproj

@ -199,6 +199,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */, D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */,
E839EB3BEF50366A68B2A46D /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -362,6 +363,23 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
E839EB3BEF50366A68B2A46D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */

4
ios/Runner/Info.plist

@ -44,5 +44,9 @@
<string>需要通知权限来保持与AI助手的实时对话</string> <string>需要通知权限来保持与AI助手的实时对话</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

18
lib/core/bindings/initial_binding.dart

@ -7,13 +7,19 @@ import '../../modules/profile/controllers/profile_controller.dart';
import '../../data/services/volcano_tts_service.dart'; import '../../data/services/volcano_tts_service.dart';
import '../../data/services/audio_service.dart'; import '../../data/services/audio_service.dart';
import '../../data/services/notification_service.dart'; import '../../data/services/notification_service.dart';
import '../../data/services/voice_recognition_service.dart';
import '../../data/services/volcano_ai_service.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings { class InitialBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// 初始化服务 // 初始化所有服务
final notificationService = Get.put(NotificationService(), permanent: true); final notificationService = Get.put(NotificationService(), permanent: true);
final voiceRecognitionService = Get.put(VoiceRecognitionService(), permanent: true);
final volcanoTtsService = Get.put(VolcanoTtsService(), permanent: true);
final audioService = Get.put(AudioServiceManager(), permanent: true);
final volcanoAiService = Get.put(VolcanoAIService(), permanent: true);
// 注册 Controllers // 注册 Controllers
Get.put(AppController(), permanent: true); Get.put(AppController(), permanent: true);
@ -21,18 +27,12 @@ class InitialBinding extends Bindings {
Get.put(ExploreController(), permanent: true); Get.put(ExploreController(), permanent: true);
Get.put(ProfileController(), permanent: true); Get.put(ProfileController(), permanent: true);
Get.put(PermissionController(), permanent: true); Get.put(PermissionController(), permanent: true);
Get.put(AudioServiceManager(), permanent: true);
Get.put(VolcanoTtsService(), permanent: true);
// 异步初始化 // 异步初始化
Future.wait([ Future.wait([
notificationService.init(), notificationService.init(),
AudioServiceManager.to.init().then((_) { voiceRecognitionService.onInit(),
print('AudioService初始化完成'); // 如果其他服务也需要异步初始化,在这里添加
}).catchError((error) {
print('AudioService初始化失败: $error');
}),
]); ]);
} }
} }

4
lib/core/routes/app_pages.dart

@ -2,7 +2,9 @@ import 'package:get/get.dart';
import '../bindings/initial_binding.dart'; import '../bindings/initial_binding.dart';
import '../views/main_view.dart'; import '../views/main_view.dart';
import '../../modules/home/views/home_view.dart'; import '../../modules/home/views/home_view.dart';
import '../../modules/home/bindings/home_binding.dart';
import '../../modules/explore/views/explore_view.dart'; import '../../modules/explore/views/explore_view.dart';
import '../../modules/explore/bindings/explore_binding.dart';
import '../../modules/profile/views/profile_view.dart'; import '../../modules/profile/views/profile_view.dart';
import '../../modules/chat/bindings/chat_binding.dart'; import '../../modules/chat/bindings/chat_binding.dart';
import '../../modules/chat/views/chat_view.dart'; import '../../modules/chat/views/chat_view.dart';
@ -18,10 +20,12 @@ abstract class AppPages {
GetPage( GetPage(
name: Routes.home, name: Routes.home,
page: () => const HomeView(), page: () => const HomeView(),
binding: HomeBinding(),
), ),
GetPage( GetPage(
name: Routes.explore, name: Routes.explore,
page: () => const ExploreView(), page: () => const ExploreView(),
binding: ExploreBinding(),
), ),
GetPage( GetPage(
name: Routes.chat, name: Routes.chat,

139
lib/data/services/notification_service.dart

@ -4,11 +4,12 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../providers/agent_provider.dart'; import '../providers/agent_provider.dart';
import 'package:flutter/foundation.dart';
class NotificationService extends GetxService { class NotificationService extends GetxService {
static NotificationService get to => Get.find(); static NotificationService get to => Get.find();
final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin? _flutterLocalNotificationsPlugin;
bool _notificationActive = false; bool _notificationActive = false;
bool _isRequestingPermission = false; bool _isRequestingPermission = false;
@ -18,35 +19,41 @@ class NotificationService extends GetxService {
} }
Future<void> init() async { Future<void> init() async {
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) {
const iosSettings = DarwinInitializationSettings( _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
requestAlertPermission: false, // 初始化时不请求权限
requestBadgePermission: false, const initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
requestSoundPermission: false,
); final initializationSettingsDarwin = DarwinInitializationSettings(
requestSoundPermission: false,
const initSettings = InitializationSettings( requestBadgePermission: false,
android: androidSettings, requestAlertPermission: false,
iOS: iosSettings, );
);
final initializationSettings = InitializationSettings(
await _notificationsPlugin.initialize( android: initializationSettingsAndroid,
initSettings, iOS: initializationSettingsDarwin,
onDidReceiveNotificationResponse: (details) { macOS: initializationSettingsDarwin,
if (Get.currentRoute != '/chat') { );
Get.toNamed('/chat');
} await _flutterLocalNotificationsPlugin?.initialize(
}, initializationSettings,
); onDidReceiveNotificationResponse: (details) {
if (Get.currentRoute != '/chat') {
Get.toNamed('/chat');
}
},
);
}
// 创建通知渠道 // 创建通知渠道
if (Platform.isAndroid) { if (Platform.isAndroid) {
await _notificationsPlugin.resolvePlatformSpecificImplementation< await _flutterLocalNotificationsPlugin?.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel( AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(
const AndroidNotificationChannel( const AndroidNotificationChannel(
'foreground_service', // id 'foreground_service',
'Foreground Service', // name 'Foreground Service',
description: 'Used for the foreground service notification', // description description: 'Used for the foreground service notification',
importance: Importance.min, importance: Importance.min,
playSound: false, playSound: false,
enableVibration: false, enableVibration: false,
@ -58,7 +65,9 @@ class NotificationService extends GetxService {
await _requestPermissionIfNeeded(); await _requestPermissionIfNeeded();
// 在初始化完成后显示前台服务通知 // 在初始化完成后显示前台服务通知
await showForegroundNotification(); if (Platform.isAndroid) {
await showForegroundNotification();
}
} }
Future<void> showForegroundNotification() async { Future<void> showForegroundNotification() async {
@ -101,7 +110,7 @@ class NotificationService extends GetxService {
iOS: iosDetails, iOS: iosDetails,
); );
await _notificationsPlugin.show( await _flutterLocalNotificationsPlugin?.show(
0, 0,
_defaultAgentName, _defaultAgentName,
'点击开始对话', '点击开始对话',
@ -113,35 +122,63 @@ class NotificationService extends GetxService {
Future<void> cancelNotification() async { Future<void> cancelNotification() async {
if (!_notificationActive) return; if (!_notificationActive) return;
await _notificationsPlugin.cancel(0); await _flutterLocalNotificationsPlugin?.cancel(0);
_notificationActive = false; _notificationActive = false;
} }
Future<void> _requestPermissionIfNeeded() async { Future<void> _requestPermissionIfNeeded() async {
if (_isRequestingPermission) return; if (Platform.isMacOS) {
_isRequestingPermission = true; // On macOS, we don't need to request notification permissions through permission_handler
// Instead, we'll use the flutter_local_notifications plugin directly
try { final bool? granted = await _flutterLocalNotificationsPlugin
if (Platform.isAndroid) { ?.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()
final androidInfo = await DeviceInfoPlugin().androidInfo; ?.requestPermissions(
if (androidInfo.version.sdkInt >= 33) { alert: true,
final status = await Permission.notification.status; badge: true,
if (status.isDenied) { sound: true,
await Permission.notification.request(); );
} if (granted != true) {
} debugPrint('MacOS Notifications: Permission request was denied');
} else if (Platform.isIOS) {
await _notificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
} }
} finally { return;
_isRequestingPermission = false; }
// For other platforms, use permission_handler as before
if (await Permission.notification.status != PermissionStatus.granted) {
await Permission.notification.request();
} }
} }
Future<void> showNotification({
required String title,
required String body,
}) async {
if (_flutterLocalNotificationsPlugin == null) return;
const androidDetails = AndroidNotificationDetails(
'chat_channel',
'聊天消息',
channelDescription: '接收新的聊天消息',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _flutterLocalNotificationsPlugin?.show(
0,
title,
body,
notificationDetails,
);
}
} }

167
lib/data/services/voice_recognition_service.dart

@ -0,0 +1,167 @@
// voice_recognition_service.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/foundation.dart';
import 'volcano_asr_service.dart';
import 'package:get/get.dart';
class VoiceRecognitionService extends GetxService {
VolcanoAsrService? _asrService;
bool _isInitialized = false;
bool _isStoppingRecognition = false;
final isRecognizing = false.obs;
Function(String)? onRecognitionResult;
Function(String)? onError;
Function()? onRecognitionComplete;
Function(String)? onSentenceComplete;
Function(String)? onConnectionStatusChanged;
VoiceRecognitionService();
void _logError(String message, [dynamic error, StackTrace? stackTrace]) {
debugPrint('🔴 VoiceRecognitionService Error: $message');
if (error != null) debugPrint('Error details: $error');
if (stackTrace != null) debugPrint('Stack trace: $stackTrace');
}
@override
Future<void> onInit() async {
super.onInit();
if (!_isInitialized) {
try {
await _initializeServices();
_isInitialized = true;
debugPrint('✅ VoiceRecognitionService initialized successfully');
} catch (e, stackTrace) {
_logError('Failed to initialize services', e, stackTrace);
rethrow;
}
}
}
Future<void> _initializeServices() async {
try {
final appKey = dotenv.env['VOLCANO_APP_KEY'];
final accessKey = dotenv.env['VOLCANO_ACCESS_KEY'];
if (appKey == null || accessKey == null) {
throw Exception('Volcano ASR service configuration is missing. Please check your .env file.');
}
_asrService = VolcanoAsrService(
appKey: appKey,
accessKey: accessKey,
onResult: _handleRecognitionResult,
onError: _handleError,
onConnectionStatusChanged: _handleConnectionStatusChanged,
);
await _setupServices();
} catch (e, stackTrace) {
_logError('Error during service initialization', e, stackTrace);
rethrow;
}
}
Future<void> _setupServices() async {
try {
_asrService?.onResult = (result) {
debugPrint('ASR Result: $result');
onRecognitionResult?.call(result);
onSentenceComplete?.call(result);
};
_asrService?.onError = (error) {
_logError('ASR service error', error);
onError?.call('ASR error: $error');
};
_asrService?.onConnectionStatusChanged = (status) {
final isConnected = status == 'connected';
debugPrint('ASR Connection status: $status');
if (!isConnected && isRecognizing.value && !_isStoppingRecognition) {
stopRecognition();
onRecognitionComplete?.call();
}
onConnectionStatusChanged?.call(status);
};
} catch (e, stackTrace) {
_logError('Error during services setup', e, stackTrace);
rethrow;
}
}
Future<void> startRecognition() async {
if (isRecognizing.value || _asrService == null) {
debugPrint('⚠️ Cannot start recognition: ${isRecognizing.value ? 'Already recognizing' : 'ASR service not initialized'}');
return;
}
try {
_isStoppingRecognition = false;
debugPrint('Starting voice recognition...');
await _asrService?.startRecognition();
isRecognizing.value = true;
} catch (e, stackTrace) {
_logError('Failed to start recognition', e, stackTrace);
onError?.call('Failed to start recognition: $e');
await stopRecognition();
}
}
Future<void> stopRecognition() async {
if (!isRecognizing.value || _asrService == null || _isStoppingRecognition) return;
try {
_isStoppingRecognition = true;
debugPrint('Stopping voice recognition...');
isRecognizing.value = false;
await _asrService?.stopRecognition().timeout(
const Duration(seconds: 3),
onTimeout: () {
debugPrint('ASR service stop timeout, forcing cleanup');
_cleanupResources();
},
);
final complete = onRecognitionComplete;
if (complete != null) {
Future.microtask(complete);
}
} catch (e, stackTrace) {
_logError('Failed to stop recognition', e, stackTrace);
onError?.call('Failed to stop recognition: $e');
_cleanupResources();
} finally {
_isStoppingRecognition = false;
}
}
void _cleanupResources() {
if (_asrService != null) {
_asrService!.onResult = null;
_asrService!.onError = null;
_asrService!.onConnectionStatusChanged = null;
}
isRecognizing.value = false;
_isStoppingRecognition = false;
}
void _handleRecognitionResult(String result) {
onRecognitionResult?.call(result);
}
void _handleError(String error) {
onError?.call(error);
}
void _handleConnectionStatusChanged(String status) {
onConnectionStatusChanged?.call(status);
}
@override
void onClose() {
debugPrint('Cleaning up VoiceRecognitionService...');
stopRecognition().then((_) {
debugPrint('VoiceRecognitionService cleanup completed');
super.onClose();
}).catchError((e) {
debugPrint('Error during VoiceRecognitionService cleanup: $e');
super.onClose();
});
}
}

384
lib/data/services/volcano_asr_service.dart

@ -0,0 +1,384 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:uuid/uuid.dart';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import 'package:record/record.dart';
/// 自定义异常类
class AsrServiceException implements Exception {
final String message;
AsrServiceException(this.message);
@override
String toString() => message;
}
/// Volcano ASR 服务,负责音频采集、数据打包、WebSocket 连接和数据传输
class VolcanoAsrService extends GetxService {
// 服务地址
static const String _baseUrl = 'wss://openspeech.bytedance.com/api/v3/sauc/bigmodel';
// 配置参数
final String appKey;
final String accessKey;
// WebSocket 与录音相关
WebSocket? _webSocket;
AudioRecorder? _audioRecorder;
StreamSubscription<List<int>>? _audioStreamSubscription;
// 状态管理
bool _isConnected = false;
bool _isRecording = false;
int _currentSequence = 1;
int _serverSequence = 0; // 添加服务器序列号跟踪
bool _isInitialized = false;
bool _waitingForInitResponse = false;
Completer<void>? _initCompleter;
// 回调
Function(String)? onResult;
Function(String)? onError;
Function(String)? onConnectionStatusChanged;
VolcanoAsrService({
required this.appKey,
required this.accessKey,
this.onResult,
this.onError,
this.onConnectionStatusChanged,
});
Future<void> _connectWebSocket() async {
if (_webSocket != null) {
debugPrint('⚠️ ASR: WebSocket already exists');
return;
}
final connectId = const Uuid().v4();
final headers = {
'X-Api-App-Key': appKey,
'X-Api-Access-Key': accessKey,
'X-Api-Resource-Id': 'volc.bigasr.sauc.duration',
'X-Api-Connect-Id': connectId,
'Connection': 'Upgrade', // 添加 WebSocket 升级头
'Upgrade': 'websocket',
};
try {
_webSocket = await WebSocket.connect(
_baseUrl,
headers: headers,
protocols: ['binary'], // 指定二进制协议
);
debugPrint('ℹ️ ASR: WebSocket connected');
_webSocket!.listen(
_handleWebSocketMessage,
onError: (error) {
debugPrint('🔴 ASR: WebSocket error: $error');
onError?.call('WebSocket error: $error');
_handleDisconnect();
},
onDone: () {
debugPrint('ℹ️ ASR: WebSocket closed');
if (_waitingForInitResponse) {
_initCompleter?.completeError('WebSocket closed during initialization');
_initCompleter = null;
}
_handleDisconnect();
},
cancelOnError: false, // 不要在错误时自动取消
);
_isConnected = true;
_currentSequence = 1; // 重置序列号
_isInitialized = false; // 重置初始化状态
onConnectionStatusChanged?.call('connected');
// 发送初始化请求前等待一下
await Future.delayed(const Duration(milliseconds: 100));
await _sendInitRequest(connectId);
} catch (e) {
debugPrint('🔴 ASR: Failed to connect: $e');
rethrow;
}
}
Future<void> _sendInitRequest(String requestId) async {
_waitingForInitResponse = true;
_initCompleter = Completer<void>();
final initRequest = {
'audio': {
'format': 'raw',
'sample_rate': 16000,
'channels': 1,
'encoding': 'pcm',
'bit_depth': 16,
},
'request': {
'request_id': requestId,
'language': 'zh',
'model_name': 'bigmodel',
'enable_punc': true,
'enable_itn': true,
'enable_voice_detection': true,
}
};
final jsonBytes = utf8.encode(json.encode(initRequest));
debugPrint('ℹ️ ASR: Sending init request: ${json.encode(initRequest)}');
final header = Uint8List(4)
..[0] = 0x11 // version=1, header_size=1
..[1] = 0x11 // message_type=1(FULL_CLIENT_REQUEST), flags=1(POS_SEQUENCE)
..[2] = 0x10 // serialization=1(JSON), compression=0
..[3] = 0x00; // reserved
final seqBytes = ByteData(4)..setInt32(0, _currentSequence, Endian.big);
final sizeBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big);
final fullRequest = Uint8List(header.length + 8 + jsonBytes.length)
..setAll(0, header)
..setAll(4, seqBytes.buffer.asUint8List())
..setAll(8, sizeBytes.buffer.asUint8List())
..setAll(12, jsonBytes);
_webSocket?.add(fullRequest);
debugPrint('ℹ️ ASR: Init request sent with sequence: $_currentSequence');
// 设置超时
Future.delayed(const Duration(seconds: 10), () {
if (_waitingForInitResponse) {
debugPrint('⚠️ ASR: Init request timeout after 10 seconds');
_waitingForInitResponse = false;
_initCompleter?.completeError('Init request timeout');
_initCompleter = null;
}
});
try {
await _initCompleter?.future;
} catch (e) {
debugPrint('🔴 ASR: Init request failed: $e');
rethrow;
}
}
void _handleWebSocketMessage(dynamic message) {
debugPrint('ℹ️ ASR: Received message type: ${message.runtimeType}');
if (message is String) {
debugPrint('ℹ️ ASR: Received string message: $message');
try {
final data = json.decode(message);
if (data is Map) {
if (data['error'] != null) {
debugPrint('🔴 ASR: Server error: ${data['error']}');
if (_waitingForInitResponse) {
_waitingForInitResponse = false;
_initCompleter?.completeError(data['error']);
_initCompleter = null;
}
onError?.call(data['error']);
return;
}
}
} catch (e) {
debugPrint('⚠️ ASR: Failed to parse string message: $e');
}
}
if (message is! Uint8List || message.length < 12) {
debugPrint('⚠️ ASR: Invalid binary message format');
return;
}
final version = message[0] >> 4;
final headerSize = message[0] & 0x0F;
final messageType = message[1] >> 4;
final flags = message[1] & 0x0F;
final serialization = message[2] >> 4;
final compression = message[2] & 0x0F;
debugPrint('ℹ️ ASR: Binary message: version=$version, headerSize=$headerSize, type=$messageType, flags=$flags, serialization=$serialization, compression=$compression');
final sequence = ByteData.view(message.buffer, 4, 4).getInt32(0, Endian.big);
debugPrint('ℹ️ ASR: Message sequence: $sequence');
_serverSequence = sequence;
final payloadSize = ByteData.view(message.buffer, 8, 4).getInt32(0, Endian.big);
if (message.length < 12 + payloadSize) {
debugPrint('⚠️ ASR: Incomplete message received');
return;
}
if (messageType == 0x0F) { // INIT_RESPONSE
if (_waitingForInitResponse) {
debugPrint('ℹ️ ASR: Received init response (type 15)');
if (payloadSize > 0) {
final payload = message.sublist(12, 12 + payloadSize);
final jsonStr = utf8.decode(payload);
debugPrint('ℹ️ ASR: Init response payload: $jsonStr');
try {
final response = json.decode(jsonStr);
if (response is Map) {
if (response['error'] != null) {
final error = response['error'];
debugPrint('🔴 ASR: Init response error: $error');
_waitingForInitResponse = false;
_initCompleter?.completeError(error);
_initCompleter = null;
return;
}
debugPrint('ℹ️ ASR: Init response success');
_waitingForInitResponse = false;
_currentSequence = (_serverSequence + 1);
_isInitialized = true;
_initCompleter?.complete();
_initCompleter = null;
return;
}
} catch (e) {
debugPrint('⚠️ ASR: Failed to parse init response: $e');
}
} else {
debugPrint('ℹ️ ASR: Empty init response success');
_waitingForInitResponse = false;
_currentSequence = (_serverSequence + 1);
_isInitialized = true;
_initCompleter?.complete();
_initCompleter = null;
return;
}
}
} else if (messageType == 0x09) { // FULL_SERVER_RESPONSE
final payload = message.sublist(12, 12 + payloadSize);
final jsonStr = utf8.decode(payload);
debugPrint('ℹ️ ASR: Received binary payload: $jsonStr');
try {
final response = json.decode(jsonStr);
if (response is Map && response['result'] != null) {
final result = response['result'];
if (result['text'] != null && result['text'].isNotEmpty) {
debugPrint('ℹ️ ASR: Result: ${result['text']}');
onResult?.call(result['text']);
}
}
} catch (e) {
debugPrint('⚠️ ASR: Failed to parse binary payload: $e');
}
}
}
Future<void> _startRecording() async {
_audioRecorder ??= AudioRecorder();
try {
if (!await _audioRecorder!.hasPermission()) {
throw Exception('Microphone permission denied');
}
const config = RecordConfig(
encoder: AudioEncoder.pcm16bits,
bitRate: 256000,
sampleRate: 16000,
numChannels: 1,
);
// 启动录音流
final stream = await _audioRecorder!.startStream(config);
_isRecording = true;
// 监听音频流
_audioStreamSubscription = stream.listen(
(data) {
if (_isRecording && _isConnected) {
_sendAudioData(data, false);
}
},
onError: (error) {
debugPrint('🔴 ASR: Stream error: $error');
_stopRecording();
},
);
} catch (e) {
debugPrint('🔴 ASR: Recording error: $e');
await _stopRecording();
rethrow;
}
}
void _sendAudioData(List<int> audioData, bool isLast) {
if (!_isConnected || _webSocket == null || !_isInitialized) {
debugPrint('⚠️ ASR: Cannot send audio data: not ready');
return;
}
final header = Uint8List(4)
..[0] = 0x11 // version=1, header_size=1
..[1] = isLast ? 0x23 : 0x21 // message_type=2(AUDIO_ONLY), flags=3/1
..[2] = 0x00 // no serialization, no compression
..[3] = 0x00; // reserved
final seqBytes = ByteData(4)..setInt32(0, _currentSequence, Endian.big);
final sizeBytes = ByteData(4)..setInt32(0, audioData.length, Endian.big);
final packet = Uint8List(12 + audioData.length)
..setAll(0, header)
..setAll(4, seqBytes.buffer.asUint8List())
..setAll(8, sizeBytes.buffer.asUint8List())
..setAll(12, audioData);
_webSocket?.add(packet);
debugPrint('🔍 ASR: Sent audio data with sequence: $_currentSequence');
_currentSequence++; // 递增序列号
}
void _handleDisconnect() {
_webSocket = null;
_isConnected = false;
_isInitialized = false; // 重置初始化状态
_currentSequence = 1; // 重置序列号
onConnectionStatusChanged?.call('disconnected');
_stopRecording();
}
Future<void> _stopRecording() async {
_isRecording = false;
try {
await _audioStreamSubscription?.cancel();
_audioStreamSubscription = null;
await _audioRecorder?.stop();
_audioRecorder = null;
} catch (e) {
debugPrint('⚠️ ASR: Error stopping recording: $e');
}
}
Future<void> startRecognition() async {
await _connectWebSocket();
await _startRecording();
}
Future<void> stopRecognition() async {
await _stopRecording();
_webSocket?.close();
_handleDisconnect();
}
@override
void onClose() {
_audioStreamSubscription?.cancel();
stopRecognition();
super.onClose();
}
}

33
lib/main.dart

@ -6,16 +6,41 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/bindings/initial_binding.dart'; import 'core/bindings/initial_binding.dart';
import 'core/routes/app_pages.dart'; import 'core/routes/app_pages.dart';
import 'core/routes/app_routes.dart'; import 'core/routes/app_routes.dart';
import 'dart:io';
import 'package:window_size/window_size.dart';
Future<void> main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// 加载环境变量 // 设置窗口大小(仅在 macOS 下生效)
if (Platform.isMacOS) {
const double width = 375.0;
const double height = 812.0;
setWindowTitle('Deep Voice');
setWindowMinSize(const Size(width, height));
setWindowMaxSize(const Size(width, height));
// 居中显示窗口
getCurrentScreen().then((screen) {
if (screen != null) {
final screenFrame = screen.frame;
final windowFrame = Rect.fromCenter(
center: screenFrame.center,
width: width,
height: height,
);
setWindowFrame(windowFrame);
}
});
}
// 初始化环境变量
await dotenv.load(); await dotenv.load();
// 初始化 GetStorage // 初始化存储
await GetStorage.init(); await GetStorage.init();
runApp(const MainApp()); runApp(const MainApp());
} }

7
lib/modules/chat/bindings/chat_binding.dart

@ -1,9 +1,16 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/chat_controller.dart'; import '../controllers/chat_controller.dart';
import '../../../data/services/volcano_tts_service.dart';
class ChatBinding implements Bindings { class ChatBinding implements Bindings {
@override @override
void dependencies() { void dependencies() {
// Ensure TTS service is registered first
if (!Get.isRegistered<VolcanoTtsService>()) {
Get.put(VolcanoTtsService());
}
// Create new controller instance
Get.put(ChatController()); Get.put(ChatController());
} }
} }

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

@ -5,73 +5,209 @@ import 'dart:convert';
import '../../../data/services/volcano_ai_service.dart'; import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/volcano_tts_service.dart'; import '../../../data/services/volcano_tts_service.dart';
import '../models/message_model.dart'; import '../models/message_model.dart';
import '../../../data/services/voice_recognition_service.dart';
import '../controllers/voice_input_controller.dart';
import 'package:flutter/services.dart';
class ChatController extends GetxController { class ChatController extends GetxController {
late final String agentId; String agentId = '';
late final String agentName; String agentName = '';
late final String agentAvatar; String agentAvatar = '';
late final String? agentSubtitle; String? agentSubtitle;
late final String systemPrompt; String systemPrompt = '';
late final String welcomeMessage; String welcomeMessage = '';
late final bool playVoiceOnEnter; bool playVoiceOnEnter = false;
final VolcanoAIService _aiService = VolcanoAIService(); final VolcanoAIService _aiService = VolcanoAIService();
late final VolcanoTtsService _ttsService; late final VolcanoTtsService _ttsService;
late final VoiceRecognitionService _recognitionService;
static const String _storagePrefix = 'chat_history_'; static const String _storagePrefix = 'chat_history_';
final messages = <Message>[].obs; final messages = <Message>[].obs;
final isLoading = false.obs; final isLoading = false.obs;
late final TextEditingController textController; late final TextEditingController messageController;
late final ScrollController scrollController; late final ScrollController scrollController;
final _storage = GetStorage(); final _storage = GetStorage();
// 用于存储当前正在流式生成的消息 // 用于存储当前正在流式生成的消息
final RxString currentStreamMessage = ''.obs; final RxString currentStreamMessage = ''.obs;
Message? _currentAssistantMessage; Message? _currentAssistantMessage;
String _pendingTtsText = ''; // 用于累积TTS文本 String _pendingTtsText = '';
static const int _minTtsLength = 20; // TTS最小文本长度 static const int _minTtsLength = 20;
bool _isDisposed = false; // 添加销毁标记 bool _isDisposed = false;
// 语音输入相关状态
final isRecording = false.obs;
final recordingText = ''.obs;
final isVoiceInputVisible = false.obs;
final isVoiceConnecting = true.obs;
final isVoiceMuted = false.obs;
Message? _currentVoiceMessage;
ChatController() { @override
_initControllers(); void onInit() {
super.onInit();
messageController = TextEditingController();
scrollController = ScrollController();
_initTtsService(); _initTtsService();
_initVoiceRecognitionService();
_initFromArguments(); _initFromArguments();
_initializeController();
// 延迟生成问候语,确保页面已完全加载
if (playVoiceOnEnter) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_isDisposed) {
generateSimpleGreeting();
}
});
}
} }
void _initFromArguments() { void _initFromArguments() {
final arguments = Get.arguments as Map<String, dynamic>; try {
agentId = arguments['agentId'] as String; final arguments = Get.arguments;
agentName = arguments['agentName'] as String; if (arguments == null) {
agentAvatar = arguments['agentAvatar'] as String; throw Exception('No arguments provided to ChatController');
agentSubtitle = arguments['agentSubtitle'] as String?; }
systemPrompt = arguments['systemPrompt'] as String;
welcomeMessage = arguments['welcomeMessage'] as String;
playVoiceOnEnter = arguments['playVoiceOnEnter'] as bool? ?? false;
}
void _initControllers() { final args = arguments as Map<String, dynamic>;
textController = TextEditingController(); agentId = args['agentId'] as String? ?? '';
scrollController = ScrollController(); agentName = args['agentName'] as String? ?? 'Assistant';
agentAvatar = args['agentAvatar'] as String? ?? 'assets/images/default_avatar.png';
agentSubtitle = args['agentSubtitle'] as String?;
systemPrompt = args['systemPrompt'] as String? ?? '';
welcomeMessage = args['welcomeMessage'] as String? ?? 'Hello! How can I help you today?';
playVoiceOnEnter = args['playVoiceOnEnter'] as bool? ?? false;
if (agentId.isEmpty) {
throw Exception('Invalid agent ID');
}
} catch (e) {
debugPrint('🔴 Error initializing ChatController: $e');
Get.back(); // 返回上一页
Get.snackbar(
'Error',
'Failed to initialize chat: Invalid parameters',
snackPosition: SnackPosition.BOTTOM,
);
}
} }
void _initTtsService() { void _initTtsService() {
try { _ttsService = Get.find<VolcanoTtsService>();
_ttsService = Get.find<VolcanoTtsService>();
} catch (_) {
_ttsService = Get.put(VolcanoTtsService(), permanent: true);
}
_ttsService.isEnabled.value = true; _ttsService.isEnabled.value = true;
} }
@override void _initVoiceRecognitionService() {
void onInit() async { _recognitionService = Get.find<VoiceRecognitionService>();
super.onInit(); _setupVoiceRecognitionCallbacks();
await _initializeController(); }
void _setupVoiceRecognitionCallbacks() {
_recognitionService.onRecognitionResult = (result) {
if (isVoiceMuted.value) return;
recordingText.value = result;
handleRecognizing(result);
};
_recognitionService.onError = (error) {
// 不要立即显示错误,先尝试重新连接
_handleRecognitionError(error);
};
_recognitionService.onRecognitionComplete = () {
stopRecording();
if (!isVoiceMuted.value && recordingText.value.isNotEmpty) {
handleVoiceResult(recordingText.value);
}
};
_recognitionService.onConnectionStatusChanged = (status) {
if (status == 'disconnected' && isRecording.value) {
// 如果在录音过程中断开连接,尝试重新连接
_handleWebSocketDisconnection();
}
};
// 添加句子完成的回调
_recognitionService.onSentenceComplete = (text) {
if (!isVoiceMuted.value && text.isNotEmpty) {
handleVoiceResult(text);
// 继续录音,等待下一句话
if (isVoiceInputVisible.value) {
startRecording();
}
}
};
}
Future<void> _handleRecognitionError(String error) async {
// 如果是权限相关错误,请求权限
if (error.toLowerCase().contains('permission')) {
try {
const platform = MethodChannel('com.example.deep_voice/audio');
await platform.invokeMethod('requestAudioPermissions');
// 权限请求后重试
if (isVoiceInputVisible.value) {
await Future.delayed(const Duration(seconds: 1));
startRecording();
}
return;
} catch (e) {
debugPrint('Failed to request permissions: $e');
}
}
// 如果是连接错误,尝试重新连接
if (error.toLowerCase().contains('connection') || error.toLowerCase().contains('websocket')) {
_handleWebSocketDisconnection();
return;
}
// 其他错误显示给用户
Get.snackbar(
'Error',
'语音识别失败: $error',
snackPosition: SnackPosition.BOTTOM,
);
stopVoiceInput();
}
Future<void> _handleWebSocketDisconnection() async {
if (!isVoiceInputVisible.value) return;
isVoiceConnecting.value = true;
stopRecording();
// 保存当前的语音文本
final currentText = recordingText.value;
// 如果需要在进入时播放语音,延迟一下以确保页面已加载 try {
if (playVoiceOnEnter) { // 等待一段时间后重试
Future.delayed(const Duration(milliseconds: 500), () { await Future.delayed(const Duration(seconds: 2));
generateSimpleGreeting(); if (!isVoiceInputVisible.value) return;
});
// 重新初始化语音识别
await _recognitionService.onInit();
// 恢复之前的状态
if (currentText.isNotEmpty) {
recordingText.value = currentText;
}
isVoiceConnecting.value = false;
if (isVoiceInputVisible.value) {
startRecording();
}
} catch (e) {
debugPrint('Failed to reconnect: $e');
Get.snackbar(
'Error',
'重新连接失败,请稍后再试',
snackPosition: SnackPosition.BOTTOM,
);
stopVoiceInput();
} }
} }
@ -91,13 +227,13 @@ class ChatController extends GetxController {
Future<void> _showWelcomeMessages() async { Future<void> _showWelcomeMessages() async {
try { try {
if (messages.isEmpty && agentName.isNotEmpty) { if (messages.isEmpty && agentName.isNotEmpty && playVoiceOnEnter) {
// 生成动态问候语 // 只有 playVoiceOnEnter 为 true 时才生成动态问候语
await _generatePersonalizedGreeting(); await _generatePersonalizedGreeting();
// 保存聊天记录并立即滚动到底部
await _saveChatHistory();
_scrollToBottom();
} }
// 保存聊天记录并滚动到底部
await _saveChatHistory();
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
} catch (e) { } catch (e) {
print('显示欢迎消息失败: $e'); print('显示欢迎消息失败: $e');
// 如果生成问候语失败,确保至少显示静态欢迎消息 // 如果生成问候语失败,确保至少显示静态欢迎消息
@ -163,47 +299,31 @@ class ChatController extends GetxController {
@override @override
void onClose() { void onClose() {
_cleanupResources();
super.onClose();
}
@override
void dispose() {
_cleanupResources();
super.dispose();
}
void _cleanupResources() {
if (_isDisposed) return; // 防止重复清理
_isDisposed = true; _isDisposed = true;
// 确保语音输入被停止
stopVoiceInput();
// 先停止所有正在进行的操作
_ttsService.stop();
currentStreamMessage.value = '';
_currentAssistantMessage = null;
_pendingTtsText = '';
// 最后再处理控制器的销毁
try { try {
if (textController.hasListeners) { messageController.dispose();
textController.dispose();
}
} catch (e) {
print('Disposing textController failed: $e');
}
try {
if (scrollController.hasClients) {
scrollController.dispose();
}
} catch (e) { } catch (e) {
print('Disposing scrollController failed: $e'); debugPrint('Error disposing messageController: $e');
} }
try { try {
_ttsService.stop(); scrollController.dispose();
} catch (e) { } catch (e) {
print('Stopping TTS service failed: $e'); debugPrint('Error disposing scrollController: $e');
} }
// 清理其他资源 super.onClose();
currentStreamMessage.value = '';
_currentAssistantMessage = null;
_pendingTtsText = '';
} }
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
@ -211,8 +331,14 @@ class ChatController extends GetxController {
final historyJson = _storage.read('${_storagePrefix}${agentId}'); final historyJson = _storage.read('${_storagePrefix}${agentId}');
if (historyJson != null) { if (historyJson != null) {
final List<dynamic> historyList = json.decode(historyJson); final List<dynamic> historyList = json.decode(historyJson);
messages.value = messages.value = historyList.map((item) => Message.fromJson(item)).toList();
historyList.map((item) => Message.fromJson(item)).toList();
// 如果有历史消息,等待下一帧再滚动到底部
if (messages.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
} }
} catch (e) { } catch (e) {
Get.snackbar( Get.snackbar(
@ -239,11 +365,7 @@ class ChatController extends GetxController {
void _scrollToBottom() { void _scrollToBottom() {
if (scrollController.hasClients) { if (scrollController.hasClients) {
scrollController.animateTo( scrollController.jumpTo(scrollController.position.maxScrollExtent);
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} }
} }
@ -259,96 +381,112 @@ class ChatController extends GetxController {
content: content, content: content,
timestamp: _currentAssistantMessage!.timestamp, timestamp: _currentAssistantMessage!.timestamp,
); );
_scrollToBottom(); // 只在非初始化阶段滚动
if (!playVoiceOnEnter || content.isNotEmpty) {
_scrollToBottom();
}
} }
} }
} }
void sendMessage(String text) async { void clearMessage() {
if (text.trim().isEmpty) return; if (!_isDisposed && messageController.text.isNotEmpty) {
messageController.clear();
}
}
final userMessage = Message( Future<void> sendMessage() async {
role: 'user', if (_isDisposed) return;
content: text,
timestamp: DateTime.now(), final text = messageController.text.trim();
); if (text.isEmpty) return;
messages.add(userMessage);
textController.clear();
await _saveChatHistory();
_scrollToBottom();
isLoading.value = true;
try { try {
currentStreamMessage.value = ''; final userMessage = Message(
_currentAssistantMessage = Message( role: 'user',
role: 'assistant', content: text,
content: '',
timestamp: DateTime.now(), timestamp: DateTime.now(),
); );
messages.add(_currentAssistantMessage!); messages.add(userMessage);
_pendingTtsText = ''; clearMessage();
await _saveChatHistory();
await for (final chunk in _aiService.sendMessageStream( _scrollToBottom();
messages: messages
.map((m) => {
'role': m.role,
'content': m.content,
})
.toList(),
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
currentStreamMessage.value += chunk;
_updateAssistantMessage(currentStreamMessage.value);
// 累积文本并合成 isLoading.value = true;
_pendingTtsText += chunk; try {
if (!_isDisposed && _ttsService.isEnabled.value) { currentStreamMessage.value = '';
// 检查是否达到最小长度 _currentAssistantMessage = Message(
if (_pendingTtsText.length >= _minTtsLength) { role: 'assistant',
// 找到最后一个句子结束的位置 content: '',
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); timestamp: DateTime.now(),
if (lastSentenceEnd > 0) { );
// 播放到最后一个句子结束的位置 messages.add(_currentAssistantMessage!);
String textToSpeak = _pendingTtsText = '';
_pendingTtsText.substring(0, lastSentenceEnd + 1);
_ttsService.speak(textToSpeak); await for (final chunk in _aiService.sendMessageStream(
// 保留剩余的文本 messages: messages
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); .map((m) => {
'role': m.role,
'content': m.content,
})
.toList(),
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
currentStreamMessage.value += chunk;
_updateAssistantMessage(currentStreamMessage.value);
// 累积文本并合成
_pendingTtsText += chunk;
if (!_isDisposed && _ttsService.isEnabled.value) {
// 检查是否达到最小长度
if (_pendingTtsText.length >= _minTtsLength) {
// 找到最后一个句子结束的位置
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText);
if (lastSentenceEnd > 0) {
// 播放到最后一个句子结束的位置
String textToSpeak =
_pendingTtsText.substring(0, lastSentenceEnd + 1);
_ttsService.speak(textToSpeak);
// 保留剩余的文本
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
}
} }
} }
} }
}
// 处理剩余的文本 // 处理剩余的文本
if (!_isDisposed && if (!_isDisposed &&
_ttsService.isEnabled.value && _ttsService.isEnabled.value &&
_pendingTtsText.isNotEmpty) { _pendingTtsText.isNotEmpty) {
_ttsService.speak(_pendingTtsText); _ttsService.speak(_pendingTtsText);
} }
_pendingTtsText = ''; _pendingTtsText = '';
if (!_isDisposed) { if (!_isDisposed) {
await _saveChatHistory(); await _saveChatHistory();
} }
} catch (e) { } catch (e) {
if (!_isDisposed) { if (!_isDisposed) {
if (_currentAssistantMessage != null) { if (_currentAssistantMessage != null) {
messages.remove(_currentAssistantMessage); messages.remove(_currentAssistantMessage);
}
Get.snackbar(
'Error',
'Failed to get response from AI: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
} finally {
if (!_isDisposed) {
_currentAssistantMessage = null;
currentStreamMessage.value = '';
isLoading.value = false;
} }
Get.snackbar(
'Error',
'Failed to get response from AI: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
} finally {
if (!_isDisposed) {
_currentAssistantMessage = null;
currentStreamMessage.value = '';
isLoading.value = false;
} }
} catch (e) {
print('发送消息失败: $e');
} }
} }
@ -385,6 +523,8 @@ class ChatController extends GetxController {
/// 生成简单的问候语 /// 生成简单的问候语
Future<void> generateSimpleGreeting() async { Future<void> generateSimpleGreeting() async {
if (_isDisposed) return;
try { try {
currentStreamMessage.value = ''; currentStreamMessage.value = '';
_currentAssistantMessage = Message( _currentAssistantMessage = Message(
@ -431,4 +571,131 @@ class ChatController extends GetxController {
} }
} }
// 开始语音输入
void startVoiceInput() {
isVoiceInputVisible.value = true;
isVoiceConnecting.value = true;
isRecording.value = false;
recordingText.value = '';
// 创建语音输入控制器
Get.put(VoiceInputController(
onRecordingResult: handleVoiceResult,
onClosePanel: stopVoiceInput,
onRecognizing: handleRecognizing,
));
// 初始化语音识别
_initializeVoiceRecognition();
}
Future<void> _initializeVoiceRecognition() async {
try {
await _recognitionService.onInit();
isVoiceConnecting.value = false;
if (isVoiceInputVisible.value) {
startRecording();
}
} catch (e) {
debugPrint('Failed to initialize voice recognition: $e');
Get.snackbar(
'Error',
'初始化语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
stopVoiceInput();
}
}
void startRecording() {
if (!isVoiceConnecting.value && isVoiceInputVisible.value) {
isRecording.value = true;
_recognitionService.startRecognition();
}
}
void stopRecording() {
if (isRecording.value) {
isRecording.value = false;
_recognitionService.stopRecognition();
}
}
void toggleMute() {
isVoiceMuted.value = !isVoiceMuted.value;
}
void stopVoiceInput() {
isVoiceInputVisible.value = false;
isVoiceConnecting.value = true;
isRecording.value = false;
recordingText.value = '';
// 停止语音识别服务
_recognitionService.stopRecognition();
// 如果存在临时语音消息但没有实际内容,则移除它
if (_currentVoiceMessage != null && _currentVoiceMessage!.content == '🎤 ...') {
messages.remove(_currentVoiceMessage);
}
_currentVoiceMessage = null;
// 删除语音输入控制器
try {
if (Get.isRegistered<VoiceInputController>()) {
Get.delete<VoiceInputController>();
}
} catch (e) {
debugPrint('Error deleting VoiceInputController: $e');
}
}
void handleRecognizing(String text) {
if (text.isNotEmpty) {
// 如果还没有创建临时消息,创建一个
if (_currentVoiceMessage == null) {
_currentVoiceMessage = Message(
role: 'user',
content: '🎤 $text',
timestamp: DateTime.now(),
);
messages.add(_currentVoiceMessage!);
} else {
// 更新已存在的临时消息
final index = messages.indexOf(_currentVoiceMessage!);
if (index != -1) {
messages[index] = Message(
role: 'user',
content: '🎤 $text',
timestamp: _currentVoiceMessage!.timestamp,
);
}
}
_scrollToBottom();
}
}
void handleVoiceResult(String text) {
if (text.isNotEmpty) {
if (_currentVoiceMessage != null) {
final index = messages.indexOf(_currentVoiceMessage!);
if (index != -1) {
messages[index] = Message(
role: 'user',
content: text,
timestamp: _currentVoiceMessage!.timestamp,
);
}
}
// 发送消息
messageController.text = text;
sendMessage();
messageController.clear();
_scrollToBottom();
}
// 不再在这里停止语音输入,让用户可以继续说下一句
_currentVoiceMessage = null;
}
} }

128
lib/modules/chat/controllers/voice_input_controller.dart

@ -0,0 +1,128 @@
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import '../../../data/services/voice_recognition_service.dart';
class VoiceInputController extends GetxController {
final VoiceRecognitionService _recognitionService;
// Observable states
final isConnecting = true.obs;
final isMuted = false.obs;
final isRecording = false.obs;
final recognizedText = ''.obs;
// Callbacks
final Function(String) onRecordingResult;
final VoidCallback onClosePanel;
final Function(String) onRecognizing;
VoiceInputController({
required this.onRecordingResult,
required this.onClosePanel,
required this.onRecognizing,
}) : _recognitionService = Get.find<VoiceRecognitionService>();
@override
void onInit() {
super.onInit();
_setupRecognitionCallbacks();
_initializeRecognition();
}
void _setupRecognitionCallbacks() {
_recognitionService.onRecognitionResult = (result) {
if (isMuted.value) return;
recognizedText.value = result;
onRecognizing(result);
};
_recognitionService.onError = (error) {
Get.snackbar(
'Error',
'语音识别失败: $error',
snackPosition: SnackPosition.BOTTOM,
);
stopRecording();
};
_recognitionService.onRecognitionComplete = () {
stopRecording();
if (!isMuted.value && recognizedText.value.isNotEmpty) {
onRecordingResult(recognizedText.value);
recognizedText.value = '';
startRecording();
}
};
}
Future<void> _initializeRecognition() async {
try {
isConnecting.value = true;
await _recognitionService.startRecognition();
isConnecting.value = false;
} catch (e) {
Get.snackbar(
'Error',
'初始化语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
onClosePanel();
}
}
Future<void> startRecording() async {
try {
await _recognitionService.startRecognition();
isRecording.value = true;
recognizedText.value = '';
} catch (e) {
Get.snackbar(
'Error',
'启动语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> stopRecording() async {
if (!isRecording.value) return;
try {
await _recognitionService.stopRecognition();
} catch (e) {
debugPrint('Error stopping recording: $e');
} finally {
isRecording.value = false;
}
}
void toggleMute() {
isMuted.value = !isMuted.value;
}
@override
void onClose() {
// 停止录音
stopRecording();
// 清理回调
_recognitionService.onRecognitionResult = null;
_recognitionService.onError = null;
_recognitionService.onRecognitionComplete = null;
// 重置状态
isConnecting.value = false;
isMuted.value = false;
isRecording.value = false;
recognizedText.value = '';
// 确保语音识别服务停止
try {
_recognitionService.stopRecognition();
} catch (e) {
debugPrint('Error stopping recognition service: $e');
}
super.onClose();
}
}

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

@ -4,10 +4,248 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/chat_controller.dart'; import '../controllers/chat_controller.dart';
import '../models/message_model.dart'; import '../models/message_model.dart';
import '../../../data/services/volcano_tts_service.dart'; import '../../../data/services/volcano_tts_service.dart';
import '../widgets/voice_input_panel.dart';
class ChatView extends GetView<ChatController> { class ChatView extends GetView<ChatController> {
const ChatView({Key? key}) : super(key: key); const ChatView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
return true;
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: SafeArea(
child: Column(
children: [
Expanded(
child: _buildMessageList(),
),
_buildInputSection(),
],
),
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.black,
size: 20.sp,
),
onPressed: () async {
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
Get.back();
},
),
title: Column(
children: [
Text(
controller.agentName,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
if (controller.agentSubtitle != null)
Text(
controller.agentSubtitle!,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
centerTitle: true,
actions: [
IconButton(
icon: Obx(() => Icon(
Get.find<VolcanoTtsService>().isEnabled.value
? Icons.volume_up
: Icons.volume_off,
color: Colors.black,
size: 24.sp,
)),
onPressed: controller.toggleTTS,
),
IconButton(
icon: Icon(
Icons.more_horiz,
color: Colors.black,
size: 24.sp,
),
onPressed: () => _showMoreOptions(),
),
],
);
}
Widget _buildMessageList() {
return Stack(
children: [
Obx(() => ListView.builder(
controller: controller.scrollController,
padding: EdgeInsets.only(
top: 16.h,
bottom: controller.isVoiceInputVisible.value ? 140.h : 80.h,
),
itemCount: controller.messages.length,
itemBuilder: (context, index) => _buildMessage(controller.messages[index]),
physics: const AlwaysScrollableScrollPhysics(),
clipBehavior: Clip.none,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
)),
Obx(() {
if (!controller.isLoading.value) return const SizedBox.shrink();
return Positioned(
bottom: controller.isVoiceInputVisible.value ? 150.h : 90.h,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 16.w,
height: 16.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4A6FE5)),
),
),
SizedBox(width: 8.w),
Text(
'正在思考...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
],
),
),
);
}),
],
);
}
Widget _buildInputSection() {
return Obx(() {
if (controller.isVoiceInputVisible.value) {
return VoiceInputPanel(
onRecordingResult: controller.handleVoiceResult,
onClose: controller.stopVoiceInput,
onRecognizing: controller.handleRecognizing,
);
}
return _buildTextInput();
});
}
Widget _buildTextInput() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.grey[200]!,
width: 1,
),
),
),
child: Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24.r),
),
child: Row(
children: [
IconButton(
icon: Icon(
Icons.mic,
color: Colors.grey[600],
size: 24.sp,
),
onPressed: controller.startVoiceInput,
),
Expanded(
child: TextField(
controller: controller.messageController,
decoration: const InputDecoration(
hintText: '输入消息...',
border: InputBorder.none,
),
maxLines: null,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
onSubmitted: (_) => controller.sendMessage(),
),
),
IconButton(
icon: const Icon(Icons.send),
color: const Color(0xFF4A6FE5),
onPressed: controller.sendMessage,
),
],
),
),
),
],
),
);
}
void _showMoreOptions() {
Get.bottomSheet(
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.r),
topRight: Radius.circular(20.r),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),
title: const Text('清空对话'),
onTap: () {
Get.back();
controller.clearChat();
},
),
],
),
),
);
}
Widget _buildMessage(Message message) { Widget _buildMessage(Message message) {
final isUser = message.role == 'user'; final isUser = message.role == 'user';
return Padding( return Padding(
@ -62,221 +300,4 @@ class ChatView extends GetView<ChatController> {
), ),
); );
} }
@override
Widget build(BuildContext context) {
// 添加布局完成后的回调
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.scrollController.hasClients) {
controller.scrollController.jumpTo(
controller.scrollController.position.maxScrollExtent,
);
}
});
return WillPopScope(
onWillPop: () async {
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
Get.delete<ChatController>(); // 删除控制器
return true;
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.black,
size: 20.sp,
),
onPressed: () async {
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
Get.delete<ChatController>(); // 删除控制器
Get.back();
},
),
title: Column(
children: [
Text(
controller.agentName,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
if (controller.agentSubtitle != null)
Text(
controller.agentSubtitle!,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
centerTitle: true,
actions: [
IconButton(
icon: Obx(() => Icon(
Get.find<VolcanoTtsService>().isEnabled.value
? Icons.volume_up
: Icons.volume_off,
color: Colors.black,
size: 24.sp,
)),
onPressed: controller.toggleTTS,
),
IconButton(
icon: Icon(
Icons.more_horiz,
color: Colors.black,
size: 24.sp,
),
onPressed: () {
Get.bottomSheet(
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.r),
topRight: Radius.circular(20.r),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),
title: const Text('清空对话'),
onTap: () {
Get.back();
controller.clearChat();
},
),
],
),
),
);
},
),
],
),
body: Column(
children: [
Expanded(
child: Obx(() => ListView.builder(
controller: controller.scrollController,
padding: EdgeInsets.symmetric(vertical: 16.h),
itemCount: controller.messages.length,
itemBuilder: (context, index) => _buildMessage(controller.messages[index]),
physics: const AlwaysScrollableScrollPhysics(),
clipBehavior: Clip.none,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
)),
),
Obx(() => controller.isLoading.value
? Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 16.w,
height: 16.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4A6FE5)),
),
),
SizedBox(width: 8.w),
Text(
'正在思考...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
],
),
)
: const SizedBox.shrink(),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.grey[200]!,
width: 1,
),
),
),
child: SafeArea(
child: Row(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24.r),
),
child: TextField(
controller: controller.textController,
decoration: InputDecoration(
hintText: '发消息...',
hintStyle: TextStyle(
color: Colors.grey[400],
fontSize: 16.sp,
),
border: InputBorder.none,
),
style: TextStyle(
fontSize: 16.sp,
),
maxLines: 1,
textInputAction: TextInputAction.send,
onSubmitted: controller.sendMessage,
),
),
),
SizedBox(width: 12.w),
GestureDetector(
onTap: () {
if (controller.textController.text.isNotEmpty) {
controller.sendMessage(controller.textController.text);
}
},
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: const Color(0xFF4A6FE5),
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(
Icons.send,
color: Colors.white,
size: 20.sp,
),
),
),
],
),
),
),
],
),
),
);
}
} }

130
lib/modules/chat/widgets/voice_input_panel.dart

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/voice_input_controller.dart';
class VoiceInputPanel extends GetView<VoiceInputController> {
const VoiceInputPanel({
Key? key,
required this.onRecordingResult,
required this.onClose,
required this.onRecognizing,
}) : super(key: key);
final Function(String) onRecordingResult;
final VoidCallback onClose;
final Function(String) onRecognizing;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white,
Colors.pink[50]!.withOpacity(0.3),
],
),
),
child: Container(
height: 120.h + MediaQuery.of(context).padding.bottom,
padding: EdgeInsets.only(
left: 40.w,
right: 40.w,
top: 12.h,
bottom: 12.h + MediaQuery.of(context).padding.bottom,
),
child: Row(
children: [
// 左侧静音开关
Obx(() => Container(
width: 52.w,
height: 52.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: controller.isMuted.value ? Colors.grey[200] : Colors.white,
border: Border.all(
color: controller.isMuted.value ? Colors.grey[400]! : Colors.grey[300]!,
width: 1,
),
),
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
controller.isMuted.value ? Icons.mic_off_rounded : Icons.mic_rounded,
color: controller.isConnecting.value ? Colors.grey : Colors.black87,
size: 28.sp,
),
onPressed: controller.isConnecting.value ? null : controller.toggleMute,
),
)),
SizedBox(width: 20.w),
// 中间文字
Expanded(
child: Center(
child: Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.isConnecting.value ? '连接中...' : '你可以开始说话',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: controller.isConnecting.value ? Colors.grey[500] : Colors.black87,
),
),
if (controller.isConnecting.value) ...[
SizedBox(width: 8.w),
SizedBox(
width: 32.w,
child: Row(
children: List.generate(
3,
(index) => Container(
width: 4.w,
height: 4.w,
margin: EdgeInsets.only(right: 4.w),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[400],
),
),
),
),
),
],
],
)),
),
),
SizedBox(width: 20.w),
// 右侧关闭按钮
Container(
width: 52.w,
height: 52.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(
color: Colors.grey[300]!,
width: 1,
),
),
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.close_rounded,
color: Colors.black87,
size: 28.sp,
),
onPressed: onClose,
),
),
],
),
),
);
}
}

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

@ -21,7 +21,7 @@ class ExploreView extends GetView<ExploreController> {
return; return;
} }
// 导航到聊天页面 // 导航到聊天页面,让 binding 来处理控制器的创建
Get.toNamed('/chat', arguments: { Get.toNamed('/chat', arguments: {
'agentId': agent.id, 'agentId': agent.id,
'agentName': agent.name, 'agentName': agent.name,

4
linux/flutter/generated_plugin_registrant.cc

@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <record_linux/record_linux_plugin.h>
#include <window_size/window_size_plugin.h> #include <window_size/window_size_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) window_size_registrar = g_autoptr(FlPluginRegistrar) window_size_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin");
window_size_plugin_register_with_registrar(window_size_registrar); window_size_plugin_register_with_registrar(window_size_registrar);

1
linux/flutter/generated_plugins.cmake

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
record_linux
window_size window_size
) )

18
macos/Podfile

@ -1,4 +1,4 @@
platform :osx, '10.14' platform :osx, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -28,8 +28,12 @@ flutter_macos_podfile_setup
target 'Runner' do target 'Runner' do
use_frameworks! use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
pod 'record_darwin', :path => 'Flutter/ephemeral/.symlinks/plugins/record_darwin/macos'
target 'RunnerTests' do target 'RunnerTests' do
inherit! :search_paths inherit! :search_paths
end end
@ -38,5 +42,17 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target) flutter_additional_macos_build_settings(target)
# Start of the permission-related additions
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_MICROPHONE=1',
]
# Ensure minimum deployment target
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.0'
end
# End of the permission-related additions
end end
end end

20
macos/Podfile.lock

@ -4,6 +4,10 @@ PODS:
- FlutterMacOS - FlutterMacOS
- audio_session (0.0.1): - audio_session (0.0.1):
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- just_audio (0.0.1): - just_audio (0.0.1):
- Flutter - Flutter
@ -11,6 +15,8 @@ PODS:
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- record_darwin (1.0.0):
- FlutterMacOS
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -20,9 +26,12 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`) - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`) - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
@ -31,12 +40,18 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin :path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin
audio_session: audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
just_audio: just_audio:
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin :path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
record_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos
sqflite_darwin: sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
window_size: window_size:
@ -45,12 +60,15 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9
PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

8
macos/Runner.xcodeproj/project.pbxproj

@ -557,8 +557,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = YES;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
@ -639,7 +639,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@ -689,7 +689,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;

23
macos/Runner/AppDelegate.swift

@ -1,8 +1,17 @@
import Cocoa import Cocoa
import FlutterMacOS import FlutterMacOS
import UserNotifications
@main @main
class AppDelegate: FlutterAppDelegate { class AppDelegate: FlutterAppDelegate, UNUserNotificationCenterDelegate {
override func applicationDidFinishLaunching(_ notification: Notification) {
if #available(macOS 10.14, *) {
UNUserNotificationCenter.current().delegate = self
}
super.applicationDidFinishLaunching(notification)
}
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true
} }
@ -10,4 +19,16 @@ class AppDelegate: FlutterAppDelegate {
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true return true
} }
// MARK: - UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if #available(macOS 11.0, *) {
completionHandler([.banner, .sound])
} else {
completionHandler([.alert, .sound])
}
}
} }

2
macos/Runner/DebugProfile.entitlements

@ -14,5 +14,7 @@
<true/> <true/>
<key>com.apple.security.assets.audio.read-write</key> <key>com.apple.security.assets.audio.read-write</key>
<true/> <true/>
<key>com.apple.security.microphone</key>
<true/>
</dict> </dict>
</plist> </plist>

2
macos/Runner/Info.plist

@ -28,5 +28,7 @@
<string>MainMenu</string> <string>MainMenu</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要使用麦克风进行语音输入</string>
</dict> </dict>
</plist> </plist>

2
macos/Runner/Release.entitlements

@ -10,5 +10,7 @@
<true/> <true/>
<key>com.apple.security.assets.audio.read-write</key> <key>com.apple.security.assets.audio.read-write</key>
<true/> <true/>
<key>com.apple.security.microphone</key>
<true/>
</dict> </dict>
</plist> </plist>

4
pubspec.yaml

@ -16,7 +16,7 @@ dependencies:
flutter_screenutil: ^5.9.0 flutter_screenutil: ^5.9.0
just_audio: ^0.9.36 just_audio: ^0.9.36
flutter_azure_tts: ^0.2.3 flutter_azure_tts: ^0.2.3
path_provider: ^2.1.2 path_provider: ^2.1.5
audio_session: ^0.1.18 audio_session: ^0.1.18
flutter_markdown: ^0.6.18+2 flutter_markdown: ^0.6.18+2
markdown: ^7.1.1 markdown: ^7.1.1
@ -32,6 +32,8 @@ dependencies:
ref: 6c66ad23ee79749f30a8eece542cf54eaf157ed8 ref: 6c66ad23ee79749f30a8eece542cf54eaf157ed8
flutter_local_notifications: ^18.0.1 flutter_local_notifications: ^18.0.1
device_info_plus: ^10.1.2 device_info_plus: ^10.1.2
uuid: ^4.3.3
record: ^5.2.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

3
windows/flutter/generated_plugin_registrant.cc

@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <window_size/window_size_plugin.h> #include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
WindowSizePluginRegisterWithRegistrar( WindowSizePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowSizePlugin")); registry->GetRegistrarForPlugin("WindowSizePlugin"));
} }

1
windows/flutter/generated_plugins.cmake

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows permission_handler_windows
record_windows
window_size window_size
) )

Loading…
Cancel
Save