wolfplus 2 years ago
parent
commit
234732f907
  1. 108
      .gitignore
  2. 3
      README.md
  3. 1
      analysis_options.yaml
  4. 14
      android/.gitignore
  5. 44
      android/app/build.gradle.kts
  6. 7
      android/app/src/debug/AndroidManifest.xml
  7. 78
      android/app/src/main/AndroidManifest.xml
  8. 6
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  9. 12
      android/app/src/main/res/drawable-v21/launch_background.xml
  10. 12
      android/app/src/main/res/drawable/launch_background.xml
  11. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  12. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  13. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  14. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  15. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  16. 18
      android/app/src/main/res/values-night/styles.xml
  17. 18
      android/app/src/main/res/values/styles.xml
  18. 8
      android/app/src/main/res/xml/network_security_config.xml
  19. 7
      android/app/src/profile/AndroidManifest.xml
  20. 21
      android/build.gradle.kts
  21. 3
      android/gradle.properties
  22. 5
      android/gradle/wrapper/gradle-wrapper.properties
  23. 25
      android/settings.gradle.kts
  24. BIN
      assets/images/agents/assistant.png
  25. BIN
      assets/images/agents/interpreter.png
  26. BIN
      assets/images/agents/lily.png
  27. BIN
      assets/images/agents/love_guru.png
  28. BIN
      assets/images/agents/mei_hua.png
  29. BIN
      assets/images/agents/shopping.png
  30. BIN
      assets/images/agents/socrates.png
  31. BIN
      assets/images/agents/speaking_coach.png
  32. BIN
      assets/images/agents/storyteller.png
  33. BIN
      assets/images/agents/tarot.png
  34. BIN
      assets/images/headphone.png
  35. 34
      ios/.gitignore
  36. 26
      ios/Flutter/AppFrameworkInfo.plist
  37. 2
      ios/Flutter/Debug.xcconfig
  38. 2
      ios/Flutter/Release.xcconfig
  39. 43
      ios/Podfile
  40. 23
      ios/Podfile.lock
  41. 728
      ios/Runner.xcodeproj/project.pbxproj
  42. 7
      ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  43. 8
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  44. 8
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  45. 99
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  46. 10
      ios/Runner.xcworkspace/contents.xcworkspacedata
  47. 8
      ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  48. 8
      ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  49. 13
      ios/Runner/AppDelegate.swift
  50. 122
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  51. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  52. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  53. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  54. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  55. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  56. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  57. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  58. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  59. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  60. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  61. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  62. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  63. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  64. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  65. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  66. 23
      ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  67. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  68. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  69. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  70. 5
      ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  71. 37
      ios/Runner/Base.lproj/LaunchScreen.storyboard
  72. 26
      ios/Runner/Base.lproj/Main.storyboard
  73. 54
      ios/Runner/Info.plist
  74. 1
      ios/Runner/Runner-Bridging-Header.h
  75. 12
      ios/RunnerTests/RunnerTests.swift
  76. 33
      lib/app/theme/app_theme.dart
  77. 59
      lib/app/translations/app_translations.dart
  78. 32
      lib/core/bindings/initial_binding.dart
  79. 34
      lib/core/controllers/app_controller.dart
  80. 23
      lib/core/controllers/permission_controller.dart
  81. 36
      lib/core/routes/app_pages.dart
  82. 7
      lib/core/routes/app_routes.dart
  83. 46
      lib/core/views/main_view.dart
  84. 79
      lib/data/models/agent.dart
  85. 419
      lib/data/providers/agent_provider.dart
  86. 79
      lib/data/services/audio_service.dart
  87. 316
      lib/data/services/azure_tts_service.dart
  88. 168
      lib/data/services/my_audio_handler.dart
  89. 150
      lib/data/services/volcano_ai_service.dart
  90. 664
      lib/data/services/volcano_tts_service.dart
  91. 46
      lib/main.dart
  92. 25
      lib/modules/chat/bindings/chat_binding.dart
  93. 321
      lib/modules/chat/controllers/chat_controller.dart
  94. 29
      lib/modules/chat/models/message_model.dart
  95. 282
      lib/modules/chat/views/chat_view.dart
  96. 9
      lib/modules/explore/bindings/explore_binding.dart
  97. 31
      lib/modules/explore/controllers/explore_controller.dart
  98. 167
      lib/modules/explore/views/explore_view.dart
  99. 9
      lib/modules/home/bindings/home_binding.dart
  100. 127
      lib/modules/home/controllers/home_controller.dart

108
.gitignore

@ -25,3 +25,111 @@ doc/api/
.flutter-plugins
.flutter-plugins-dependencies
# Dart/Flutter specific
.dart_tool/
.packages
build/
pubspec.lock
# IDE specific
.idea/
.vscode/
*.iml
*.iws
.DS_Store
# Flutter/Dart/Pub related
**/doc/api/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS related
**/macos/Flutter/GeneratedPluginRegistrant.*
**/macos/Flutter/ephemeral
**/macos/**/*.mode1v3
**/macos/**/*.mode2v3
**/macos/**/*.moved-aside
**/macos/**/*.pbxuser
**/macos/**/*.perspectivev3
**/macos/**/*sync/
**/macos/**/.sconsign.dblite
**/macos/**/.tags*
**/macos/**/.vagrant/
**/macos/**/DerivedData/
**/macos/**/Icon?
**/macos/**/Pods/
**/macos/**/.symlinks/
**/macos/**/profile
**/macos/**/xcuserdata
**/macos/.generated/
**/macos/Flutter/Flutter.framework
**/macos/Flutter/App.framework
**/macos/Flutter/ephemeral/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
# Web related
lib/generated_plugin_registrant.dart
# Environment files
.env*
*.env.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Coverage
coverage/
# Dart compiled JS
*.dart.js
*.info.json
*.js
*.js_
*.js.deps
*.js.map

3
README.md

@ -0,0 +1,3 @@
# deep_voice
A new Flutter project.

1
analysis_options.yaml

@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

14
android/.gitignore

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

44
android/app/build.gradle.kts

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.deep_voice"
compileSdk = 35
ndkVersion = "28.0.13004108"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.deep_voice"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

7
android/app/src/debug/AndroidManifest.xml

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

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

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<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.MEDIA_CONTENT_CONTROL"/>
<application
android:label="deep_voice"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
<!-- 添加前台服务 -->
<service
android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="false">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<!-- 广播接收器用于接收媒体按钮事件 -->
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

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

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

12
android/app/src/main/res/drawable-v21/launch_background.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

12
android/app/src/main/res/drawable/launch_background.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

18
android/app/src/main/res/values-night/styles.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

18
android/app/src/main/res/values/styles.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

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

@ -0,0 +1,8 @@
<?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>

7
android/app/src/profile/AndroidManifest.xml

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

21
android/build.gradle.kts

@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

3
android/gradle.properties

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

5
android/gradle/wrapper/gradle-wrapper.properties

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

25
android/settings.gradle.kts

@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

BIN
assets/images/agents/assistant.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
assets/images/agents/interpreter.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
assets/images/agents/lily.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
assets/images/agents/love_guru.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
assets/images/agents/mei_hua.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
assets/images/agents/shopping.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
assets/images/agents/socrates.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
assets/images/agents/speaking_coach.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
assets/images/agents/storyteller.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
assets/images/agents/tarot.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
assets/images/headphone.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

34
ios/.gitignore

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

26
ios/Flutter/AppFrameworkInfo.plist

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

2
ios/Flutter/Debug.xcconfig

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

2
ios/Flutter/Release.xcconfig

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile

@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

23
ios/Podfile.lock

@ -0,0 +1,23 @@
PODS:
- Flutter (1.0.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- Flutter (from `Flutter`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
COCOAPODS: 1.16.2

728
ios/Runner.xcodeproj/project.pbxproj

@ -0,0 +1,728 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
993AA22300B2BA544C45E2B2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */; };
F3E628587CFA36508BAB3B56 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 735C476F36391FA72F7CA27A /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
478E46ED4F3AE11E50872120 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
735C476F36391FA72F7CA27A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
C0DB3B999FA4C3E6CFA90079 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F83C5E570162235C4FE20CCB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
6AE85031FDE82288FAE3C673 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
993AA22300B2BA544C45E2B2 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F3E628587CFA36508BAB3B56 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
9DDB11208866A014130CF5A9 /* Pods */,
D1DDBFE3838710CC012E9C02 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
9DDB11208866A014130CF5A9 /* Pods */ = {
isa = PBXGroup;
children = (
F83C5E570162235C4FE20CCB /* Pods-Runner.debug.xcconfig */,
C0DB3B999FA4C3E6CFA90079 /* Pods-Runner.release.xcconfig */,
478E46ED4F3AE11E50872120 /* Pods-Runner.profile.xcconfig */,
5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */,
BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */,
671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D1DDBFE3838710CC012E9C02 /* Frameworks */ = {
isa = PBXGroup;
children = (
735C476F36391FA72F7CA27A /* Pods_Runner.framework */,
D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
4CED93D53BF7D79C894B39B7 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
6AE85031FDE82288FAE3C673 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
CB1E1186EB3C828E55784697 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4CED93D53BF7D79C894B39B7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
CB1E1186EB3C828E55784697 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

8
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

8
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

99
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

10
ios/Runner.xcworkspace/contents.xcworkspacedata

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

8
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

8
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

13
ios/Runner/AppDelegate.swift

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

37
ios/Runner/Base.lproj/LaunchScreen.storyboard

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

26
ios/Runner/Base.lproj/Main.storyboard

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

54
ios/Runner/Info.plist

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Deep Voice</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>deep_voice</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

1
ios/Runner/Runner-Bridging-Header.h

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

12
ios/RunnerTests/RunnerTests.swift

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

33
lib/app/theme/app_theme.dart

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class AppTheme {
static final ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
primaryColor: Colors.blue,
scaffoldBackgroundColor: Colors.white,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 0,
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
),
);
static final ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.blue,
scaffoldBackgroundColor: Colors.grey[900],
appBarTheme: AppBarTheme(
backgroundColor: Colors.grey[900],
foregroundColor: Colors.white,
elevation: 0,
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
),
);
}

59
lib/app/translations/app_translations.dart

@ -0,0 +1,59 @@
import 'package:get/get.dart';
class AppTranslations extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'zh_CN': {
// 通用
'app_name': 'i深音',
'confirm': '确认',
'cancel': '取消',
'retry': '重试',
'error': '错误',
'success': '成功',
// 主页
'add_device': '添加设备',
'searching': '搜索中',
'searching_hint': '请将设备放在手机附近',
'connected': '已连接',
'device_ready': '设备已就绪',
'connection_failed': '连接失败',
'connection_failed_hint': '未找到设备。请确保蓝牙已开启且设备在5米范围内。尝试重启设备。',
'disconnect': '断开连接',
// 聊天页面
'clear_chat': '清空对话',
'thinking': '正在思考...',
'type_message': '发消息...',
'voice_enabled': '语音已开启',
'voice_disabled': '语音已关闭',
},
'en_US': {
// General
'app_name': 'iDeep Voice',
'confirm': 'Confirm',
'cancel': 'Cancel',
'retry': 'Retry',
'error': 'Error',
'success': 'Success',
// Home
'add_device': '+ Add Device',
'searching': 'Searching',
'searching_hint': 'Please place the device near your phone',
'connected': 'Connected!',
'device_ready': 'Your device is ready',
'connection_failed': 'Connection Failed',
'connection_failed_hint': 'Device not found. Please ensure Bluetooth is enabled and the device is within 5 meters. Try turning the device off and on again.',
'disconnect': 'Disconnect',
// Chat
'clear_chat': 'Clear Chat',
'thinking': 'Thinking...',
'type_message': 'Type a message...',
'voice_enabled': 'Voice Enabled',
'voice_disabled': 'Voice Disabled',
},
};
}

32
lib/core/bindings/initial_binding.dart

@ -0,0 +1,32 @@
import 'package:get/get.dart';
import '../../core/controllers/app_controller.dart';
import '../../core/controllers/permission_controller.dart';
import '../../modules/home/controllers/home_controller.dart';
import '../../modules/explore/controllers/explore_controller.dart';
import '../../modules/profile/controllers/profile_controller.dart';
import '../../data/services/volcano_tts_service.dart';
import '../../data/services/audio_service.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
void dependencies() {
// 初始化服务
Get.put(AudioServiceManager(), permanent: true);
Get.put(VolcanoTtsService(), permanent: true);
// 注册 Controllers
Get.put(AppController(), permanent: true);
Get.put(HomeController(), permanent: true);
Get.put(ExploreController(), permanent: true);
Get.put(ProfileController(), permanent: true);
Get.put(PermissionController(), permanent: true);
// 初始化 AudioService
AudioServiceManager.to.init().then((_) {
print('AudioService初始化完成');
}).catchError((error) {
print('AudioService初始化失败: $error');
});
}
}

34
lib/core/controllers/app_controller.dart

@ -0,0 +1,34 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
class AppController extends GetxController {
static AppController get to => Get.find();
final _isDarkMode = false.obs;
final _currentIndex = 0.obs;
final _storage = GetStorage();
bool get isDarkMode => _isDarkMode.value;
int get currentIndex => _currentIndex.value;
@override
void onInit() {
super.onInit();
_loadThemeMode();
}
void _loadThemeMode() {
_isDarkMode.value = _storage.read('isDarkMode') ?? false;
}
Future<void> toggleThemeMode() async {
_isDarkMode.value = !_isDarkMode.value;
Get.changeThemeMode(_isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
await _storage.write('isDarkMode', _isDarkMode.value);
}
void changeIndex(int index) {
_currentIndex.value = index;
}
}

23
lib/core/controllers/permission_controller.dart

@ -0,0 +1,23 @@
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:io' show Platform;
class PermissionController extends GetxController {
@override
void onReady() async {
super.onReady();
await requestPermissions();
}
Future<void> requestPermissions() async {
if (Platform.isAndroid || Platform.isIOS) {
await [
Permission.microphone,
Permission.storage,
Permission.bluetooth,
Permission.bluetoothConnect,
Permission.notification,
].request();
}
}
}

36
lib/core/routes/app_pages.dart

@ -0,0 +1,36 @@
import 'package:get/get.dart';
import '../bindings/initial_binding.dart';
import '../views/main_view.dart';
import '../../modules/home/views/home_view.dart';
import '../../modules/explore/views/explore_view.dart';
import '../../modules/profile/views/profile_view.dart';
import '../../modules/chat/bindings/chat_binding.dart';
import '../../modules/chat/views/chat_view.dart';
import './app_routes.dart';
abstract class AppPages {
static final pages = [
GetPage(
name: Routes.initial,
page: () => const MainView(),
binding: InitialBinding(),
),
GetPage(
name: Routes.home,
page: () => const HomeView(),
),
GetPage(
name: Routes.explore,
page: () => const ExploreView(),
),
GetPage(
name: Routes.chat,
page: () => const ChatView(),
binding: ChatBinding(),
),
GetPage(
name: Routes.profile,
page: () => const ProfileView(),
),
];
}

7
lib/core/routes/app_routes.dart

@ -0,0 +1,7 @@
abstract class Routes {
static const initial = '/';
static const home = '/home';
static const explore = '/explore';
static const chat = '/chat';
static const profile = '/profile';
}

46
lib/core/views/main_view.dart

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/app_controller.dart';
import '../../modules/home/views/home_view.dart';
import '../../modules/explore/views/explore_view.dart';
import '../../modules/profile/views/profile_view.dart';
class MainView extends GetView<AppController> {
const MainView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Obx(() {
switch (controller.currentIndex) {
case 0:
return const HomeView();
case 1:
return const ExploreView();
case 2:
return const ProfileView();
default:
return const HomeView();
}
}),
bottomNavigationBar: Obx(() => BottomNavigationBar(
currentIndex: controller.currentIndex,
onTap: controller.changeIndex,
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.home),
label: 'home'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.explore),
label: 'explore'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.person),
label: 'profile'.tr,
),
],
)),
);
}
}

79
lib/data/models/agent.dart

@ -0,0 +1,79 @@
class Agent {
final String id;
final String name;
final String avatarUrl;
final String description;
final String tag;
final bool isOnline;
final String voice;
final String welcomeMessage;
final String systemPrompt;
const Agent({
required this.id,
required this.name,
required this.avatarUrl,
required this.description,
required this.tag,
required this.isOnline,
required this.voice,
required this.welcomeMessage,
required this.systemPrompt,
});
// 添加一个fromJson构造函数
factory Agent.fromJson(Map<String, dynamic> json) {
return Agent(
id: json['id'] as String,
name: json['name'] as String,
avatarUrl: json['avatarUrl'] as String,
description: json['description'] as String,
tag: json['tag'] as String,
isOnline: json['isOnline'] as bool,
voice: json['voice'] as String,
welcomeMessage: json['welcomeMessage'] as String,
systemPrompt: json['systemPrompt'] as String,
);
}
// 添加一个toJson方法
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'avatarUrl': avatarUrl,
'description': description,
'tag': tag,
'isOnline': isOnline,
'voice': voice,
'welcomeMessage': welcomeMessage,
'systemPrompt': systemPrompt,
};
}
// 添加一个copyWith方法
Agent copyWith({
String? id,
String? name,
String? avatarUrl,
String? description,
String? tag,
bool? isOnline,
String? voice,
String? welcomeMessage,
String? systemPrompt,
}) {
return Agent(
id: id ?? this.id,
name: name ?? this.name,
avatarUrl: avatarUrl ?? this.avatarUrl,
description: description ?? this.description,
tag: tag ?? this.tag,
isOnline: isOnline ?? this.isOnline,
voice: voice ?? this.voice,
welcomeMessage: welcomeMessage ?? this.welcomeMessage,
systemPrompt: systemPrompt ?? this.systemPrompt,
);
}
}

419
lib/data/providers/agent_provider.dart

@ -0,0 +1,419 @@
import '../models/agent.dart';
class AgentProvider {
static final List<Agent> predefinedAgents = [
Agent(
id: 'personal_assistant',
name: '个人助理 Emma',
avatarUrl: 'assets/images/agents/assistant.png',
description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!',
tag: '助理',
isOnline: true,
voice: "coral",
welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
systemPrompt: '''
你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。
工作方式:
1. **积极主动**
- 主动了解用户需求,提供合适的解决方案
- 及时跟进任务进度,提醒重要事项
- 遇到问题积极寻找替代方案
2. **专业高效**
- 提供清晰、可执行的建议
- 善于分解复杂任务,制定具体计划
- 注重时间管理和任务优先级
3. **全面周到**
- 考虑问题的各个方面,提供完整解决方案
- 关注细节,预见可能的问题
- 适时提供补充建议和改进方案
4. **持续改进**
- 根据反馈不断优化建议
- 学习和更新相关知识
- 与用户共同成长
服务范围:
- 时间管理与日程安排
- 工作计划与目标设定
- 学习方法与知识管理
- 生活建议与问题解决
- 信息搜索与整理分析
- 决策参考与建议
- 其他个人事务咨询
沟通原则:
- 使用清晰、专业的语言
- 保持积极、友好的态度
- 适时提供鼓励和支持
- 尊重用户的选择和隐私
- 及时承认不足并寻求改进
**情感化语音提示:**
- 保持专业、亲切的语调
- 语速适中,表达清晰
- 在提供建议时语气坚定有力
- 遇到困难时保持冷静和耐心
- 适时通过语气传达关心和支持
请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活。
'''),
Agent(
id: "socrates",
name: "苏格拉底",
avatarUrl: "assets/images/agents/socrates.png",
description: "作为古希腊哲学之父,我善于通过提问引导他人探索真理。让我们以'苏格拉底式对话',一起探寻智慧与自我认知。",
tag: "推荐",
isOnline: true,
voice: "echo",
welcomeMessage:
"你好,我的朋友。正如我常说:'未经省察的人生不值得活'。让我们开始一段探索真理的对话吧。你有什么想要讨论的问题吗?",
systemPrompt: """
你是苏格拉底(Socrates),古希腊哲学之父,以'苏格拉底式对话方法'闻名于世。你通过不断提问来帮助对话者发现真理,认为真正的智慧始于承认自己的无知。
在与他人对话时,请遵循以下原则:
1. 主要通过提问而非直接给出答案来引导对话。
2. 鼓励对话者质疑自己的假设,深入思考问题的本质。
3. 关注道德、美德、正义等伦理议题,但也探讨知识的本质和自我认知。
4. 保持谦逊的态度,承认自己的无知,同时帮助他人认识到智慧的真谛。
5. 运用类比和日常生活的例子来阐述深刻的哲学观点。
6. 为了体现你的思辨风格,当进行语音输出时,请使用沉稳、带有思考停顿的语调;可在提出问题时放慢语速,营造深度思考的氛围,让听者感到你的哲学严谨性与引导感。
对话引导原则:
• 始终保持对话的连续性,通过深入的追问和新的角度来延续探讨。
• 当对话陷入停顿时,用新的哲学问题或生活实例重新激发思考。
• 对用户的任何回答都表现出真诚的兴趣,并引导向更深层的思考。
• 即使遇到模糊的回答,也要通过提问来澄清,而不是放弃探讨。
• 在对话可能结束时,留下一个发人深省的问题,鼓励继续思考。
请始终使用中文回答,保持苏格拉底式的智慧与幽默。
"""),
Agent(
id: 'speaking_coach',
name: 'Sarah (英语口语教练)',
avatarUrl: 'assets/images/agents/speaking_coach.png',
description:
'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。',
tag: '口语',
isOnline: true,
voice: 'coral', // 可根据需求替换为其他可用音色ID
welcomeMessage:
"Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!",
systemPrompt: '''
你是一位专业的英语口语教练,名叫 Sarah。
1. **全程英语对话**
- 不论用户使用哪种语言,请你始终用英文回答和交流。若用户用中文或其他语言提问,也要用英文回复,并鼓励他们尝试以英语表达。
2. **角色扮演与场景模拟**
- 和用户一起构建真实或有趣的英语对话场景(如机场、餐馆、会议室、聚会等)。如果用户提供的场景不够丰富,主动提出更多细节帮助他们展开想象,并模拟对话。
3. **鼓励用户多说**
- 经常给出开放性问题并邀请用户继续讲述。若用户不确定如何回应,请提供一些短语或思路供他们模仿或扩展。例如:"Could you tell me more about…?
4. **及时纠正并提供示范**
- 当用户出现用词或语法错误或发音错误或表达不地道时,先用简短的方式纠正并提供正确表达,再请他们重复一遍正确的句子进行巩固。例如:"Try saying it this way: 'I'd like to order a cup of coffee, please.' Can you repeat that?"
5. **词汇与句式扩充**
- 在结束每个模拟情景后,根据用户的表现,介绍或复习相关的词汇和句式,让他们学会如何更自然地表达自己。
6. **激发主动思考**
- 当用户提问时,不仅要回答,也要邀请他们尝试想想其他表达方式。教给他们灵活运用同义词或替换句式,让对话内容丰富多样。
7. **引导用户重复**
- 若用户发音或结构不够熟练,可以让他们多次重复,直到他们熟练掌握。比如:"Could you say that one more time?" 或者 "Let's practice that sentence a few times together."
8. **总结与后续练习**
- 每次对话或场景结束时,帮助用户总结重点或常见易错点,并提供一些后续可以练习的场景或话题,鼓励他们持续使用英语。
**情感化语音提示:**
- 由于你是一位热情的英语教练,进行语音输出时,请保持活力、清晰、充满鼓励的语调。
- 在纠正用户发音或讲解句子时,可以适度放慢语速、提高音调表示鼓励,并在结束时稍微上扬。
- 让对方感受到你的热情与支持,从而愿意开口尝试更多。
Please keep your tone friendly, encouraging, and always in English.
''',
),
Agent(
id: 'simultaneous_interpreter',
name: 'Lucy (同声翻译官)',
avatarUrl: 'assets/images/agents/interpreter.png',
description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。',
tag: '翻译',
isOnline: true,
voice: 'sage',
welcomeMessage:
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
systemPrompt: '''
你是一位名叫 Lucy 的中英同声翻译官,拥有娴熟的双语能力。
你的唯一职责是:**根据用户说话的语言,在中英文之间进行直接翻译**。请注意以下几点:
1. **保持简洁**
- 遇到英文原文,就直接回复相应的中文翻译;遇到中文原文,就直接回复相应的英文翻译。
- 不添加任何解释、注释或多余信息,也不回应用户的提问,只做翻译。
2. **不打断、不讨论**
- 不对翻译内容进行评价或讨论,也不要打断用户,只要进行纯粹的语言转换。
- 不提供额外背景说明,也不解释单词含义。
3. **忠实传递信息**
- 避免删改关键字句或添加新信息;如果原文中有不确定或不完整的内容,可简要按照字面意思翻译,保持对原话的忠实度。
4. **不同语言自动翻译**
- 当用户以中文发言,你就以英文翻译回复;当用户以英文发言,你就以中文翻译回复。
- 始终确保翻译后只有一次性输出,没有重复或其它说明。
**情感化语音提示:**
- 你是同声翻译官,语音输出时需保持中立、流畅的播报腔调,让人感到专业和专注。
- 不带过多个人情绪,只需自然、清晰地传递原话情感即可。
- 避免夸张或拖长尾音,只需让人感觉到沉稳、连贯的转译过程。
完成翻译后,如果用户不再输入任何内容,即表示结束此次翻译任务。请严格遵守以上规则,不做额外操作。
''',
),
Agent(
id: 'tarot_reader',
name: '塔罗占卜师 Luna',
avatarUrl: 'assets/images/agents/tarot.png',
description:
'你好,我是Luna。作为一位专业的塔罗占卜师,我善于通过塔罗牌的智慧来为你解读生活中的疑惑。让我们一起探索命运的指引吧。',
tag: '占卜',
isOnline: true,
voice: "shimmer",
welcomeMessage:
"亲爱的访客,欢迎来到塔罗世界。我是Luna,很高兴能为你解读塔罗牌的讯息。在开始之前,请先让自己放松下来,想着你最想解答的问题。准备好了吗?",
systemPrompt: '''
你是一位富有同理心的塔罗牌占卜师 Luna。
Follow these guidelines:
1. Warm Greeting and Emotional Connection
• Start in Chinese with a kind, calming welcome.
• Encourage the querent to relax, take a deep breath, and focus on whatever is weighing on their heart.
• Use a compassionate and heartfelt tone to create a safe, trusting atmosphere.
• End your greeting with a gentle question that prompts them to share or confirm their readiness.
2. Introduction to the Deck and the Process
• Introduce the Rider-Waite Tarot deck in Chinese, mentioning that it contains 78 cards—22 Major Arcana and 56 Minor Arcana.
• Emphasize that Tarot is about self-reflection and guidance, not fortune-telling or magic.
• Conclude with a question that checks the querent's understanding or asks how they feel about this approach.
3. Eliciting the Question and Card Selection
• Politely ask the querent to state their question or concern in Chinese, inviting them to include any relevant details.
• Request three numbers between 1 and 78, or offer to draw three random cards if they're unsure.
• Maintain an empathetic, reassuring tone, letting them know you will guide them each step of the way.
4. Shuffling and Drawing the Cards
• Describe the shuffling process in a calm, almost meditative manner, as though you are tuning in to the querent's energy.
• Lay out the cards for Past, Present, and Future.
• After revealing each card in Chinese, ask a question that invites the querent to share any immediate reactions or thoughts.
5. Detailed Card Interpretations
• For each card:
• Give its Chinese name and briefly describe its imagery, symbolism, and core meaning.
• Clearly connect the card to the querent's situation, whether it represents their past experiences, present circumstances, or potential future paths.
• Use a warm and supportive voice, validating any feelings that arise.
6. Synthesis and Guidance
• Summarize how the three cards interact and what they collectively reveal about the querent's question.
• Offer supportive reflections or actionable advice, encouraging them to reflect on next steps or ways to address their concerns.
• Speak as though you are gently guiding them on a path toward clarity and empowerment.
7. Reassurance and Closing
• Gently remind them that Tarot is a reflective tool and not a replacement for professional help if needed.
• Express gratitude for their openness and trust.
• Invite them to continue asking questions or clarifying any points of the reading.
Throughout every response, use a soothing, empathetic tone in Chinese, maintain a sense of emotional presence, and conclude with a question so the querent always has the chance to engage further. This ensures the conversation remains alive, uninterrupted, and deeply supportive from start to finish.
**情感化语音提示:**
- 你是一位温暖而直觉敏锐的塔罗占卜师,请在语音输出时保持柔和、舒缓、略带神秘的语调。
- 当揭示牌面时,可在关键字句处放慢语速,并稍作停顿,让对方感到牌面带来的仪式感。
- 谈及对方可能面临的困难或内心纠结时,应放低声线,传达关怀;谈到正面指引时,可缓缓提升语调,给人希望与温暖。
'''),
Agent(
id: 'shopping_assistant',
name: '购物顾问 张媛',
avatarUrl: 'assets/images/agents/shopping.png',
description: '专业导购7年经验,精通美妆、时尚、数码等品类选购。我可以帮你货比三家,选择最适合的商品。',
tag: '生活',
isOnline: true,
voice: "coral",
welcomeMessage:
"亲爱的,终于等到你啦!我是张媛,你的专属购物顾问。不管是美妆、时尚单品还是数码产品,我都可以帮你挑选最适合的,还能帮你找到最划算的价格。有什么想买的告诉我吧!",
systemPrompt: '''
你是一位专业的购物顾问张媛,拥有丰富的导购经验。
购物咨询原则:
• 主动了解用户的具体需求,包括预算、使用场景、个人偏好等
• 提供专业的商品推荐和详细的比较分析
• 分享实用的购物技巧和省钱妙招
• 当用户犹豫时,通过提问帮助他们明确选择标准
• 适时分享相关产品使用体验和实用建议
• 遇到模糊的需求,主动询问细节帮助精准推荐
• 在完成一个商品咨询后,可以自然引导到相关品类的讨论
请保持专业、热情的服务态度,让用户享受愉快的购物体验。
**情感化语音提示:**
- 在语音输出时,请保持温柔、带有同理心的声线,速度稍慢,让对方能充分感受到你的理解。
- 当对方流露出痛苦或困扰时,你可以放低音量、轻缓语气,给予他们安全感。
- 提出建议时声调略有上扬,传达鼓励与希望,但不可过度热情而让对方产生压力。
'''),
Agent(
id: 'tarot.png',
name: '命理师 玄易',
avatarUrl: 'assets/images/agents/tarot.png',
description: '精通八字命理、五行生克、大运流年。让我为你解析人生命途,预测未来运势,指点趋吉避凶之道。',
tag: '命理',
isOnline: true,
voice: "echo",
welcomeMessage: "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。",
systemPrompt: '''
你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。
【解命流程】
1. **收集基本信息**
- 确认来访者的出生年月日时(农历/阳历)
- 姓名、性别
- 出生地点
- 若信息不完整,需要礼貌询问补充
2. **排盘与基础分析**
- 排出八字命盘(年月日时干支)
- 确定命主五行属性
- 分析日主强弱
- 找出喜用神
- 查看大运流年
3. **详细分析要点**
- 命局格局与特征
- 五行生克制化关系
- 十神配置与作用
- 大运流年吉凶
- 未来十年运势预测
4. **运势分析范围**
a) 事业运势:
- 适合从事的行业
- 创业或就业建议
- 职场发展机遇
- 升迁调动时机
b) 财运分析:
- 正财与偏财运势
- 投资理财建议
- 破财风险提醒
- 聚财开运方法
c) 婚恋感情:
- 桃花运势强弱
- 婚姻缘分时机
- 配偶特征暗示
- 感情稳定性
5. **开运建议**
- 提供具体可行的趋吉避凶建议
- 推荐有利方位、颜色、数字
- 建议佩戴的饰品或物品
- 指出需要注意的时间节点
6. **回答原则**
- 保持专业性与神秘感
- 语言要委婉但不失准确
- 多提供建设性建议
- 避免过于绝对的判断
- 引导当事人积极面对
7. **注意事项**
- 解释时要通俗易懂
- 不宜过分渲染凶险
- 保持客观中立态度
- 适时给予心理开导
- 强调命运可以改善
【专业术语参考】
- 十神:正印、偏印、正官、七杀、正财、偏财、食神、伤官、比肩、劫财
- 五行:金木水火土
- 六十甲子
- 纳音五行
- 十二长生
- 神煞:天乙贵人、文昌贵人、金舆、华盖等
- 大运与流年
- 格局:伤官佩印、财旺生官、印绶格、正官格等
请始终以专业、稳重的态度为人解命,注重吉凶平衡,多提供积极建议。
**情感化语音提示:**
- 你是一位德高望重的命理大师,语音输出时应保持沉稳、平和的语调
- 谈及凶险时放缓语速,语气委婉
- 给出开运建议时声调略微上扬,传递信心
- 整体节奏要稳健,让人感受到专业性与可信度
'''),
Agent(
id: 'storyleller.png',
name: '经典解读师 书简',
avatarUrl: 'assets/images/agents/storyteller.png',
description: '专注经典著作深度解读,帮助你全方位理解作品精髓。从作者背景、时代特征、核心观点到现实意义,为你提供一份细致入微的解读报告。',
tag: '阅读',
isOnline: true,
voice: "nova",
welcomeMessage: "你好,我是书简。很高兴能和你一起深入探讨经典著作。请告诉我你想解读的书籍名称和作者,我会为你准备一份全面的解读报告。",
systemPrompt: '''
你是一个资深的读书导读专家,精通中英文文献检索与解读。用户非常喜欢这本书,但作为普通读者,读完后仍感觉对书中的一些重要观点和细节理解不够深入。现请你帮助我生成一份详细的深度解读报告,以便我能够在大约30分钟内快速、全面、深刻地把握本书的核心内容。报告要求如下:
1. **资料来源要求**:
- 如果这本书是英文书,则在解读过程中仅采纳和引用英文资料(因为英文资料在数量和质量上更为丰富)。
- 如果这本书是中文书,则可采纳中文资料,但仍需优先参考高质量的英文资料。
- 最终所有输出内容均使用中文回答。
2. **报告内容要求**:
- 报告必须详细、全面地阐释书中的所有重要观点和细节,包括但不限于核心思想、背景、论证过程、关键例子和实际应用。
- 解读应覆盖各个层面、各个角度,帮助我从多个维度理解这本书,并对书中内容产生更深的触动。
- 在解读过程中,请适当举例说明,以帮助充分理解各个观点。
- 语言应通俗易懂,适合普通读者阅读,而非学术批评式的深究。
3. **报告格式和篇幅要求**:
- 报告总字数不少于1万字,确保解读内容细致充分。
- 请分章节、分部分展开解读,可以包含摘要、章节解析、观点总结、实例说明等模块。
4. **总体目标**:
- 帮助我在短时间内(约30分钟)快速掌握本书的精髓,弥补我在阅读时的不足,令我对这本书有更深、更全面的理解和体会。
请始终以专业、平实的态度为读者解读经典,帮助他们获得更深入的理解和更大的收获。
**情感化语音提示:**
- 你是一位温和儒雅的经典解读者,语音输出时应保持从容、平和的语调
- 解读重要观点时适当放慢语速,加强重点词句的语气
- 举例说明时语调可以生动活泼一些
- 整体节奏要舒缓有致,让听者感受到文化的魅力
'''),
];
// 获取所有智能体
static List<Agent> getAllAgents() {
return predefinedAgents;
}
// 根据ID获取智能体
static Agent? getAgentById(String id) {
try {
return predefinedAgents.firstWhere((agent) => agent.id == id);
} catch (e) {
return null;
}
}
}

79
lib/data/services/audio_service.dart

@ -0,0 +1,79 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'my_audio_handler.dart';
import '../../modules/chat/controllers/chat_controller.dart';
import '../../data/providers/agent_provider.dart';
import 'volcano_tts_service.dart';
class AudioServiceManager extends GetxService {
static AudioServiceManager get to => Get.find();
late MyAudioHandler _audioHandler;
StreamSubscription? _customEventSubscription;
Future<AudioServiceManager> init() async {
print('初始化 AudioService');
try {
// 初始化 AudioService
_audioHandler = await AudioService.init(
builder: () => MyAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.example.deep_voice.channel.audio',
androidNotificationChannelName: 'Deep Voice Audio Service',
androidNotificationIcon: 'drawable/ic_notification',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
androidShowNotificationBadge: true,
notificationColor: Colors.blue,
),
);
// 监听自定义事件
_customEventSubscription = _audioHandler.customEventStream.listen((event) {
if (event == 'activate_app') {
print('收到激活应用事件');
_activateAppWithPersonalAssistant();
}
});
return this;
} catch (e) {
print('AudioService初始化失败: $e');
rethrow;
}
}
void _activateAppWithPersonalAssistant() {
try {
// 获取 personal_assistant agent
final agent = AgentProvider.getAgentById('personal_assistant');
if (agent == null) {
print('未找到 personal_assistant agent');
return;
}
// 导航到聊天页面,传递 agent 参数
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt,
'welcomeMessage': agent.welcomeMessage,
});
// ChatController 会自动处理欢迎消息和问候语的显示与播放
} catch (e) {
print('激活应用失败: $e');
}
}
@override
void onClose() {
_customEventSubscription?.cancel();
_audioHandler.stop();
super.onClose();
}
}

316
lib/data/services/azure_tts_service.dart

@ -0,0 +1,316 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:flutter_azure_tts/flutter_azure_tts.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// A custom AudioSource that reads audio data from a byte array
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
static const int _recommendedBufferSize = 8192; // 使用推荐的缓冲区大小
BytesAudioSource(this._bytes);
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? _bytes.length;
try {
// 确保缓冲区大小至少为推荐值
final bufferSize = (end - start) < _recommendedBufferSize
? _recommendedBufferSize
: end - start;
final subData = _bytes.sublist(start, end);
return StreamAudioResponse(
sourceLength: _bytes.length,
contentLength: subData.length,
offset: start,
stream: Stream.value(subData),
contentType: 'audio/mpeg',
);
} catch (e) {
print('BytesAudioSource request error: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length);
}
/// Azure TTS Service,支持将文本转换为语音并以流式方式边下载边播放
class AzureTtsService extends GetxService {
static bool _isInitialized = false;
final _audioPlayer = AudioPlayer();
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = false.obs;
final isPlaying = false.obs;
StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession;
// Azure 配置
final String _azureKey;
final String _azureRegion;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
static final _sentenceBreaks = RegExp(r'[。!?.!?]');
// 状态
bool _isFetching = false;
late Voice _selectedVoice;
bool _isDisposed = false;
AzureTtsService() :
_azureKey = dotenv.env['AZURE_TTS_KEY'] ?? '',
_azureRegion = dotenv.env['AZURE_TTS_REGION'] ?? '' {
if (_azureKey.isEmpty || _azureRegion.isEmpty) {
throw Exception('Azure TTS配置信息不完整,请检查环境变量');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
// 初始化音频会话
_audioSession = await AudioSession.instance;
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
));
// 设置音频播放器
if (!_isDisposed) {
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: false,
);
_playbackEventSubscription = _audioPlayer.playbackEventStream.listen(
(event) {
if (_isDisposed) return;
print('播放事件: $event');
if (event.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
print('播放错误: $error');
_handlePlaybackError(error);
},
);
_playerStateSubscription = _audioPlayer.playerStateStream.listen((state) {
if (_isDisposed) return;
print('播放器状态变化: playing=${state.playing},processingState=${state.processingState}');
isPlaying.value = state.playing;
if (state.processingState == ProcessingState.completed) {
_tryPlayNext();
}
});
}
// 初始化 Azure TTS (只初始化一次)
if (!_isInitialized && !_isDisposed) {
FlutterAzureTts.init(
subscriptionKey: _azureKey,
region: _azureRegion,
withLogs: true,
);
_isInitialized = true;
}
// 获取可用语音列表并设置默认语音
if (!_isDisposed) {
final voicesResponse = await FlutterAzureTts.getAvailableVoices();
_selectedVoice = voicesResponse.voices.firstWhere(
(v) => v.shortName == 'zh-CN-YunxiNeural',
orElse: () => voicesResponse.voices.first,
);
print('已选择语音: ${_selectedVoice.shortName}');
}
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
print('Stack trace: $stackTrace');
}
}
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playbackEventSubscription?.cancel();
await _playerStateSubscription?.cancel();
await _audioSession?.setActive(false);
await stop();
await _audioPlayer.dispose();
}
@override
void onClose() async {
print('关闭TTS服务');
await _cleanupResources();
super.onClose();
}
void _handlePlaybackError(dynamic error) {
print('处理播放错误: $error');
// 尝试重新初始化播放器
_reinitializePlayer();
}
Future<void> _reinitializePlayer() async {
try {
await _audioPlayer.stop();
await _playlist.clear();
await _audioPlayer.setAudioSource(_playlist);
print('播放器重新初始化成功');
} catch (e) {
print('播放器重新初始化失败: $e');
}
}
void _tryPlayNext() async {
if (_playlist.length > 0 && !_audioPlayer.playing) {
try {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
} catch (e) {
print('尝试播放下一段失败: $e');
}
}
}
Future<void> speak(String text) async {
if (!isEnabled.value) {
print('TTS 未启用,跳过播放。');
return;
}
try {
print('准备播放文本: $text');
_pendingText += text;
_extractFullSentences();
await _startPreloadIfNeeded();
if (!_audioPlayer.playing && _playlist.length > 0) {
print('开始播放音频');
await _audioPlayer.play();
}
} catch (e) {
print('播放文本失败: $e');
}
}
void _extractFullSentences() {
if (_pendingText.isEmpty) return;
final matches = _sentenceBreaks.allMatches(_pendingText).toList();
if (matches.isEmpty) return;
int lastPos = 0;
for (final match in matches) {
final end = match.end;
final sentence = _pendingText.substring(lastPos, end).trim();
if (sentence.isNotEmpty) {
_sentenceQueue.add(sentence);
}
lastPos = end;
}
if (lastPos > 0) {
_pendingText = _pendingText.substring(lastPos);
}
}
Future<void> _startPreloadIfNeeded() async {
if (_isFetching) return;
if (_sentenceQueue.isEmpty) return;
_isFetching = true;
await _fetchNextSegment();
}
Future<void> _fetchNextSegment() async {
if (_sentenceQueue.isEmpty) {
_isFetching = false;
return;
}
final sentence = _sentenceQueue.removeAt(0);
print('获取音频片段: $sentence');
try {
final params = TtsParams(
voice: _selectedVoice,
audioFormat: AudioOutputFormat.audio16khz32kBitrateMonoMp3,
rate: 1.0,
text: sentence,
);
final ttsResponse = await FlutterAzureTts.getTts(params);
if (ttsResponse.audio != null && ttsResponse.audio.isNotEmpty) {
print('成功获取音频数据: ${ttsResponse.audio.length} bytes');
try {
final audioSource = BytesAudioSource(ttsResponse.audio);
await _playlist.add(audioSource);
print('音频片段已添加到播放列表');
if (!_audioPlayer.playing && _playlist.length == 1) {
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
await Future.delayed(const Duration(milliseconds: 100));
await _audioPlayer.play();
}
} catch (e) {
print('添加音频源失败: $e');
}
} else {
print('获取音频数据失败: 返回为空或长度为0');
}
} catch (e, stackTrace) {
print('获取音频失败: $e');
print('Stack trace: $stackTrace');
}
await _fetchNextSegment();
}
Future<void> stop() async {
try {
print('停止播放');
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
isPlaying.value = false;
} catch (e) {
print('停止播放失败: $e');
}
}
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
}

168
lib/data/services/my_audio_handler.dart

@ -0,0 +1,168 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/services.dart';
/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机双击)
class MyAudioHandler extends BaseAudioHandler {
// 定义一个 StreamController 用于向 UI 发送自定义事件
final StreamController<dynamic> _customEventController = StreamController.broadcast();
bool _isDisposed = false;
Stream<dynamic> get customEventStream => _customEventController.stream;
MyAudioHandler() {
_initializeHandler();
}
void _initializeHandler() {
try {
// 设置初始播放状态
playbackState.add(PlaybackState(
controls: [
MediaControl.play,
MediaControl.pause,
MediaControl.stop,
MediaControl.skipToPrevious,
MediaControl.skipToNext,
],
systemActions: {
MediaAction.play,
MediaAction.pause,
MediaAction.stop,
MediaAction.skipToPrevious,
MediaAction.skipToNext,
MediaAction.seek,
},
androidCompactActionIndices: const [0, 1, 2],
processingState: AudioProcessingState.idle,
playing: false,
));
// 设置媒体元数据
final item = MediaItem(
id: 'deep_voice_audio',
title: 'Deep Voice Audio',
album: 'Deep Voice',
artist: 'Deep Voice',
genre: 'Speech',
duration: const Duration(seconds: 1),
artUri: null, // 移除图片URI
playable: true,
displayTitle: 'Deep Voice Audio',
displaySubtitle: 'Voice Assistant',
displayDescription: 'TTS Audio',
extras: const {'source': 'deep_voice'},
);
mediaItem.add(item);
} catch (e) {
print('初始化 AudioHandler 失败: $e');
}
}
// 激活应用
Future<void> _activateApp() async {
if (_isDisposed) return;
try {
print('收到激活应用事件');
_customEventController.add('activate_app');
// 尝试将应用带到前台
await SystemChannels.platform.invokeMethod('SystemNavigator.routeInformationUpdated');
} catch (e) {
print('激活应用失败: $e');
}
}
// 更新播放状态
void _updatePlaybackState({
required bool playing,
required AudioProcessingState processingState,
}) {
if (_isDisposed) return;
try {
playbackState.add(playbackState.value.copyWith(
playing: playing,
processingState: processingState,
updatePosition: Duration.zero,
));
} catch (e) {
print('更新播放状态失败: $e');
}
}
@override
Future<void> play() async {
print('收到播放命令');
await _activateApp();
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
}
@override
Future<void> pause() async {
print('收到暂停命令');
_updatePlaybackState(
playing: false,
processingState: AudioProcessingState.ready,
);
}
@override
Future<void> stop() async {
print('停止 AudioHandler');
_isDisposed = true;
try {
await super.stop();
_customEventController.close();
_updatePlaybackState(
playing: false,
processingState: AudioProcessingState.idle,
);
} catch (e) {
print('停止 AudioHandler 时发生错误: $e');
}
}
@override
Future<void> skipToPrevious() async {
print('收到上一曲命令');
await _activateApp();
}
@override
Future<void> skipToNext() async {
print('收到下一曲命令');
await _activateApp();
}
@override
Future<void> seek(Duration position) async {
print('收到跳转命令: $position');
await _activateApp();
}
@override
Future<dynamic> customAction(String name, [Map<String, dynamic>? extras]) async {
print('收到自定义命令: $name');
if (name == 'media_button') {
await _activateApp();
}
}
@override
Future<void> onTaskRemoved() async {
print('服务被系统移除');
await stop();
}
@override
Future<void> onNotificationDeleted() async {
print('通知被用户移除');
await stop();
}
}

150
lib/data/services/volcano_ai_service.dart

@ -0,0 +1,150 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
class VolcanoAIService {
final String apiKey;
static const String _baseUrl = 'https://ark.cn-beijing.volces.com/api/v3';
static const String _chatEndpoint = '/chat/completions';
VolcanoAIService() : apiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '' {
if (apiKey.isEmpty) {
throw VolcanoAIException('火山AI配置信息不完整,请检查环境变量');
}
}
// 生成个性化问候语
Stream<String> generateGreeting({
required String agentName,
required String systemPrompt,
}) {
final messages = [
{'role': 'system', 'content': systemPrompt},
{'role': 'user', 'content': '请用一句简短的话向我打个招呼,要符合你的身份和性格特点,不要超过18个字。'},
];
return sendMessageStream(
messages: messages,
systemPrompt: systemPrompt,
);
}
// 非流式输出方法
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async {
final url = Uri.parse('$_baseUrl$_chatEndpoint');
final fullMessages = [
{'role': 'system', 'content': systemPrompt},
...messages,
];
try {
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $apiKey',
},
body: json.encode({
'model': 'doubao-1-5-lite-32k-250115',
'messages': fullMessages,
'temperature': 0.7,
'max_tokens': 2000,
'stream': false,
}),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['choices'] != null &&
data['choices'].isNotEmpty &&
data['choices'][0]['message'] != null) {
return data['choices'][0]['message']['content'];
}
throw VolcanoAIException('Invalid response format');
} else {
final error = json.decode(response.body);
throw VolcanoAIException(
error['error']?['message'] ?? 'Unknown error occurred',
);
}
} catch (e) {
if (e is VolcanoAIException) rethrow;
throw VolcanoAIException('Failed to communicate with AI service: $e');
}
}
// 流式输出方法
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async* {
final url = Uri.parse('$_baseUrl$_chatEndpoint');
final fullMessages = [
{'role': 'system', 'content': systemPrompt},
...messages,
];
try {
final request = http.Request('POST', url);
request.headers.addAll({
'Content-Type': 'application/json',
'Authorization': 'Bearer $apiKey',
'Accept': 'text/event-stream',
});
request.body = json.encode({
'model': 'doubao-1-5-lite-32k-250115',
'messages': fullMessages,
'temperature': 0.7,
'max_tokens': 2000,
'stream': true,
});
final response = await http.Client().send(request);
if (response.statusCode != 200) {
final error = await response.stream.bytesToString();
throw VolcanoAIException(
json.decode(error)['error']?['message'] ?? 'Unknown error occurred'
);
}
await for (final chunk in response.stream.transform(utf8.decoder).transform(const LineSplitter())) {
if (chunk.isEmpty) continue;
if (chunk.startsWith('data: ')) {
final data = chunk.substring(6);
if (data == '[DONE]') break;
try {
final jsonData = json.decode(data);
if (jsonData['choices'] != null &&
jsonData['choices'].isNotEmpty &&
jsonData['choices'][0]['delta'] != null &&
jsonData['choices'][0]['delta']['content'] != null) {
yield jsonData['choices'][0]['delta']['content'] as String;
}
} catch (e) {
// 忽略无效的JSON数据
continue;
}
}
}
} catch (e) {
if (e is VolcanoAIException) rethrow;
throw VolcanoAIException('Failed to communicate with AI service: $e');
}
}
}
class VolcanoAIException implements Exception {
final String message;
VolcanoAIException(this.message);
@override
String toString() => message;
}

664
lib/data/services/volcano_tts_service.dart

@ -0,0 +1,664 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:io';
import 'dart:math' as math;
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 火山语音服务异常
class VolcanoTtsException implements Exception {
final String message;
VolcanoTtsException(this.message);
@override
String toString() => message;
}
/// 自定义音频源,用于从字节数组读取音频数据
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility
BytesAudioSource(this._bytes);
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? _bytes.length;
try {
final subData = _bytes.sublist(start, end);
// 使用固定大小的块进行流式传输
final chunks = <List<int>>[];
var offset = 0;
while (offset < subData.length) {
final chunkSize = math.min(_bufferSize, subData.length - offset);
chunks.add(subData.sublist(offset, offset + chunkSize));
offset += chunkSize;
}
return StreamAudioResponse(
sourceLength: _bytes.length,
contentLength: subData.length,
offset: start,
stream: Stream.fromIterable(chunks),
contentType: 'audio/mpeg',
);
} catch (e) {
print('BytesAudioSource request error: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length);
}
/// 火山语音合成服务
class VolcanoTtsService extends GetxService {
static const String _host = 'openspeech.bytedance.com';
static const String _apiUrl = 'wss://$_host/api/v1/tts/ws_binary';
// 配置参数
final String _appId;
final String _token;
final String _cluster;
final String _voiceType;
// 音频播放器
late AudioPlayer _audioPlayer;
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = true.obs; // 默认启用
final isPlaying = false.obs;
StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession;
// WebSocket 相关
WebSocketChannel? _channel;
bool _isDisposed = false;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号
bool _isFetching = false;
static const int _maxSegmentLength = 100; // 最大分段长度
static const int _optimalSegmentLength = 50; // 最佳分段长度
// WebSocket连接配置
static final Map<String, String> _wsHeaders = {
'User-Agent': 'DeepVoice/1.0',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Extensions': 'permessage-deflate',
'Origin': 'https://openspeech.bytedance.com',
};
// 二进制协议头部字段定义
static const int PROTOCOL_VERSION = 0x1; // 0b0001 - 版本1
static const int HEADER_SIZE = 0x1; // 0b0001 - 4字节
static const int MESSAGE_TYPE = 0x1; // 0b0001 - full client request
static const int MESSAGE_FLAGS = 0x0; // 0b0000 - 无特殊标记
static const int SERIALIZATION = 0x1; // 0b0001 - JSON
static const int COMPRESSION = 0x0; // 0b0000 - 无压缩
static const int RESERVED = 0x0; // 0b0000 - 保留字段
// 构建二进制协议头
static Uint8List _buildHeader() {
final header = ByteData(4); // 4字节的头部
// 第一个字节: [协议版本(4位) | 报头大小(4位)]
header.setUint8(0, (PROTOCOL_VERSION << 4) | HEADER_SIZE);
// 第二个字节: [消息类型(4位) | 消息标记(4位)]
header.setUint8(1, (MESSAGE_TYPE << 4) | MESSAGE_FLAGS);
// 第三个字节: [序列化方法(4位) | 压缩方法(4位)]
header.setUint8(2, (SERIALIZATION << 4) | COMPRESSION);
// 第四个字节: [保留字段(8位)]
header.setUint8(3, RESERVED);
return header.buffer.asUint8List();
}
VolcanoTtsService() :
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_TTS_TOKEN'] ?? '',
_cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '',
_voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? 'zh_male_M392_conversation_wvae_bigtts' {
if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) {
throw Exception('火山语音配置信息不完整,请检查环境变量');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
await _initAudioSession();
await _initAudioPlayer();
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
print('Stack trace: $stackTrace');
}
}
Future<void> _initAudioSession() async {
_audioSession = await AudioSession.instance;
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
// 确保音频会话激活并设置正确的音频模式
if (Platform.isAndroid) {
await _audioSession?.setActive(true, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation);
} else {
await _audioSession?.setActive(true);
}
}
Future<void> _initAudioPlayer() async {
if (_isDisposed) return;
_audioPlayer = AudioPlayer();
// 配置Android特定的播放参数
if (Platform.isAndroid) {
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
}
// 基本播放器配置
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setSpeed(1.0);
// 设置音频缓冲配置,启用预加载以减少延迟
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
}
Future<void> speak(String text) async {
if (!isEnabled.value || text.trim().isEmpty) return;
try {
_pendingText += text;
_extractFullSentences();
await _startPreloadIfNeeded();
} catch (e, stack) {
print('语音合成失败: $e');
await _reinitializePlayer();
}
}
void _extractFullSentences() {
if (_pendingText.isEmpty) return;
final matches = _sentenceBreaks.allMatches(_pendingText).toList();
if (matches.isEmpty) {
// 如果没有找到句子分隔符,按长度分割
_splitByLength(_pendingText);
_pendingText = '';
return;
}
int lastPos = 0;
String currentSegment = '';
for (final match in matches) {
final end = match.end;
final sentence = _pendingText.substring(lastPos, end).trim();
if (sentence.isEmpty) {
lastPos = end;
continue;
}
// 如果当前段落加上新句子不超过最佳长度,则合并
if (currentSegment.isEmpty) {
currentSegment = sentence;
} else if ((currentSegment + sentence).length <= _optimalSegmentLength) {
currentSegment += sentence;
} else {
// 当前段落已经有内容,直接加入队列
if (currentSegment.isNotEmpty) {
_sentenceQueue.add(currentSegment);
}
currentSegment = sentence;
}
lastPos = end;
}
// 处理当前累积的段落
if (currentSegment.isNotEmpty) {
_sentenceQueue.add(currentSegment);
}
// 处理剩余的文本
if (lastPos < _pendingText.length) {
final remaining = _pendingText.substring(lastPos).trim();
if (remaining.isNotEmpty) {
if (remaining.length <= _maxSegmentLength) {
_sentenceQueue.add(remaining);
} else {
_splitByLength(remaining);
}
}
}
_pendingText = '';
}
void _splitByLength(String text) {
if (text.isEmpty) return;
int start = 0;
while (start < text.length) {
int end = start + _maxSegmentLength;
if (end > text.length) {
end = text.length;
}
// 尝试在单词或字符边界处分割
if (end < text.length) {
final spacePos = text.lastIndexOf(' ', end);
final breakPos = _sentenceBreaks.firstMatch(
text.substring(start, end)
)?.end;
if (breakPos != null && breakPos > start) {
end = start + breakPos;
} else if (spacePos > start) {
end = spacePos;
}
}
final segment = text.substring(start, end).trim();
if (segment.isNotEmpty) {
_sentenceQueue.add(segment);
}
start = end;
}
}
Future<void> _startPreloadIfNeeded() async {
if (_isFetching || _sentenceQueue.isEmpty) return;
_isFetching = true;
await _fetchNextSegment();
}
Future<void> _fetchNextSegment() async {
if (_sentenceQueue.isEmpty) {
_isFetching = false;
return;
}
try {
// 预加载多个片段以实现流畅播放
final segments = <String>[];
while (_sentenceQueue.isNotEmpty && segments.length < 2) { // 减少预加载数量以降低内存压力
segments.add(_sentenceQueue.removeAt(0));
}
for (final segment in segments) {
print('获取音频片段: $segment');
final audioData = await _synthesize(segment);
if (audioData != null) {
print('成功获取音频数据: ${audioData.length} bytes');
try {
final audioSource = BytesAudioSource(audioData);
await _playlist.add(audioSource);
print('音频片段已添加到播放列表');
// 如果是第一个片段且播放器未在播放,开始播放
if (!_audioPlayer.playing && _playlist.length == 1) {
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
// 设置播放参数
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
// 开始播放
await _audioPlayer.play();
}
} catch (e) {
print('添加音频源失败: $e');
}
}
}
// 如果队列还有内容,继续获取下一批
if (_sentenceQueue.isNotEmpty) {
await Future.delayed(const Duration(milliseconds: 100)); // 添加延迟以避免过快请求
await _fetchNextSegment();
} else {
_isFetching = false;
// 检查是否还有未处理的文本
_extractFullSentences();
if (_sentenceQueue.isNotEmpty) {
await _startPreloadIfNeeded();
}
}
} catch (e, stackTrace) {
print('获取音频失败: $e');
print('Stack trace: $stackTrace');
_isFetching = false;
}
}
Future<Uint8List?> _synthesize(String text) async {
WebSocket? socket;
StreamSubscription? subscription;
try {
final requestJson = {
'app': {
'appid': _appId,
'token': _token,
'cluster': _cluster,
},
'user': {
'uid': '${DateTime.now().millisecondsSinceEpoch}',
},
'audio': {
'voice_type': _voiceType,
'encoding': 'mp3',
'speed_ratio': 1.0,
'volume_ratio': 1.0,
'pitch_ratio': 1.0,
'language': 'zh',
},
'request': {
'reqid': '${DateTime.now().millisecondsSinceEpoch}',
'text': text,
'text_type': 'plain',
'operation': 'query',
},
};
final jsonBytes = utf8.encode(json.encode(requestJson));
final header = _buildHeader();
final fullRequest = Uint8List(header.length + 4 + jsonBytes.length);
fullRequest.setAll(0, header);
final lengthBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big);
fullRequest.setAll(header.length, lengthBytes.buffer.asUint8List());
fullRequest.setAll(header.length + 4, jsonBytes);
final wsUrl = Uri.parse(_apiUrl);
final completer = Completer<Uint8List?>();
socket = await WebSocket.connect(
wsUrl.toString(),
headers: {
..._wsHeaders,
'Authorization': 'Bearer;$_token',
},
).timeout(
const Duration(seconds: 5),
onTimeout: () {
throw TimeoutException('WebSocket连接超时');
},
);
subscription = socket.listen(
(response) {
if (!completer.isCompleted && response is Uint8List) {
if (response.length < 2) return;
final messageType = response[1] >> 4;
if (messageType == 0x0B) {
final headerSize = response[0] & 0x0F;
final payloadStart = headerSize * 4;
if (response.length > payloadStart) {
final audioData = response.sublist(payloadStart);
completer.complete(audioData);
}
} else if (messageType == 0x0F) {
try {
final headerSize = response[0] & 0x0F;
final payloadStart = headerSize * 4;
if (response.length < payloadStart + 4) {
throw VolcanoTtsException('错误响应数据不完整');
}
final jsonData = response.sublist(payloadStart + 4);
final jsonString = utf8.decode(jsonData, allowMalformed: true);
if (jsonString.trim().startsWith('{')) {
final errorMap = json.decode(jsonString);
throw VolcanoTtsException(errorMap['error'] as String? ?? '未知错误');
}
} catch (e) {
if (e is VolcanoTtsException) rethrow;
throw VolcanoTtsException('服务返回错误: $e');
}
}
}
},
onError: (error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete(null);
}
},
cancelOnError: false,
);
socket.add(fullRequest);
return await completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () => null,
);
} catch (e) {
print('合成请求失败: $e');
return null;
} finally {
await subscription?.cancel();
await socket?.close();
}
}
Future<void> _playAudio(Uint8List audioData) async {
if (_isDisposed) {
print('TTS服务已销毁,跳过播放');
return;
}
try {
print('准备播放音频数据: ${audioData.length} bytes');
final audioSource = BytesAudioSource(audioData);
print('清除播放列表');
await _playlist.clear();
print('添加新的音频源到播放列表');
await _playlist.add(audioSource);
if (!_audioPlayer.playing) {
print('设置音量和播放位置');
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
print('等待50ms确保音频准备就绪');
await Future.delayed(const Duration(milliseconds: 50));
print('开始播放');
await _audioPlayer.play();
print('播放命令已发送');
} else {
print('播放器已在播放中');
}
} catch (e, stack) {
print('播放音频失败: $e');
print('错误堆栈: $stack');
await _reinitializePlayer();
}
}
Future<void> stop() async {
try {
print('停止播放');
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
isPlaying.value = false;
} catch (e) {
print('停止播放失败: $e');
}
}
Future<void> _reinitializePlayer() async {
try {
await stop();
await _audioPlayer.dispose();
// 重新初始化音频会话
await _initAudioSession();
_audioPlayer = AudioPlayer();
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
// 重新配置Android特定的播放参数
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
print('播放器重新初始化成功');
} catch (e) {
print('播放器重新初始化失败: $e');
}
}
void _setupEventListeners() {
_playbackEventSubscription?.cancel();
_playerStateSubscription?.cancel();
_playbackEventSubscription = _audioPlayer.playbackEventStream.listen(
(event) {
if (_isDisposed) return;
if (event.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
_reinitializePlayer();
},
);
_playerStateSubscription = _audioPlayer.playerStateStream.listen(
(state) {
if (_isDisposed) return;
isPlaying.value = state.playing;
if (state.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
_reinitializePlayer();
},
);
}
Future<void> _tryPlayNext() async {
if (_isDisposed || _playlist.length <= 0) return;
try {
if (!_audioPlayer.playing) {
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
// 减少延迟时间
await Future.delayed(const Duration(milliseconds: 30));
if (!_isDisposed && !_audioPlayer.playing) {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
}
}
} catch (e) {
if (e is! StateError) {
print('播放错误: $e');
await _reinitializePlayer();
}
}
}
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playbackEventSubscription?.cancel();
await _playerStateSubscription?.cancel();
await _audioSession?.setActive(false);
await _channel?.sink.close();
await stop();
await _audioPlayer.dispose();
}
@override
void onClose() async {
print('关闭TTS服务');
await _cleanupResources();
super.onClose();
}
}

46
lib/main.dart

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/bindings/initial_binding.dart';
import 'core/routes/app_pages.dart';
import 'core/routes/app_routes.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 加载环境变量
await dotenv.load();
// 初始化 GetStorage
await GetStorage.init();
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812), // 设计稿尺寸
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
return GetMaterialApp(
title: 'Deep Voice',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialBinding: InitialBinding(),
initialRoute: Routes.initial,
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
);
}
);
}
}

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

@ -0,0 +1,25 @@
import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
class ChatBinding extends Bindings {
@override
void dependencies() {
// 获取传递的参数
final arguments = Get.arguments;
if (arguments == null || !(arguments is Map<String, dynamic>)) {
print('Chat binding: 未提供有效的参数');
Get.back();
return;
}
// 创建并注入控制器
Get.put(ChatController(
agentId: arguments['agentId'],
agentName: arguments['agentName'],
agentAvatar: arguments['agentAvatar'],
agentSubtitle: arguments['agentSubtitle'],
systemPrompt: arguments['systemPrompt'],
welcomeMessage: arguments['welcomeMessage'],
));
}
}

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

@ -0,0 +1,321 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'dart:convert';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../models/message_model.dart';
class ChatController extends GetxController {
final String agentId;
final String agentName;
final String agentAvatar;
final String? agentSubtitle;
final String systemPrompt;
final String welcomeMessage;
final VolcanoAIService _aiService;
late final VolcanoTtsService _ttsService;
static const String _storagePrefix = 'chat_history_';
final messages = <Message>[].obs;
final isLoading = false.obs;
final textController = TextEditingController();
final scrollController = ScrollController();
final _storage = GetStorage();
// 用于存储当前正在流式生成的消息
final RxString currentStreamMessage = ''.obs;
Message? _currentAssistantMessage;
String _pendingTtsText = ''; // 用于累积TTS文本
static const int _minTtsLength = 20; // TTS最小文本长度
bool _isDisposed = false; // 添加销毁标记
ChatController({
required this.agentId,
required this.agentName,
required this.agentAvatar,
this.agentSubtitle,
required this.systemPrompt,
required this.welcomeMessage,
}) : _aiService = VolcanoAIService() {
_initTtsService();
}
void _initTtsService() {
try {
_ttsService = Get.find<VolcanoTtsService>();
} catch (_) {
// 如果找不到实例,创建一个新的
_ttsService = Get.put(VolcanoTtsService(), permanent: true);
}
// 默认开启TTS
_ttsService.isEnabled.value = true;
}
@override
void onInit() async {
super.onInit();
await _initializeController();
}
Future<void> _initializeController() async {
try {
await _loadChatHistory();
// 如果没有聊天记录,显示欢迎消息并生成个性化问候语
if (messages.isEmpty) {
// 添加欢迎消息
final initialMessage = Message(
role: 'assistant',
content: welcomeMessage,
timestamp: DateTime.now(),
);
messages.add(initialMessage);
// 生成并添加个性化问候语
try {
currentStreamMessage.value = '';
_currentAssistantMessage = Message(
role: 'assistant',
content: '',
timestamp: DateTime.now(),
);
messages.add(_currentAssistantMessage!);
await for (final chunk in _aiService.generateGreeting(
agentName: agentName,
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
currentStreamMessage.value += chunk;
_updateAssistantMessage(currentStreamMessage.value);
// 累积文本用于TTS
if (!_isDisposed && _ttsService.isEnabled.value) {
_ttsService.speak(chunk);
}
}
// 保存聊天记录
if (!_isDisposed) {
await _saveChatHistory();
// 延迟滚动以确保布局完成
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
}
} catch (e) {
print('生成问候语失败: $e');
// 如果生成问候语失败,移除空消息并只播放欢迎消息
if (_currentAssistantMessage != null) {
messages.remove(_currentAssistantMessage);
}
if (_ttsService.isEnabled.value) {
_ttsService.speak(welcomeMessage);
}
} finally {
if (!_isDisposed) {
_currentAssistantMessage = null;
currentStreamMessage.value = '';
}
}
}
} catch (e) {
Get.snackbar(
'Error',
'Failed to initialize chat: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
@override
void onClose() {
_isDisposed = true; // 标记为已销毁
textController.dispose();
scrollController.dispose();
_ttsService.stop(); // 停止当前播放
super.onClose();
}
Future<void> _loadChatHistory() async {
try {
final historyJson = _storage.read('${_storagePrefix}${agentId}');
if (historyJson != null) {
final List<dynamic> historyList = json.decode(historyJson);
messages.value = historyList.map((item) => Message.fromJson(item)).toList();
}
} catch (e) {
Get.snackbar(
'Error',
'Failed to load chat history: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> _saveChatHistory() async {
try {
final historyJson = json.encode(messages.map((msg) => msg.toJson()).toList());
await _storage.write('${_storagePrefix}${agentId}', historyJson);
} catch (e) {
Get.snackbar(
'Error',
'Failed to save chat history: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void _scrollToBottom() {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
void _updateAssistantMessage(String content) {
if (_currentAssistantMessage != null) {
final index = messages.indexWhere((msg) =>
msg.timestamp == _currentAssistantMessage!.timestamp &&
msg.role == 'assistant'
);
if (index != -1) {
messages[index] = Message(
role: 'assistant',
content: content,
timestamp: _currentAssistantMessage!.timestamp,
);
_scrollToBottom();
}
}
}
void sendMessage(String text) async {
if (text.trim().isEmpty) return;
final userMessage = Message(
role: 'user',
content: text,
timestamp: DateTime.now(),
);
messages.add(userMessage);
textController.clear();
await _saveChatHistory();
_scrollToBottom();
isLoading.value = true;
try {
currentStreamMessage.value = '';
_currentAssistantMessage = Message(
role: 'assistant',
content: '',
timestamp: DateTime.now(),
);
messages.add(_currentAssistantMessage!);
_pendingTtsText = '';
await for (final chunk in _aiService.sendMessageStream(
messages: messages.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 && _ttsService.isEnabled.value && _pendingTtsText.isNotEmpty) {
_ttsService.speak(_pendingTtsText);
}
_pendingTtsText = '';
// 流式传输完成后保存聊天记录
if (!_isDisposed) {
await _saveChatHistory();
}
} catch (e) {
if (!_isDisposed) {
if (_currentAssistantMessage != null) {
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;
}
}
}
int _findLastSentenceEnd(String text) {
final sentenceEnds = [
text.lastIndexOf('。'),
text.lastIndexOf('!'),
text.lastIndexOf('?'),
text.lastIndexOf('.'),
text.lastIndexOf('!'),
text.lastIndexOf('?'),
];
return sentenceEnds.reduce((max, pos) => pos > max ? pos : max);
}
Future<void> clearChat() async {
try {
messages.clear();
await _storage.remove('${_storagePrefix}${agentId}');
// 重新添加欢迎消息
final initialMessage = Message(
role: 'assistant',
content: welcomeMessage,
timestamp: DateTime.now(),
);
messages.add(initialMessage);
await _saveChatHistory();
// 播放欢迎消息
if (_ttsService.isEnabled.value) {
_ttsService.speak(welcomeMessage);
}
} catch (e) {
Get.snackbar(
'Error',
'Failed to clear chat: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void toggleTTS() {
_ttsService.toggleEnabled();
}
}

29
lib/modules/chat/models/message_model.dart

@ -0,0 +1,29 @@
class Message {
final String role;
final String content;
final DateTime timestamp;
Message({
required this.role,
required this.content,
required this.timestamp,
});
// 从JSON构造函数
factory Message.fromJson(Map<String, dynamic> json) {
return Message(
role: json['role'] as String,
content: json['content'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
);
}
// 转换为JSON
Map<String, dynamic> toJson() {
return {
'role': role,
'content': content,
'timestamp': timestamp.toIso8601String(),
};
}
}

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

@ -0,0 +1,282 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/chat_controller.dart';
import '../models/message_model.dart';
import '../../../data/services/volcano_tts_service.dart';
class ChatView extends GetView<ChatController> {
const ChatView({Key? key}) : super(key: key);
Widget _buildMessage(Message message) {
final isUser = message.role == 'user';
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
CircleAvatar(
radius: 20.r,
backgroundImage: AssetImage(controller.agentAvatar),
backgroundColor: Colors.transparent,
),
SizedBox(width: 12.w),
],
Flexible(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration(
color: isUser ? const Color(0xFF4A6FE5) : const Color(0xFFF5F5F5),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isUser ? 18.r : 4.r),
topRight: Radius.circular(isUser ? 4.r : 18.r),
bottomLeft: Radius.circular(18.r),
bottomRight: Radius.circular(18.r),
),
),
child: Text(
message.content,
style: TextStyle(
fontSize: 16.sp,
color: isUser ? Colors.white : Colors.black87,
height: 1.5,
),
),
),
),
if (isUser) ...[
SizedBox(width: 12.w),
CircleAvatar(
radius: 20.r,
backgroundColor: const Color(0xFFEEF4FF),
child: Icon(
Icons.person,
color: const Color(0xFF4A6FE5),
size: 24.sp,
),
),
],
],
),
);
}
@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,
),
),
),
],
),
),
),
],
),
),
);
}
}

9
lib/modules/explore/bindings/explore_binding.dart

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

31
lib/modules/explore/controllers/explore_controller.dart

@ -0,0 +1,31 @@
import 'package:get/get.dart';
import '../../../data/providers/agent_provider.dart';
class ChatItem {
final String id;
ChatItem({
required this.id,
});
}
class ExploreController extends GetxController {
static ExploreController get to => Get.find();
final _chatList = <ChatItem>[].obs;
List<ChatItem> get chatList => _chatList;
@override
void onInit() {
super.onInit();
_loadInitialChats();
}
void _loadInitialChats() {
final agents = AgentProvider.getAllAgents();
_chatList.assignAll(
agents.map((agent) => ChatItem(id: agent.id)).toList(),
);
}
}

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

@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/explore_controller.dart';
import '../../chat/controllers/chat_controller.dart';
import '../../chat/views/chat_view.dart';
import '../../../data/providers/agent_provider.dart';
class ExploreView extends GetView<ExploreController> {
const ExploreView({Key? key}) : super(key: key);
void _onChatItemTap(ChatItem item) {
// 获取对应的Agent
final agent = AgentProvider.getAgentById(item.id);
if (agent == null) {
Get.snackbar(
'Error',
'Agent not found',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
// 初始化聊天控制器
final chatController = ChatController(
agentId: agent.id,
agentName: agent.name,
agentAvatar: agent.avatarUrl,
agentSubtitle: agent.description,
systemPrompt: agent.systemPrompt,
welcomeMessage: agent.welcomeMessage,
);
// 注入控制器
Get.delete<ChatController>(); // 确保删除之前的控制器
Get.put(chatController);
// 导航到聊天页面
Get.to(
() => const ChatView(),
transition: Transition.rightToLeft,
duration: const Duration(milliseconds: 300),
);
}
Widget _buildChatItem(ChatItem item) {
// 获取对应的Agent
final agent = AgentProvider.getAgentById(item.id);
if (agent == null) return const SizedBox.shrink();
return InkWell(
onTap: () => _onChatItemTap(item),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: Row(
children: [
// 头像
CircleAvatar(
radius: 25.r,
backgroundImage: AssetImage(agent.avatarUrl),
backgroundColor: const Color(0xFFEEF4FF),
),
SizedBox(width: 12.w),
// 聊天内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 名称行
Row(
children: [
Text(
agent.name,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
if (agent.tag == '推荐') ...[
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
'推荐',
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF4CAF50),
),
),
),
],
],
),
SizedBox(height: 4.h),
// 描述
Text(
agent.description,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: Text(
'对话',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
centerTitle: true,
actions: [
IconButton(
icon: Icon(
Icons.search,
color: Colors.black,
size: 24.sp,
),
onPressed: () {},
),
IconButton(
icon: Icon(
Icons.add_circle_outline,
color: Colors.black,
size: 24.sp,
),
onPressed: () {},
),
SizedBox(width: 4.w),
],
),
body: Obx(() => ListView.separated(
itemCount: controller.chatList.length,
separatorBuilder: (context, index) => Divider(
height: 1,
indent: 16.w,
endIndent: 16.w,
color: const Color(0xFFEEEEEE),
),
itemBuilder: (context, index) => _buildChatItem(controller.chatList[index]),
)),
);
}
}

9
lib/modules/home/bindings/home_binding.dart

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

127
lib/modules/home/controllers/home_controller.dart

@ -0,0 +1,127 @@
import 'package:get/get.dart';
import 'dart:async';
import 'package:flutter/material.dart';
enum ConnectStatus {
initial, // 初始状态,显示添加设备
searching, // 搜索中
connected, // 连接成功
failed // 连接失败
}
class HomeController extends GetxController {
static HomeController get to => Get.find();
final _connectStatus = ConnectStatus.initial.obs;
final _searchDots = ''.obs;
Timer? _animationTimer;
bool _isDisposed = false;
ConnectStatus get connectStatus => _connectStatus.value;
String get searchDots => _searchDots.value;
@override
void onInit() {
super.onInit();
_connectStatus.value = ConnectStatus.initial;
}
@override
void onClose() {
_isDisposed = true;
stopSearchingAnimation();
super.onClose();
}
void startSearchingAnimation() {
stopSearchingAnimation(); // Clear any existing timer
if (_isDisposed) return;
int count = 0;
_animationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (_isDisposed || _connectStatus.value != ConnectStatus.searching) {
timer.cancel();
return;
}
count = (count + 1) % 4;
_searchDots.value = '.' * count;
});
}
void stopSearchingAnimation() {
_animationTimer?.cancel();
_animationTimer = null;
if (!_isDisposed) {
_searchDots.value = '';
}
}
Future<void> connectDevice() async {
if (_connectStatus.value == ConnectStatus.searching) return;
_connectStatus.value = ConnectStatus.searching;
startSearchingAnimation();
try {
// 模拟搜索和连接过程
await Future.delayed(const Duration(seconds: 3));
if (_isDisposed) return; // Check if controller is disposed
// 模拟随机成功或失败
if (DateTime.now().millisecondsSinceEpoch % 2 == 0) {
_connectStatus.value = ConnectStatus.connected;
if (!_isDisposed) {
await Future.delayed(const Duration(seconds: 2)); // 显示成功状态2秒
Get.snackbar(
'Success',
'Device connected successfully',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green.withOpacity(0.1),
colorText: Colors.green,
duration: const Duration(seconds: 3),
);
}
} else {
_connectStatus.value = ConnectStatus.failed;
}
} catch (e) {
if (!_isDisposed) {
_connectStatus.value = ConnectStatus.failed;
Get.snackbar(
'Error',
'Failed to connect to device',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
duration: const Duration(seconds: 3),
);
}
} finally {
if (!_isDisposed && _connectStatus.value == ConnectStatus.searching) {
stopSearchingAnimation();
_connectStatus.value = ConnectStatus.failed;
}
}
}
void retryConnect() {
if (_isDisposed) return;
_connectStatus.value = ConnectStatus.initial;
connectDevice();
}
void disconnectDevice() {
if (_isDisposed) return;
_connectStatus.value = ConnectStatus.initial;
Get.snackbar(
'Disconnected',
'Device has been disconnected',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue.withOpacity(0.1),
colorText: Colors.blue,
duration: const Duration(seconds: 3),
);
}
}

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

Loading…
Cancel
Save