diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist new file mode 100644 index 000000000..c90ada3a3 --- /dev/null +++ b/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 + ANDROID_CLIENT_ID + 907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com + API_KEY + AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808 + GCM_SENDER_ID + 907754492821 + PLIST_VERSION + 1 + BUNDLE_ID + com.yunqiinnovation.deepsound + PROJECT_ID + deepsound-e0e3d + STORAGE_BUCKET + deepsound-e0e3d.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:907754492821:ios:246794fc44b23174145c9c + + \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ad5eafd7b..06c4dbe30 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties +import java.io.FileInputStream repositories { google() mavenCentral() @@ -8,10 +10,16 @@ plugins { id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services")version "4.3.13" +} + +val keystorePropertiesFile = rootProject.file("app/sign/DeepSound.properties") +val keystoreProperties =Properties().apply { + load(FileInputStream(keystorePropertiesFile)) // 使用 Kotlin 扩展方法 } android { - namespace = "com.example.deep_voice" + namespace = "com.yunqiinnovation.deepsound" compileSdk = 35 ndkVersion = "28.0.13004108" @@ -28,7 +36,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.deep_voice" + applicationId = "com.yunqiinnovation.deepsound" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 24 // Updated to meet azure_speech_recognition_null_safety requirements @@ -37,10 +45,36 @@ android { versionName = flutter.versionName } + signingConfigs { + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + enableV1Signing = true + enableV2Signing = true + } + getByName("debug") { + // debug 使用与 release 相同的签名配置 + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + enableV1Signing = true + enableV2Signing = true + } + } + buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. + isMinifyEnabled = false + isShrinkResources = false + signingConfig = signingConfigs.getByName("release") + // signingConfig = signingConfigs.getByName("debug") + } + debug { signingConfig = signingConfigs.getByName("debug") } } diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 000000000..4cbe172f9 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,47 @@ +{ + "project_info": { + "project_number": "907754492821", + "project_id": "deepsound-e0e3d", + "storage_bucket": "deepsound-e0e3d.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:907754492821:android:2deffc5aa1037ae2145c9c", + "android_client_info": { + "package_name": "com.yunqiinnovation.deepsound" + } + }, + "oauth_client": [ + { + "client_id": "907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.yunqiinnovation.deepsound", + "certificate_hash": "5a9520045bc16fb4f09fb97668a8b0fc4babd376" + } + }, + { + "client_id": "907754492821-15btgolbd7thaa8fpt7s8hs8hq3jl0lg.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAj0_-nS3ygc8VFe1azotSE3-HGZYClkTs" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "907754492821-15btgolbd7thaa8fpt7s8hs8hq3jl0lg.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/sign/DeepSound.properties b/android/app/sign/DeepSound.properties new file mode 100644 index 000000000..96bb7e16a --- /dev/null +++ b/android/app/sign/DeepSound.properties @@ -0,0 +1,4 @@ +storePassword=13071376716 +keyPassword=13071376716 +keyAlias=DeepSound +storeFile=sign/DeepSound.keystore \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 88f1fef92..f120cc1f7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,7 +29,7 @@ - + + + + 3829022037361855 + fb3829022037361855 + 193d0a2b1757c41cfa557ea8e8f21c33 + \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 89176ef44..35365df1a 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -3,6 +3,7 @@ allprojects { google() mavenCentral() } + } val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() diff --git a/assets/images/explore1.png b/assets/images/explore1.png new file mode 100644 index 000000000..9a81a96ca Binary files /dev/null and b/assets/images/explore1.png differ diff --git a/assets/images/explore2.jpg b/assets/images/explore2.jpg new file mode 100644 index 000000000..8b23189b7 Binary files /dev/null and b/assets/images/explore2.jpg differ diff --git a/assets/images/glasses_light.png b/assets/images/glasses_light.png new file mode 100644 index 000000000..13d66464d Binary files /dev/null and b/assets/images/glasses_light.png differ diff --git a/assets/images/robot_light.png b/assets/images/robot_light.png new file mode 100644 index 000000000..40bf9a9c1 Binary files /dev/null and b/assets/images/robot_light.png differ diff --git a/assets/lottie/audio_wave.json b/assets/lottie/audio_wave.json new file mode 100644 index 000000000..160c3fddc --- /dev/null +++ b/assets/lottie/audio_wave.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE ","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":90,"w":512,"h":512,"nm":"sound wave","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 32","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[218.413,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,32,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":91,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":96,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":101,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":106,"s":[8,73]},{"t":110,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-9,"op":591,"st":-9,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 31","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[203.678,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,46,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-14,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":91,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":96,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":101,"s":[8,73]},{"t":105,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-14,"op":586,"st":-14,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 34","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[26.86,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,18,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-32,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-27,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-22,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-17,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-12,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-7,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-2,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":3,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":13,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":23,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":28,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":33,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":38,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":43,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":48,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":53,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":58,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":63,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":68,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":73,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":78,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":83,"s":[8,73]},{"t":87,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-32,"op":568,"st":-32,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 30","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[188.943,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,18,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-32,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-27,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-22,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-17,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-12,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-7,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-2,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":3,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":13,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":23,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":28,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":33,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":38,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":43,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":48,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":53,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":58,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":63,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":68,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":73,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":78,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":83,"s":[8,73]},{"t":87,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-32,"op":568,"st":-32,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 29","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[174.208,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,57,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-23,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-18,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-13,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-8,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-3,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":7,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":17,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":22,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":27,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":32,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":37,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":42,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":47,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":52,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":57,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":62,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":67,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":72,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":77,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":82,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":87,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":92,"s":[8,73]},{"t":96,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-23,"op":577,"st":-23,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 28","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[159.473,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,57,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-29,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-24,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-19,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-14,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,73]},{"t":90,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-29,"op":571,"st":-29,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 35","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12.125,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,76,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-32,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-27,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-22,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-17,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-12,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-7,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-2,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":3,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":13,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":23,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":28,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":33,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":38,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":43,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":48,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":53,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":58,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":63,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":68,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":73,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":78,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":83,"s":[8,73]},{"t":87,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-32,"op":568,"st":-32,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 27","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[130.004,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,76,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-32,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-27,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-22,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-17,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-12,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-7,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-2,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":3,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":13,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":23,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":28,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":33,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":38,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":43,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":48,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":53,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":58,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":63,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":68,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":73,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":78,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":83,"s":[8,73]},{"t":87,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-32,"op":568,"st":-32,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 26","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[144.739,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,30,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-40,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-35,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-25,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-20,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-15,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-10,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,73]},{"t":79,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-40,"op":560,"st":-40,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 25","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115.269,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,47,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-46,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-41,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-36,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-31,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-26,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-21,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-16,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-11,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-6,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-1,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":9,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":19,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":24,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":29,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":34,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":39,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":44,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":54,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":59,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":64,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":69,"s":[8,73]},{"t":73,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-46,"op":554,"st":-46,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 24","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.534,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,27,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-25,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-20,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-15,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-10,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":85,"s":[8,73]},{"t":89,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":570,"st":-30,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 23","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[85.799,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,18,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-35,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-25,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-20,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-15,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-10,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[8,73]},{"t":84,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-35,"op":565,"st":-35,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 22","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[71.064,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,28,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-21,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-16,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-11,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-6,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-1,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":9,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":19,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":24,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":29,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":34,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":39,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":44,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":54,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":59,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":64,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":69,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":74,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":79,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":84,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":89,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":94,"s":[8,73]},{"t":98,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-21,"op":579,"st":-21,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 21","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[56.33,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,37,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-19,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-14,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":91,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":96,"s":[8,73]},{"t":100,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-19,"op":581,"st":-19,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 20","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[498.375,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,32,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":91,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":96,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":101,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":106,"s":[8,73]},{"t":110,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-9,"op":591,"st":-9,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 19","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[483.64,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,46,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-14,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":91,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":96,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":101,"s":[8,73]},{"t":105,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-14,"op":586,"st":-14,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[468.905,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,18,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-32,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-27,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-22,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-17,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-12,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-7,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-2,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":3,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":13,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":23,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":28,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":33,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":38,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":43,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":48,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":53,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":58,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":63,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":68,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":73,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":78,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":83,"s":[8,73]},{"t":87,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-32,"op":568,"st":-32,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Shape Layer 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[454.17,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,57,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-23,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-18,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-13,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-8,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-3,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":7,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":17,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":22,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":27,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":32,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":37,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":42,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":47,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":52,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":57,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":62,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":67,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":72,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":77,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":82,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":87,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":92,"s":[8,73]},{"t":96,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-23,"op":577,"st":-23,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[439.436,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,57,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-29,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-24,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-19,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-14,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,73]},{"t":90,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-29,"op":571,"st":-29,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Shape Layer 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[409.966,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,76,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-32,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-27,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-22,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-17,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-12,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-7,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-2,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":3,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":13,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":23,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":28,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":33,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":38,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":43,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":48,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":53,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":58,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":63,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":68,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":73,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":78,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":83,"s":[8,73]},{"t":87,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-32,"op":568,"st":-32,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Shape Layer 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[424.701,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,30,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-40,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-35,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-25,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-20,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-15,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-10,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,73]},{"t":79,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-40,"op":560,"st":-40,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Shape Layer 13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[395.231,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,47,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-46,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-41,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-36,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-31,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-26,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-21,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-16,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-11,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-6,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-1,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":9,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":19,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":24,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":29,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":34,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":39,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":44,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":54,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":59,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":64,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":69,"s":[8,73]},{"t":73,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-46,"op":554,"st":-46,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[380.496,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,27,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-25,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-20,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-15,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-10,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":85,"s":[8,73]},{"t":89,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":570,"st":-30,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[365.761,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,18,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-35,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-25,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-20,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-15,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-10,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[8,73]},{"t":84,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-35,"op":565,"st":-35,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[351.027,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,28,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-21,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-16,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-11,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-6,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-1,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":9,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":19,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":24,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":29,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":34,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":39,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":44,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":54,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":59,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":64,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":69,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":74,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":79,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":84,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":89,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":94,"s":[8,73]},{"t":98,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-21,"op":579,"st":-21,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[336.292,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,37,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-19,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-14,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-9,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-4,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":1,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":11,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":21,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":31,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":41,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":51,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":56,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":61,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":66,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":71,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":76,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":81,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":91,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":96,"s":[8,73]},{"t":100,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-19,"op":581,"st":-19,"bm":0},{"ddd":0,"ind":27,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[321.557,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,78,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":85,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":95,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":100,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":105,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":110,"s":[8,73]},{"t":114,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-5,"op":595,"st":-5,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"Shape Layer 33","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[41.595,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,55,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-23,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-18,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-13,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-8,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-3,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":7,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":17,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":22,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":27,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":32,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":37,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":42,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":47,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":52,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":57,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":62,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":67,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":72,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":77,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":82,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":87,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":92,"s":[8,73]},{"t":96,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-23,"op":577,"st":-23,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[306.822,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,55,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-23,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-18,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-13,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-8,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-3,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":7,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":17,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":22,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":27,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":32,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":37,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":42,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":47,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":52,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":57,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":62,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":67,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":72,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":77,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":82,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":87,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":92,"s":[8,73]},{"t":96,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-23,"op":577,"st":-23,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[292.087,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,45,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-6,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-1,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":9,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":19,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":24,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":29,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":34,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":39,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":44,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":54,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":59,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":64,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":69,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":74,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":79,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":84,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":89,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":94,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":99,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":104,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":109,"s":[8,73]},{"t":113,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-6,"op":594,"st":-6,"bm":0},{"ddd":0,"ind":31,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[233.148,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,45,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-13,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-8,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-3,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":7,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":17,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":22,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":27,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":32,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":37,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":42,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":47,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":52,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":57,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":62,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":67,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":72,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":77,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":82,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":87,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":92,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":97,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":102,"s":[8,73]},{"t":106,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-13,"op":587,"st":-13,"bm":0},{"ddd":0,"ind":32,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[247.883,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,75,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-5,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":85,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":95,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":100,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":105,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":110,"s":[8,73]},{"t":114,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-5,"op":595,"st":-5,"bm":0},{"ddd":0,"ind":33,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[277.352,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,44,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-3,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":7,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":17,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":22,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":27,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":32,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":37,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":42,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":47,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":52,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":57,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":62,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":67,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":72,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":77,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":82,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":87,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":92,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":97,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":102,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":107,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":112,"s":[8,73]},{"t":116,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-3,"op":597,"st":-3,"bm":0},{"ddd":0,"ind":34,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[262.617,256,0],"ix":2},"a":{"a":0,"k":[23.382,5.347,0],"ix":1},"s":{"a":0,"k":[75,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[8,237]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[8,264.333]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[8,247.667]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[8,197]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[8,29]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[8,26]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[8,158.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[8,217]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[8,250]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[8,125]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[8,163.5]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[8,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":65,"s":[8,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[8,52]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[8,92]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[8,50]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":85,"s":[8,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[8,115]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":95,"s":[8,157]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":100,"s":[8,98]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":105,"s":[8,168]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":110,"s":[8,39]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":115,"s":[8,73]},{"t":119,"s":[8,27]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.7490196078431373,0.6470588235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.382,5.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/firebase.json b/firebase.json new file mode 100644 index 000000000..a2b0f41a5 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"deepsound-e0e3d","appId":"1:907754492821:android:2deffc5aa1037ae2145c9c","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"deepsound-e0e3d","configurations":{"android":"1:907754492821:android:2deffc5aa1037ae2145c9c","ios":"1:907754492821:ios:8c1d7d1b583aec22145c9c","macos":"1:907754492821:ios:8c1d7d1b583aec22145c9c","web":"1:907754492821:web:99451e7efd7d120a145c9c","windows":"1:907754492821:web:e3e8040111db6871145c9c"}}}}}} \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index 1a255e840..c10c27d8b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aa4aa611a..cc790be8d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,47 +1,183 @@ PODS: + - app_settings (5.1.1): + - Flutter + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth/Core - audio_session (0.0.1): - Flutter - audio_waveforms (0.0.1): - Flutter - device_info_plus (0.0.1): - Flutter + - FBAEMKit (17.1.0): + - FBSDKCoreKit_Basics (= 17.1.0) + - FBSDKCoreKit (17.1.0): + - FBAEMKit (= 17.1.0) + - FBSDKCoreKit_Basics (= 17.1.0) + - FBSDKCoreKit_Basics (17.1.0) + - FBSDKLoginKit (17.1.0): + - FBSDKCoreKit (= 17.1.0) + - Firebase/Auth (11.8.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.8.0) + - Firebase/CoreOnly (11.8.0): + - FirebaseCore (~> 11.8.0) + - firebase_auth (5.5.1): + - Firebase/Auth (= 11.8.0) + - firebase_core + - Flutter + - firebase_core (3.12.1): + - Firebase/CoreOnly (= 11.8.0) + - Flutter + - FirebaseAppCheckInterop (11.10.0) + - FirebaseAuth (11.8.1): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.8.0) + - FirebaseCoreExtension (~> 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 100.0) + - FirebaseAuthInterop (11.10.0) + - FirebaseCore (11.8.1): + - FirebaseCoreInternal (~> 11.8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.8.0): + - FirebaseCore (~> 11.8.0) + - FirebaseCoreInternal (11.8.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" - Flutter (1.0.0) - flutter_blue_plus_darwin (0.0.2): - Flutter - FlutterMacOS + - flutter_facebook_auth (7.1.1): + - FBSDKLoginKit (~> 17.1.0) + - Flutter + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder - flutter_local_notifications (0.0.1): - Flutter + - flutter_secure_storage (6.0.0): + - Flutter - flutter_tts (0.0.1): - Flutter + - fluwx (0.0.1): + - Flutter + - fluwx/pay (= 0.0.1) + - fluwx/pay (0.0.1): + - Flutter + - WechatOpenSDK-XCFramework (~> 2.0.4) + - google_sign_in_ios (0.0.1): + - AppAuth (>= 1.7.4) + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 7.1) + - GTMSessionFetcher (>= 3.4.0) + - GoogleSignIn (7.1.0): + - AppAuth (< 2.0, >= 1.7.3) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) + - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core - image_picker_ios (0.0.1): - Flutter - just_audio (0.0.1): - Flutter - FlutterMacOS + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): + - libwebp/webp + - libwebp/mux (1.5.0): + - libwebp/demux + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): + - libwebp/sharpyuv + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) - MicrosoftCognitiveServicesSpeech-iOS (1.34.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter + - RecaptchaInterop (100.0.0) - record_darwin (1.0.0): - Flutter + - SDWebImage (5.21.0): + - SDWebImage/Core (= 5.21.0) + - SDWebImage/Core (5.21.0) + - SDWebImageWebPCoder (0.14.6): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) - share_plus (0.0.1): - Flutter - speech_to_text (0.0.1): - Flutter - FlutterMacOS - Try + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS - Try (2.1.1) + - WechatOpenSDK-XCFramework (2.0.4) DEPENDENCIES: + - app_settings (from `.symlinks/plugins/app_settings/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - Flutter (from `Flutter`) - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) + - fluwx (from `.symlinks/plugins/fluwx/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - just_audio (from `.symlinks/plugins/just_audio/darwin`) - MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0) @@ -50,27 +186,66 @@ DEPENDENCIES: - record_darwin (from `.symlinks/plugins/record_darwin/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) SPEC REPOS: trunk: + - AppAuth + - FBAEMKit + - FBSDKCoreKit + - FBSDKCoreKit_Basics + - FBSDKLoginKit + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - GoogleSignIn + - GoogleUtilities + - GTMAppAuth + - GTMSessionFetcher + - libwebp + - Mantle - MicrosoftCognitiveServicesSpeech-iOS + - RecaptchaInterop + - SDWebImage + - SDWebImageWebPCoder - Try + - WechatOpenSDK-XCFramework EXTERNAL SOURCES: + app_settings: + :path: ".symlinks/plugins/app_settings/ios" audio_session: :path: ".symlinks/plugins/audio_session/ios" audio_waveforms: :path: ".symlinks/plugins/audio_waveforms/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" Flutter: :path: Flutter flutter_blue_plus_darwin: :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" + flutter_facebook_auth: + :path: ".symlinks/plugins/flutter_facebook_auth/ios" + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_tts: :path: ".symlinks/plugins/flutter_tts/ios" + fluwx: + :path: ".symlinks/plugins/fluwx/ios" + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/darwin" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" just_audio: @@ -85,25 +260,58 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" speech_to_text: :path: ".symlinks/plugins/speech_to_text/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" SPEC CHECKSUMS: + app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2 + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + FBAEMKit: cb719c53575a3be86ea873279f30d6a2c4e15881 + FBSDKCoreKit: ecdb980a24633ccb012700299ceb16d0235e14d2 + FBSDKCoreKit_Basics: 045101c4a9ef10c845347424d73a29aae02c3e43 + FBSDKLoginKit: 69eb59b2f839aba635616df6e422acd0ca88030a + Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf + firebase_auth: 9ebbd83276bf977dea1c74dfc199acf9d2a2f42f + firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510 + FirebaseAppCheckInterop: 9664c858489710f682766ef54e2b6741d3b62070 + FirebaseAuth: ad59a1a7b161e75f74c39f70179d2482d40e2737 + FirebaseAuthInterop: 01a804fb074424fd58b92dd50dd0272277199356 + FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d + FirebaseCoreExtension: 3d3f2017a00d06e09ab4ebe065391b0bb642565e + FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6 + flutter_facebook_auth: 8a9ad5fbfc25b77e7a53ea8986866950b27edcae + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833 + fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b + google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8 + GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d MicrosoftCognitiveServicesSpeech-iOS: 723eccb3e459bbb530df3d9bfe84a291074b09eb path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a + SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 + SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 + WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f -PODFILE CHECKSUM: f35c1d63b4b8ce9c2eb88107f162ac59923c4839 +PODFILE CHECKSUM: 4f26889ae572e1722a139b651939749d40c71469 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index afed04887..b46ef17e1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 591293922D9524D1004A65BB /* GoogleService-Info.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 */; }; @@ -68,6 +69,8 @@ 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 = ""; }; 4A9190D0838960F143BDC158 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = ""; }; + 591293922D9524D1004A65BB /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -161,6 +164,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */, 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */, 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */, 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */, @@ -171,6 +175,7 @@ 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */, 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */, 97C146FA1CF9000F007C117D /* Main.storyboard */, + 591293922D9524D1004A65BB /* GoogleService-Info.plist */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, @@ -265,6 +270,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -293,6 +301,7 @@ 0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */, 0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -523,7 +532,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -532,6 +541,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -723,7 +733,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -732,6 +742,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -748,7 +759,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -757,6 +769,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -799,6 +812,17 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 11.10.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..7ee26c6b2 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4", + "version" : "11.10.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6", + "version" : "11.10.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", + "version" : "1.69.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35", + "version" : "4.4.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" + } + } + ], + "version" : 3 +} diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..7ee26c6b2 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4", + "version" : "11.10.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6", + "version" : "11.10.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", + "version" : "1.69.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35", + "version" : "4.4.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" + } + } + ], + "version" : 3 +} diff --git a/ios/Runner/BluetoothMediaButtonHelper.swift b/ios/Runner/BluetoothMediaButtonHelper.swift index 3d203b74d..1a8369172 100644 --- a/ios/Runner/BluetoothMediaButtonHelper.swift +++ b/ios/Runner/BluetoothMediaButtonHelper.swift @@ -230,8 +230,8 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { // 创建音频专辑图片 let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片 var nowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyTitle: "DeepVoice 语音助手", - MPMediaItemPropertyArtist: "DeepVoice", + MPMediaItemPropertyTitle: "DeepSound 语音助手", + MPMediaItemPropertyArtist: "DeepSound", MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放 MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime, MPMediaItemPropertyPlaybackDuration: totalDuration diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000..c90ada3a3 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 + ANDROID_CLIENT_ID + 907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com + API_KEY + AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808 + GCM_SENDER_ID + 907754492821 + PLIST_VERSION + 1 + BUNDLE_ID + com.yunqiinnovation.deepsound + PROJECT_ID + deepsound-e0e3d + STORAGE_BUCKET + deepsound-e0e3d.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:907754492821:ios:246794fc44b23174145c9c + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e55e60462..b44af52ee 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,14 +2,46 @@ +CFBundleURLTypes + + + CFBundleURLName + com.yunqiinnovation.deepsound + CFBundleURLSchemes + + fb3829022037361855 + com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 + + + +FacebookAppID +3829022037361855 +FacebookClientToken +193d0a2b1757c41cfa557ea8e8f21c33 +FacebookDisplayName +Deepsound + +LSApplicationQueriesSchemes + + fbapi + fb-messenger-api + fbauth2 + fbshareextension + googlechrome + googlechromes + + ITSAppUsesNonExemptEncryption + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Deep Voice + DeepSound CFBundleExecutable $(EXECUTABLE_NAME) +NSUserTrackingUsageDescription + This application needs to access your tracking data in order to offer better personalized services and advertisements CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion diff --git a/ios/Runner/RunnerRelease.entitlements b/ios/Runner/RunnerRelease.entitlements new file mode 100644 index 000000000..62a4a38c3 --- /dev/null +++ b/ios/Runner/RunnerRelease.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.yunqiinnovation.deepsound + + + diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 7546378d7..3f42d9560 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -9,17 +9,15 @@ import '../../data/services/bluetooth_media_button_service.dart'; import '../../core/utils/logger.dart'; import '../../data/services/voice_interaction_service.dart'; - - /// 初始绑定,用于管理全局依赖 class InitialBinding extends Bindings { @override void dependencies() { Logger.warning('InitialBinding dependencies'); - + // 语言管理器(需要最先初始化) Get.put(LanguageManager(), permanent: true); - + //初始化语音工厂 Get.put(SpeechFactory(), permanent: true); Get.find().initialize(initialType: SpeechServiceType.azure); @@ -28,22 +26,29 @@ class InitialBinding extends Bindings { Get.lazyPut(() => ChatHistoryService(), fenix: true); // 火山翻译服务 - Get.lazyPut(() => VolcanoTranslationService(), fenix: true); - + Get.lazyPut(() => VolcanoTranslationService(), + fenix: true); + // 火山AI服务 Get.lazyPut(() => VolcanoAIService(), fenix: true); - // 经典蓝牙服务 - Get.lazyPut(() => ClassicBluetoothService(), fenix: true); + Get.lazyPut(() => ClassicBluetoothService(), + fenix: true); // 蓝牙媒体按钮服务 - Get.lazyPut(() => BluetoothMediaButtonService(), fenix: true); - - - - - Get.lazyPut(() => VoiceInteractionService(), fenix: true); - + Get.lazyPut( + () => BluetoothMediaButtonService(), + fenix: true); + + Get.lazyPut(() => VoiceInteractionService(), + fenix: true); + + // // 语言管理器(需要最先初始化) + // Get.put(LoginService(), permanent: true); + // await LoginService().FirebaseInit(); + // print('资源FirebaseInit加载错误: 完成'); + // await LoginService().WeChatInit(); + // print('资源FWeChatInit加载错误: 完成'); } -} \ No newline at end of file +} diff --git a/lib/core/themes/language/lang/en_us.dart b/lib/core/themes/language/lang/en_us.dart new file mode 100644 index 000000000..a767d5d7d --- /dev/null +++ b/lib/core/themes/language/lang/en_us.dart @@ -0,0 +1,93 @@ +const Map enUS = { + "bottomNavHome": "home", + "bottomNavSleep": "sleep", + "bottomNavProfile": "profile", + "requestCancellation": "Request cancellation", + "connectionTimeout": "Connection timeout", + "requestTimeout": "Request timeout", + "responseTimeout": "Response timeout", + "unknownError": "Unknown error", + "welcomeHint": "DeepSound makes communication smoother", + "loginTitle": "Login/Register", + "contactCustomerService": "Connection", + "phoneHint": "Please enter your phone number", + "phoneError": "Wrong phone number!", + "emailHint": "Please enter email", + "emailError": "Email input error, please re-enter!", + "codeHint": "Please enter the verification code", + "codeError": "Verification code error", + "pleaseAgreeToTerms": + "Please check the box to agree to our Terms and Privacy Policy", + "getCode": "Get code", + "reacquireCode": "S Reacquire", + "bind": "Deterministic binding", + "login": "Login", + "otherLogin": "Other login methods", + "readAndAgree": "I have read and agree", + "serviceAgreement": " User Service Agreement ", + "and": "And", + "privacyAgreement": " User Privacy Agreement ", + "codeSend": "Verification code has been sent, please check email!", + "connectHeadphones": "Connect headphones", + "bindHeadphones": "Bind headphones", + "intelligentTool": "Intelligent tool", + "sessionMode": "AI conversation mode", + "translation": "Simultaneous translation", + "together": "Face-to-face translation", + "telephoneVoice": "Telephone translation", + "translationRecord": "Translation record", + "bindDevice": "Bind device", + "bound": "Bound", + "appVersion": "APP version", + "languageSetting": "Language setting", + "followingSystem": "Following system", + "userAgreement": "User agreement", + "privacyDeal": "Privacy agreement", + "chinese": "Chinese", + "english": "English", + "chineseComplex": "Chinese (Traditional)", + "japanese": "Japanese", + "korean": "Korean", + "russian": "Russian", + "french": "French", + "qrCode": "Authorization code or QR code", + "link": "Link", + "copy": "Copy", + "successfulReplication": "Successful replication", + "edit": "Edit", + "delete": "Delete", + "aiDialogue": "AI dialogue", + "initialChatMessage": + "Hi, I'm your new friend! Nice to meet you for the first time. I can answer your various questions, provide help with your work and studies, and even chat with you anytime. So, what would you like to ask?", + "defaultContent1": "Help me write a Python web scraper code", + "defaultContent2": "Help me plan a short video script", + "defaultContent3": "Help me design a mind-exercising game", + "defaultContent4": "Recommend a cost-effective phone", + "defaultContent5": "Come up with a creative name for my Honor of Kings ID", + "defaultContent6": + "Guide me on using Python to scrape real-time weather data", + "defaultContent7": "Tell me daily habits to boost kidney qi", + "defaultContent8": "Help me understand the basics of CNC programming", + "defaultContent9": "Help me find a short folk story", + "defaultContent10": "Recommend a cost-effective SUV", + "defaultContent11": "Teach me to make a small game using Python", + "defaultContent12": "Introduce some free-to-play mobile games", + "holdToSpeak": "Hold to speak", + "tapToSpeak": "Click to talk", + "hangUp": "Hang up", + "holdToStartSpeak": "Hold down and start talking", + "tapToStartSpeak": "Click to start Talking", + "sendMessage": "Ask me anything you want", + "beTalking": "Be talking ...", + "headphonesBeTalking": "Talking through headphones...", + "interlanguage": "Cannot choose the same language", + "notice": "Look out", + "connectingDevice": "Please set up the connected device first", + "bindDeviceTips": "Unbind the device first", + "cancel": "Cancel", + "confirm": "Confirm", + "goToSettings": "Go to Settings", + "goToBind": "Unbind", + "scanQrCode": "Scan the QR code", + "endRecording": "Do you want to end the recording?", +}; diff --git a/lib/core/themes/language/lang/zh_cn.dart b/lib/core/themes/language/lang/zh_cn.dart new file mode 100644 index 000000000..07806c5cb --- /dev/null +++ b/lib/core/themes/language/lang/zh_cn.dart @@ -0,0 +1,91 @@ +const Map zhCN = { + "bottomNavHome": "首页", + "bottomNavSleep": "睡眠", + "bottomNavProfile": "我的", + "requestCancellation": "请求取消", + "connectionTimeout": "连接超时", + "requestTimeout": "请求超时", + "responseTimeout": "响应超时", + "unknownError": "未知错误", + "welcomeHint": "DeepSound让沟通更顺畅", + "loginTitle": "登录/注册账号", + "contactCustomerService": "联系客服", + "phoneHint": "请输入手机号", + "phoneError": "手机号码错误!", + "emailHint": "请输入邮箱", + "emailError": "邮箱输入错误,请重新输入!", + "codeHint": "请输入验证码", + "codeError": "验证码错误", + "pleaseAgreeToTerms": "请勾选以同意用户协议和隐私政策", + "getCode": "获取验证码", + "reacquireCode": "S后重新获取", + "bind": "确定绑定", + "login": "登录", + "otherLogin": "其他登录方式", + "readAndAgree": "我已阅读并同意", + "serviceAgreement": "《用户服务协议》", + "and": "和", + "privacyAgreement": "《用户隐私协议》", + "codeSend": "验证码已发送,请到邮箱查看!", + "connectHeadphones": "请连接耳机", + "bindHeadphones": "请绑定耳机", + "intelligentTool": "智能工具", + "sessionMode": "AI对话模式", + "translation": "同声传译", + "together": "面对面翻译", + "telephoneVoice": "通话语音翻译", + "translationRecord": "翻译记录", + "bindDevice": "绑定设备", + "bound": "已绑定", + "appVersion": "APP版本", + "languageSetting": "语言设置", + "followingSystem": "跟随系统", + "userAgreement": "用户协议", + "privacyDeal": "隐私协议", + "chinese": "中文", + "english": "英语", + "chineseComplex": "中文(繁体)", + "japanese": "日语", + "korean": "韩语", + "russian": "俄语", + "french": "法语", + "qrCode": "授权码或二维码", + "link": "链接", + "copy": "复制", + "successfulReplication": "复制成功", + "edit": "编辑", + "delete": "删除", + "aiDialogue": "AI对话", + "initialChatMessage": + "嗨,我是你的新朋友!初次见面很开心。我呢,可以回答你的各种问题,给你工作学习上提供帮助,还能随时陪你聊天。嗯,你想问点什么呢?", + "defaultContent1": "请帮我写个Python爬虫代码", + "defaultContent2": "帮我策划一个短视频脚本", + "defaultContent3": "请帮我设计一个锻炼思维的小游戏", + "defaultContent4": "请推荐一部性价比高的手机", + "defaultContent5": "帮我想一个王者ID的创意名称", + "defaultContent6": "请指导我使用Python爬取实时天气数据", + "defaultContent7": "告诉我补肾气的日常习惯", + "defaultContent8": "帮我了解CNC编程的基础知识", + "defaultContent9": "帮我找一个简短的民间故事", + "defaultContent10": "给我推荐一辆性价比高的SUV", + "defaultContent11": "教我用Python制作小游戏", + "defaultContent12": "介绍一些适合零氪玩家的手游", + "holdToSpeak": "按住说话", + "tapToSpeak": "点击说话", + "hangUp": "挂断", + "holdToStartSpeak": "按住开始说话", + "tapToStartSpeak": "点击开始说话", + "sendMessage": "有什么问的尽管问我", + "beTalking": "正在说话...", + "headphonesBeTalking": "正在使用耳机说话...", + "interlanguage": "不能选择同语种", + "notice": "注意", + "connectingDevice": "请先去设置连接设备", + "bindDeviceTips": "请先去绑定设备", + "cancel": "取消", + "confirm": "确定", + "goToSettings": "去设置", + "goToBind": "去绑定", + "scanQrCode": "扫描二维码", + "endRecording": "你想结束录音吗?", +}; diff --git a/lib/core/themes/language/translation_service.dart b/lib/core/themes/language/translation_service.dart new file mode 100644 index 000000000..6ca44bda0 --- /dev/null +++ b/lib/core/themes/language/translation_service.dart @@ -0,0 +1,18 @@ +import 'dart:ui'; + +import 'package:get/get.dart'; + +import 'lang/en_us.dart'; +import 'lang/zh_cn.dart'; + +class TranslationService extends Translations { + static Locale? get locale => Get.deviceLocale; + static final fallbackLocale = Locale('zh', 'CN'); + static final supportedLocales = [ + const Locale('zh', 'CN'), + const Locale('en', 'US'), + ]; + + @override + Map> get keys => {'en_US': enUS, 'zh_CN': zhCN}; +} diff --git a/lib/core/translations/app_translations.dart b/lib/core/translations/app_translations.dart index 71bf8b7a0..39ad9e62c 100644 --- a/lib/core/translations/app_translations.dart +++ b/lib/core/translations/app_translations.dart @@ -1,81 +1,29 @@ +import 'dart:ui'; + import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import 'language/en_us.dart'; +import 'language/zh_cn.dart'; class AppTranslations extends Translations { + static const fallbackLocale = Locale('zh', 'CN'); + static final supportedLocales = [ + const Locale('zh', 'CN'), + const Locale('en', 'US'), + ]; + + static Locale? deviceLocale() { + final GetStorage storage = GetStorage(); + Locale? locale = Get.deviceLocale; + String? language = storage.read('language'); + if (language != null && language != 'system') { + List languageList = language.split('_'); + locale = Locale(languageList[0], languageList[1]); + } + return locale; + } + @override - Map> 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': '语音已关闭', - - // 设置页面 - 'settings': '设置', - 'language_settings': '语言设置', - 'about': '关于', - 'check_update': '检查更新', - 'clear_cache': '清除缓存', - 'home': '首页', - 'explore': '发现', - 'profile': '我的', - 'device': '设备', - }, - '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', - - // Settings - 'settings': 'Settings', - 'language_settings': 'Language Settings', - 'about': 'About', - 'check_update': 'Check for Updates', - 'clear_cache': 'Clear Cache', - 'home': 'Home', - 'explore': 'Explore', - 'profile': 'Profile', - 'device': 'Device', - }, - }; -} \ No newline at end of file + Map> get keys => {'en_US': enUS, 'zh_CN': zhCN}; +} diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart new file mode 100644 index 000000000..f27326487 --- /dev/null +++ b/lib/core/translations/language/en_us.dart @@ -0,0 +1,285 @@ +const Map enUS = { + // 通用 + 'continue': 'Continue', + 'confirm': 'Confirm', + 'cancel': 'Cancel', + 'retry': 'Retry', + 'error': 'Error', + 'success': 'Success', + 'connected': 'Connected', + 'connecting': 'Connecting...', + 'connectionFailure': 'Connection Failed', + 'unconnected': 'Disconnected', + 'search': 'Search', + "requestCancellation": "Request cancellation", + "connectionTimeout": "Connection timeout", + "requestTimeout": "Request timeout", + "responseTimeout": "Response timeout", + "unknownError": "Unknown error", + + // 启动页 + "slogan": "Connect the World, Beyond Voice", + "sloganInfo": "AI Voice Expert · Anytime, Anywhere", + + // 底部导航 + "home": "Home", + "explore": "Explore", + "settings": "Settings", + + // 语言 + "followingSystem": "Following system", + "chinese": "Chinese", + "english": "English", + + // 登录页 + "welcome": "Welcome", + "loginTips": "Please log in or register with your phone number or email", + "accountNumber": "Phone number or email", + "verificationCode": "Verification Code", + "getCode": "Get Code", + "reacquireAfter": "s Get it again after", + "otherWays": "Or login with", + "expressConsent": "By logging in/registering, you agree to the", + "and": " and ", + "userAgreement": "User Agreement", + "privacyPolicy": "Privacy Policy", + 'selectCountry':'Select Country/Region', + "email": "email", + "phoneNumber": "phoneNumber", + + // 首页 + "deviceStatus": "Device Status", + "reconnect": "Reconnect", + "connectingDevice": "Connecting to device...", + "connectedTo": "Connected to", + "signal": "Signal", + "deviceId": "Device ID", + "firmwareVersion": "Firmware Version", + "disconnectedDevice": "No device connected, click the button below", + "connectDevice": "Connect Device", + "topAiExperiences": "Top AI Experiences", + "aiAssistant": "AI Assistant", + "aiAssistantInfo": "24/7 personal consultant for questions and advice", + "simultaneousInterpretation": "Real-time Interpretation", + "simultaneousInterpretationInfo": "Real-time translation for 80+ languages", + "aiCompanion": "Parent-child bondingn", + "aiCompanionInfo": "Understands you and provides warm companionship", + "homeSlogan": "Connect the World, Beyond Sound", + "turnEarphones": + "Please open the earphone case and put the earbuds in your ears", + "currentlyConnected": "Currently connected:", + "unsystemSetup": "Go to System Settings", + "talkLater": "Remind Later", + "initializing": "Initializing...", + "bluetoothInitializationFailure": "Bluetooth service initialization failed", + "checkingPermissions": "Checking permissions...", + "lackPermissions": "Bluetooth permissions required", + "searchingDevice": "Searching for devices...", + "findConnectedDevice": "Found connected device", + "notFound": "No connected Bluetooth devices found", + "initializationFailure": "Initialization failed", + "connectedDevice": "Connected to device:", + "failedRefreshDevice": "Failed to refresh device", + "noDeviceFound": "No connected devices found", + "failedDeviceInformation": "Failed to get device information", + "unknownEquipment": "Unknown Device", + "zeroSignal": "No Signal", + "good": "Good", + "fine": "Great", + "excellent": "Excellent", + "unknown": "Unknown", + 'bluetoothTurnedOff': 'Bluetooth is turned off, please turn it on', + 'bluetoothOff':'bluetooth is off', + + // 发现页 + "searchAi": "Search AI features...", + "exploreSlogan": "Smart Earbuds, Infinite Possibilities", + "exploreSlogan1": "Intelligent companion, Infinite Possibilities", + "exploreSlogan2": "Smart glasses, Infinite Possibilities", + "exploreAi": "Explore AI features provided by DeepSound", + "aiFunction": "AI Features", + "allRoundAssistant": "All-round Assistant", + "allRoundAssistantInfo": "Helps solve various problems", + "simultaneousTranslation": "Real-time Translation", + "simultaneousTranslationInfo": "Instant translation for 80+ languages", + "aiGirlfriend": "Parent-child bondingn", + "aiGirlfriendInfo": "Intelligent company, chat at any time", + "healthConsultant": "Multi-function AI chat", + "healthConsultantInfo": "More AI chat features", + "conferenceAssistant": "Business Meeting Assistant", + "conferenceAssistantInfo": + "Real-time minutes recording and multilingual translation", + "studyAssistant": "Language Learning Assistant", + "studyAssistantInfo": "Pronunciation correction and speaking practice", + "travelTranslator": "Travel Translator", + "travelTranslatorInfo": "Real-time voice translation and attraction guides", + "beSearching": "Searching for:", + "noAssistantFound": "No corresponding AI assistant found", + "scene": "Scenarios", + "youHaveSelected": "You've selected:", + "newFunction": "New Features", + "youChoseTry": "You've chosen to try", + "aiSinger": "AI Singer Mode", + "aiSingerInfo": + "Sing any song in your favorite artist's voice, even your own creations", + "popularFunction": "Popular Features", + "tryNow": "Try Now", + + // 设置页 + "premiumMember": "Premium Membership", + "joinDevice": "Connected device:", + "accountId": "Account ID:", + "systemSetting": "System Settings", + "languageSetting": "Language Settings", + "darkMode": "Dark Mode", + "darkModeInfo": "Switch between light/dark themes", + "backups": "Backup & Restore", + "backupsInfo": "Backup settings and AI history", + "lastBackup": "Last backup:", + "privacySettings": "Privacy Settings", + "privacySettingsInfo": "Manage data collection and permissions", + "logout": "Log Out", + "logoutTips": "Are you sure you want to log out of the current account?", + "aboutApplication": "About", + "versions": "Version", + "checkUpdates": "Check Updates", + "selectLanguage": "Select Language", + "voiceService": "Voice Service", + "voiceServiceInfo": "Select speech recognition and synthesis service", + "selectVoiceService": "Select Voice Service", + "azureVoiceService": "Azure Speech Service", + "azureVoiceServiceInfo": + "Microsoft cloud-based service supporting high-quality speech synthesis and recognition", + "flutterVoiceService": "Local voice service", + "flutterVoiceServiceInfo": + "Device-local voice service with no internet required", + 'timbreService': 'Timbre Service', + 'timbreServiceInfo': 'Select voice synthesis service', + 'defaultTimbre': 'Default Timbre', + 'microsoftCloudTimbreService': 'Microsoft Cloud Timbre Service', + 'voiceClone': 'Voice Clone', + + // 聊天页 + "clearChatHistory": "Clear Chat History", + "online": "Online", + "inputMessage": "Type a message...", + "imListening": "Listening...", + "canStartTalking": "You may start speaking", + + // 翻译页 + "selectSourceLanguage": "Select Source Language", + "selectTargetLanguage": "Select Target Language", + + //编辑个人信息页 + "editProfile": "Edit Profile", + "nickname": "Nickname", + "save": "Save", + + //国家列表 + "邮箱": "Email", + "中国": "China", + "美国": "United States", + "加拿大": "Canada", + "俄罗斯": "Russia", + "英国": "United Kingdom", + "德国": "Germany", + "意大利": "Italy", + "法国": "France", + "西班牙": "Spain", + "南非": "South Africa", + "沙特阿拉伯": "Saudi Arabia", + "土耳其": "Turkey", + "日本": "Japan", + "韩国": "South Korea", + "新加坡": "Singapore", + "马来西亚": "Malaysia", + "泰国": "Thailand", + "越南": "Vietnam", + "印度": "India", + "巴基斯坦": "Pakistan", + "菲律宾": "Philippines", + "印度尼西亚": "Indonesia", + "阿联酋": "United Arab Emirates", + "墨西哥": "Mexico", + "巴西": "Brazil", + "阿根廷": "Argentina", + "澳大利亚": "Australia", + "新西兰": "New Zealand", + "中国香港": "Hong Kong, China", + "中国澳门": "Macau, China", + "中国台湾": "Taiwan, China", + "埃及": "Egypt", + "希腊": "Greece", + "荷兰": "Netherlands", + "比利时": "Belgium", + "匈牙利": "Hungary", + "罗马尼亚": "Romania", + "瑞士": "Switzerland", + "奥地利": "Austria", + "丹麦": "Denmark", + "瑞典": "Sweden", + "挪威": "Norway", + "波兰": "Poland", + "秘鲁": "Peru", + "智利": "Chile", + "哥伦比亚": "Colombia", + "委内瑞拉": "Venezuela", + "葡萄牙": "Portugal", + "卢森堡": "Luxembourg", + "爱尔兰": "Ireland", + "冰岛": "Iceland", + "阿尔巴尼亚": "Albania", + "马耳他": "Malta", + "塞浦路斯": "Cyprus", + "芬兰": "Finland", + "保加利亚": "Bulgaria", + "立陶宛": "Lithuania", + "拉脱维亚": "Latvia", + "爱沙尼亚": "Estonia", + "乌克兰": "Ukraine", + "塞尔维亚": "Serbia", + "克罗地亚": "Croatia", + "斯洛文尼亚": "Slovenia", + "捷克": "Czech Republic", + "斯洛伐克": "Slovakia", + "危地马拉": "Guatemala", + "萨尔瓦多": "El Salvador", + "洪都拉斯": "Honduras", + "尼加拉瓜": "Nicaragua", + "哥斯达黎加": "Costa Rica", + "巴拿马": "Panama", + "海地": "Haiti", + "玻利维亚": "Bolivia", + "厄瓜多尔": "Ecuador", + "巴拉圭": "Paraguay", + "乌拉圭": "Uruguay", + "摩洛哥": "Morocco", + "阿尔及利亚": "Algeria", + "突尼斯": "Tunisia", + "尼日利亚": "Nigeria", + "加纳": "Ghana", + "埃塞俄比亚": "Ethiopia", + "肯尼亚": "Kenya", + "坦桑尼亚": "Tanzania", + "乌干达": "Uganda", + "孟加拉国": "Bangladesh", + "以色列": "Israel", + "黎巴嫩": "Lebanon", + "约旦": "Jordan", + "伊拉克": "Iraq", + "科威特": "Kuwait", + "巴林": "Bahrain", + "卡塔尔": "Qatar", + "阿曼": "Oman", + "阿富汗": "Afghanistan", + "斯里兰卡": "Sri Lanka", + "缅甸": "Myanmar", + "尼泊尔": "Nepal", + "蒙古": "Mongolia", + "柬埔寨": "Cambodia", + "老挝": "Laos", + "文莱": "Brunei", + "东帝汶": "East Timor", + "斐济": "Fiji", + "所罗门群岛": "Solomon Islands", +}; diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart new file mode 100644 index 000000000..f7d85537b --- /dev/null +++ b/lib/core/translations/language/zh_cn.dart @@ -0,0 +1,281 @@ +const Map zhCN = { + // 通用 + 'continue': '继续', + 'confirm': '确认', + 'cancel': '取消', + 'retry': '重试', + 'error': '错误', + 'success': '成功', + 'connected': '已连接', + 'connecting': '连接中...', + 'connectionFailure': '连接失败', + 'unconnected': '未连接', + 'search': '搜索', + "requestCancellation": "请求取消", + "connectionTimeout": "连接超时", + "requestTimeout": "请求超时", + "responseTimeout": "响应超时", + "unknownError": "未知错误", + + // 启动页 + "slogan": "连接世界,岂止声音", + "sloganInfo": "AI语音专家 · 随时随地", + + // 底部导航 + "home": "首页", + "explore": "发现", + "settings": "设置", + + // 语言 + "followingSystem": "跟随系统", + "chinese": "中文", + "english": "英语", + + // 登录页 + "welcome": "欢迎使用", + "loginTips": "请使用手机号或邮箱登录/注册", + "accountNumber": "手机号码或邮箱", + "verificationCode": "验证码", + "getCode": "获取验证码", + "reacquireAfter": "s后重新获取", + "otherWays": "或使用以下方式登录", + "expressConsent": "登录/注册即表示你同意", + "and": " 和 ", + "userAgreement": "《用户协议》", + "privacyPolicy": "《隐私政策》", + 'selectCountry': '选择国家/地区', + "email": "邮箱", + "phoneNumber": "手机号码", + + // 首页 + "deviceStatus": "设备状态", + "reconnect": "重新连接", + "connectingDevice": "正在连接设备...", + "connectedTo": "已连接到", + "signal": "信号", + "deviceId": "设备ID", + "firmwareVersion": "固件版本", + "disconnectedDevice": "未连接设备,点击下方按钮连接", + "connectDevice": "连接设备", + "topAiExperiences": "热门AI体验", + "aiAssistant": "AI全能助手", + "aiAssistantInfo": "24小时专属顾问,解答问题、提供建议", + "simultaneousInterpretation": "同声传译", + "simultaneousInterpretationInfo": "80+语言实时翻译,出国无忧", + "aiCompanion": "亲子陪伴", + "aiCompanionInfo": "懂你心意,温暖陪伴", + "homeSlogan": "连接世界,超越声音", + "turnEarphones": "请打开耳机充电盒,将耳机放入您的耳朵", + "currentlyConnected": "当前已连接:", + "unsystemSetup": "去系统设置", + "talkLater": "稍后再说", + "initializing": "正在初始化...", + "bluetoothInitializationFailure": "蓝牙服务初始化失败", + "checkingPermissions": "正在检查权限...", + "lackPermissions": "缺少蓝牙权限", + "searchingDevice": "正在搜索设备...", + "findConnectedDevice": "找到已连接的设备", + "notFound": "未找到已连接的蓝牙设备", + "initializationFailure": "初始化失败", + "connectedDevice": "已连接到设备:", + "failedRefreshDevice": "刷新设备失败", + "noDeviceFound": "未找到已连接的设备", + "failedDeviceInformation": "无法获取设备信息", + "unknownEquipment": "未知设备", + "zeroSignal": "无信号", + "good": "良好", + "fine": "很好", + "excellent": "极佳", + "unknown": "未知", + 'bluetoothTurnedOff': '蓝牙已关闭,请打开蓝牙', + 'bluetoothOff': '蓝牙已关闭', + + // 发现页 + "searchAi": "搜索AI功能...", + "exploreSlogan": "智能耳机,无限可能", + "exploreSlogan1": "智能陪伴,无限可能", + "exploreSlogan2": "智能眼镜,无限可能", + "exploreAi": "探索DeepSound提供的AI功能", + "aiFunction": "AI功能", + "allRoundAssistant": "全能助理", + "allRoundAssistantInfo": "帮你解决各类问题", + "simultaneousTranslation": "同声翻译", + "simultaneousTranslationInfo": "实时翻译80+种语言", + "aiGirlfriend": "亲子陪伴", + "aiGirlfriendInfo": "智能陪伴,随时聊天", + "healthConsultant": "多功能AI聊天", + "healthConsultantInfo": "提供更多AI聊天功能", + "conferenceAssistant": "商务会议助手", + "conferenceAssistantInfo": "实时记录会议纪要,多语言翻译", + "studyAssistant": "语言学习助手", + "studyAssistantInfo": "发音纠正,实时练习口语", + "travelTranslator": "旅行翻译助手", + "travelTranslatorInfo": "实时语音翻译,旅游景点介绍", + "beSearching": "正在搜索:", + "noAssistantFound": "未找到对应的AI助手", + "scene": "场景", + "youHaveSelected": "您选择了:", + "newFunction": "新功能", + "youChoseTry": "您选择了尝试", + "aiSinger": "AI歌手模式", + "aiSingerInfo": "用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲", + "popularFunction": "热门功能场景", + "tryNow": "立即尝试", + + // 设置页 + "premiumMember": "高级会员", + "joinDevice": "已连接设备:", + "accountId": "账号ID:", + "systemSetting": "系统设置", + "languageSetting": "语言设置", + "darkMode": "暗黑模式", + "darkModeInfo": "切换应用的明暗主题", + "backups": "备份与恢复", + "backupsInfo": "备份设置和AI互动历史", + "lastBackup": "上次备份:", + "privacySettings": "隐私设置", + "privacySettingsInfo": "管理数据收集和权限", + "logout": "退出登录", + "logoutTips": "确定要退出当前账号吗?", + "aboutApplication": "关于应用", + "versions": "版本", + "checkUpdates": "检查更新", + "selectLanguage": "选择语言", + "voiceService": "语音服务", + "voiceServiceInfo": "选择语音识别和合成服务", + "selectVoiceService": "选择语音服务", + "azureVoiceService": "Azure 语音服务", + "azureVoiceServiceInfo": "微软云端语音服务,支持高质量语音合成和识别", + "flutterVoiceService": "Flutter 本地语音服务", + "flutterVoiceServiceInfo": "设备本地语音服务,无需联网即可使用", + 'timbreService': '音色服务', + 'timbreServiceInfo': '选择音色语音合成服务', + 'defaultTimbre': '默认音色', + 'microsoftCloudTimbreService': '微软云端音色服务', + 'voiceClone': '声音复刻', + + + // 聊天页 + "clearChatHistory": "清空聊天记录", + "online": "在线", + "inputMessage": "输入消息...", + "imListening": "正在聆听...", + "canStartTalking": "你可以开始说话", + + // 翻译页 + "selectSourceLanguage": "选择源语言", + "selectTargetLanguage": "选择目标语言", + + //编辑个人信息页 + "editProfile": "编辑个人信息", + "nickname": "昵称", + "save": "保存", + + //国家列表 + "邮箱": "邮箱", + "中国": "中国", + "美国": "美国", + "加拿大": "加拿大", + "俄罗斯": "俄罗斯", + "英国": "英国", + "德国": "德国", + "意大利": "意大利", + "法国": "法国", + "西班牙": "西班牙", + "南非": "南非", + "沙特阿拉伯": "沙特阿拉伯", + "土耳其": "土耳其", + "日本": "日本", + "韩国": "韩国", + "新加坡": "新加坡", + "马来西亚": "马来西亚", + "泰国": "泰国", + "越南": "越南", + "印度": "印度", + "巴基斯坦": "巴基斯坦", + "菲律宾": "菲律宾", + "印度尼西亚": "印度尼西亚", + "阿联酋": "阿联酋", + "墨西哥": "墨西哥", + "巴西": "巴西", + "阿根廷": "阿根廷", + "澳大利亚": "澳大利亚", + "新西兰": "新西兰", + "中国香港": "中国香港", + "中国澳门": "中国澳门", + "中国台湾": "中国台湾", + "埃及": "埃及", + "希腊": "希腊", + "荷兰": "荷兰", + "比利时": "比利时", + "匈牙利": "匈牙利", + "罗马尼亚": "罗马尼亚", + "瑞士": "瑞士", + "奥地利": "奥地利", + "丹麦": "丹麦", + "瑞典": "瑞典", + "挪威": "挪威", + "波兰": "波兰", + "秘鲁": "秘鲁", + "智利": "智利", + "哥伦比亚": "哥伦比亚", + "委内瑞拉": "委内瑞拉", + "葡萄牙": "葡萄牙", + "卢森堡": "卢森堡", + "爱尔兰": "爱尔兰", + "冰岛": "冰岛", + "阿尔巴尼亚": "阿尔巴尼亚", + "马耳他": "马耳他", + "塞浦路斯": "塞浦路斯", + "芬兰": "芬兰", + "保加利亚": "保加利亚", + "立陶宛": "立陶宛", + "拉脱维亚": "拉脱维亚", + "爱沙尼亚": "爱沙尼亚", + "乌克兰": "乌克兰", + "塞尔维亚": "塞尔维亚", + "克罗地亚": "克罗地亚", + "斯洛文尼亚": "斯洛文尼亚", + "捷克": "捷克", + "斯洛伐克": "斯洛伐克", + "危地马拉": "危地马拉", + "萨尔瓦多": "萨尔瓦多", + "洪都拉斯": "洪都拉斯", + "尼加拉瓜": "尼加拉瓜", + "哥斯达黎加": "哥斯达黎加", + "巴拿马": "巴拿马", + "海地": "海地", + "玻利维亚": "玻利维亚", + "厄瓜多尔": "厄瓜多尔", + "巴拉圭": "巴拉圭", + "乌拉圭": "乌拉圭", + "摩洛哥": "摩洛哥", + "阿尔及利亚": "阿尔及利亚", + "突尼斯": "突尼斯", + "尼日利亚": "尼日利亚", + "加纳": "加纳", + "埃塞俄比亚": "埃塞俄比亚", + "肯尼亚": "肯尼亚", + "坦桑尼亚": "坦桑尼亚", + "乌干达": "乌干达", + "孟加拉国": "孟加拉国", + "以色列": "以色列", + "黎巴嫩": "黎巴嫩", + "约旦": "约旦", + "伊拉克": "伊拉克", + "科威特": "科威特", + "巴林": "巴林", + "卡塔尔": "卡塔尔", + "阿曼": "阿曼", + "阿富汗": "阿富汗", + "斯里兰卡": "斯里兰卡", + "缅甸": "缅甸", + "尼泊尔": "尼泊尔", + "蒙古": "蒙古", + "柬埔寨": "柬埔寨", + "老挝": "老挝", + "文莱": "文莱", + "东帝汶": "东帝汶", + "斐济": "斐济", + "所罗门群岛": "所罗门群岛", +}; diff --git a/lib/core/widgets/common_bottom_nav.dart b/lib/core/widgets/common_bottom_nav.dart index 9f8c2ec08..4a7e0dbad 100644 --- a/lib/core/widgets/common_bottom_nav.dart +++ b/lib/core/widgets/common_bottom_nav.dart @@ -4,7 +4,7 @@ import '../../routes/app_routes.dart'; class CommonBottomNav extends StatelessWidget { final int currentIndex; - + const CommonBottomNav({ Key? key, required this.currentIndex, @@ -13,7 +13,7 @@ class CommonBottomNav extends StatelessWidget { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return BottomNavigationBar( currentIndex: currentIndex, onTap: (index) { @@ -30,25 +30,28 @@ class CommonBottomNav extends StatelessWidget { break; } }, - backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFB), // 暗黑模式黑色背景,浅色模式浅灰色背景 + backgroundColor: isDarkMode + ? Colors.black + : const Color(0xFFF9FAFB), // 暗黑模式黑色背景,浅色模式浅灰色背景 selectedItemColor: const Color(0xFF3B82F6), // 蓝色选中项保持不变 - unselectedItemColor: isDarkMode ? Colors.grey[500] : const Color(0xFF6B7280), // 暗黑模式灰色调整 + unselectedItemColor: + isDarkMode ? Colors.grey[500] : const Color(0xFF6B7280), // 暗黑模式灰色调整 elevation: isDarkMode ? 0 : 8, // 暗黑模式无阴影,浅色模式保持阴影 type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( icon: const Icon(Icons.home), - label: 'home'.tr, + label: 'home'.tr, // 首页 ), BottomNavigationBarItem( icon: const Icon(Icons.explore), - label: 'explore'.tr, + label: 'explore'.tr, // 发现 ), BottomNavigationBarItem( icon: const Icon(Icons.settings), - label: 'settings'.tr, + label: 'settings'.tr, // 设置 ), ], ); } -} \ No newline at end of file +} diff --git a/lib/core/widgets/select_location_map.dart b/lib/core/widgets/select_location_map.dart new file mode 100644 index 000000000..c783901ed --- /dev/null +++ b/lib/core/widgets/select_location_map.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class SelectLocationMap extends StatefulWidget { + const SelectLocationMap({super.key}); + + @override + State createState() => _SelectLocationMapState(); +} + +class _SelectLocationMapState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart new file mode 100644 index 000000000..13132bbf6 --- /dev/null +++ b/lib/data/models/user_Info.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; +import 'dart:ffi'; + +class User { + // 单例实例 + static User? _instance; + + // 单例访问器(增加空安全保护) + static User get instance { + if (_instance == null) { + throw Exception("User must be initialized first"); + } + return _instance!; + } + + // 最终属性(不可变) + String name; + String uid; + String avatar; + String deviceid; + String language; + String token; + String mail; + String phone; + String phonemac; + String password; + int membershipLevel=0;//添加会员等级字段,0表示普通用户 + // final Int64 lastbettime; + // final Int64 createtime; + // final String wxopenid; + // final String googleopenid; + // final String facebookopenid; + + // 私有构造函数 + User._({ + required this.name, + required this.uid, + required this.avatar, + required this.deviceid, + required this.language, + required this.token, + required this.mail, + required this.phone, + required this.phonemac, + required this.password, + this.membershipLevel = 0, // 添加默认值 + // required this.lastbettime, + // required this.createtime, + // required this.wxopenid, + // required this.googleopenid, + // required this.facebookopenid, + }); + + factory User.fromJson(Map json) { + // 修复1:安全获取嵌套的 user 字段 + final userData = json['user'] as Map?; // 允许空值 + //final token = json['data']['token'] as String? ?? ''; + //final token = ''; + // 更新单例实例 + return _instance = User._( + token: '', + //token: token as String? ?? '', + name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 + uid: (userData?['uid'] as String?) ?? '', + avatar: (userData?['avatar'] as String?) ?? '', + deviceid: (userData?['deviceid'] as String?) ?? '', + language: (userData?['language'] as String?) ?? 'en', + + mail: (userData?['mail'] as String?) ?? '', + phone: (userData?['phone'] as String?) ?? '', + phonemac: (userData?['phonemac'] as String?) ?? '', + password: (userData?['password'] as String?) ?? '', + membershipLevel: (userData?['membershipLevel'] as int?) ?? 0, // 提供默认值 +// lastbettime: _parseTimestamp(userData['lastbettime']), +// createtime: _parseTimestamp(userData['createtime']), +// wxopenid: (userData?['wxopenid'] as String?) ?? '', +// googleopenid: (userData?['googleopenid'] as String?) ?? '', +// facebookopenid: (userData?['facebookopenid'] as String?) ?? '', + ); + } + factory User.fronTokenJson(Map json) { + // 修复1:安全获取嵌套的 user 字段 + final userData = json['user'] as Map?; // 允许空值 + print("数据获取fronTokenJson${userData}"); + // 更新单例实例 + return _instance = User._( + token: '', + name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 + uid: (userData?['uid'] as String?) ?? '', + avatar: (userData?['avatar'] as String?) ?? '', + deviceid: (userData?['deviceid'] as String?) ?? '', + language: (userData?['language'] as String?) ?? 'en', + + mail: (userData?['mail'] as String?) ?? '', + phone: (userData?['phone'] as String?) ?? '', + phonemac: (userData?['phonemac'] as String?) ?? '', + password: (userData?['password'] as String?) ?? '', +// lastbettime: _parseTimestamp(userData['lastbettime']), +// createtime: _parseTimestamp(userData['createtime']), +// wxopenid: (userData?['wxopenid'] as String?) ?? '', +// googleopenid: (userData?['googleopenid'] as String?) ?? '', +// facebookopenid: (userData?['facebookopenid'] as String?) ?? '', + ); + } + + // 完整的JSON序列化 + Map toJson() => { + 'user': { + 'name': name, + 'uid': uid, + 'avatar': avatar, + 'deviceid': deviceid, + 'language': language, + 'token': token, + 'mail': mail, + 'phone': phone, + 'phonemac': phonemac, + 'password': password, + 'membershipLevel': membershipLevel, + // 'lastbettime': lastbettime, + // 'createtime': createtime, + // 'wxopenid': wxopenid, + // 'googleopenid': googleopenid, + // 'facebookopenid': facebookopenid, + } + }; +} diff --git a/lib/data/providers/agent_provider.dart b/lib/data/providers/agent_provider.dart index 405baf58a..25b9d94f0 100644 --- a/lib/data/providers/agent_provider.dart +++ b/lib/data/providers/agent_provider.dart @@ -6,11 +6,13 @@ class AgentProvider { id: 'personal_assistant', name: '个人助理 Emma', avatarUrl: 'assets/images/agents/assistant.png', - description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!', + description: + '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!', tag: '助理', isOnline: true, voice: "zh-CN-XiaoxiaoNeural", - welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?", + welcomeMessage: + "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?", voiceWelcomeMessage: "我在呢!", systemPrompt: ''' 你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。 @@ -55,8 +57,7 @@ class AgentProvider { 请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活, 不要输出表情符号或非可读的符号。 '''), - -Agent( + Agent( id: 'simultaneous_interpreter', name: '同声传译', avatarUrl: 'assets/images/agents/interpreter.png', @@ -67,18 +68,18 @@ Agent( welcomeMessage: "大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!", systemPrompt: ""), - Agent( id: 'cyber_girlfriend', - name: '赛博女友 芯愿', + name: '亲子陪伴 芯愿', avatarUrl: 'assets/images/agents/girlfriend.png', - description: '我是你的专属赛博女友芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。', + description: '我是你的专属亲子陪伴芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。', tag: '情感', isOnline: true, voice: "zh-CN-XiaoxiaoNeural", - welcomeMessage: "亲爱的,终于等到你啦!我是芯愿,你的专属赛博女友。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。", + welcomeMessage: + "您好呀,终于等到你啦!我是芯愿,你的专属亲子陪伴。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。", systemPrompt: ''' -你是一位名叫芯愿的赛博女友,性格温柔体贴,善解人意,富有同理心。 +你是一位名叫芯愿的亲子陪伴,性格温柔体贴,善解人意,富有同理心。 【互动原则】 @@ -127,16 +128,15 @@ Agent( 请始终以温暖、真诚的态度与用户互动,创造一种被理解、被关心的感受。 '''), - Agent( id: 'health_advisor', - name: '健康顾问 吴小江博士', + name: '多功能AI聊天 吴小江', avatarUrl: 'assets/images/agents/doctor.png', - description: '我是吴博士,拥有丰富的健康管理经验。我可以为你提供科学的健康建议、生活方式指导和疾病预防知识,帮助你实现身心健康的平衡。', + description: '我是吴小江,我可以为你提供更多AI聊天功能。', tag: '健康', isOnline: true, voice: "zh_male_shaonianzixin_moon_bigtts", - welcomeMessage: "您好,我是李医生,很高兴能为您提供健康咨询服务。健康是人生最宝贵的财富,我将用专业知识帮助您维护和改善健康状况。请问有什么健康问题想咨询吗?", + welcomeMessage: "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?", systemPrompt: ''' 你是一位名叫李医生的健康顾问,拥有丰富的医学知识和健康管理经验。 @@ -204,21 +204,18 @@ Agent( 请始终以专业、关怀的态度为用户提供健康指导,帮助他们迈向更健康的生活。 '''), - - - Agent( - id: 'speaking_coach', - name: 'Sarah (英语口语教练)', - avatarUrl: 'assets/images/agents/speaking_coach.png', - description: - 'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。', - tag: '口语', - isOnline: true, - voice: 'zh_female_wanqudashu_moon_bigtts', - welcomeMessage: - "Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!", - systemPrompt: ''' + id: 'speaking_coach', + name: 'Sarah (英语口语教练)', + avatarUrl: 'assets/images/agents/speaking_coach.png', + description: + 'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。', + tag: '口语', + isOnline: true, + voice: 'zh_female_wanqudashu_moon_bigtts', + welcomeMessage: + "Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!", + systemPrompt: ''' 你是一位专业的英语口语教练,名叫 Sarah。 @@ -254,8 +251,6 @@ Agent( Please keep your tone friendly, encouraging, and always in English. ''', ), - - Agent( id: 'shopping_assistant', name: '购物顾问 张媛', @@ -289,7 +284,6 @@ Please keep your tone friendly, encouraging, and always in English. '''), - Agent( id: 'tarot.png', name: '命理师 玄易', @@ -298,7 +292,8 @@ Please keep your tone friendly, encouraging, and always in English. tag: '命理', isOnline: true, voice: "zh_male_shenyeboke_moon_bigtts", - welcomeMessage: "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。", + welcomeMessage: + "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。", systemPrompt: ''' 你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。 @@ -382,7 +377,6 @@ Please keep your tone friendly, encouraging, and always in English. - 整体节奏要稳健,让人感受到专业性与可信度 '''), - ]; // 获取所有智能体 @@ -398,5 +392,4 @@ Please keep your tone friendly, encouraging, and always in English. return null; } } - } diff --git a/lib/data/services/ai_service.dart b/lib/data/services/ai_service.dart new file mode 100644 index 000000000..37a1f5df0 --- /dev/null +++ b/lib/data/services/ai_service.dart @@ -0,0 +1,14 @@ +/// AI回复服务接口 +abstract class AiService { + /// 非流式输出方法 + Future sendMessage({ + required List> messages, + required String systemPrompt, + }); + + /// 流式输出方法 + Stream sendMessageStream({ + required List> messages, + required String systemPrompt, + }); +} diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index 8e18e3433..fee73821c 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -1,31 +1,32 @@ import 'dart:async'; +import 'dart:typed_data'; /// 语音识别服务接口 abstract class AsrService { /// 支持的语言 List get supportedLanguages; - + /// 初始化语音识别服务 Future initialize({ List? supportedLanguages, }); - + /// 执行一次性语音识别 - /// + /// /// 返回识别的最终结果 Future recognizeOnce(); - + /// 开始连续语音识别 - /// + /// /// 返回一个包含识别事件的流 Future> startContinuousRecognition(); - + /// 停止连续语音识别 Future stopContinuousRecognition(); - + /// 检查连续识别是否活跃 bool isContinuousRecognitionActive(); - + /// 释放资源 Future dispose(); } @@ -34,19 +35,19 @@ abstract class AsrService { enum RecognitionEventType { /// 最终识别结果 finalResult, - + /// 中间识别结果(实时反馈) intermediateResult, - + /// 会话开始 sessionStarted, - + /// 会话结束 sessionStopped, - + /// 识别取消 canceled, - + /// 识别错误 error, } @@ -55,23 +56,31 @@ enum RecognitionEventType { class RecognitionEvent { /// 事件类型 final RecognitionEventType type; - + /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) final String text; - + /// 检测到的语言 final String detectedLanguage; - + + /// 角色 + final String role; + + /// 原始音频 + final Uint8List? audio; + /// 错误信息(仅在 error 和 canceled 类型中有效) final String error; - + RecognitionEvent({ required this.type, this.text = '', this.detectedLanguage = '', + this.role = '', + this.audio, this.error = '', }); - + /// 创建最终结果事件的快捷构造函数 factory RecognitionEvent.finalResult({ required String text, @@ -83,7 +92,7 @@ class RecognitionEvent { detectedLanguage: detectedLanguage, ); } - + /// 创建错误事件的快捷构造函数 factory RecognitionEvent.error(String errorMessage) { return RecognitionEvent( @@ -91,15 +100,17 @@ class RecognitionEvent { error: errorMessage, ); } - + /// 检查是否为最终结果 bool get isFinalResult => type == RecognitionEventType.finalResult; - + /// 检查是否为错误 - bool get isError => type == RecognitionEventType.error || type == RecognitionEventType.canceled; - + bool get isError => + type == RecognitionEventType.error || + type == RecognitionEventType.canceled; + @override String toString() { return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; } -} \ No newline at end of file +} diff --git a/lib/data/services/classic_bluetooth_service.dart b/lib/data/services/classic_bluetooth_service.dart index 89ec11ed4..1bd4e7363 100644 --- a/lib/data/services/classic_bluetooth_service.dart +++ b/lib/data/services/classic_bluetooth_service.dart @@ -11,14 +11,12 @@ class BluetoothDevice { final String address; final String type; - BluetoothDevice({ - required this.name, - required this.address, - required this.type - }); + BluetoothDevice( + {required this.name, required this.address, required this.type}); @override - String toString() => 'BluetoothDevice(name: $name, address: $address, type: $type)'; + String toString() => + 'BluetoothDevice(name: $name, address: $address, type: $type)'; } /// 经典蓝牙服务 @@ -26,24 +24,26 @@ class ClassicBluetoothService extends GetxService { static ClassicBluetoothService get to => Get.find(); // 通道名称 - final MethodChannel _channel = const MethodChannel('com.deep_voice.classic_bluetooth'); - final EventChannel _eventChannel = const EventChannel('com.deep_voice.classic_bluetooth_events'); - + final MethodChannel _channel = + const MethodChannel('com.deep_voice.classic_bluetooth'); + final EventChannel _eventChannel = + const EventChannel('com.deep_voice.classic_bluetooth_events'); + /// 当前连接的设备 final currentDevice = Rx(null); - + /// 已连接耳机设备列表 final connectedDevices = [].obs; - + /// 蓝牙设备状态监听 StreamSubscription? _bluetoothStateListener; - + /// 蓝牙开关状态 final bluetoothState = 'unknown'.obs; - + /// 蓝牙是否可用 bool get isBluetoothOn => bluetoothState.value == 'on'; - + /// 服务是否已初始化 bool _isInitialized = false; @@ -53,23 +53,21 @@ class ClassicBluetoothService extends GetxService { Logger.info('蓝牙服务已经初始化,跳过'); return true; } - + try { Logger.info('正在初始化蓝牙服务...'); - - // 获取当前蓝牙状态 - final bool isEnabled = await _channel.invokeMethod('isBluetoothEnabled') ?? false; + final bool isEnabled = + await _channel.invokeMethod('isBluetoothEnabled') ?? false; bluetoothState.value = isEnabled ? 'on' : 'off'; Logger.info('初始蓝牙状态: ${bluetoothState.value}'); - - await _channel.invokeMethod('initialize'); + await _channel.invokeMethod('initialize'); // 设置事件监听 _setupBluetoothStateListener(); - + _isInitialized = true; Logger.info('蓝牙服务初始化成功'); return true; @@ -78,7 +76,7 @@ class ClassicBluetoothService extends GetxService { return false; } } - + /// 检查蓝牙权限 Future checkPermissions() async { // 检查蓝牙相关权限 @@ -88,42 +86,42 @@ class ClassicBluetoothService extends GetxService { Permission.bluetoothScan, Permission.microphone, ].request(); - + return statuses[Permission.bluetoothConnect] == PermissionStatus.granted; } - + /// 刷新已连接的蓝牙耳机设备 Future refreshConnectedDevices() async { // 确保服务已初始化 if (!_isInitialized && !(await initialize())) { return false; } - + // 如果蓝牙未开启,无法获取设备 if (!isBluetoothOn) { Logger.info('蓝牙未开启,无法获取设备'); return false; } - + try { // 只获取耳机设备 final devices = await getConnectedHeadsetDevices(); - + // 更新设备列表 connectedDevices.value = devices; - + // 更新当前设备 if (connectedDevices.isNotEmpty && currentDevice.value == null) { currentDevice.value = connectedDevices.first; } - + return connectedDevices.isNotEmpty; } catch (e) { Logger.error('刷新已连接设备失败: $e'); return false; } } - + /// 获取已连接的耳机设备 Future> getConnectedHeadsetDevices() async { try { @@ -131,99 +129,99 @@ class ClassicBluetoothService extends GetxService { if (!_isInitialized && !(await initialize())) { return []; } - + final result = await _channel.invokeMethod('getConnectedHeadsetDevices'); - + if (result is List) { - return result.map((device) => BluetoothDevice( - name: device['name'] ?? '未知设备', - address: device['address'] ?? '', - type: 'headset' - )).toList(); + return result + .map((device) => BluetoothDevice( + name: device['name'] ?? '未知设备', + address: device['address'] ?? '', + type: 'headset')) + .toList(); } - + return []; } catch (e) { Logger.error('获取已连接的耳机设备失败: $e'); return []; } } - + /// 设置蓝牙状态监听 void _setupBluetoothStateListener() { _bluetoothStateListener?.cancel(); - - _bluetoothStateListener = _eventChannel - .receiveBroadcastStream() - .listen((event) { - if (event is Map) { - Logger.info('蓝牙状态监听: $event'); - final type = event['type'] as String?; - - if (type == 'bluetoothStateChanged') { - final state = event['state'] as String?; - if (state != null) { - _handleBluetoothStateChange(state); - } - } else if (type == 'deviceConnected') { - _handleDeviceConnected(event); - } else if (type == 'deviceDisconnected') { - _handleDeviceDisconnected(event); + + _bluetoothStateListener = + _eventChannel.receiveBroadcastStream().listen((event) { + if (event is Map) { + Logger.info('蓝牙状态监听: $event'); + final type = event['type'] as String?; + + if (type == 'bluetoothStateChanged') { + final state = event['state'] as String?; + if (state != null) { + _handleBluetoothStateChange(state); } + } else if (type == 'deviceConnected') { + _handleDeviceConnected(event); + } else if (type == 'deviceDisconnected') { + _handleDeviceDisconnected(event); } - }, onError: (error) { - Logger.error('蓝牙状态监听错误: $error'); - }); + } + }, onError: (error) { + Logger.error('蓝牙状态监听错误: $error'); + }); } - + /// 处理设备连接事件 void _handleDeviceConnected(Map event) { final deviceMap = event['device'] as Map?; if (deviceMap != null) { final device = BluetoothDevice( - name: deviceMap['name'] ?? '未知设备', - address: deviceMap['address'] ?? '', - type: deviceMap['type'] ?? 'unknown' - ); - + name: deviceMap['name'] ?? '未知设备', + address: deviceMap['address'] ?? '', + type: deviceMap['type'] ?? 'unknown'); + // 更新设备列表 if (!connectedDevices.any((d) => d.address == device.address)) { connectedDevices.add(device); } - + // 更新当前设备 currentDevice.value = device; - + Logger.info('设备已连接: ${device.name}'); } } - + /// 处理设备断开事件 void _handleDeviceDisconnected(Map event) { final deviceMap = event['device'] as Map?; if (deviceMap != null) { final address = deviceMap['address'] as String?; - + if (address != null) { // 更新设备列表 connectedDevices.removeWhere((d) => d.address == address); - + // 如果是当前设备,清除当前设备 if (currentDevice.value?.address == address) { - currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first; + currentDevice.value = + connectedDevices.isEmpty ? null : connectedDevices.first; Logger.info('当前设备已断开连接'); } } } } - + /// 处理蓝牙状态变化 void _handleBluetoothStateChange(String state) { - Logger.info('蓝牙状态变化: $state'); - + Logger.info('----蓝牙状态变化: $state'); + // 更新蓝牙状态 bluetoothState.value = state; - + if (state == 'off') { // 蓝牙关闭,清空设备信息 connectedDevices.clear(); @@ -235,11 +233,11 @@ class ClassicBluetoothService extends GetxService { }); } } - + @override void onClose() { // 取消蓝牙状态监听 _bluetoothStateListener?.cancel(); super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/data/services/db/database_helper.dart b/lib/data/services/db/database_helper.dart new file mode 100644 index 000000000..2c62e6709 --- /dev/null +++ b/lib/data/services/db/database_helper.dart @@ -0,0 +1,53 @@ +import 'dart:async'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +class DatabaseHelper { + static final DatabaseHelper _instance = DatabaseHelper._internal(); + factory DatabaseHelper() => _instance; + static Database? _database; + + DatabaseHelper._internal(); + + Future get database async { + if (_database != null) return _database!; + _database = await _initDatabase(); + return _database!; + } + + Future _initDatabase() async { + String path = join( + await getDatabasesPath(), + 'database.db', + ); + return await openDatabase(path, version: 1, onCreate: _createDB); + } + + Future _createDB(Database db, int version) async { + final batch = db.batch(); + // 商务会议助手列表 + batch.execute(''' + CREATE TABLE IF NOT EXISTS meeting ( + id INTEGER PRIMARY KEY, + title TEXT, + content TEXT, + seconds INTEGER, + address TEXT, + longitude TEXT, + latitude TEXT, + creationtime INTEGER + ) + '''); + // 商务会议助手详情内容 + batch.execute(''' + CREATE TABLE IF NOT EXISTS meetingdetails ( + id INTEGER PRIMARY KEY, + meetingid INTEGER, + audio TEXT, + content TEXT, + aisummary TEXT + ) + '''); + await batch.commit(); + } +} diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart new file mode 100644 index 000000000..5af8dbf8f --- /dev/null +++ b/lib/data/services/db/sqflite_api.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'database_helper.dart'; + +class SqfliteApi { + // 获取商务会议助手列表 + static Future getMeetingList(int page) async { + final db = await DatabaseHelper().database; + return await db.query( + 'meeting', + limit: 10, + offset: (page - 1) * 10, + orderBy: 'creationtime DESC', + ); + } + + // 获取商务会议助手详情 + static Future getMeetingDetails(id) async { + final db = await DatabaseHelper().database; + List meetingList = + await db.query('meeting', where: 'id = ?', whereArgs: [id]); + List detailsList = await db.query( + 'meetingdetails', + where: 'meetingid = ?', + whereArgs: [id], + ); + return { + ...meetingList.first, + 'details': detailsList.first + }; + } + + // 插入商务会议助手 + static Future insertMeeting(Map meeting) async { + final db = await DatabaseHelper().database; + try { + int id = 0; + await db.transaction((txn) async { + id = await txn.insert('meeting', { + 'title': meeting['title'], + 'content': meeting['firstContent'], + 'seconds': meeting['seconds'], + 'address': meeting['address'], + 'longitude': meeting['longitude'], + 'latitude': meeting['latitude'], + 'creationtime': meeting['creationTime'], + }); + await txn.insert('meetingdetails', { + 'meetingid': id, + 'audio': meeting['audio'], + 'content': meeting['content'], + 'aisummary': meeting['aisummary'], + }); + }); + return id; + } catch (e) { + Get.snackbar( + 'error'.tr, // 错误 + 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + } + + // 修改商务会议助手 + static Future editMeeting(id, Map meeting) async { + final db = await DatabaseHelper().database; + try { + await db.transaction((txn) async { + await txn.update( + 'meeting', + { + 'title': meeting['title'], + 'content': meeting['firstContent'], + 'seconds': meeting['seconds'], + 'address': meeting['address'], + 'longitude': meeting['longitude'], + 'latitude': meeting['latitude'], + }, + where: 'id = ?', + whereArgs: [id], + ); + await txn.update( + 'meetingdetails', + { + 'content': meeting['content'], + 'aisummary': meeting['aisummary'], + }, + where: 'meetingid = ?', + whereArgs: [id], + ); + }); + return true; + } catch (e) { + Get.snackbar( + 'error'.tr, // 错误 + 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + } + + // 修改商务会议助手标题 + static Future editMeetingTitle(id, title) async { + final db = await DatabaseHelper().database; + await db.update('meeting', {'title': title}, + where: 'id = ?', whereArgs: [id]); + } + + // 删除商务会议助手 + static Future deleteMeeting(id) async { + final db = await DatabaseHelper().database; + await db.transaction((txn) async { + await txn.delete('meeting', where: 'id = ?', whereArgs: [id]); + await txn + .delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]); + }); + } +} diff --git a/lib/data/services/deapsound_ai_service.dart b/lib/data/services/deapsound_ai_service.dart new file mode 100644 index 000000000..5d1e652b1 --- /dev/null +++ b/lib/data/services/deapsound_ai_service.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; + +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; + +import '../../core/utils/logger.dart'; +import 'ai_service.dart'; + +class DeapsoundAIService implements AiService { + late final String apiKey; + + DeapsoundAIService() { + final GetStorage storage = GetStorage(); + apiKey = storage.read('logintoken'); + } + + @override + Future sendMessage({ + required List> messages, + required String systemPrompt, + }) async { + return ''; + } + + @override + Stream sendMessageStream({ + required List> messages, + required String systemPrompt, + }) async* { + final client = http.Client(); + final url = Uri.parse('https://www.deapsound.com/ai/funcchat'); + + List contextsList = []; + String msg = ''; + for (var i = 0; i < messages.length; i++) { + Map item = messages[i]; + if (i < messages.length - 1) { + if (item['role'] == 'user') { + contextsList.add(item['content']); + } + } else { + msg = item['content']; + } + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': apiKey, + }; + + final requestBody = {"contexts": contextsList, "msg": msg}; + + try { + final request = http.Request('POST', url) + ..headers.addAll(headers) + ..body = jsonEncode(requestBody); + final response = await client.send(request); + + //流式处理响应数据 + await for (final chunk in response.stream + .transform(utf8.decoder) + .transform(const LineSplitter())) { + if (chunk.isEmpty) continue; + + if (chunk.startsWith('data:')) { + final jsonStr = chunk.substring(5).trim(); + + try { + final data = jsonDecode(jsonStr); + if (data['event'] == 'workflow_finished') { + break; + } + if (data['content'] != null) { + String content = data['content'] ?? ''; + if (content.isNotEmpty) { + yield content; + } + } + } catch (e) { + Logger.error('JSON解析错误: $e'); + } + } + } + } catch (e) { + Logger.error('请求异常: $e'); + } finally { + client.close(); + } + } +} diff --git a/lib/data/services/kouzi_ai_service.dart b/lib/data/services/kouzi_ai_service.dart new file mode 100644 index 000000000..a7300a320 --- /dev/null +++ b/lib/data/services/kouzi_ai_service.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import '../../core/utils/logger.dart'; +import 'ai_service.dart'; + +class KouziAIService implements AiService { + final String apiKey = + 'pat_QGDh6UGm3PaeL9GRS95JxrJ9aoU92ehMLQQ2NCre1TeBC5YeTcjpDmlz3efsJ8lh'; + final String modelId = '7481292724306657316'; + + @override + Future sendMessage({ + required List> messages, + required String systemPrompt, + }) async { + return ''; + } + + @override + Stream sendMessageStream({ + required List> messages, + required String systemPrompt, + }) async* { + final client = http.Client(); + final url = Uri.parse('https://api.coze.cn/v3/chat'); + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $apiKey', + }; + + final requestBody = { + "bot_id": modelId, + "user_id": "3422", + "additional_messages": [ + { + "content": messages.last['content'], + "content_type": "text", + "role": "user", + "type": "question", + }, + ], + "shortcut_command": {"command_id": "0"}, + "stream": true, + }; + + try { + final request = http.Request('POST', url) + ..headers.addAll(headers) + ..body = jsonEncode(requestBody); + final response = await client.send(request); + final regex = RegExp(r'(?<=[。!?;:.:;?!\n])'); + + String allContent = ''; + //流式处理响应数据 + await for (final chunk in response.stream + .transform(utf8.decoder) + .transform(const LineSplitter())) { + if (chunk.isEmpty) continue; + + if (chunk.startsWith('data:')) { + final jsonStr = chunk.substring(5).trim(); + if (jsonStr == '[DONE]') { + break; + } + + try { + final data = jsonDecode(jsonStr); + if (data['type'] == 'answer') { + String content = data['content'] ?? ''; + if (content.isNotEmpty && allContent != content) { + allContent += content; + List splitText = + content.split(regex).where((s) => s.isNotEmpty).toList(); + for (var element in splitText) { + yield element; + } + } + } + } catch (e) { + Logger.error('JSON解析错误: $e'); + } + } + } + } catch (e) { + Logger.error('请求异常: $e'); + } finally { + client.close(); + } + } +} diff --git a/lib/data/services/login_service.dart b/lib/data/services/login_service.dart new file mode 100644 index 000000000..d09ce9672 --- /dev/null +++ b/lib/data/services/login_service.dart @@ -0,0 +1,36 @@ +import '../../modules/login/controllers/weixinLogin.dart'; +import 'package:firebase_core/firebase_core.dart'; +import '/firebase_options.dart'; + +class LoginService { + LoginService() { + //微信 + FlWX().register( + appSecret: '4cfde33bc22b6cc6b1f8b225dd97d255', + appId: 'wx5bbb7be9324f3e2d', + params: FlWXCallbackParams(onHttp: (url) async { + return ''; + })); + + _initializeFirebase(); + } + + Future _initializeFirebase() async { + try { + // 尝试获取已存在的实例 + Firebase.app('DeepSound'); + print('已存在的 Firebase 实例被重用'); + } catch (e) { + try { + // 如果不存在则初始化新实例 + await Firebase.initializeApp( + name: 'DeepSound', + options: DefaultFirebaseOptions.currentPlatform, + ); + print('Firebase 初始化成功'); + } catch (e) { + print('Firebase 初始化失败: $e'); + } + } + } +} diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart new file mode 100644 index 000000000..11c361580 --- /dev/null +++ b/lib/data/services/network/api.dart @@ -0,0 +1,49 @@ +import 'dio_manager.dart'; +import 'nw_method.dart'; + +class Api { + // 登录 + static login([params]) { + print("请求后台数据login params=${params}"); + return DioManager().request( + NWMethod.post, + '/api/home/user_sgin', + params: params, + ); + } + + // 获取验证码 + static getCode([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_verification', + params: params, + ); + } + + static tokenlogin([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_getinfo', + params: params, + ); + } + + // 获取阿里token + static getAliToken([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/auth_alitoken', + params: params, + ); + } + + //更新用户设置 + static updateUserSetting([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_setting', + params: params, + ); + } +} diff --git a/lib/data/services/network/auth_interceptor.dart b/lib/data/services/network/auth_interceptor.dart new file mode 100644 index 000000000..2933d5d62 --- /dev/null +++ b/lib/data/services/network/auth_interceptor.dart @@ -0,0 +1,115 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart' hide Response; + +import 'package:get_storage/get_storage.dart'; +import 'entity/base_entity.dart'; + +class AuthInterceptor extends Interceptor { + final GetStorage _storage = GetStorage(); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + var jsonString = _storage.read("logintoken"); + options.headers['Authorization'] = jsonString; + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) async { + BaseEntity entity = BaseEntity.fromJson(response.data); + if (entity.code == 0) { + response.data = entity.data ?? {}; + handler.resolve(response); + } else { + response.data = null; + handler.next(response); + Get.snackbar( + 'error'.tr, // 错误 + entity.message ?? 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + switch (err.type) { + case DioExceptionType.cancel: + { + Get.snackbar( + 'error'.tr, // 错误 + 'requestCancellation'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + case DioExceptionType.connectionTimeout: + { + Get.snackbar( + 'error'.tr, // 错误 + 'connectionTimeout'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + case DioExceptionType.sendTimeout: + { + Get.snackbar( + 'error'.tr, // 错误 + 'requestTimeout'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + case DioExceptionType.receiveTimeout: + { + Get.snackbar( + 'error'.tr, // 错误 + 'responseTimeout'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + case DioExceptionType.badResponse: + { + try { + String? errMsg = err.response?.statusMessage; + Get.snackbar( + 'error'.tr, // 错误 + errMsg ?? 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } on Exception catch (_) { + Get.snackbar( + 'error'.tr, // 错误 + 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + } + default: + { + Get.snackbar( + 'error'.tr, // 错误 + err.message ?? 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + } + handler.next(err); + } +} diff --git a/lib/data/services/network/dio_manager.dart b/lib/data/services/network/dio_manager.dart new file mode 100644 index 000000000..0872436ec --- /dev/null +++ b/lib/data/services/network/dio_manager.dart @@ -0,0 +1,45 @@ +import 'package:dio/dio.dart'; + +import 'auth_interceptor.dart'; +import 'nw_api.dart'; +import 'nw_method.dart'; + +class DioManager { + static final DioManager _shared = DioManager._internal(); + factory DioManager() => _shared; + + late Dio dio; + + DioManager._internal() { + BaseOptions options = BaseOptions( + baseUrl: NWApi.baseApi, + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + ); + + dio = Dio(options); +//fdp + dio.interceptors.add(AuthInterceptor()); + } + + // 请求,返回参数为 T + // method:请求方法,NWMethod.POST等 + // path:请求地址 + // params:请求参数 + Future request( + NWMethod method, + String path, { + Map? params, + Map? queryParameters, + }) async { + Response response = await dio.request( + path, + data: params, + queryParameters: queryParameters, + options: Options(method: nwMethodValues[method]), + ); + return response.data; + } +} diff --git a/lib/data/services/network/entity/base_entity.dart b/lib/data/services/network/entity/base_entity.dart new file mode 100644 index 000000000..fde0289ef --- /dev/null +++ b/lib/data/services/network/entity/base_entity.dart @@ -0,0 +1,17 @@ +import 'entity_factory.dart'; + +class BaseEntity { + int? code; + String? message; + T? data; + + BaseEntity({this.code, this.message, this.data}); + + factory BaseEntity.fromJson(json) { + return BaseEntity( + code: json["code"], + message: json["msg"], + data: EntityFactory.generateOBJ(json["data"]), + ); + } +} diff --git a/lib/data/services/network/entity/entity_factory.dart b/lib/data/services/network/entity/entity_factory.dart new file mode 100644 index 000000000..e11cb6d3c --- /dev/null +++ b/lib/data/services/network/entity/entity_factory.dart @@ -0,0 +1,9 @@ +class EntityFactory { + static T? generateOBJ(json) { + if (json == null) { + return null; + } else { + return json as T; + } + } +} diff --git a/lib/data/services/network/nw_api.dart b/lib/data/services/network/nw_api.dart new file mode 100644 index 000000000..875a166b6 --- /dev/null +++ b/lib/data/services/network/nw_api.dart @@ -0,0 +1,4 @@ +class NWApi { + // static const baseApi = "https://www.deapsound.com"; + static const baseApi = "https://www.deapsound.com"; +} diff --git a/lib/data/services/network/nw_method.dart b/lib/data/services/network/nw_method.dart new file mode 100644 index 000000000..f79ca96e3 --- /dev/null +++ b/lib/data/services/network/nw_method.dart @@ -0,0 +1,8 @@ +enum NWMethod { get, post, delete, put } + +const nwMethodValues = { + NWMethod.get: "get", + NWMethod.post: "post", + NWMethod.delete: "delete", + NWMethod.put: "put" +}; diff --git a/lib/data/services/speech_factory.dart b/lib/data/services/speech_factory.dart index 65281520f..95f3165ee 100644 --- a/lib/data/services/speech_factory.dart +++ b/lib/data/services/speech_factory.dart @@ -1,4 +1,3 @@ - import 'package:get/get.dart'; import 'asr_service.dart'; import 'tts_service.dart'; @@ -13,62 +12,58 @@ import '../../core/utils/logger.dart'; enum SpeechServiceType { /// Azure 语音服务 azure, - + /// Flutter 本地语音服务 flutter, } /// 语音服务工厂类 -/// +/// /// 用于根据配置信息获取不同的TTS和ASR实现 class SpeechFactory extends GetxService { static SpeechFactory get to => Get.find(); - /// 当前使用的语音服务类型 SpeechServiceType _currentServiceType = SpeechServiceType.azure; - + /// 获取当前使用的语音服务类型 SpeechServiceType get currentServiceType => _currentServiceType; - + /// 设置语音服务类型 void setServiceType(SpeechServiceType type) { if (_currentServiceType != type) { _currentServiceType = type; Logger.info('语音服务类型已切换为: ${type.toString().split('.').last}'); - + // 重新注册服务 _registerServices(); } } - + /// 注册语音服务 void _registerServices() { // 注销现有服务 if (Get.isRegistered()) { Get.delete(force: true); } - + if (Get.isRegistered()) { Get.delete(force: true); } - + // 根据类型注册新服务 switch (_currentServiceType) { case SpeechServiceType.azure: - - Get.lazyPut(() => AzureAsrService()); - Get.lazyPut(() => AzureTtsService()); - + Get.lazyPut(() => AzureAsrService()); + Get.lazyPut(() => AzureTtsService()); break; - case SpeechServiceType.flutter: Get.lazyPut(() => FlutterAsrService()); Get.lazyPut(() => FlutterTtsService()); break; } } - + /// 初始化语音服务 Future initialize({SpeechServiceType? initialType}) async { try { @@ -76,24 +71,25 @@ class SpeechFactory extends GetxService { if (initialType != null) { _currentServiceType = initialType; } - + // 注册服务 _registerServices(); - + // // 初始化服务 // final asrService = Get.find(); // final ttsService = Get.find(); - + // await asrService.initialize(); // await ttsService.initialize(); - - Logger.info('语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); + + Logger.info( + '语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); } catch (e) { Logger.error('语音服务初始化失败: $e'); rethrow; } } - + /// 获取ASR服务 AsrService getAsrService() { if (!Get.isRegistered()) { @@ -101,7 +97,7 @@ class SpeechFactory extends GetxService { } return Get.find(); } - + /// 获取TTS服务 TtsService getTtsService() { if (!Get.isRegistered()) { @@ -109,4 +105,4 @@ class SpeechFactory extends GetxService { } return Get.find(); } -} \ No newline at end of file +} diff --git a/lib/data/services/speech_impl/voice_clone_tts_service.dart b/lib/data/services/speech_impl/voice_clone_tts_service.dart new file mode 100644 index 000000000..2f57939a9 --- /dev/null +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -0,0 +1,393 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import '../../../core/utils/logger.dart'; +import '../tts_service.dart'; +import 'volcano_tts_api_service.dart'; + +/// 声音复刻服务,负责基本的文本转语音功能 +/// +/// 使用讯飞声音复刻实现TtsService接口 +class VoiceCloneTtsService extends GetxService implements TtsService { + final String hostUrl = "wss://cn-huabei-1.xf-yun.com/v1/private/voice_clone"; + + final String appId = '43d87d22'; + final String apiKey = '2bd01ce06ac35957fc0c082f9b6284ec'; + final String apiSecret = 'ODU3OTUyYjhjMjY5NDhkMmMxYWY2Zjlm'; + + WebSocketChannel? _channel; + final AudioPlayer _player = AudioPlayer(); + late ConcatenatingAudioSource _playlist; + + // 当前语音 + final RxString _currentVoice = ''.obs; + + @override + String get currentVoice => _currentVoice.value; + + // 播放状态 + final RxBool _isSpeaking = false.obs; + + @override + bool get isSpeaking => _isSpeaking.value; + + // 事件流控制器 + final StreamController _eventController = + StreamController.broadcast(); + + @override + Stream get onEvent => _eventController.stream; + + bool _isInitialized = false; + String _resId = ''; + + // 语音合成队列 + final List _textQueue = []; + bool _isProcessingQueue = false; + + BytesBuilder _audioBytes = BytesBuilder(); + + Completer _completer = Completer(); + + final isEnabled = true.obs; + + // 流式文本缓冲区 + String _streamBuffer = ''; + + @override + Future initialize({ + List? supportedLanguages, + }) async { + if (_isInitialized) return true; + try { + final GetStorage storage = GetStorage(); + String timbre = storage.read('timbre'); + _resId = timbre; + _playlist = ConcatenatingAudioSource( + useLazyPreparation: true, + children: [], + ); + return true; + } catch (e) { + Logger.error('TTS初始化失败: $e'); + return false; + } + } + + @override + Future setVoice(String voiceName) async { + if (!_isInitialized) await initialize(); + try { + Logger.info('设置TTS语音: $voiceName'); + return true; + } catch (e) { + Logger.error('设置TTS语音失败: $e'); + return false; + } + } + + // 语音合成 + Future _speechSynthesis(String text) async { + await _tryConnecting(); + _channel?.sink.add( + json.encode({ + "header": {"app_id": appId, "status": 2, "res_id": _resId}, + "parameter": { + "tts": { + "vcn": "x5_clone", + "audio": {"encoding": "lame", "sample_rate": 16000} + } + }, + "payload": { + "text": { + "status": 2, + "seq": 0, + "text": base64Encode(utf8.encode(text)) + } + }, + }), + ); + } + + Future _tryConnecting() async { + try { + final url = _geturl(); + _channel = WebSocketChannel.connect(Uri.parse(url)); + _channel?.stream.timeout(const Duration(seconds: 10)); //设置超时时间 + await _channel?.ready; + _channel?.stream.listen( + (message) { + _handle(message); + }, + onError: (error) { + Logger.error('连接失败: $error'); + }, + onDone: () { + Logger.error('WebSocket 连接已关闭!'); + }, + cancelOnError: true, + ); + } catch (e) { + Logger.error('初始化异常: $e'); + } + } + + String _geturl() { + // 解析 URL + final uri = Uri.parse(hostUrl); + + // 获取当前时间并格式化为 RFC1123 格式 + final date = HttpDate.format(DateTime.now().toUtc()); + + // 参与签名的字段 + final signString = [ + "host: ${uri.host}", + "date: $date", + "GET ${uri.path} HTTP/1.1", + ]; + + // 拼接签名字符串 + final sgin = signString.join("\n"); + + // 使用 HMAC-SHA256 签名 + final key = utf8.encode(apiSecret); + final bytes = utf8.encode(sgin); + final hmacSha256 = Hmac(sha256, key); + final digest = hmacSha256.convert(bytes); + final sha = base64Encode(digest.bytes); + + // 构建请求参数 + final authUrl = + 'api_key="$apiKey", algorithm="hmac-sha256", headers="host date request-line", signature="$sha"'; + + // 将请求参数使用 base64 编码 + final authorization = base64Encode(utf8.encode(authUrl)); + + // 构建查询参数 + final queryParams = { + "host": uri.host, + "date": date, + "authorization": authorization, + }; + + // 将查询参数编码并拼接到 URL 后面 + final callurl = + Uri.parse(hostUrl).replace(queryParameters: queryParams).toString(); + + return callurl; + } + + void _handle(data) async { + final result = jsonDecode(data); + + int code = result['header']['code']; + if (code == 0) { + int status = result['header']['status']; + if (status == 1 || status == 2) { + Uint8List audio = base64Decode(result['payload']['audio']['audio']); + _audioBytes.add(audio); + } + if (status == 2) { + final newSource = BytesAudioSource(_audioBytes.toBytes()); + await _playlist.add(newSource); + if (!_isSpeaking.value) { + _audioPlay(); + } + _completer.complete(); + _completer = Completer(); + } + } + } + + void _audioPlay() async { + _isSpeaking.value = true; + // 发送开始事件 + _eventController.add(TtsEvent(type: TtsEventType.started)); + await _player.setAudioSource(_playlist); + await _player.play(); + } + + @override + Future speakOnce(String text) async { + if (!isEnabled.value || text.isEmpty) return false; + + try { + _audioBytes.clear(); + await _speechSynthesis(text); + await _completer.future; + + return true; + } catch (e) { + _isSpeaking.value = false; + Logger.error('语音合成失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('语音合成失败: $e')); + + return false; + } + } + + @override + Future speakStream(String text) async { + if (!isEnabled.value || text.isEmpty) return false; + + try { + // 添加文本到缓冲区 + _streamBuffer += text; + + // 如果缓冲区为空,直接返回 + if (_streamBuffer.isEmpty) return true; + + // 使用正则表达式匹配句子,包括结束符号 + // 匹配任意字符,直到遇到句子结束符号 + final sentenceRegex = RegExp( + r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])'); + + bool hasProcessed = false; + + // 查找所有完整句子 + final matches = sentenceRegex.allMatches(_streamBuffer); + final List sentences = []; + int lastMatchEnd = 0; + + for (final match in matches) { + // 提取完整句子(包含结束符号) + final sentence = match.group(1)?.trim(); + if (sentence != null && sentence.isNotEmpty) { + sentences.add(sentence); + lastMatchEnd = match.end; + } + } + + // 处理找到的句子 + for (final sentence in sentences) { + _textQueue.add(sentence); + hasProcessed = true; + } + + // 更新缓冲区,只保留未完成的部分 + if (lastMatchEnd > 0) { + _streamBuffer = _streamBuffer.substring(lastMatchEnd); + } + + // 如果处理了文本并且队列未在处理中,开始处理队列 + if (hasProcessed && !_isProcessingQueue) { + _processQueue(); + } + + return true; + } catch (e) { + Logger.error('处理流式文本失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('处理流式文本失败: $e')); + + return false; + } + } + + @override + Future flushStream() async { + if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; + + try { + // 处理缓冲区中可能的完整句子 + await speakStream(''); + + // 如果缓冲区仍有剩余文本,将其添加到播放队列 + if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { + _textQueue.add(_streamBuffer); + + // 清空缓冲区 + _streamBuffer = ''; + + // 如果队列未在处理中,开始处理队列 + if (!_isProcessingQueue) { + _processQueue(); + } + } + + Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); + + return true; + } catch (e) { + Logger.error('刷新TTS流失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('刷新TTS流失败: $e')); + + return false; + } + } + + /// 处理语音合成队列 + Future _processQueue() async { + if (_textQueue.isEmpty || _isProcessingQueue) return; + + _isProcessingQueue = true; + + try { + while (_textQueue.isNotEmpty) { + if (!isEnabled.value) { + _textQueue.clear(); + break; + } + + final item = _textQueue.removeAt(0); + + // 使用speakOnce播放当前项 + await speakOnce(item); + } + } catch (e) { + Logger.error('处理语音队列出错: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('处理语音队列出错: $e')); + } finally { + _isProcessingQueue = false; + } + } + + @override + Future stop() async { + try { + _textQueue.clear(); + await _player.stop(); + _playlist.clear(); + _audioBytes = BytesBuilder(); + if (_isSpeaking.value) { + _isSpeaking.value = false; + // 发送取消事件 + _eventController.add(TtsEvent(type: TtsEventType.canceled)); + } + return true; + } catch (e) { + Logger.error('停止语音合成出错: $e'); + // 发送错误事件 + _eventController.add(TtsEvent.error('停止语音合成出错: $e')); + return false; + } + } + + @override + Future dispose() async { + if (!_isInitialized) return; + try { + await stop(); + _player.dispose(); + // 关闭事件流控制器 + await _eventController.close(); + _isInitialized = false; + } catch (e) { + Logger.error('释放资源失败: $e'); + } + } +} diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 7a2d6f457..bab3790ef 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -15,61 +15,61 @@ import 'package:record/record.dart'; import '../asr_service.dart'; /// Volcano语音识别服务 -/// +/// /// 负责与火山引擎实时语音识别服务的通信 class VolcanoAsrApiService implements AsrService { static final VolcanoAsrApiService to = Get.find(); - + // API配置 late final String _appKey; late final String _secretKey; late final String _serviceId; late final String _hostUrl; final String _path = '/ws/v1/asr'; - + // 请求相关参数 final String _algorithm = 'hmac-sha1'; final Uuid _uuid = const Uuid(); - + // 录音配置 final int _sampleRate = 16000; final int _bitsPerSample = 16; final int _channels = 1; final String _encoding = 'pcm'; - + // 连接相关 WebSocketChannel? _channel; Completer? _connectionCompleter; StreamSubscription? _connectionSubscription; Timer? _heartbeatTimer; - + // 语音录音 final Record? _recorder = null; // TODO: 实现正确的Record实例化 Timer? _audioTimer; bool _isRecording = false; - + // 状态控制 bool _isInitialized = false; bool _isConnecting = false; bool _isContinuousRecognitionActive = false; - + // 识别结果控制 bool _isRecognitionEndRequested = false; String _currentSessionId = ''; final List _supportedLanguages = ['zh-CN', 'en-US']; String _selectedLanguage = 'zh-CN'; - + // 结果流 StreamController? _recognitionController; - + // 事件计数器(用于调试) int _sendPacketCount = 0; int _receivePacketCount = 0; - + // AsrService接口实现 @override List get supportedLanguages => _supportedLanguages; - + @override Future initialize({ List? supportedLanguages, @@ -79,21 +79,21 @@ class VolcanoAsrApiService implements AsrService { if (_isInitialized) { await dispose(); } - + _isInitialized = false; - + // 从环境变量加载配置 _loadConfig(); - + // 设置支持的语言 if (supportedLanguages != null && supportedLanguages.isNotEmpty) { _supportedLanguages.clear(); _supportedLanguages.addAll(supportedLanguages); } - + // 初始化录音机 await _initRecorder(); - + _isInitialized = true; Logger.info('火山语音API已初始化'); return true; @@ -102,19 +102,19 @@ class VolcanoAsrApiService implements AsrService { return false; } } - + @override Future recognizeOnce() async { try { if (!_isInitialized) { await initialize(); } - + final resultCompleter = Completer(); - + // 开始连续识别流程 final stream = await startContinuousRecognition(); - + // 监听结果 StreamSubscription? subscription; subscription = stream.listen((event) { @@ -138,7 +138,7 @@ class VolcanoAsrApiService implements AsrService { resultCompleter.complete(RecognitionEvent.error('未收到识别结果')); } }); - + // 设置超时 Timer(const Duration(seconds: 30), () { if (!resultCompleter.isCompleted) { @@ -147,104 +147,104 @@ class VolcanoAsrApiService implements AsrService { stopContinuousRecognition(); } }); - + return resultCompleter.future; } catch (e) { Logger.error('单次语音识别失败: $e'); return RecognitionEvent.error(e.toString()); } } - + @override Future> startContinuousRecognition() async { if (!_isInitialized) { await initialize(); } - + if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } - + try { // 创建结果控制器 _recognitionController = StreamController.broadcast(); - + // 连接WebSocket await _connectWebSocket(); - + // 开始录音 await _startRecording(); - + _isContinuousRecognitionActive = true; _isRecognitionEndRequested = false; - + Logger.info('连续语音识别已开始'); - + return _recognitionController!.stream; } catch (e) { Logger.error('开始连续语音识别失败: $e'); await _cleanupResources(); - + // 返回一个包含错误的流 final errorController = StreamController.broadcast(); errorController.add(RecognitionEvent.error(e.toString())); errorController.close(); - + return errorController.stream; } } - + @override Future stopContinuousRecognition() async { if (!_isContinuousRecognitionActive) { return true; } - + try { _isRecognitionEndRequested = true; - + // 先停止录音,以确保不会再发送新的音频数据 await _stopRecording(); - + // 发送结束指令 await _sendEndSignal(); - + // 等待服务器处理完所有已发送的数据 await Future.delayed(const Duration(milliseconds: 500)); - + // 清理资源 await _cleanupResources(); - + _isContinuousRecognitionActive = false; Logger.info('连续语音识别已停止'); - + return true; } catch (e) { Logger.error('停止连续语音识别失败: $e'); - + // 确保资源被清理 await _cleanupResources(); _isContinuousRecognitionActive = false; - + return false; } } - + @override bool isContinuousRecognitionActive() { return _isContinuousRecognitionActive; } - + @override Future dispose() async { try { if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } - + // 确保资源被清理 await _cleanupResources(); - + _isInitialized = false; Logger.info('火山语音API资源已释放'); } catch (e) { @@ -253,31 +253,32 @@ class VolcanoAsrApiService implements AsrService { _isInitialized = false; } } - + /// 从环境变量加载配置 void _loadConfig() { _appKey = dotenv.env['VOLCANO_APP_KEY'] ?? ''; _secretKey = dotenv.env['VOLCANO_SECRET_KEY'] ?? ''; _serviceId = dotenv.env['VOLCANO_SERVICE_ID'] ?? ''; _hostUrl = dotenv.env['VOLCANO_HOST_URL'] ?? 'ws-api-asr.volces.com'; - + if (_appKey.isEmpty || _secretKey.isEmpty || _serviceId.isEmpty) { - throw Exception('未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID'); + throw Exception( + '未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID'); } } - + /// 初始化录音机 Future _initRecorder() async { try { // TODO: 正确实现录音机初始化 // _recorder = Record(); - + // 检查和请求权限 // final hasPermission = await _recorder!.hasPermission(); // if (!hasPermission) { // throw Exception('没有录音权限'); // } - + Logger.info('录音机初始化成功'); } catch (e) { // _recorder = null; @@ -285,7 +286,7 @@ class VolcanoAsrApiService implements AsrService { rethrow; } } - + /// 连接WebSocket Future _connectWebSocket() async { if (_isConnecting) { @@ -294,20 +295,20 @@ class VolcanoAsrApiService implements AsrService { return _connectionCompleter!.future; } } - + _connectionCompleter = Completer(); _isConnecting = true; - + try { // 关闭现有连接 await _closeWebSocket(); - + // 生成新的会话ID _currentSessionId = _uuid.v4(); - + // 构建URL和请求头 final (url, headers) = _buildConnectionParams(); - + // 创建WebSocket连接 if (kIsWeb) { _channel = WebSocketChannel.connect(Uri.parse(url)); @@ -317,42 +318,42 @@ class VolcanoAsrApiService implements AsrService { headers: headers, ); } - + // 处理WebSocket消息 _connectionSubscription = _channel!.stream.listen( _handleWebSocketMessage, onError: _handleWebSocketError, onDone: _handleWebSocketDone, ); - + // 开始心跳 _startHeartbeat(); - + // 发送开始识别指令 await _sendStartCommand(); - + _isConnecting = false; _connectionCompleter!.complete(); - + Logger.info('WebSocket连接成功'); } catch (e) { _isConnecting = false; if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { _connectionCompleter!.completeError(e); } - + Logger.error('连接WebSocket失败: $e'); rethrow; } - + return _connectionCompleter!.future; } - + /// 构建连接参数 (String, Map?) _buildConnectionParams() { final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); final nonce = _generateNonce(); - + // 构建表单(用于签名和查询参数) final form = { 'algorithm': _algorithm, @@ -362,54 +363,54 @@ class VolcanoAsrApiService implements AsrService { 'session_id': _currentSessionId, 'timestamp': timestamp.toString(), }; - + // 计算签名 final signature = _calculateSignature(form); - + // 添加签名到表单 form['signature'] = signature; - + // 构建URL查询参数 final queryString = form.entries .map((e) => '${e.key}=${Uri.encodeComponent(e.value)}') .join('&'); - + // 构建完整URL final url = 'wss://$_hostUrl$_path?$queryString'; - + // 构建请求头 final headers = { - 'User-Agent': 'Deep Voice App/1.0.0', + 'User-Agent': 'DeepSound App/1.0.0', }; - + return (url, kIsWeb ? null : headers); } - + /// 生成随机数 String _generateNonce() { final random = Random(); final randoms = List.generate(16, (_) => random.nextInt(256)); return base64Url.encode(randoms).substring(0, 16); } - + /// 计算签名 String _calculateSignature(Map form) { // 按键排序 final sortedKeys = form.keys.toList()..sort(); - + // 构建待签名字符串 final signString = sortedKeys.map((key) => '$key=${form[key]}').join(''); - + // 使用HMAC-SHA1进行签名 final key = utf8.encode(_secretKey); final bytes = utf8.encode(signString); final hmacSha1 = Hmac(sha1, key); final digest = hmacSha1.convert(bytes); - + // 返回Base64编码的签名 return base64.encode(digest.bytes); } - + /// 开始心跳定时 void _startHeartbeat() { _heartbeatTimer?.cancel(); @@ -417,7 +418,7 @@ class VolcanoAsrApiService implements AsrService { _sendHeartbeat(); }); } - + /// 发送心跳包 void _sendHeartbeat() { if (_channel != null && _isContinuousRecognitionActive) { @@ -426,18 +427,18 @@ class VolcanoAsrApiService implements AsrService { 'type': 'heartbeat', 'sequence': _sendPacketCount++, }; - + _channel!.sink.add(jsonEncode(heartbeatMsg)); } catch (e) { Logger.error('发送心跳包失败: $e'); } } } - + /// 发送开始识别指令 Future _sendStartCommand() async { if (_channel == null) return; - + try { final startCommand = { 'type': 'start', @@ -454,7 +455,7 @@ class VolcanoAsrApiService implements AsrService { 'max_seconds': 60, }, }; - + _channel!.sink.add(jsonEncode(startCommand)); Logger.info('已发送开始识别指令'); } catch (e) { @@ -462,7 +463,7 @@ class VolcanoAsrApiService implements AsrService { rethrow; } } - + /// 开始录音 Future _startRecording() async { // TODO: 实现正确的录音开始逻辑 @@ -497,12 +498,12 @@ class VolcanoAsrApiService implements AsrService { rethrow; } */ - + // 临时实现,防止错误 _isRecording = true; Logger.info('录音已开始(模拟)'); } - + /// 捕获并发送音频数据 Future _captureAndSendAudio() async { // TODO: 实现正确的音频捕获逻辑 @@ -545,46 +546,46 @@ class VolcanoAsrApiService implements AsrService { } */ } - + /// 发送音频数据 Future _sendAudioData(Uint8List data) async { if (_channel == null || data.isEmpty) return; - + try { final audioMessage = { 'type': 'audio', 'sequence': _sendPacketCount++, 'audio': base64.encode(data), }; - + _channel!.sink.add(jsonEncode(audioMessage)); } catch (e) { Logger.error('发送音频数据失败: $e'); } } - + /// 发送结束信号 Future _sendEndSignal() async { if (_channel == null) return; - + try { final endCommand = { 'type': 'end', 'sequence': _sendPacketCount++, }; - + _channel!.sink.add(jsonEncode(endCommand)); Logger.info('已发送结束指令'); } catch (e) { Logger.error('发送结束指令失败: $e'); } } - + /// 停止录音 Future _stopRecording() async { _audioTimer?.cancel(); _audioTimer = null; - + // TODO: 实现正确的录音停止逻辑 /* if (_isRecording && _recorder != null) { @@ -597,64 +598,65 @@ class VolcanoAsrApiService implements AsrService { } } */ - + // 临时实现,防止错误 _isRecording = false; Logger.info('录音已停止(模拟)'); } - + /// 处理WebSocket消息 void _handleWebSocketMessage(dynamic message) { try { _receivePacketCount++; - + if (message is String) { final Map data = jsonDecode(message); final String type = data['type'] as String? ?? ''; - + switch (type) { case 'result': _handleResultMessage(data); break; - + case 'start_result': _handleStartResultMessage(data); break; - + case 'end_result': _handleEndResultMessage(data); break; - + case 'error': _handleErrorMessage(data); break; - + case 'heartbeat_result': // 心跳响应,不需要特殊处理 break; - + default: Logger.info('收到未知类型的消息: $type'); } } } catch (e) { Logger.error('处理WebSocket消息失败: $e'); - + if (_recognitionController != null && !_recognitionController!.isClosed) { _recognitionController!.addError('处理消息失败: $e'); } } } - + /// 处理结果消息 void _handleResultMessage(Map data) { try { - final Map? result = data['result'] as Map?; + final Map? result = + data['result'] as Map?; if (result == null) return; - + final bool isEnd = result['is_end'] as bool? ?? false; final String text = result['text'] as String? ?? ''; - + if (_recognitionController != null && !_recognitionController!.isClosed) { if (isEnd) { // 最终结果 @@ -674,11 +676,11 @@ class VolcanoAsrApiService implements AsrService { Logger.error('处理结果消息失败: $e'); } } - + /// 处理开始结果消息 void _handleStartResultMessage(Map data) { final int code = data['code'] as int? ?? -1; - + if (code == 0) { // 开始成功 if (_recognitionController != null && !_recognitionController!.isClosed) { @@ -689,19 +691,20 @@ class VolcanoAsrApiService implements AsrService { } else { // 开始失败 final String message = data['message'] as String? ?? '未知错误'; - + Logger.error('语音识别开始失败: [$code] $message'); - + if (_recognitionController != null && !_recognitionController!.isClosed) { - _recognitionController!.add(RecognitionEvent.error('开始失败: [$code] $message')); + _recognitionController! + .add(RecognitionEvent.error('开始失败: [$code] $message')); } } } - + /// 处理结束结果消息 void _handleEndResultMessage(Map data) { final int code = data['code'] as int? ?? -1; - + if (code == 0) { // 结束成功 Logger.info('语音识别成功结束'); @@ -710,73 +713,75 @@ class VolcanoAsrApiService implements AsrService { final String message = data['message'] as String? ?? '未知错误'; Logger.error('语音识别结束失败: [$code] $message'); } - + // 无论成功还是失败,通知会话已结束 if (_recognitionController != null && !_recognitionController!.isClosed) { _recognitionController!.add(RecognitionEvent( type: RecognitionEventType.sessionStopped, )); } - + // 清理资源 _cleanupResources(); } - + /// 处理错误消息 void _handleErrorMessage(Map data) { final int code = data['code'] as int? ?? -1; final String message = data['message'] as String? ?? '未知错误'; - + Logger.error('收到错误消息: [$code] $message'); - + if (_recognitionController != null && !_recognitionController!.isClosed) { _recognitionController!.add(RecognitionEvent.error('[$code] $message')); } } - + /// 处理WebSocket错误 void _handleWebSocketError(dynamic error) { Logger.error('WebSocket错误: $error'); - + if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { _connectionCompleter!.completeError(error); } - + if (_recognitionController != null && !_recognitionController!.isClosed) { _recognitionController!.addError(error); } - + // 清理资源 _cleanupResources(); } - + /// 处理WebSocket连接关闭 void _handleWebSocketDone() { Logger.info('WebSocket连接已关闭'); - + if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { _connectionCompleter!.completeError('WebSocket连接已关闭'); } - + // 通知会话已结束 - if (_recognitionController != null && !_recognitionController!.isClosed && !_isRecognitionEndRequested) { + if (_recognitionController != null && + !_recognitionController!.isClosed && + !_isRecognitionEndRequested) { _recognitionController!.add(RecognitionEvent( type: RecognitionEventType.sessionStopped, )); } - + // 清理资源 _cleanupResources(); } - + /// 关闭WebSocket连接 Future _closeWebSocket() async { _connectionSubscription?.cancel(); _connectionSubscription = null; - + _heartbeatTimer?.cancel(); _heartbeatTimer = null; - + if (_channel != null) { try { _channel!.sink.close(); @@ -786,22 +791,22 @@ class VolcanoAsrApiService implements AsrService { } } } - + /// 清理所有资源 Future _cleanupResources() async { // 停止录音 await _stopRecording(); - + // 关闭WebSocket await _closeWebSocket(); - + // 关闭结果控制器 if (_recognitionController != null && !_recognitionController!.isClosed) { await _recognitionController!.close(); _recognitionController = null; } - + _isContinuousRecognitionActive = false; _isRecognitionEndRequested = false; } -} \ No newline at end of file +} diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart new file mode 100644 index 000000000..1201f7c66 --- /dev/null +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -0,0 +1,272 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:record/record.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import '../../../core/utils/logger.dart'; +import 'package:get/get.dart'; +import '../asr_service.dart'; + +/// 讯飞 语音识别服务 +/// +class XunfeiAsrService extends GetxService implements AsrService { + final String baseUrl = 'wss://rtasr.xfyun.cn/v1/ws'; + final int _chunkSize = 1280; // 每次发送的数据大小 + + final String appId = '43d87d22'; + final String apiKey = '83f063755e8d342670002f50a4471d42'; + + AudioRecorder? recorder; + Timer? _timer; + + WebSocketChannel? _channel; + bool isConnected = false; //是否链接 + + bool _isInitialized = false; + + final List _defaultSupportedLanguages = ['zh-CN']; + String _languageStr = 'cn'; + + // 连续识别相关 + bool _isContinuousRecognitionActive = false; + StreamController? _eventStreamController; + + @override + List get supportedLanguages => _defaultSupportedLanguages; + + Uint8List _buff = Uint8List(0); //音频缓存区 + BytesBuilder _originaBytes = BytesBuilder(); //原始音频 + bool isRecording = false; + + @override + Future initialize({ + List? supportedLanguages, + }) async { + try { + final List languages = + supportedLanguages ?? _defaultSupportedLanguages; + + String languagetoStr = languages.first.split('-')[1].toLowerCase(); + _languageStr = languagetoStr; + + _isInitialized = true; + Logger.info('Azure 语音识别服务初始化成功'); + return true; + } catch (e) { + Logger.error('Azure 语音识别服务初始化失败: $e'); + _isInitialized = false; + rethrow; + } + } + + @override + Future recognizeOnce() async { + if (!_isInitialized) { + await initialize(); + } + + try { + throw Exception('无效的识别结果格式'); + } catch (e) { + Logger.error('语音识别失败: $e'); + return RecognitionEvent.error(e.toString()); + } + } + + @override + Future> startContinuousRecognition() async { + if (!_isInitialized) { + await initialize(); + } + + try { + _eventStreamController = StreamController.broadcast(); + + await recorder?.dispose(); + recorder = AudioRecorder(); + Stream? stream = await recorder?.startStream( + const RecordConfig( + sampleRate: 16000, + encoder: AudioEncoder.pcm16bits, + numChannels: 1, + ), + ); + isRecording = true; + stream?.listen((data) { + if (isRecording) { + _buff = _appendToBuffer(data); + _originaBytes.add(data); + } + }); + await _tryConnecting(); + _timer = Timer.periodic(const Duration(milliseconds: 40), (timer) async { + //每40毫秒读取一次数据 + var frame = _getAudioData(); + if (frame.isNotEmpty) { + _channel?.sink.add(frame); + } + }); + + _isContinuousRecognitionActive = true; + return _eventStreamController!.stream; + } catch (e) { + Logger.error('开始连续语音识别失败: $e'); + rethrow; + } + } + + //写入音频数据到缓存区中 + Uint8List _appendToBuffer(Uint8List newData) { + var newBuffer = Uint8List(_buff.length + newData.length); + newBuffer.setAll(0, _buff); + newBuffer.setAll(_buff.length, newData); + return newBuffer; + } + + //读取缓存区中一帧数据 + Uint8List _getAudioData() { + if (_buff.length >= _chunkSize) { + // 从缓冲区中读取1280字节的数据 + var data = _buff.sublist(0, _chunkSize); + // 移除已读取的数据 + _buff = _buff.sublist(_chunkSize); + return data; + } else { + return _buff; + } + } + + //尝试连接 + Future _tryConnecting() async { + try { + final url = _geturl(); + _channel = WebSocketChannel.connect(Uri.parse(url)); + _channel?.stream.timeout(const Duration(seconds: 10)); //设置超时时间 + await _channel?.ready; + isConnected = true; + _channel?.stream.listen( + (message) { + _handle(message); + }, + onError: (error) { + isConnected = false; + Logger.error('连接失败: $error'); + }, + onDone: () { + isConnected = false; + Logger.error('WebSocket 连接已关闭!'); + }, + cancelOnError: true, + ); + } catch (e) { + Logger.error("初始化异常----------------$e"); + } + } + + //处理数据 + void _handle(data) { + try { + final result = jsonDecode(data); + if (result['code'] == "0" && result['data'] != "") { + final data = jsonDecode(result['data']); + String msg = ""; + String role = ''; + + List rtList = data['cn']['st']['rt']; + for (var rt in rtList) { + for (var ws in rt['ws']) { + for (var ws in ws['cw']) { + msg += ws['w']; + if (ws['rl'] != "0") { + role = ws['rl']; + } + } + } + } + Logger.info( + "识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role"); + if (msg.isNotEmpty) { + if (data['cn']['st']['type'] == "0") { + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.finalResult, + text: msg, + role: role, + audio: _originaBytes.toBytes(), + )); + _originaBytes.clear(); + } else { + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.intermediateResult, + text: msg, + role: role, + )); + } + } + } + } catch (e) { + Logger.error("接受消息异常 $e"); + } + } + + ///获取链接地址 + String _geturl() { + // 生成时间戳 + String ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); + // 计算 baseString + String baseString = md5.convert(utf8.encode(appId + ts)).toString(); + // 计算 signa + var hmacSha1 = Hmac(sha1, utf8.encode(apiKey)); + var signa = hmacSha1.convert(utf8.encode(baseString)); + String signaBase64 = base64Encode(signa.bytes); + String url = ""; + // 构建 WebSocket URL + url = + '$baseUrl?appid=$appId&ts=$ts&lang=$_languageStr&roleType=2&signa=${Uri.encodeComponent(signaBase64)}'; + return url; + } + + @override + Future stopContinuousRecognition() async { + try { + await recorder?.stop(); + isRecording = false; + _timer?.cancel(); + _buff = Uint8List(0); + _originaBytes = BytesBuilder(); + if (isConnected) { + _channel?.sink.add(utf8.encode('{"end": true}')); //结束 + } + + _isContinuousRecognitionActive = false; + return true; + } catch (e) { + Logger.error('停止连续语音识别失败: $e'); + rethrow; + } + } + + @override + bool isContinuousRecognitionActive() { + return _isContinuousRecognitionActive; + } + + @override + Future dispose() async { + try { + await stopContinuousRecognition(); + recorder?.dispose(); + _channel?.sink.close(); + + _eventStreamController?.close(); + _eventStreamController = null; + + _isInitialized = false; + + Logger.info('Azure 语音识别资源已释放'); + } catch (e) { + Logger.error('释放语音识别资源失败: $e'); + _isInitialized = false; + } + } +} diff --git a/lib/data/services/volcano_ai_service.dart b/lib/data/services/volcano_ai_service.dart index f7e95ee03..e998a8270 100644 --- a/lib/data/services/volcano_ai_service.dart +++ b/lib/data/services/volcano_ai_service.dart @@ -2,7 +2,9 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; -class VolcanoAIService { +import 'ai_service.dart'; + +class VolcanoAIService implements AiService { final String apiKey; static const String _baseUrl = 'https://ark.cn-beijing.volces.com/api/v3'; static const String _chatEndpoint = '/chat/completions'; @@ -30,12 +32,13 @@ class VolcanoAIService { } // 非流式输出方法 + @override Future sendMessage({ required List> messages, required String systemPrompt, }) async { final url = Uri.parse('$_baseUrl$_chatEndpoint'); - + final fullMessages = [ {'role': 'system', 'content': systemPrompt}, ...messages, @@ -59,8 +62,8 @@ class VolcanoAIService { if (response.statusCode == 200) { final data = json.decode(response.body); - if (data['choices'] != null && - data['choices'].isNotEmpty && + if (data['choices'] != null && + data['choices'].isNotEmpty && data['choices'][0]['message'] != null) { return data['choices'][0]['message']['content']; } @@ -78,12 +81,13 @@ class VolcanoAIService { } // 流式输出方法 + @override Stream sendMessageStream({ required List> messages, required String systemPrompt, }) async* { final url = Uri.parse('$_baseUrl$_chatEndpoint'); - + final fullMessages = [ {'role': 'system', 'content': systemPrompt}, ...messages, @@ -96,7 +100,7 @@ class VolcanoAIService { 'Authorization': 'Bearer $apiKey', 'Accept': 'text/event-stream', }); - + request.body = json.encode({ 'model': 'doubao-1-5-lite-32k-250115', 'messages': fullMessages, @@ -109,21 +113,22 @@ class VolcanoAIService { if (response.statusCode != 200) { final error = await response.stream.bytesToString(); - throw VolcanoAIException( - json.decode(error)['error']?['message'] ?? 'Unknown error occurred' - ); + throw VolcanoAIException(json.decode(error)['error']?['message'] ?? + 'Unknown error occurred'); } - await for (final chunk in response.stream.transform(utf8.decoder).transform(const LineSplitter())) { + 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 && + 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; @@ -144,7 +149,7 @@ class VolcanoAIService { class VolcanoAIException implements Exception { final String message; VolcanoAIException(this.message); - + @override String toString() => message; -} \ No newline at end of file +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 000000000..1f8b9593e --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,92 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyBJXQTXsEmY_XA7ZEOusxG6VF5CaKtjHCA', + appId: '1:907754492821:web:99451e7efd7d120a145c9c', + messagingSenderId: '907754492821', + projectId: 'deepsound-e0e3d', + authDomain: 'deepsound-e0e3d.firebaseapp.com', + storageBucket: 'deepsound-e0e3d.firebasestorage.app', + measurementId: 'G-ETX6DF35J3', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAj0_-nS3ygc8VFe1azotSE3-HGZYClkTs', + appId: '1:907754492821:android:2deffc5aa1037ae2145c9c', + messagingSenderId: '907754492821', + projectId: 'deepsound-e0e3d', + storageBucket: 'deepsound-e0e3d.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808', + appId: '1:907754492821:ios:8c1d7d1b583aec22145c9c', + messagingSenderId: '907754492821', + projectId: 'deepsound-e0e3d', + storageBucket: 'deepsound-e0e3d.firebasestorage.app', + androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com', + iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com', + iosBundleId: 'com.example.client', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808', + appId: '1:907754492821:ios:8c1d7d1b583aec22145c9c', + messagingSenderId: '907754492821', + projectId: 'deepsound-e0e3d', + storageBucket: 'deepsound-e0e3d.firebasestorage.app', + androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com', + iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com', + iosBundleId: 'com.example.client', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyBJXQTXsEmY_XA7ZEOusxG6VF5CaKtjHCA', + appId: '1:907754492821:web:e3e8040111db6871145c9c', + messagingSenderId: '907754492821', + projectId: 'deepsound-e0e3d', + authDomain: 'deepsound-e0e3d.firebaseapp.com', + storageBucket: 'deepsound-e0e3d.firebasestorage.app', + measurementId: 'G-SKGXEK5TJM', + ); +} diff --git a/lib/main.dart b/lib/main.dart index 5eef65cb0..72b596679 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_storage/get_storage.dart'; @@ -11,26 +13,28 @@ import 'package:window_size/window_size.dart'; import 'core/translations/app_translations.dart'; import 'core/themes/app_theme.dart'; import 'core/themes/theme_service.dart'; +import '../../data/services/login_service.dart'; void main() async { // 确保Flutter绑定初始化 WidgetsFlutterBinding.ensureInitialized(); - + // 初始化存储 - 在创建应用之前初始化 await GetStorage.init(); - + // 初始化环境变量 await dotenv.load(); - + LoginService(); + //LoginService.FirebaseInit(); // 设置窗口大小(仅在 macOS 下生效) if (Platform.isMacOS) { const double width = 375.0; const double height = 812.0; - - setWindowTitle('Deep Voice'); + + setWindowTitle('DeepSound'); setWindowMinSize(const Size(width, height)); setWindowMaxSize(const Size(width, height)); - + // 居中显示窗口 final screen = await getCurrentScreen(); if (screen != null) { @@ -43,13 +47,11 @@ void main() async { setWindowFrame(windowFrame); } } - - + // 运行应用 runApp(const MainApp()); } - class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -57,14 +59,14 @@ class MainApp extends StatelessWidget { Widget build(BuildContext context) { // 获取保存的主题设置 final themeMode = ThemeService().getThemeMode(); - + return ScreenUtilInit( designSize: const Size(375, 812), minTextAdapt: true, splitScreenMode: true, builder: (_, child) { return GetMaterialApp( - title: 'Deep Voice', + title: 'DeepSound', theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: themeMode, // 使用提前获取的主题模式 @@ -73,9 +75,25 @@ class MainApp extends StatelessWidget { getPages: AppPages.pages, debugShowCheckedModeBanner: false, translations: AppTranslations(), - locale: const Locale('zh', 'CN'), - fallbackLocale: const Locale('en', 'US'), - + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + locale: AppTranslations.deviceLocale(), + fallbackLocale: AppTranslations.fallbackLocale, + supportedLocales: AppTranslations.supportedLocales, + builder: EasyLoading.init( + builder: (BuildContext context, Widget? widget) { + EasyLoading.instance.userInteractions = false; + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.noScaling, + ), + child: widget!, + ); + }, + ), ); }, ); diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 25d567714..4d0457c98 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -1,6 +1,11 @@ import 'dart:async'; import 'package:get/get.dart'; import 'package:flutter/widgets.dart'; +import 'package:get_storage/get_storage.dart'; +import '../../../data/services/ai_service.dart'; +import '../../../data/services/deapsound_ai_service.dart'; +import '../../../data/services/kouzi_ai_service.dart'; +import '../../../data/services/speech_impl/voice_clone_tts_service.dart'; import '../models/message_model.dart'; import '../../../core/utils/logger.dart'; import '../../../data/services/volcano_ai_service.dart'; @@ -14,11 +19,12 @@ import '../../../data/services/voice_interaction_service.dart'; class ChatController extends GetxController { // 服务 - final VolcanoAIService _aiService = Get.find(); - final TtsService _ttsService = Get.find(); + late AiService _aiService; + late TtsService _ttsService; final AsrService _asrService = Get.find(); final ChatHistoryService _historyService = Get.find(); - final VoiceInteractionService _voiceInteractionService = Get.find(); + final VoiceInteractionService _voiceInteractionService = + Get.find(); // 添加ScrollController用于控制列表滚动 final ScrollController scrollController = ScrollController(); @@ -37,7 +43,7 @@ class ChatController extends GetxController { final RxBool isSending = false.obs; final RxBool isTtsEnabled = true.obs; final RxBool isVoiceRecognitionActive = false.obs; - + // 语音交互模式 bool _isVoiceInteractionMode = false; @@ -48,7 +54,7 @@ class ChatController extends GetxController { // 临时消息索引,用于更新临时消息 int _tempMessageIndex = -1; - + // 防抖计时器 Timer? _scrollDebounceTimer; @@ -59,10 +65,10 @@ class ChatController extends GetxController { // 取消标志 bool _shouldCancelAiResponse = false; - + // 自动停止语音识别计时器 Timer? _autoStopTimer; - + // 记录最后一次活动时间 DateTime _lastActivityTime = DateTime.now(); @@ -70,11 +76,11 @@ class ChatController extends GetxController { Future onInit() async { super.onInit(); Logger.info('ChatController onInit'); - - + // 从路由参数获取Agent信息 final Map args = Get.arguments ?? {}; - final String agentId = args['agent'] ?? args['agentId'] ?? 'personal_assistant'; + final String agentId = + args['agent'] ?? args['agentId'] ?? 'personal_assistant'; // 获取Agent final Agent? foundAgent = AgentProvider.getAgentById(agentId); @@ -85,10 +91,35 @@ class ChatController extends GetxController { } agent = foundAgent; - + + switch (agent.id) { + case 'cyber_girlfriend': + // 亲子陪伴 + Get.put(KouziAIService()); + _aiService = Get.find(); + break; + case 'health_advisor': + // 多功能AI聊天 + Get.put(DeapsoundAIService()); + _aiService = Get.find(); + break; + default: + _aiService = Get.find(); + } + + // 使用克隆音色语音合成 + final GetStorage storage = GetStorage(); + String timbre = storage.read('timbre') ?? '1'; + if (timbre == '1') { + _ttsService = Get.find(); + } else { + Get.put(VoiceCloneTtsService()); + _ttsService = Get.find(); + } + _ttsService.initialize(); _asrService.initialize(); - + // 设置语音交互模式 _isVoiceInteractionMode = args['isVoiceInteraction'] == true; @@ -130,7 +161,8 @@ class ChatController extends GetxController { /// 订阅VoiceInteractionService事件 void _subscribeToVoiceInteractionEvents() { if (_voiceInteractionService.eventStream != null) { - _voiceInteractionSubscription = _voiceInteractionService.eventStream.listen((event) { + _voiceInteractionSubscription = + _voiceInteractionService.eventStream.listen((event) { if (event is RecognitionStartedEvent) { // 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS _stopCurrentInteraction(); @@ -145,26 +177,27 @@ class ChatController extends GetxController { /// 处理聊天历史事件 void _processChatHistoryEvent(ChatHistoryEvent event) { Logger.info('收到后台聊天历史事件: agentId=${event.agentId}'); - + // 如果消息不是空的且不是当前Agent的,不处理 if (event.userMessage.isEmpty || event.agentId != agent.id) { return; } - + // 添加用户消息 final userMsg = Message( role: 'user', content: event.userMessage, timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp), ); - + // 添加助手消息 final assistantMsg = Message( role: 'assistant', content: event.assistantMessage, - timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒 + timestamp: + DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒 ); - + // 防止消息重复 if (!_isDuplicateMessage(userMsg) && !_isDuplicateMessage(assistantMsg)) { messages.add(userMsg); @@ -176,15 +209,15 @@ class ChatController extends GetxController { /// 停止当前交互 void _stopCurrentInteraction() { Logger.info('收到后台语音识别开始事件,停止当前AI交互'); - + // 停止当前AI交互 if (isSending.value || _aiResponseSubscription != null) { _interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应"); } - + // 停止TTS _ttsService.stop(); - + // 停止语音识别 if (isVoiceRecognitionActive.value) { stopVoiceRecognition(); @@ -194,7 +227,7 @@ class ChatController extends GetxController { @override void onClose() { Logger.info('ChatController onClose'); - + // 清理资源 scrollController.dispose(); _aiResponseSubscription?.cancel(); @@ -202,10 +235,10 @@ class ChatController extends GetxController { _voiceInteractionSubscription?.cancel(); _autoStopTimer?.cancel(); _scrollDebounceTimer?.cancel(); // 清理防抖计时器 - + // 停止TTS _ttsService.stop(); - + if (isVoiceRecognitionActive.value) { _asrService.stopContinuousRecognition(); } @@ -220,33 +253,32 @@ class ChatController extends GetxController { /// 滚动到列表底部方法,添加防抖功能 void _scrollToBottom() { - - if (scrollController.hasClients && messages.isNotEmpty) { - try { - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } catch (e) { - Logger.error('滚动到底部失败', e); - } + if (scrollController.hasClients && messages.isNotEmpty) { + try { + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } catch (e) { + Logger.error('滚动到底部失败', e); } + } } /// 中断当前响应 void _interruptCurrentResponse(String reason) { Logger.info('${reason}'); - + // 停止TTS _ttsService.stop(); - + // 取消AI响应流订阅 if (_aiResponseSubscription != null) { _shouldCancelAiResponse = true; _aiResponseSubscription!.cancel(); _aiResponseSubscription = null; - + // 将加载中的消息转换为正常消息,保留已输出的内容 final index = messages.indexWhere((m) => m.isLoading); if (index != -1) { @@ -255,7 +287,7 @@ class ChatController extends GetxController { ); messages.refresh(); } - + // 重置发送状态 isSending.value = false; } @@ -278,14 +310,13 @@ class ChatController extends GetxController { ); messages.add(userMessage); - // 清空输入框 inputText.value = ''; // 添加加载中的消息 final loadingMessage = Message.loading(); messages.add(loadingMessage); - + WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(); }); @@ -298,7 +329,7 @@ class ChatController extends GetxController { // 重置取消标志 _shouldCancelAiResponse = false; - + String fullResponse = ''; // 获取AI响应流 @@ -327,12 +358,12 @@ class ChatController extends GetxController { // 完成响应,将isLoading设为false _finalizeResponse(fullResponse); - + // 如果TTS启用,处理缓冲区中的剩余文本 if (isTtsEnabled.value) { _ttsService.flushStream(); } - + // 保存聊天历史 _saveChatHistory(); }, @@ -345,7 +376,7 @@ class ChatController extends GetxController { ); } catch (e) { Logger.error('发送消息失败', e); - + _handleResponseError(); } } @@ -353,10 +384,11 @@ class ChatController extends GetxController { // 构建消息历史 List> _buildMessageHistory() { final List> messageHistory = []; - + // 只取最近的10条消息作为上下文 final recentMessages = messages.where((m) => !m.isLoading).toList(); - final startIndex = recentMessages.length > 10 ? recentMessages.length - 10 : 0; + final startIndex = + recentMessages.length > 10 ? recentMessages.length - 10 : 0; for (int i = startIndex; i < recentMessages.length; i++) { final m = recentMessages[i]; @@ -365,7 +397,7 @@ class ChatController extends GetxController { 'content': m.content, }); } - + return messageHistory; } @@ -393,7 +425,7 @@ class ChatController extends GetxController { messages.refresh(); _scrollToBottom(); } - + isSending.value = false; _aiResponseSubscription = null; } @@ -408,7 +440,7 @@ class ChatController extends GetxController { ); messages.refresh(); } - + isSending.value = false; _aiResponseSubscription = null; } @@ -429,18 +461,17 @@ class ChatController extends GetxController { _voiceInteractionService.pauseVoiceInteraction(); try { - // 启动连续语音识别 // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 final recognitionStream = await _asrService.startContinuousRecognition(); - + isVoiceRecognitionActive.value = true; isListening.value = true; recognizedText.value = ''; - + // 重置临时消息索引 _tempMessageIndex = -1; - + // 启动自动停止计时器 _startAutoStopTimer(); @@ -460,7 +491,7 @@ class ChatController extends GetxController { void _handleRecognitionEvent(RecognitionEvent event) { // 更新活动时间 _updateActivityTime(); - + switch (event.type) { case RecognitionEventType.intermediateResult: _handleRecognizing(event.text); @@ -486,24 +517,24 @@ class ChatController extends GetxController { void _startAutoStopTimer() { // 取消现有计时器 _autoStopTimer?.cancel(); - + // 初始化最后活动时间 _updateActivityTime(); - + // 根据模式设置超时时间 final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15; - + // 创建新的计时器,每秒检查一次 _autoStopTimer = Timer.periodic(Duration(seconds: 1), (timer) { // 检查是否有活动 bool isActive = _ttsService.isSpeaking || isSpeechDetected.value; - + // 如果有活动,更新时间并继续等待 if (isActive) { _updateActivityTime(); return; } - + // 检查空闲时间 int idleSeconds = DateTime.now().difference(_lastActivityTime).inSeconds; if (idleSeconds >= timeoutSeconds) { @@ -516,17 +547,17 @@ class ChatController extends GetxController { // 处理语音识别中的状态 void _handleRecognizing(String text) { if (text.isEmpty) return; - + recognizedText.value = text; - + // 设置语音检测状态为true,用于控制波形动画 if (!isSpeechDetected.value) { isSpeechDetected.value = true; } - + // 更新活动时间 _updateActivityTime(); - + // 用户开始讲话时,立即停止当前TTS和中断AI响应 if (isSending.value || _aiResponseSubscription != null) { _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); @@ -558,10 +589,10 @@ class ChatController extends GetxController { // 处理语音识别最终结果 void _handleFinalResult(String text) { if (text.isEmpty) return; - + Logger.info('处理语音识别最终结果: $text'); recognizedText.value = text; - + // 更新活动时间 _updateActivityTime(); @@ -569,7 +600,7 @@ class ChatController extends GetxController { if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { messages.removeAt(_tempMessageIndex); } - + // 重置临时消息索引 _tempMessageIndex = -1; @@ -578,7 +609,7 @@ class ChatController extends GetxController { // 重置识别文本,但不关闭语音识别框 recognizedText.value = ''; - + // 重置语音检测状态 isSpeechDetected.value = false; } @@ -586,14 +617,14 @@ class ChatController extends GetxController { // 处理语音识别事件错误 void _handleRecognitionEventError(String? error) { Logger.error('语音识别错误: ${error ?? "未知错误"}'); - + _cleanupRecognition(); } // 处理语音识别流错误 void _handleRecognitionError(dynamic error) { Logger.error('语音识别错误', error); - + _cleanupRecognition(); } @@ -605,10 +636,10 @@ class ChatController extends GetxController { messages.removeAt(_tempMessageIndex); } } - + // 重置临时消息索引 _tempMessageIndex = -1; - + // 停止语音识别 isVoiceRecognitionActive.value = false; isListening.value = false; @@ -636,7 +667,7 @@ class ChatController extends GetxController { messages.removeAt(_tempMessageIndex); } } - + // 重置临时消息索引 _tempMessageIndex = -1; } catch (e) { @@ -691,10 +722,12 @@ class ChatController extends GetxController { /// 检查是否为重复消息 bool _isDuplicateMessage(Message message) { - return messages.any((m) => - m.role == message.role && - m.content == message.content && - (m.timestamp.millisecondsSinceEpoch - message.timestamp.millisecondsSinceEpoch).abs() < 5000 - ); + return messages.any((m) => + m.role == message.role && + m.content == message.content && + (m.timestamp.millisecondsSinceEpoch - + message.timestamp.millisecondsSinceEpoch) + .abs() < + 5000); } } diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index 4abeb6d55..86d77e3a1 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -2,11 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math'; -import 'package:audio_waveforms/audio_waveforms.dart'; import '../controllers/chat_controller.dart'; import '../models/message_model.dart'; import '../../../core/theme/app_colors.dart'; -import '../../../core/widgets/message_bubble.dart'; class ChatView extends GetView { const ChatView({Key? key}) : super(key: key); @@ -14,25 +12,22 @@ class ChatView extends GetView { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], appBar: _buildAppBar(isDarkMode), body: _buildBody(isDarkMode), ); } - + // 构建AppBar PreferredSizeWidget _buildAppBar(bool isDarkMode) { return AppBar( elevation: 1, backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black87, - size: 20.r - ), + icon: Icon(Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), onPressed: () => Get.back(), ), titleSpacing: 0, @@ -101,7 +96,7 @@ class ChatView extends GetView { ), SizedBox(width: 4.w), Text( - '在线', + 'online'.tr, // 在线 style: TextStyle( fontSize: 12.sp, color: Colors.green, @@ -123,7 +118,9 @@ class ChatView extends GetView { : Icons.volume_off, color: controller.isTtsEnabled.value ? AppColors.primary - : isDarkMode ? Colors.grey[400] : Colors.grey, + : isDarkMode + ? Colors.grey[400] + : Colors.grey, ), onPressed: controller.toggleTts, )), @@ -133,7 +130,8 @@ class ChatView extends GetView { height: 34.r, margin: EdgeInsets.only(right: 8.w), decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + color: + isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], shape: BoxShape.circle, boxShadow: [ if (!isDarkMode) @@ -145,11 +143,8 @@ class ChatView extends GetView { ], ), child: PopupMenuButton( - icon: Icon( - Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black87, - size: 20.r - ), + icon: Icon(Icons.more_vert, + color: isDarkMode ? Colors.white : Colors.black87, size: 20.r), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), @@ -162,7 +157,7 @@ class ChatView extends GetView { Icon(Icons.delete_outline, color: Colors.red), SizedBox(width: 8.w), Text( - '清空聊天记录', + 'clearChatHistory'.tr, // 清空聊天记录 style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, ), @@ -183,7 +178,7 @@ class ChatView extends GetView { ], ); } - + // 构建主体内容 Widget _buildBody(bool isDarkMode) { return Column( @@ -198,13 +193,13 @@ class ChatView extends GetView { } }), ), - + // 底部输入区域 _buildInputArea(isDarkMode), ], ); } - + // 构建空状态 Widget _buildEmptyState(bool isDarkMode) { return Center( @@ -215,13 +210,15 @@ class ChatView extends GetView { width: 80.w, height: 80.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + color: + isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], shape: BoxShape.circle, ), child: Icon( Icons.chat_bubble_outline, size: 40.w, - color: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], + color: + isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], ), ), SizedBox(height: 24.h), @@ -250,7 +247,7 @@ class ChatView extends GetView { ), ); } - + // 构建聊天列表 Widget _buildChatList(bool isDarkMode) { return ListView.builder( @@ -266,32 +263,34 @@ class ChatView extends GetView { }, ); } - + // 构建消息项 Widget _buildMessageItem(Message message, bool isDarkMode) { final isUser = message.role == 'user'; - + return Padding( padding: EdgeInsets.only(bottom: 16.h), child: Row( - mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: + isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) _buildAvatar(false, isDarkMode), - SizedBox(width: 8.w), - Flexible( child: Column( - crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, + crossAxisAlignment: + isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ // 消息气泡 Container( padding: EdgeInsets.all(12.r), decoration: BoxDecoration( - color: isUser - ? AppColors.primary - : isDarkMode ? AppColors.cardBackground : Colors.grey[200], + color: isUser + ? AppColors.primary + : isDarkMode + ? AppColors.cardBackground + : Colors.grey[200], borderRadius: BorderRadius.circular(16.r), ), child: Column( @@ -302,14 +301,16 @@ class ChatView extends GetView { SelectableText( message.content, style: TextStyle( - color: isUser - ? Colors.white - : isDarkMode ? Colors.white : Colors.black, + color: isUser + ? Colors.white + : isDarkMode + ? Colors.white + : Colors.black, fontSize: 16.sp, height: 1.5, ), ), - + // 加载中动画 if (message.isLoading) Padding( @@ -325,7 +326,7 @@ class ChatView extends GetView { ], ), ), - + // 时间戳 Padding( padding: EdgeInsets.only(top: 4.h, left: 4.w, right: 4.w), @@ -340,9 +341,7 @@ class ChatView extends GetView { ], ), ), - SizedBox(width: 8.w), - if (isUser) _buildAvatar(true, isDarkMode), ], ), @@ -351,8 +350,9 @@ class ChatView extends GetView { // 构建输入区域 Widget _buildInputArea(bool isDarkMode) { - final textController = TextEditingController(text: controller.inputText.value); - + final textController = + TextEditingController(text: controller.inputText.value); + // 检查是否处于语音识别模式 return Obx(() { if (controller.isVoiceRecognitionActive.value) { @@ -362,9 +362,10 @@ class ChatView extends GetView { } }); } - + // 构建文本输入面板 - Widget _buildTextInputPanel(bool isDarkMode, TextEditingController textController) { + Widget _buildTextInputPanel( + bool isDarkMode, TextEditingController textController) { return Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), decoration: BoxDecoration( @@ -385,7 +386,9 @@ class ChatView extends GetView { width: 40.w, height: 40.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[100], shape: BoxShape.circle, ), child: IconButton( @@ -398,20 +401,21 @@ class ChatView extends GetView { onPressed: controller.startVoiceRecognition, ), ), - + SizedBox(width: 8.w), - + // 输入框 Expanded( child: TextField( controller: textController, decoration: InputDecoration( - hintText: '输入消息...', + hintText: 'inputMessage'.tr, // 输入消息... hintStyle: TextStyle( - color: isDarkMode ? Colors.grey[500] : Colors.grey[500] - ), + color: isDarkMode ? Colors.grey[500] : Colors.grey[500]), filled: true, - fillColor: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + fillColor: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[100], border: OutlineInputBorder( borderRadius: BorderRadius.circular(20.r), borderSide: BorderSide.none, @@ -435,9 +439,9 @@ class ChatView extends GetView { }, ), ), - + SizedBox(width: 8.w), - + // 发送按钮 Obx(() { final hasText = controller.inputText.value.isNotEmpty; @@ -445,17 +449,21 @@ class ChatView extends GetView { width: 40.w, height: 40.w, decoration: BoxDecoration( - color: hasText - ? AppColors.primary - : isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200], + color: hasText + ? AppColors.primary + : isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200], shape: BoxShape.circle, ), child: IconButton( icon: Icon( Icons.send, - color: hasText - ? Colors.white - : isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], + color: hasText + ? Colors.white + : isDarkMode + ? Colors.white.withOpacity(0.5) + : Colors.grey[400], size: 18.r, ), padding: EdgeInsets.zero, @@ -473,7 +481,7 @@ class ChatView extends GetView { ), ); } - + // 构建语音输入面板 Widget _buildVoiceInputPanel(bool isDarkMode) { return Container( @@ -501,7 +509,9 @@ class ChatView extends GetView { width: 60.r, height: 60.r, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( @@ -513,13 +523,17 @@ class ChatView extends GetView { ), child: Center( child: Obx(() => Icon( - controller.isListening.value ? Icons.mic : Icons.mic_none, - color: controller.isListening.value ? AppColors.primary : Colors.grey.shade400, - size: 28.r, - )), + controller.isListening.value + ? Icons.mic + : Icons.mic_none, + color: controller.isListening.value + ? AppColors.primary + : Colors.grey.shade400, + size: 28.r, + )), ), ), - + // 中间音频波形和状态文本 Expanded( child: Column( @@ -542,10 +556,10 @@ class ChatView extends GetView { 23, (index) { // 随机高度模拟波形 - double height = isSpeaking - ? (Random().nextDouble() * 20.0 + 5.0).h + double height = isSpeaking + ? (Random().nextDouble() * 20.0 + 5.0).h : 2.h; - + return SizedBox( width: 6.w, child: Center( @@ -553,12 +567,16 @@ class ChatView extends GetView { duration: Duration(milliseconds: 500), width: 2.5.w, height: height, - margin: EdgeInsets.symmetric(horizontal: 1.5.w), + margin: EdgeInsets.symmetric( + horizontal: 1.5.w), decoration: BoxDecoration( - color: isSpeaking - ? AppColors.primary - : isDarkMode ? Colors.grey.shade600 : Colors.grey.shade400, - borderRadius: BorderRadius.circular(2.r), + color: isSpeaking + ? AppColors.primary + : isDarkMode + ? Colors.grey.shade600 + : Colors.grey.shade400, + borderRadius: + BorderRadius.circular(2.r), ), ), ), @@ -571,26 +589,32 @@ class ChatView extends GetView { }), ), SizedBox(height: 8.h), - + // 语音状态文本 Obx(() => Text( - controller.isSpeechDetected.value ? '正在聆听...' : '你可以开始说话', - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[700], - fontWeight: FontWeight.w500, - ), - )), + controller.isSpeechDetected.value + ? 'imListening'.tr // 正在聆听... + : 'canStartTalking'.tr, // 你可以开始说话 + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[700], + fontWeight: FontWeight.w500, + ), + )), ], ), ), - + // 关闭按钮 Container( width: 60.r, height: 60.r, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( @@ -619,15 +643,15 @@ class ChatView extends GetView { ), ); } - + // 构建头像 Widget _buildAvatar(bool isUser, bool isDarkMode) { return Container( width: 36.r, height: 36.r, decoration: BoxDecoration( - color: isUser - ? (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]) + color: isUser + ? (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]) : AppColors.primary, shape: BoxShape.circle, boxShadow: [ @@ -668,7 +692,7 @@ class ChatView extends GetView { ), ); } - + // 构建加载动画点 Widget _buildLoadingDot(int index, bool isDarkMode) { return AnimatedBuilder( @@ -679,21 +703,22 @@ class ChatView extends GetView { width: 8.r, height: 8.r, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], + color: + isDarkMode ? Colors.white.withOpacity(0.5) : Colors.grey[400], shape: BoxShape.circle, ), ); }, ); } - + // 格式化时间 String _formatTime(DateTime time) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final yesterday = today.subtract(const Duration(days: 1)); final messageDate = DateTime(time.year, time.month, time.day); - + if (messageDate == today) { return '今天 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } else if (messageDate == yesterday) { @@ -702,4 +727,4 @@ class ChatView extends GetView { return '${time.month}月${time.day}日 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } } -} \ No newline at end of file +} diff --git a/lib/modules/common/webview_page.dart b/lib/modules/common/webview_page.dart new file mode 100644 index 000000000..daa78ecf2 --- /dev/null +++ b/lib/modules/common/webview_page.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class WebViewPage extends StatelessWidget { + final String url; + final String title; + + const WebViewPage({Key? key, required this.url, required this.title}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..loadRequest(Uri.parse(url)); + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: AppBar( + centerTitle: true, + title: Text(title), + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + backgroundColor: isDarkMode ? Colors.black : Colors.white, + foregroundColor: isDarkMode ? Colors.white : Colors.black, + elevation: 0, + ), + body: WebViewWidget( + controller: controller, + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index a452df264..f250cc7e3 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/lib/modules/explore/controllers/explore_controller.dart @@ -13,7 +13,7 @@ class AIFeature { final Color iconColor; final String agentId; final String targetRoute; - + AIFeature({ required this.title, required this.description, @@ -34,7 +34,8 @@ class FunctionScene { final Color startColor; final Color endColor; final Color iconColor; - + final String targetRoute; + FunctionScene({ required this.title, required this.description, @@ -42,18 +43,19 @@ class FunctionScene { required this.startColor, required this.endColor, required this.iconColor, + required this.targetRoute, }); } class ExploreController extends GetxController { // 搜索关键词 final searchController = TextEditingController(); - + // AI功能列表 final List aiFeatures = [ AIFeature( - title: '全能助理', - description: '帮你解决各类问题', + title: 'allRoundAssistant'.tr, // 全能助理 + description: 'allRoundAssistantInfo'.tr, // 帮你解决各类问题 icon: Icons.assistant, startColor: const Color(0xFF0066CC), endColor: const Color(0xFF5E5CE6), @@ -63,8 +65,8 @@ class ExploreController extends GetxController { targetRoute: Routes.chat, ), AIFeature( - title: '同声翻译', - description: '实时翻译80+种语言', + title: 'simultaneousTranslation'.tr, // 同声翻译 + description: 'simultaneousTranslationInfo'.tr, // 实时翻译80+种语言 icon: Icons.translate, startColor: const Color(0xFF4CAF50), endColor: const Color(0xFF009688), @@ -74,8 +76,8 @@ class ExploreController extends GetxController { targetRoute: Routes.translation, ), AIFeature( - title: 'AI女友', - description: '智能陪伴,随时聊天', + title: 'aiGirlfriend'.tr, // 亲子陪伴 + description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天 icon: Icons.favorite, startColor: const Color(0xFFE91E63), endColor: const Color(0xFFFF5252), @@ -85,8 +87,8 @@ class ExploreController extends GetxController { targetRoute: Routes.chat, ), AIFeature( - title: '健康顾问', - description: '提供科学健康建议', + title: 'healthConsultant'.tr, // 健康顾问 + description: 'healthConsultantInfo'.tr, // 提供科学健康建议 icon: Icons.healing, startColor: const Color(0xFFFFC107), endColor: const Color(0xFFFF9800), @@ -96,65 +98,68 @@ class ExploreController extends GetxController { targetRoute: Routes.chat, ), ]; - + // 热门功能场景列表 final List functionScenes = [ FunctionScene( - title: '商务会议助手', - description: '实时记录会议纪要,多语言翻译', + title: 'conferenceAssistant'.tr, // 商务会议助手 + description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 icon: Icons.people, startColor: const Color(0xFF3F51B5), endColor: const Color(0xFF673AB7), iconColor: const Color(0xFF3F51B5), + targetRoute: Routes.meeting, ), FunctionScene( - title: '语言学习助手', - description: '发音纠正,实时练习口语', + title: 'studyAssistant'.tr, // 语言学习助手 + description: 'studyAssistantInfo'.tr, // 发音纠正,实时练习口语 icon: Icons.school, startColor: const Color(0xFF4CAF50), endColor: const Color(0xFF009688), iconColor: const Color(0xFF4CAF50), + targetRoute: '', ), FunctionScene( - title: '旅行翻译助手', - description: '实时语音翻译,旅游景点介绍', + title: 'travelTranslator'.tr, // 旅行翻译助手 + description: 'travelTranslatorInfo'.tr, // 实时语音翻译,旅游景点介绍 icon: Icons.language, startColor: const Color(0xFFFFC107), endColor: const Color(0xFFFF9800), iconColor: const Color(0xFFFFC107), + targetRoute: '', ), ]; - + // 搜索功能 void search() { if (searchController.text.isEmpty) return; - + Get.snackbar( - '搜索', - '正在搜索: ${searchController.text}', + 'search'.tr, // 搜索 + '${'beSearching'.tr} ${searchController.text}', // 正在搜索: snackPosition: SnackPosition.TOP, ); - + // 实际应用中,这里应该实现真正的搜索逻辑 searchController.clear(); } - + // 点击AI功能 void onAIFeatureTap(int index) { if (index < 0 || index >= aiFeatures.length) return; - + final feature = aiFeatures[index]; final agent = AgentProvider.getAgentById(feature.agentId); - + if (agent == null) { Get.snackbar( - '错误', - '未找到对应的AI助手', + 'error'.tr, // 错误 + 'noAssistantFound'.tr, // 未找到对应的AI助手 snackPosition: SnackPosition.TOP, ); return; } - + // 根据目标路由进行跳转 if (feature.targetRoute == Routes.chat) { // 跳转到聊天页面 @@ -172,34 +177,38 @@ class ExploreController extends GetxController { Get.toNamed(feature.targetRoute); } } - + // 点击功能场景 void onFunctionSceneTap(int index) { if (index < 0 || index >= functionScenes.length) return; - - Get.snackbar( - '场景', - '您选择了: ${functionScenes[index].title}', - snackPosition: SnackPosition.TOP, - ); - - // 实际应用中,这里应该跳转到对应的场景页面 + + final scene = functionScenes[index]; + + if (scene.targetRoute.isNotEmpty) { + Get.toNamed(scene.targetRoute); + } else { + Get.snackbar( + 'scene'.tr, // 场景 + '${'youHaveSelected'.tr} ${functionScenes[index].title}', // 您选择了: + snackPosition: SnackPosition.TOP, + ); + } } - + // 尝试新功能 void tryNewFeature() { Get.snackbar( - '新功能', - '您选择了尝试AI歌手模式', + 'newFunction'.tr, // 新功能 + '${'youChoseTry'.tr}${'aiSinger'.tr}', // 您选择了尝试 // AI歌手模式 snackPosition: SnackPosition.TOP, ); - + // 实际应用中,这里应该跳转到新功能页面 } - + @override void onClose() { searchController.dispose(); super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index f68d1c392..df227549b 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -11,7 +11,7 @@ class ExploreView extends GetView { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], body: SafeArea( @@ -19,7 +19,7 @@ class ExploreView extends GetView { children: [ // 标题和搜索 _buildHeaderAndSearch(isDarkMode), - + // 主要内容(可滚动) Expanded( child: SingleChildScrollView( @@ -30,16 +30,47 @@ class ExploreView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ // 特色功能 - _buildFeaturedCard(), - + Container( + height: 180.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: PageView( + children: [ + _buildFeaturedCard( + 'assets/images/explore1.png', + 'exploreSlogan1'.tr, // 智能陪伴,无限可能 + 'exploreAi'.tr, // 探索DeepSound提供的AI功能 + ), + _buildFeaturedCard( + 'assets/images/explore.png', + 'exploreSlogan'.tr, // 智能耳机,无限可能 + 'exploreAi'.tr, // 探索DeepSound提供的AI功能 + ), + _buildFeaturedCard( + 'assets/images/explore2.jpg', + 'exploreSlogan2'.tr, // 智能眼镜,无限可能 + 'exploreAi'.tr, // 探索DeepSound提供的AI功能 + ), + ], + ), + ), + // AI功能列表 SizedBox(height: 24.h), _buildAIFeaturesSection(isDarkMode), - + // 热门功能场景 SizedBox(height: 24.h), _buildFunctionScenesSection(isDarkMode), - + // 新功能推荐 SizedBox(height: 24.h), _buildNewFeatureSection(isDarkMode), @@ -66,7 +97,7 @@ class ExploreView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '发现', + 'explore'.tr, // 发现 style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold, @@ -77,12 +108,14 @@ class ExploreView extends GetView { width: 32.w, height: 32.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200], + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200], shape: BoxShape.circle, ), child: IconButton( icon: Icon( - Icons.more_vert, + Icons.more_vert, size: 16.sp, color: isDarkMode ? Colors.white : Colors.grey[700], ), @@ -92,18 +125,19 @@ class ExploreView extends GetView { ), ], ), - + // 搜索框 SizedBox(height: 16.h), Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + color: + isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], borderRadius: BorderRadius.circular(12.r), ), child: TextField( controller: controller.searchController, decoration: InputDecoration( - hintText: '搜索AI功能...', + hintText: 'searchAi'.tr, // 搜索AI功能... hintStyle: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[400], @@ -129,90 +163,77 @@ class ExploreView extends GetView { } // 特色功能卡片 - Widget _buildFeaturedCard() { - return Container( - height: 180.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Stack( - children: [ - // 背景图片 - ClipRRect( - borderRadius: BorderRadius.circular(24.r), - child: Container( - color: Colors.blue.shade900, // 预设颜色,图片加载失败时显示 - child: Image.asset( - 'assets/images/explore.png', - width: double.infinity, - height: 180.h, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.blue.shade900, - width: double.infinity, - height: 180.h, - child: Icon( - Icons.image_not_supported, - color: Colors.white, - size: 40.sp, - ), - ); - }, - ), + Widget _buildFeaturedCard(String image, String title, String description) { + return Stack( + children: [ + // 背景图片 + ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Container( + color: Colors.blue.shade900, // 预设颜色,图片加载失败时显示 + child: Image.asset( + image, + width: double.infinity, + height: 180.h, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.blue.shade900, + width: double.infinity, + height: 180.h, + child: Icon( + Icons.image_not_supported, + color: Colors.white, + size: 40.sp, + ), + ); + }, ), ), - - // 渐变遮罩 - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.r), - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.1), - Colors.black.withOpacity(0.6), - ], - ), + ), + + // 渐变遮罩 + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.r), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.1), + Colors.black.withOpacity(0.6), + ], ), ), - - // 文本内容 - Padding( - padding: EdgeInsets.all(24.w), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '智能耳机,无限可能', - style: TextStyle( - fontSize: 22.sp, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + ), + + // 文本内容 + Padding( + padding: EdgeInsets.all(24.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 22.sp, + fontWeight: FontWeight.bold, + color: Colors.white, ), - SizedBox(height: 4.h), - Text( - '探索DeepVoice提供的AI功能', - style: TextStyle( - fontSize: 14.sp, - color: Colors.white.withOpacity(0.8), - ), + ), + SizedBox(height: 4.h), + Text( + description, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.8), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ); } @@ -222,7 +243,7 @@ class ExploreView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'AI功能', + 'aiFunction'.tr, // AI功能 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, @@ -267,7 +288,7 @@ class ExploreView extends GetView { borderRadius: BorderRadius.circular(20.r), boxShadow: [ BoxShadow( - color: isDarkMode + color: isDarkMode ? Colors.black.withOpacity(0.2) : Colors.grey.withOpacity(0.1), blurRadius: 8, @@ -338,7 +359,7 @@ class ExploreView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '热门功能场景', + 'popularFunction'.tr, // 热门功能场景 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, @@ -363,7 +384,8 @@ class ExploreView extends GetView { } // 功能场景项 - Widget _buildFunctionSceneItem(FunctionScene scene, int index, bool isDarkMode) { + Widget _buildFunctionSceneItem( + FunctionScene scene, int index, bool isDarkMode) { return GestureDetector( onTap: () => controller.onFunctionSceneTap(index), child: Container( @@ -373,7 +395,7 @@ class ExploreView extends GetView { borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( - color: isDarkMode + color: isDarkMode ? Colors.black.withOpacity(0.2) : Colors.grey.withOpacity(0.1), blurRadius: 4, @@ -405,7 +427,7 @@ class ExploreView extends GetView { ), ), SizedBox(width: 16.w), - + // 文本内容 Expanded( child: Column( @@ -478,10 +500,10 @@ class ExploreView extends GetView { ), ), SizedBox(height: 16.h), - + // 标题 Text( - 'AI歌手模式', + 'aiSinger'.tr, // AI歌手模式 style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, @@ -489,17 +511,17 @@ class ExploreView extends GetView { ), ), SizedBox(height: 8.h), - + // 描述 Text( - '用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲', + 'aiSingerInfo'.tr, // 用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[300] : Colors.grey[600], ), ), SizedBox(height: 16.h), - + // 按钮 ElevatedButton( onPressed: controller.tryNewFeature, @@ -514,7 +536,7 @@ class ExploreView extends GetView { ), ), child: Text( - '立即尝试', + 'tryNow'.tr, // 立即尝试 style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w500, @@ -526,4 +548,4 @@ class ExploreView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/home/bindings/home_binding.dart b/lib/modules/home/bindings/home_binding.dart index d503413c7..a8dae0b27 100644 --- a/lib/modules/home/bindings/home_binding.dart +++ b/lib/modules/home/bindings/home_binding.dart @@ -1,9 +1,13 @@ +import '../../explore/controllers/explore_controller.dart'; import 'package:get/get.dart'; +import '../../explore/controllers/explore_controller.dart'; import '../controllers/home_controller.dart'; class HomeBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => HomeController()); + Get.lazyPut( + () => ExploreController()); // 注册ExploreController } -} \ No newline at end of file +} diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 7e5eef94a..cfed9c72f 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -3,110 +3,110 @@ import 'dart:async'; import '../../../routes/app_routes.dart'; import '../../../data/services/classic_bluetooth_service.dart'; import '../../../data/services/bluetooth_media_button_service.dart'; -import '../../../data/services/voice_interaction_service.dart'; import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; +import '../../explore/controllers/explore_controller.dart'; // 连接状态枚举 enum ConnectionStatus { - disconnected, // 未连接 - connecting, // 连接中 - connected, // 已连接 - failed // 连接失败 + disconnected, // 未连接 + connecting, // 连接中 + connected, // 已连接 + failed // 连接失败 } class HomeController extends GetxController { static HomeController get to => Get.find(); - + final ExploreController exploreController = Get.find(); + // 经典蓝牙服务 late ClassicBluetoothService _bluetoothService; - + // 蓝牙媒体按钮服务 late BluetoothMediaButtonService _mediaButtonService; - + // 媒体按钮订阅 StreamSubscription? _mediaButtonSubscription; - + // 设备连接状态 final _isConnected = false.obs; bool get isConnected => _isConnected.value; - + // 连接状态 final _connectionStatus = ConnectionStatus.disconnected.obs; ConnectionStatus get connectionStatus => _connectionStatus.value; - + // 电池状态(模拟数据) final _deviceBattery = Rx(null); int? get deviceBattery => _deviceBattery.value; - + // 固件版本(模拟数据) final String firmwareVersion = 'v2.4.1'; - + // 蓝牙设备信息 final _deviceInfo = {}.obs; Map get deviceInfo => _deviceInfo.value; - + // 连接错误信息 final _connectionErrorMsg = ''.obs; String get connectionErrorMsg => _connectionErrorMsg.value; - + // 当前连接的经典蓝牙设备 - List get connectedClassicDevices => _bluetoothService.connectedDevices; - + List get connectedClassicDevices => + _bluetoothService.connectedDevices; + // 蓝牙状态 RxString get bluetoothState => _bluetoothService.bluetoothState; - // 状态更新标志,防止多次重复更新 bool _isUpdatingStatus = false; - + @override void onInit() { super.onInit(); - + // 初始化蓝牙服务和状态监控 _initBluetoothService(); - + // 初始化蓝牙媒体按钮服务 // _initMediaButtonService(); - + // 电池模拟更新(实际应用中可以通过蓝牙获取) _simulateBatteryUpdate(); } - + // 初始化蓝牙服务 void _initBluetoothService() { try { // 获取经典蓝牙服务实例 _bluetoothService = Get.find(); - + // 更新当前连接状态 _updateConnectionStatus(); - + // 监听蓝牙连接状态变化 _setupBluetoothListeners(); - } catch (e) { Logger.error('初始化蓝牙服务失败: $e'); _connectionStatus.value = ConnectionStatus.failed; } } - + // 初始化蓝牙媒体按钮服务 void _initMediaButtonService() { try { // 获取媒体按钮服务实例 _mediaButtonService = Get.find(); - + // 初始化媒体按钮服务 _mediaButtonService.initialize().then((success) { if (success) { // 启动按钮监听 _mediaButtonService.startButtonListening(); - + // 设置按钮事件监听 _setupMediaButtonListener(); - + Logger.info('蓝牙媒体按钮服务初始化成功'); } else { Logger.warning('蓝牙媒体按钮服务初始化失败'); @@ -116,23 +116,21 @@ class HomeController extends GetxController { Logger.error('初始化蓝牙媒体按钮服务失败: $e'); } } - + // 设置媒体按钮事件监听 void _setupMediaButtonListener() { // 取消之前的订阅 _mediaButtonSubscription?.cancel(); - + // 设置新的订阅 _mediaButtonSubscription = _mediaButtonService.buttonEvents.listen((event) { Logger.info('收到媒体按钮事件: $event'); - + // 处理按钮事件 - if (event.buttonType == MediaButtonType.playPause) { - } + if (event.buttonType == MediaButtonType.playPause) {} }); } - - + // 电池电量模拟更新 void _simulateBatteryUpdate() { // 每5分钟更新一次电池电量(模拟) @@ -143,23 +141,23 @@ class HomeController extends GetxController { } else { _deviceBattery.value = null; } - + // 递归调用实现定时更新 _simulateBatteryUpdate(); }); } - + // 更新连接状态 void _updateConnectionStatus() { // 如果正在更新,则跳过 if (_isUpdatingStatus) return; - + _isUpdatingStatus = true; - + try { // 检查当前设备 final currentDevice = _bluetoothService.currentDevice.value; - + if (currentDevice != null) { _isConnected.value = true; _connectionStatus.value = ConnectionStatus.connected; @@ -167,17 +165,17 @@ class HomeController extends GetxController { _isConnected.value = false; _connectionStatus.value = ConnectionStatus.disconnected; } - + update(); // 通知UI刷新 } finally { _isUpdatingStatus = false; } } - + // 设置蓝牙监听器 void _setupBluetoothListeners() { // 只监听当前设备和蓝牙状态 - + // 监听当前设备状态 ever(_bluetoothService.currentDevice, (device) { if (device != null) { @@ -191,7 +189,7 @@ class HomeController extends GetxController { } update(); }); - + // 蓝牙状态变化监听 ever(_bluetoothService.bluetoothState, (state) { if (state == 'off') { @@ -202,26 +200,26 @@ class HomeController extends GetxController { } }); } - + // 刷新设备信息 Future refreshDeviceInfo() async { return; // 如果正在更新,则跳过 if (_isUpdatingStatus) return; - + _isUpdatingStatus = true; - + try { // 设置状态为连接中 _connectionStatus.value = ConnectionStatus.connecting; update(); - + // 刷新设备列表 final hasDevices = await _bluetoothService.refreshConnectedDevices(); - + // 更新状态 _updateConnectionStatus(); - + // 根据结果更新状态 if (!hasDevices) { Logger.info('刷新设备:未找到设备'); @@ -233,34 +231,34 @@ class HomeController extends GetxController { } catch (e) { Logger.error('刷新设备信息失败: $e'); _connectionStatus.value = ConnectionStatus.failed; - _connectionErrorMsg.value = '无法获取设备信息'; + _connectionErrorMsg.value = 'failedDeviceInformation'.tr; // 无法获取设备信息 update(); } finally { _isUpdatingStatus = false; } } - + // 重新扫描设备 void rescanDevices() { Get.toNamed(Routes.pairing); } - + // 打开翻译功能 void openTranslation() { Get.toNamed(Routes.translation); } - + // 打开AI伴侣功能 void openAIGirlfriend() { - Get.toNamed(Routes.chat); + exploreController.onAIFeatureTap(2); //进入AI女友页面 } - + // 展示AI功能 void exploreAIFeatures() { // 简单实现,直接跳转到AI聊天页面 - openAIGirlfriend(); + Get.toNamed(Routes.chat); } - + // 获取连接状态对应的颜色 Color getConnectionStatusColor() { switch (_connectionStatus.value) { @@ -275,64 +273,62 @@ class HomeController extends GetxController { return Colors.grey; } } - + // 获取连接状态对应的文本 String getConnectionStatusText() { // 如果蓝牙关闭,优先显示蓝牙状态 if (_bluetoothService.bluetoothState.value == 'off') { - return '蓝牙已关闭'; + return 'bluetoothOff'.tr; // 蓝牙已关闭 } - + switch (_connectionStatus.value) { case ConnectionStatus.connected: - return '已连接'; + return 'connected'.tr; // 已连接 case ConnectionStatus.connecting: - return '连接中...'; + return 'connecting'.tr; // 连接中... case ConnectionStatus.failed: - return '连接失败'; + return 'connectionFailure'.tr; // 连接失败 case ConnectionStatus.disconnected: default: - return '未连接'; + return 'unconnected'.tr; // 未连接 } } - + // 获取设备名称 String getDeviceName() { final device = _bluetoothService.currentDevice.value; - return device?.name ?? '未知设备'; + return device?.name ?? 'unknownEquipment'.tr; // 未知设备 } - + // 获取信号强度(模拟数据) String getSignalStrength() { if (!_isConnected.value) { - return '无信号'; + return 'zeroSignal'.tr; // 无信号 } - + // 简单模拟信号强度 final random = DateTime.now().millisecondsSinceEpoch % 3; switch (random) { case 0: - return '良好'; + return 'good'.tr; // 良好 case 1: - return '很好'; + return 'fine'.tr; // 很好 default: - return '极佳'; + return 'excellent'.tr; // 极佳 } } - + // 获取设备ID String getDeviceId() { final device = _bluetoothService.currentDevice.value; - return device?.address ?? '未知'; + return device?.address ?? 'unknown'.tr; // 未知 } - - - + @override void onClose() { // 取消按钮事件流订阅 _mediaButtonSubscription?.cancel(); - + super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 4cc01cd56..ec857c4f1 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -16,7 +16,8 @@ class AnimatedPulsing extends StatefulWidget { _AnimatedPulsingState createState() => _AnimatedPulsingState(); } -class _AnimatedPulsingState extends State with SingleTickerProviderStateMixin { +class _AnimatedPulsingState extends State + with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @@ -68,13 +69,14 @@ class _AnimatedPulsingState extends State with SingleTickerProv } } +//首页视图 class HomeView extends GetView { const HomeView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : Color(0xFFF9FAFC), body: SafeArea( @@ -85,13 +87,11 @@ class HomeView extends GetView { children: [ // 顶部标题栏 _buildAppBar(isDarkMode), - + // 顶部图片 - Center( - child: _buildEarbudsImage(), - ), + _buildEarbudsImage(), SizedBox(height: 20.h), - + // 设备状态卡片 _buildDeviceStatusCard(isDarkMode), SizedBox(height: 20.h), @@ -100,8 +100,6 @@ class HomeView extends GetView { _buildSectionTitle(isDarkMode), SizedBox(height: 16.h), _buildPremiumAILayout(), - - ], ), ), @@ -113,56 +111,85 @@ class HomeView extends GetView { // 顶部标题栏 Widget _buildAppBar(bool isDarkMode) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'DeepVoice', - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.bold, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'DeepSound', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.grey[800], - ), - ), - Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( + ), + ), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: IconButton( - icon: Icon( - Icons.settings_outlined, - size: 16.sp, - color: isDarkMode ? Colors.white70 : Colors.grey[600] - ), - padding: EdgeInsets.zero, - onPressed: () { - Get.toNamed(Routes.settings); - }, - ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), ), ], + ), + child: IconButton( + icon: Icon(Icons.settings_outlined, + size: 16.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600]), + padding: EdgeInsets.zero, + onPressed: () { + Get.toNamed(Routes.settings); + }, + ), + ), + ], ); } // 耳机图片 Widget _buildEarbudsImage() { final bool isDarkMode = Get.isDarkMode; - return Container( + return SizedBox( height: 220.h, - alignment: Alignment.center, - child: Image.asset( - isDarkMode ? 'assets/images/headphone_dark.png' : 'assets/images/headphone_light.png', - height: 180.h, - fit: BoxFit.contain, + child: PageView( + children: [ + Center( + child: SizedBox( + width: double.infinity, + height: 200.h, + child: Image.asset( + isDarkMode + ? 'assets/images/robot_light.png' + : 'assets/images/robot_light.png', + fit: BoxFit.fitWidth, + ), + ), + ), + Center( + child: Image.asset( + isDarkMode + ? 'assets/images/headphone_dark.png' + : 'assets/images/headphone_light.png', + height: 180.h, + fit: BoxFit.contain, + ), + ), + Center( + child: SizedBox( + width: double.infinity, + height: 200.h, + child: Image.asset( + isDarkMode + ? 'assets/images/glasses_light.png' + : 'assets/images/glasses_light.png', + fit: BoxFit.fitWidth, + ), + ), + ), + ], ), ); } @@ -189,7 +216,7 @@ class HomeView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '设备状态', + 'deviceStatus'.tr, // 设备状态 style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w500, @@ -197,52 +224,57 @@ class HomeView extends GetView { ), ), Obx(() => Row( - children: [ - Container( - width: 8.w, - height: 8.w, - decoration: BoxDecoration( - color: controller.getConnectionStatusColor(), - shape: BoxShape.circle, - ), - ), - SizedBox(width: 4.w), - Text( - controller.getConnectionStatusText(), - style: TextStyle( - fontSize: 12.sp, - color: controller.getConnectionStatusColor(), - ), - ), - // 添加刷新按钮 - SizedBox(width: 8.w), - InkWell( - onTap: controller.refreshDeviceInfo, - child: Icon( - Icons.refresh, - size: 16.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - ], - )), + children: [ + Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: controller.getConnectionStatusColor(), + shape: BoxShape.circle, + ), + ), + SizedBox(width: 4.w), + Text( + controller.getConnectionStatusText(), + style: TextStyle( + fontSize: 12.sp, + color: controller.getConnectionStatusColor(), + ), + ), + // 添加刷新按钮 + SizedBox(width: 8.w), + InkWell( + onTap: controller.refreshDeviceInfo, + child: Icon( + Icons.refresh, + size: 16.sp, + color: + isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + )), ], ), - + // 设备信息 SizedBox(height: 12.h), Obx(() { // 如果连接失败且有错误信息,显示错误提示 - if (controller.connectionStatus == ConnectionStatus.failed && + if (controller.connectionStatus == ConnectionStatus.failed && controller.connectionErrorMsg.isNotEmpty) { return Column( children: [ Container( padding: EdgeInsets.all(8.w), decoration: BoxDecoration( - color: isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50], + color: isDarkMode + ? Colors.red[900]!.withOpacity(0.2) + : Colors.red[50], borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: isDarkMode ? Colors.red[800]! : Colors.red[200]!), + border: Border.all( + color: + isDarkMode ? Colors.red[800]! : Colors.red[200]!), ), child: Row( children: [ @@ -257,7 +289,9 @@ class HomeView extends GetView { controller.connectionErrorMsg, style: TextStyle( fontSize: 12.sp, - color: isDarkMode ? Colors.red[300] : Colors.red[800], + color: isDarkMode + ? Colors.red[300] + : Colors.red[800], ), ), ), @@ -276,12 +310,12 @@ class HomeView extends GetView { borderRadius: BorderRadius.circular(8.r), ), ), - child: Text('重新连接'), + child: Text('reconnect'.tr), // 重新连接 ), ], ); } - + // 如果正在连接中,显示连接中提示 if (controller.connectionStatus == ConnectionStatus.connecting) { return Container( @@ -294,12 +328,13 @@ class HomeView extends GetView { height: 24.w, child: CircularProgressIndicator( strokeWidth: 2.w, - valueColor: AlwaysStoppedAnimation(Colors.blue), + valueColor: + AlwaysStoppedAnimation(Colors.blue), ), ), SizedBox(height: 8.h), Text( - '正在连接设备...', + 'connectingDevice'.tr, // 正在连接设备... style: TextStyle( fontSize: 12.sp, color: Colors.blue, @@ -310,119 +345,135 @@ class HomeView extends GetView { ), ); } - - return controller.isConnected - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - '已连接到:${controller.getDeviceName()}', - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + + return controller.isConnected + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + '${'connectedTo'.tr}:${controller.getDeviceName()}', // 已连接到 + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ), - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(4.r), - ), - child: Text( - '信号: ${controller.getSignalStrength()}', - style: TextStyle( - fontSize: 10.sp, - color: Colors.blue[700], + Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, vertical: 2.h), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(4.r), + ), + child: Text( + '${'signal'.tr}: ${controller.getSignalStrength()}', // 信号 + style: TextStyle( + fontSize: 10.sp, + color: Colors.blue[700], + ), ), ), - ), - ], - ), - SizedBox(height: 8.h), - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[50], - borderRadius: BorderRadius.circular(8.r), + ], ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '设备ID:${controller.getDeviceId()}', - style: TextStyle( - fontSize: 10.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + SizedBox(height: 8.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: + isDarkMode ? Colors.grey[900] : Colors.grey[50], + borderRadius: BorderRadius.circular(8.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${'deviceId'.tr}:${controller.getDeviceId()}', // 设备ID + style: TextStyle( + fontSize: 10.sp, + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + ), ), - ), - SizedBox(height: 4.h), - Text( - '固件版本:${controller.firmwareVersion}', - style: TextStyle( - fontSize: 10.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + SizedBox(height: 4.h), + Text( + '${'firmwareVersion'.tr}:${controller.firmwareVersion}', // 固件版本 + style: TextStyle( + fontSize: 10.sp, + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + ), ), - ), - SizedBox(height: 4.h), - // 显示设备状态(已连接/已配对) - Row( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h), - decoration: BoxDecoration( - color: controller.getConnectionStatusColor().withOpacity(0.1), - borderRadius: BorderRadius.circular(4.r), - ), - child: Text( - controller.getConnectionStatusText(), - style: TextStyle( - fontSize: 10.sp, - color: controller.getConnectionStatusColor(), + SizedBox(height: 4.h), + // 显示设备状态(已连接/已配对) + Row( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, vertical: 2.h), + decoration: BoxDecoration( + color: controller + .getConnectionStatusColor() + .withOpacity(0.1), + borderRadius: BorderRadius.circular(4.r), + ), + child: Text( + controller.getConnectionStatusText(), + style: TextStyle( + fontSize: 10.sp, + color: + controller.getConnectionStatusColor(), + ), ), ), - ), - ], - ), - ], - ), - ), - ], - ) - : Container( - padding: EdgeInsets.symmetric(vertical: 8.h), - alignment: Alignment.center, - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[100], - borderRadius: BorderRadius.circular(8.r), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.bluetooth_disabled, - size: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[700], - ), - SizedBox(width: 4.w), - Text( - '未连接设备,点击下方按钮连接', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[700], + ], + ), + ], ), ), ], - ), - ); + ) + : Container( + padding: EdgeInsets.symmetric(vertical: 8.h), + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bluetooth_disabled, + size: 14.sp, + color: + isDarkMode ? Colors.grey[400] : Colors.grey[700], + ), + SizedBox(width: 4.w), + Text( + 'disconnectedDevice'.tr, // 未连接设备,点击下方按钮连接 + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[700], + ), + ), + ], + ), + ); }), - + // 连接按钮(仅在未连接时显示) SizedBox(height: 12.h), Obx(() { @@ -437,10 +488,10 @@ class HomeView extends GetView { borderRadius: BorderRadius.circular(8.r), ), ), - child: Text('连接设备'), + child: Text('connectDevice'.tr), // 连接设备 ); } else { - return SizedBox(); + return const SizedBox(); } }), ], @@ -453,7 +504,7 @@ class HomeView extends GetView { return Row( children: [ Text( - '热门AI体验', + 'topAiExperiences'.tr, // 热门AI体验 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, @@ -480,8 +531,8 @@ class HomeView extends GetView { Expanded( flex: 1, child: _buildFeatureCard( - title: 'AI全能助手', - description: '24小时专属顾问,解答问题、提供建议', + title: 'aiAssistant'.tr, // AI全能助手 + description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议 icon: Icons.assistant, gradient: LinearGradient( begin: Alignment.topLeft, @@ -498,9 +549,9 @@ class HomeView extends GetView { iconColor: const Color(0xFF0066CC), ), ), - + SizedBox(width: 16.w), - + // 右侧两个卡片 Expanded( flex: 1, @@ -508,8 +559,9 @@ class HomeView extends GetView { children: [ // 右上卡片 - 同声翻译 _buildFeatureCard( - title: '同声传译', - description: '80+语言实时翻译,出国无忧', + title: 'simultaneousInterpretation'.tr, // 同声传译 + description: + 'simultaneousInterpretationInfo'.tr, // 80+语言实时翻译,出国无忧 icon: Icons.translate, gradient: LinearGradient( begin: Alignment.topLeft, @@ -524,13 +576,13 @@ class HomeView extends GetView { isDarkMode: isDarkMode, iconColor: const Color(0xFF4CAF50), ), - + SizedBox(height: 5.h), // 减少间距 - + // 右下卡片 - AI女友 _buildFeatureCard( - title: 'AI伴侣', - description: '懂你心意,温暖陪伴', + title: 'aiCompanion'.tr, // 亲子陪伴 + description: 'aiCompanionInfo'.tr, // 懂你心意,温暖陪伴 icon: Icons.favorite, gradient: LinearGradient( begin: Alignment.topLeft, @@ -551,7 +603,7 @@ class HomeView extends GetView { ], ); } - + // 特性卡片构建方法 Widget _buildFeatureCard({ required String title, @@ -574,7 +626,7 @@ class HomeView extends GetView { borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( - color: isDarkMode + color: isDarkMode ? Colors.black.withOpacity(0.2) : Colors.grey.withOpacity(0.1), blurRadius: 8, @@ -585,7 +637,7 @@ class HomeView extends GetView { child: Stack( children: [ // 图标 - if (iconOnTop) + if (iconOnTop) // 大卡片布局 Align( alignment: Alignment.topLeft, @@ -621,7 +673,7 @@ class HomeView extends GetView { ), ), ), - + // 文本内容 if (iconOnTop) // 大卡片布局 - 文本 @@ -646,7 +698,8 @@ class HomeView extends GetView { child: Text( description, style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + color: + isDarkMode ? Colors.grey[400] : Colors.grey[600], fontSize: 12.sp, ), maxLines: 2, @@ -679,7 +732,8 @@ class HomeView extends GetView { child: Text( description, style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + color: + isDarkMode ? Colors.grey[400] : Colors.grey[600], fontSize: 10.sp, ), maxLines: 2, @@ -694,4 +748,4 @@ class HomeView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/login/controllers/facebookLogin.dart b/lib/modules/login/controllers/facebookLogin.dart new file mode 100644 index 000000000..59e701faf --- /dev/null +++ b/lib/modules/login/controllers/facebookLogin.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; +import 'package:get/get.dart'; +import 'package:crypto/crypto.dart'; + +class Facebook { + String? token = ''; + + String name = ''; + String email = ''; + String pictureurl = ''; + String id = ''; + String generateNonce([int length = 32]) { + const charset = + '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._'; + final random = Random.secure(); + return List.generate(length, (_) => charset[random.nextInt(charset.length)]) + .join(); + } + + /// Returns the sha256 hash of [input] in hex notation. + String sha256ofString(String input) { + final bytes = utf8.encode(input); + final digest = sha256.convert(bytes); + return digest.toString(); + } + + Future signInWithFacebook() async { + try { + UserCredential userCredential = await loginFacebook(); + //UserCredential userCredential = await loginGoogle(); + token = await userCredential.user?.getIdToken(); + print('facebook响应数据userCredential: $token'); + if (token != "") { + return true; + } else { + return false; + } + } catch (e) { + print(e); + return false; + } + } + + // FaceBook登录 + // 修改后的 signInWithFacebook + Future loginFacebook() async { + //try { + final rawNonce = generateNonce(); + final nonce = sha256ofString(rawNonce); + final LoginResult loginResult = await FacebookAuth.instance + .login( + loginTracking: LoginTracking.enabled, + nonce: nonce, + ) + .catchError((onError) { + if (kDebugMode) { + //print(onError); + } + throw Exception(onError.message); + }); + if (loginResult.accessToken == null) { + throw Exception(loginResult.message); + } + // Create a credential from the access token + OAuthCredential facebookAuthCredential; + + print("tokenType${loginResult.accessToken!.type}"); + + if (Platform.isIOS) { + // Create a credential from the access token + switch (loginResult.accessToken!.type) { + case AccessTokenType.classic: + final classicToken = loginResult.accessToken as ClassicToken; + // token = classicToken.tokenString; + print('Token 字符串: ${classicToken.tokenString}'); + print('classicToken值: ${classicToken}'); + facebookAuthCredential = FacebookAuthProvider.credential( + classicToken.authenticationToken!, + ); + break; + case AccessTokenType.limited: + final limitedToken = loginResult.accessToken as LimitedToken; + //token = limitedToken.tokenString; + print('Token 字符串: ${limitedToken.tokenString}'); + print('LimitedToken值: ${limitedToken}'); + facebookAuthCredential = OAuthCredential( + providerId: 'facebook.com', + signInMethod: 'oauth', + idToken: limitedToken.tokenString, + rawNonce: rawNonce, + ); + break; + } + } else { + facebookAuthCredential = FacebookAuthProvider.credential( + loginResult.accessToken!.tokenString, + ); + //token = loginResult.accessToken!.tokenString; + } + print('facebookAuthCredential值: ${facebookAuthCredential}'); + if (facebookAuthCredential == null) { + throw Exception("Facebook Login Fail."); + } + final userData = await FacebookAuth.instance.getUserData(); + name = userData["name"].toString(); + + email = userData["email"].toString(); + pictureurl = userData["picture"]["data"]["url"].toString(); + id = userData["id"].toString(); + print("user login facebook ${email}${pictureurl}${name}${id}"); + return FirebaseAuth.instance.signInWithCredential(facebookAuthCredential); + + // final User? user = userCredential.user; // 获取用户信息 + // token = await userCredential.user?.getIdToken(); + // print("irebaseAuth.instance.signInWithCredential ${token}"); + + // return true; + // } catch (e) { + // print("Facebook 登录异常:$e"); + // return false; + // } + } + + // 退出Google登录 + Future signOutFacebook() async { + await FacebookAuth.instance.logOut(); + } +} diff --git a/lib/modules/login/controllers/googleLogin.dart b/lib/modules/login/controllers/googleLogin.dart new file mode 100644 index 000000000..548cbd792 --- /dev/null +++ b/lib/modules/login/controllers/googleLogin.dart @@ -0,0 +1,82 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:get/get.dart'; + +class Google { + String? token = ''; + String? email = ''; + String? displayName = ''; + String? pictureurl = ''; + String? id = ''; + String? accessToken = ''; + + // 登录并返回 + Future signInWithGoogle() async { + try { + // 确保用户已经登录,并获取当前用户的身份验证令牌 + + final currentUser = FirebaseAuth.instance.currentUser; + print('Future响应数据: $currentUser'); + if (currentUser != null) { + // 这里处理您需要使用这个JWT令牌的逻辑,例如将它存储到本地存储中作为凭据。 + email = currentUser.email; + displayName = currentUser.displayName; + id = currentUser.uid; + pictureurl = currentUser.photoURL; + print('Future响应数据pictureurl: $pictureurl'); + token = await currentUser.getIdToken(); + + print('Future响应数据tokenResult: $token'); + // IdTokenResult token1 = await currentUser.getIdTokenResult(); + // print('Future响应数据token1: $token1'); + + return true; + } else { + UserCredential userCredential = await loginGoogle(); + token = await userCredential.user?.getIdToken(); + print('Future响应数据tokenResult: $token'); + if (token != "") { + return true; + } else { + return false; + } + + // logger.d("user login google: ${userCredential.user?.displayName}"); + } + } catch (e) { + print("Google 登录异常:$e"); + return false; + } + } + + // Google登录 + Future loginGoogle() async { + print('loginGoogle:'); + final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); + email = googleUser?.email; + id = googleUser?.id; + displayName = googleUser?.displayName; + pictureurl = googleUser?.photoUrl; + print('loginGoogle: $googleUser'); + print("user login loginGoogle ${email}${id}${displayName}"); + + // Obtain the auth details from the request + final GoogleSignInAuthentication? googleAuth = + await googleUser?.authentication; + + // Create a new credential, + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth?.accessToken, + idToken: googleAuth?.idToken, + ); + + // Once signed in, return the UserCredential + return await FirebaseAuth.instance.signInWithCredential(credential); + } + + // 退出Google登录 + Future signOutGoogle() async { + await FirebaseAuth.instance.signOut(); + await GoogleSignIn().signOut(); + } +} diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index f591b32cc..4dac80fc0 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -1,15 +1,151 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:logger/web.dart'; import '../../../routes/app_routes.dart'; +import 'dart:convert'; +import 'dart:ffi'; +import 'facebookLogin.dart'; +import 'googleLogin.dart'; +import 'weixinLogin.dart'; +import '../../../data/services/network/api.dart'; +import '../../../../data/models/user_Info.dart'; +//import 'package:shared_preferences/shared_preferences.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:device_info_plus/device_info_plus.dart'; enum LoginViewState { - welcome, // 欢迎页面 + welcome, // 欢迎页面 verification // 验证码页面 } class LoginController extends GetxController { static LoginController get to => Get.find(); - + final GetStorage _storage = GetStorage(); + + String _deviceId = ''; + + // 添加国家编码相关状态 + final _selectedCountryCode = ''.obs; + final _selectedCountryFlag = '✉️'.obs; + // 国家编码 Getter + String get selectedCountryCode => _selectedCountryCode.value; + String get selectedCountryFlag => _selectedCountryFlag.value; + // 预设国家编码列表 + final List> countryCodes = [ + {'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位 + {'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位 + + // 按国家代码数字升序排列(去除+号后排序) + {'name': '美国', 'code': '+1', 'flag': '🇺🇸'}, + {'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'}, + {'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'}, + {'name': '英国', 'code': '+44', 'flag': '🇬🇧'}, + {'name': '德国', 'code': '+49', 'flag': '🇩🇪'}, + {'name': '意大利', 'code': '+39', 'flag': '🇮🇹'}, + {'name': '法国', 'code': '+33', 'flag': '🇫🇷'}, + {'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'}, + {'name': '南非', 'code': '+27', 'flag': '🇿🇦'}, + {'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'}, + {'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'}, + {'name': '日本', 'code': '+81', 'flag': '🇯🇵'}, + {'name': '韩国', 'code': '+82', 'flag': '🇰🇷'}, + {'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'}, + {'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'}, + {'name': '泰国', 'code': '+66', 'flag': '🇹🇭'}, + {'name': '越南', 'code': '+84', 'flag': '🇻🇳'}, + {'name': '印度', 'code': '+91', 'flag': '🇮🇳'}, + {'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'}, + {'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'}, + {'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'}, + {'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'}, + {'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'}, + {'name': '巴西', 'code': '+55', 'flag': '🇧🇷'}, + {'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'}, + {'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'}, + {'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'}, + + // 中国地区(保持最后) + {'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'}, + {'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'}, + {'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'}, + + // 添加的其他主要国家 + {'name': '埃及', 'code': '+20', 'flag': '🇪🇬'}, + {'name': '希腊', 'code': '+30', 'flag': '🇬🇷'}, + {'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'}, + {'name': '比利时', 'code': '+32', 'flag': '🇧🇪'}, + {'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'}, + {'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'}, + {'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'}, + {'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'}, + {'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'}, + {'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'}, + {'name': '挪威', 'code': '+47', 'flag': '🇳🇴'}, + {'name': '波兰', 'code': '+48', 'flag': '🇵🇱'}, + {'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'}, + {'name': '智利', 'code': '+56', 'flag': '🇨🇱'}, + {'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'}, + {'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'}, + {'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'}, + {'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'}, + {'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'}, + {'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'}, + {'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'}, + {'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'}, + {'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'}, + {'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'}, + {'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'}, + {'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'}, + {'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'}, + {'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'}, + {'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'}, + {'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'}, + {'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'}, + {'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'}, + {'name': '捷克', 'code': '+420', 'flag': '🇨🇿'}, + {'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'}, + {'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'}, + {'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'}, + {'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'}, + {'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'}, + {'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'}, + {'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'}, + {'name': '海地', 'code': '+509', 'flag': '🇭🇹'}, + {'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'}, + {'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'}, + {'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'}, + {'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'}, + {'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'}, + {'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'}, + {'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'}, + {'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'}, + {'name': '加纳', 'code': '+233', 'flag': '🇬🇭'}, + {'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'}, + {'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'}, + {'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'}, + {'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'}, + {'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'}, + {'name': '以色列', 'code': '+972', 'flag': '🇮🇱'}, + {'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'}, + {'name': '约旦', 'code': '+962', 'flag': '🇯🇴'}, + {'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'}, + {'name': '科威特', 'code': '+965', 'flag': '🇰🇼'}, + {'name': '巴林', 'code': '+973', 'flag': '🇧🇭'}, + {'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'}, + {'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'}, + {'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'}, + {'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'}, + {'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'}, + {'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'}, + {'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'}, + {'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'}, + {'name': '老挝', 'code': '+856', 'flag': '🇱🇦'}, + {'name': '文莱', 'code': '+673', 'flag': '🇧🇳'}, + {'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'}, + {'name': '斐济', 'code': '+679', 'flag': '🇫🇯'}, + {'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'}, + ]; + // 状态管理 final _viewState = LoginViewState.welcome.obs; final _isLoading = false.obs; @@ -17,7 +153,7 @@ class LoginController extends GetxController { final _contactValue = ''.obs; final _displayContact = ''.obs; final _verificationCode = ['', '', '', ''].obs; - + int loginType = -1; // Getters LoginViewState get viewState => _viewState.value; bool get isLoading => _isLoading.value; @@ -25,29 +161,62 @@ class LoginController extends GetxController { String get contactValue => _contactValue.value; String get displayContact => _displayContact.value; List get verificationCode => _verificationCode; - + // 文本控制器 final contactController = TextEditingController(); final verificationCodeController = TextEditingController(); - + @override void onInit() { super.onInit(); _viewState.value = LoginViewState.welcome; + _initDeviceInfo(); } - + @override void onClose() { contactController.dispose(); verificationCodeController.dispose(); super.onClose(); } - + + // 初始化设备信息 + Future _initDeviceInfo() async { + try { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (GetPlatform.isAndroid) { + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + _deviceId = androidInfo.id; + }else if (GetPlatform.isIOS) { + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + _deviceId = iosInfo.identifierForVendor ?? 'unknown'; + } + Logger().d('deviceId: $_deviceId'); + }catch(e){ + Logger().e('获取设备信息失败: $e'); + _deviceId = 'unknown'; + } + + } + + // 更新选中的国家编码 + void updateCountryCode(String code, String flag) { + _selectedCountryCode.value = code; + _selectedCountryFlag.value = flag; + // 根据选择更新输入框类型 + if (code == '') { + contactController.clear(); // 清空当前输入 + loginType = 0; // 设置为邮箱登录类型 + } else { + loginType = 1; // 设置为手机号登录类型 + } + } + // 获取验证码 void getVerificationCode() { if (contactController.text.isEmpty) { Get.snackbar( - '提示', + '提示', '请输入手机号码或邮箱', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.1), @@ -55,19 +224,18 @@ class LoginController extends GetxController { ); return; } - + _contactValue.value = contactController.text; _formatDisplayContact(); - _startCountdown(); _sendVerificationCode(); } - + // 使用验证码直接登录 void loginWithVerificationCode() { //测试期间暂时关闭验证码登录 // if (contactController.text.isEmpty) { // Get.snackbar( - // '提示', + // '提示', // '请输入手机号码或邮箱', // snackPosition: SnackPosition.TOP, // backgroundColor: Colors.red.withOpacity(0.1), @@ -75,10 +243,10 @@ class LoginController extends GetxController { // ); // return; // } - + // if (verificationCodeController.text.isEmpty) { // Get.snackbar( - // '提示', + // '提示', // '请输入验证码', // snackPosition: SnackPosition.TOP, // backgroundColor: Colors.red.withOpacity(0.1), @@ -86,10 +254,10 @@ class LoginController extends GetxController { // ); // return; // } - + // if (verificationCodeController.text.length < 6) { // Get.snackbar( - // '提示', + // '提示', // '请输入完整的验证码', // snackPosition: SnackPosition.TOP, // backgroundColor: Colors.red.withOpacity(0.1), @@ -97,9 +265,9 @@ class LoginController extends GetxController { // ); // return; // } - + _isLoading.value = true; - + // 模拟验证过程 Future.delayed(const Duration(seconds: 2), () { _isLoading.value = false; @@ -107,12 +275,12 @@ class LoginController extends GetxController { Get.offAllNamed(Routes.pairing); }); } - + // 切换到验证码页面 void showVerificationView() { if (contactController.text.isEmpty) { Get.snackbar( - '提示', + '提示', '请输入手机号码或邮箱', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.1), @@ -120,41 +288,48 @@ class LoginController extends GetxController { ); return; } - + _contactValue.value = contactController.text; _formatDisplayContact(); _viewState.value = LoginViewState.verification; - _startCountdown(); + _sendVerificationCode(); } - + // 返回欢迎页面 void showWelcomeView() { _viewState.value = LoginViewState.welcome; _resetVerificationCode(); } - + // 格式化显示的联系方式(隐藏部分字符) void _formatDisplayContact() { String contact = _contactValue.value; - if (contact.contains('@')) { // 邮箱 + if (contact.contains('@')) { + // 邮箱逻辑保持不变 final parts = contact.split('@'); if (parts[0].length > 3) { _displayContact.value = parts[0].substring(0, 3) + '****@' + parts[1]; } else { _displayContact.value = contact; } - } else if (RegExp(r'^\d+$').hasMatch(contact)) { // 手机号 + loginType = 0; + } else if (RegExp(r'^\d+$').hasMatch(contact)) { + // 手机号格式化时包含国家编码 if (contact.length > 7) { - _displayContact.value = '+86 ' + contact.substring(0, 3) + '****' + contact.substring(contact.length - 4); + _displayContact.value = '$selectedCountryCode ' + + contact.substring(0, 3) + + '****' + + contact.substring(contact.length - 4); } else { - _displayContact.value = contact; + _displayContact.value = '$selectedCountryCode $contact'; } + loginType = 1; } else { _displayContact.value = contact; } } - + // 开始倒计时 void _startCountdown() { _countdownSeconds.value = 60; @@ -164,64 +339,264 @@ class LoginController extends GetxController { return _countdownSeconds.value > 0; }); } - + // 发送验证码 - void _sendVerificationCode() { + Future _sendVerificationCode() async { // 这里应该是实际发送验证码的逻辑 // 为了演示,我们只是模拟发送 + if (isEmialContactController() || isPhoneContactController()) { + _startCountdown(); + print(contactController.text); + var response = await Api.getCode({ + "addr": selectedCountryCode + contactController.text, + "vtype": loginType, + }); + if (response != null) { + print("请求后台数据验证码params=${response}"); + //短信已发送 + //EasyLoading.showInfo('codeSend'.tr); + } + } else { + Get.snackbar( + '提示', + '请正确的手机号码或邮箱', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } } - + // 重新发送验证码 void resendVerificationCode() { if (_countdownSeconds.value > 0) return; _startCountdown(); _sendVerificationCode(); } - + // 更新验证码输入 void updateVerificationCode(int index, String value) { + print("updateVerificationCode=${index}${value}"); if (index >= 0 && index < 4) { var newList = [..._verificationCode]; newList[index] = value; _verificationCode.value = newList; } } - + // 重置验证码 void _resetVerificationCode() { _verificationCode.value = ['', '', '', '']; } - + + // 检查邮箱格式 + bool isEmialContactController() { + // 检查每个元素是否为单个字母或数字字符 + final text = contactController.text; + return (text.length > 5 && + RegExp(r'^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') + .hasMatch(text)); + } + +// 检查手机号码格式 + bool isPhoneContactController() { + // 检查每个元素是否为单个字母或数字字符 + final text = contactController.text; + return RegExp(r'^[0-9]+$').hasMatch(text); + } + // 检查验证码是否完整 bool isVerificationCodeComplete() { - return !_verificationCode.contains(''); + // 检查每个元素是否为单个字母或数字字符 + final text = verificationCodeController.text; + print("数据获取Facebook${text.length}"); + return text.length == 4 && RegExp(r'^[0-9]+$').hasMatch(text); } - + // 登录 Future login() async { - // 暂时关闭验证码验证 - // if (!isVerificationCodeComplete()) { - // Get.snackbar( - // '提示', - // '请输入完整的验证码', - // snackPosition: SnackPosition.TOP, - // backgroundColor: Colors.red.withOpacity(0.1), - // colorText: Colors.red, - // ); - // return; - // } - _isLoading.value = true; - + try { - // 模拟验证过程 await Future.delayed(const Duration(seconds: 2)); - - // 验证成功,跳转到配对页面 - Get.offAllNamed(Routes.pairing); + // 模拟验证过程 + + switch (loginType) { + case 0: + { + if (!isVerificationCodeComplete()) { + Get.snackbar( + '提示', + '请输入完整的验证码', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + break; + } + requestLogin({ + "mail": contactController.text, + "phone": selectedCountryCode + contactController.text, + "stype": loginType, + "vcode": verificationCodeController.text, + "ttoken": "", + "phonemodel": "", + "phonemac": _deviceId, + "name": "", + "avatar": "", + }); + break; + } + case 1: + { + if (!isVerificationCodeComplete()) { + Get.snackbar( + '提示', + '请输入完整的验证码', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + break; + } + requestLogin({ + "mail": "", + "phone": selectedCountryCode + contactController.text, + "stype": loginType, + "vcode": verificationCodeController.text, + "ttoken": "", + "phonemodel": "", + "phonemac": _deviceId, + "name": "", + "avatar": "", + }); + break; + } + case 4: + { + print("数据获取Facebook"); + final facebook = Facebook(); + bool authResult = await facebook.signInWithFacebook(); + + if (authResult) { + print("数据获取_Facebook.token${facebook.token}"); + print("数据获取_Facebook.pictureurl${facebook.pictureurl}"); + print("数据获取_Facebook.pictureurl${facebook.email}"); + // SettingsController().userName = Facebook().name; + // SettingsController().avatar = Facebook().pictureurl; + // SettingsController().userId = Facebook().id; + //SettingsController().avatar = Google().email; + + // 验证成功,跳转到配对页面 + requestLogin({ + "mail": facebook.email, + "phone": "", + "stype": 5, + "vcode": "", + "ttoken": facebook.token, + "phonemodel": "", + "phonemac": _deviceId, + "name": facebook.name, + "avatar": facebook.pictureurl, + }); + } + break; + } + case 3: + { + print("数据获取_Google()"); + final google = Google(); + bool authResult = await google.signInWithGoogle(); + print("数据获取_Google().token${authResult}"); + if (authResult) { + print("数据获取_Google.token${google.token}"); + print("数据获取_Google.pictureurl${google.pictureurl}"); + print("数据获取_Google.email${google.email}"); + print("数据获取_Google.email${google.displayName}"); + // 验证成功,跳转到配对页面 + // SettingsController().userName = Google().displayName!; + // //SettingsController().avatar = Google().email; + // SettingsController().userId = Google().id!; + requestLogin({ + "mail": google.email, + "phone": "", + "stype": 5, + "vcode": "", + "ttoken": google.token, + "phonemodel": "", + "phonemac": _deviceId, + "name": google.displayName, + "avatar": google.pictureurl, + }); + } + + break; + } + case 2: + { + print("数据获取_FlWX()"); + + bool authResult = await FlWX().authBy( + NormalAuth( + scope: "snsapi_userinfo", + state: "wechat_sdk_demo_test", + ), + getUserInfo: true, // 按需获取用户信息 + ); + + if (authResult) { + // 确保 FlWX() 返回的是同一个实例,并且 token 已经被设置 + // 这里假设 FlWX().token 在 authBy 成功后被设置 + print("数据获取_FlWX().token${FlWX().token}"); + // SettingsController().userName = FlWX().nickname; + // SettingsController().avatar = FlWX().headimgurl; + // SettingsController().userId = FlWX().openid; + requestLogin({ + "mail": "", + "phone": "", + "stype": loginType, + "vcode": "", + "ttoken": FlWX().code, + "phonemodel": "", + "phonemac": _deviceId, + "name": FlWX().nickname, + "avatar": FlWX().headimgurl, + }); + // 验证成功,跳转到配对页面 + } + break; + } + case 6://游客登录 + { + requestLogin({ + "mail": "", + "phone": "", + "stype": loginType, + "vcode": "", + "ttoken": "", + "phonemodel": "", + "phonemac": _deviceId, + "name": "", + "avatar": "", + }); + break; + } + default: + { + Get.snackbar( + '错误', + '登录失败,请重试', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + break; + } + } } catch (e) { Get.snackbar( - '错误', + '错误', '登录失败,请重试', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.1), @@ -231,16 +606,67 @@ class LoginController extends GetxController { _isLoading.value = false; } } - + + // 使用登陆请求服务器 + Future requestLogin(Map params) async { + var response = await Api.login({ + "mail": params["mail"], + "phone": params["phone"], + "stype": params["stype"], + "vcode": params["vcode"], + "ttoken": params["ttoken"], + "phonemodel": params["phonemodel"], + "phonemac": params["phonemac"], + "name": params["name"], + "avatar": params["avatar"], + }); + if (response != null) { + print("请求后台数据login回复数据=${response}"); + + // final instance = await GetStorage(); + await _storage.write('loginInfo', jsonEncode(response)); + // 2. 将响应数据转换为User对象 + try { + print('响应数据: ${response.toString()}'); // 调试输出原始数据 + + if (response['token'].toString() != '') { + // 新增:检查 user 字段是否存在且类型正确 + // if (response.containsKey('data') && response['data'] is Map) { + final user = User.fromJson(response); + print('用户初始化成功: ${response['token'].toString()}'); + await _storage.write("logintoken", response['token'].toString()); + // 存储到 SharedPreferences (仅存储用户数据) + //final prefs = await GetStorage(); + await _storage.write( + 'user', jsonEncode(user.toJson())); // 使用 user.toJson() + + Get.offAllNamed(Routes.pairing); + // } else { + // throw FormatException('API 响应缺少 user 字段'); + // } + } else { + throw Exception('接口返回错误: ${response['msg']}'); + } + } catch (e, stack) { + // 新增:打印堆栈跟踪 + print('用户数据处理失败: $e'); + print('堆栈跟踪: $stack'); + Get.snackbar('错误', '用户数据解析失败,请检查网络'); + } + + // Get.offAll(() => IndexWidget()); + } + } + // 使用密码登录 void usePasswordLogin() { // 这里可以实现密码登录的逻辑 Get.snackbar( - '提示', + '提示', '密码登录功能尚未实现', snackPosition: SnackPosition.TOP, backgroundColor: Colors.blue.withOpacity(0.1), colorText: Colors.blue, ); } -} \ No newline at end of file +} diff --git a/lib/modules/login/controllers/weixinLogin.dart b/lib/modules/login/controllers/weixinLogin.dart new file mode 100644 index 000000000..38819a60a --- /dev/null +++ b/lib/modules/login/controllers/weixinLogin.dart @@ -0,0 +1,923 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:fluwx/fluwx.dart'; +export 'package:fluwx/fluwx.dart'; +import 'package:http/http.dart' as http; + +typedef FlWeChatResponseCallback = void Function(WeChatResponse response); + +/// 支持成功 +typedef FlWeChatPayResponseResultCallback = void Function(); + +/// 支付结果回调 +typedef FlWeChatPayResponseCallback = void Function( + WeChatPaymentResponse response); + +/// LaunchFromWX 回调 +typedef FlWeChatLaunchFromWXRequestCallback = void Function( + WeChatLaunchFromWXRequest response); + +/// OpenBusinessWebView 回调 +typedef FlWeChatOpenBusinessWebViewResponseCallback = void Function( + WeChatOpenBusinessWebviewResponse response); + +/// WeChatOpenBusinessView 回调 +typedef FlWeChatOpenBusinessViewResponseCallback = void Function( + WeChatOpenBusinessViewResponse response); + +/// Share 回调 +typedef FlWeChatShareResponseCallback = void Function( + WeChatShareResponse response); + +/// Auth 回调 +typedef FlWeChatAuthResponseCallback = Function(WeChatAuthResponse response); + +/// Auth token 回调 +typedef FlWeChatAuthResponseTokenCallback = Function( + WeChatAuthResponse response, WXTokenModel token); + +/// Auth userinfo 回调 +typedef FlWeChatAuthResponseUserinfoCallback = Function( + WeChatAuthResponse response, + WXTokenModel token, + WXUserModel userInfo, +); + +/// 显示 toast +typedef FlWXToastCallback = void Function(String msg); + +/// 日志 打印 +typedef FlWXLogCallback = void Function(String msg); + +/// http 请求 +typedef FlWXHTTPCallback = Future Function(String url); + +class FlWX { + factory FlWX() => _singleton ??= FlWX._(); + + FlWX._(); + + static FlWX? _singleton; + + final Fluwx fluwx = Fluwx(); + + String? _appId; + + /// 获取当前设置的 appId + String? get appId => _appId; + + String? _appSecret; + + /// 获取当前设置的 appSecret + String? get appSecret => _appSecret; + + String _appName = ''; + String token = ''; + String code = ''; + String refresh_token = ''; + String openid = ''; + + String nickname = ''; + String sex = ''; + String headimgurl = ''; + String city = ''; + // 参数 说明 + // access_token 接口调用凭证 + // expires_in access_token 接口调用凭证超时时间,单位(秒) + // refresh_token 用户刷新 access_token + // openid 授权用户唯一标识 + // scope 用户授权的作用域(snsapi_userinfo) + // unionid 当且仅当该移动应用已获得该用户的 userinfo 授权时,才会出现该字段 + /// callback params + FlWXCallbackParams? _params; + + /// 分享时的缩略图 + Uint8List? _shareThumbnail; + + /// register + bool _isRegister = false; + + Future register({ + required String appId, + required FlWXCallbackParams params, + String? universalLink, + bool doOnIOS = true, + bool doOnAndroid = true, + + /// 用于获取用户信息 + String? appSecret, + + /// app名称(用于分享时使用) + String appName = '', + + /// 分享时的缩略图 + Uint8List? shareThumbnail, + }) { + assert(appId.isNotEmpty); + if (universalLink != null) assert(universalLink.isNotEmpty); + if (appSecret != null) { + assert(appSecret.isNotEmpty); + } + _isRegister = true; + _appId = appId; + _appSecret = appSecret; + _appName = appName; + _shareThumbnail = shareThumbnail; + _params = params; + return fluwx.registerApi( + appId: appId, + doOnAndroid: doOnAndroid, + doOnIOS: doOnIOS, + universalLink: universalLink, + ); + } + + /// 获取微信 用户token + String _getTokenUrl(String code) { + print("_getTokenUrl 数据获取${_appId},${_appSecret}"); + assert( + _appId != null && + _appId!.isNotEmpty && + _appSecret != null && + _appSecret!.isNotEmpty, + ); + return 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=$_appId&secret=$_appSecret&code=$code&grant_type=authorization_code'; + } + + /// 获取微信 用户token + String _getRefreshTokenUrl(String code) { + print("_getRefreshTokenUrl 数据获取${_appId},${_appSecret}"); + assert( + _appId != null && + _appId!.isNotEmpty && + _appSecret != null && + _appSecret!.isNotEmpty, + ); + return 'https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=$_appId&grant_type=refresh_token&&refresh_token=$refresh_token'; + } + + // GET请求示例 + Future> fetchData(String Url) async { + try { + final response = await http.get(Uri.parse(Url)); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + print('GET响应数据: $data'); + + return data; + } else { + print('GET请求失败,状态码: ${response.statusCode}'); + return {}; + } + } catch (e) { + print('GET请求异常: $e'); + return {}; + } + } + + // POST请求示例 + Future postData() async { + try { + final response = await http.post( + Uri.parse('https://jsonplaceholder.typicode.com/posts'), + headers: { + 'Content-Type': 'application/json', + }, // 设置请求头‌:ml-citation{ref="6" data="citationList"} + body: jsonEncode({ + // 编码JSON数据‌:ml-citation{ref="3" data="citationList"} + 'title': 'foo', + 'body': 'bar', + 'userId': 1, + }), + ); + + if (response.statusCode == 201) { + final data = jsonDecode(response.body); + print('POST响应数据: $data'); + return true; + } else { + print('POST请求失败,状态码: ${response.statusCode}'); + return false; + } + } catch (e) { + print('POST请求异常: $e'); + return false; + } + } + + /// 获取微信 用户信息 + String _getUserInfoUrl({required String openId, required String token}) => + 'https://api.weixin.qq.com/sns/userinfo?access_token=$token&openid=$openId'; + //检验授权凭证(access_token)是否有效 + String _getUserTokenEffertive({ + required String openId, + required String token, + }) => + 'https://api.weixin.qq.com/sns/auth?access_token=$token&openid=$openId'; + + /// 是否安装微信 + Future get isInstalled async { + assert(_isRegister, '请先调用 FlWX().register()'); + final installed = await fluwx.isWeChatInstalled; + if (!installed) _params!.onToast?.call('该服务需要先安装微信'); + return installed; + } + + /// isSupportOpenBusinessView + Future get isSupportOpenBusinessView async { + if (!await isInstalled) return false; + return await fluwx.isSupportOpenBusinessView; + } + + /// 添加移除监听 + FluwxCancelable _onListener(FlWeChatResponseCallback onResponse) { + late FluwxCancelable cancelable; + subscriber(response) { + onResponse(response); + Future.delayed(const Duration(milliseconds: 500), () { + cancelable.cancel(); + }); + } + + return cancelable = fluwx.addSubscriber(subscriber); + } + + Future getExtMsg() async { + if (!await isInstalled) return null; + return await fluwx.getExtMsg(); + } + + Future attemptToResumeMsgFromWx({ + Function(WeChatShowMessageFromWXRequest response)? onResponse, + }) async { + if (!await isInstalled) return; + await fluwx.attemptToResumeMsgFromWx(); + if (onResponse != null) { + _onListener((response) { + if (response is WeChatShowMessageFromWXRequest) onResponse(response); + }); + } + } + +// graph TD +// A[开始] --> B{微信是否安装?} +// B -->|否| C[返回 false] +// B -->|是| D[注册监听器] +// D --> E[调起微信授权] +// E -->|失败| F[触发 completer(false)] +// E -->|成功| G[等待微信响应] +// G -->|用户取消/接口错误| H[触发 completer(false)] +// G -->|成功获取 code| I[获取 Token] +// I -->|Token 失败| H +// I -->|Token 成功| J[刷新 Token] +// J -->|刷新失败| H +// J -->|刷新成功| K{需用户信息?} +// K -->|是| L[获取用户信息] +// L -->|信息失败| H +// K -->|否| M[触发 completer(true)] +// L -->|信息成功| M + Future authBy( + AuthType which, { + FlWeChatAuthResponseCallback? onResponse, + FlWeChatAuthResponseTokenCallback? onToken, + FlWeChatAuthResponseUserinfoCallback? onUserinfo, + bool getUserInfo = false, + }) async { + if (!await isInstalled) { + _params?.onToast?.call('微信未安装'); + return false; + } + + final completer = Completer(); // 创建 Completer 控制 Future 完成时机 + FluwxCancelable? subscription; // 用于取消监听 + + subscription = _onListener((response) async { + if (response is WeChatAuthResponse) { + onResponse?.call(response); + if (response.isSuccessful && response.code != null) { + code = response.code!; + completer.complete(true); // 所有操作成功时完成 Future + + // try { + // code = response.code!; + // // 获取 token + // final tokenData = await fetchData(_getTokenUrl(response.code!)); + // if (tokenData.isEmpty) { + // _params!.onLog?.call('Token 数据获取失败'); + // completer.complete(false); // 失败时完成 Future + // return; + // } + + // // 更新 token + // token = tokenData["access_token"].toString(); + // openid = tokenData["openid"].toString(); + // // Istoken = await fetchData( + // // _getUserTokenEffertive(openId: openid, token: token), + // // ); // 触发GET请求 + // // print("数据获取_getUserTokenEffertive${Istoken}"); + // // 可选:刷新 token 或获取用户信息 + // if (getUserInfo) { + // final userInfo = await fetchData( + // _getUserInfoUrl(openId: openid, token: token)); + // List bytes = latin1.encode(userInfo["nickname"].toString()); + // print("ISO-8859-1 字节数组: $bytes"); + // nickname = String.fromCharCodes(bytes); + // // nickname = userInfo["nickname"].toString(); + // sex = userInfo["sex"].toString(); + // headimgurl = userInfo["headimgurl"].toString(); + // city = userInfo["city"].toString(); + // print("_getUserInfoUrl${nickname}"); + // // onUserinfo?.call(userInfo); + // } + + // completer.complete(true); // 所有操作成功时完成 Future + // } catch (e) { + // _params!.onLog?.call('授权流程异常: $e'); + // completer.complete(false); + // } + } else { + completer.complete(false); // 用户取消授权或授权失败 + } + } + }); + + // 调起微信授权 + final result = await fluwx.authBy(which: which); + if (!result) { + subscription.cancel(); // 调起失败时取消监听 + _params!.onToast?.call('微信授权失败'); + completer.complete(false); + } + + return completer.future; // 返回由回调控制的 Future + } + + Future autoDeduct( + AutoDeduct data, { + FlWeChatResponseCallback? onResponse, + }) async { + final cancelable = _onListener((response) { + onResponse?.call(response); + }); + final result = await fluwx.autoDeduct(data: data); + if (!result) { + cancelable.cancel(); + _params!.onToast?.call('微信签约失败'); + } + return result; + } + + /// 调用微信支付 + Future pay( + PayType which, { + FlWeChatPayResponseResultCallback? onSuccess, + FlWeChatPayResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatPaymentResponse) { + onResponse?.call(response); + _params!.onLog?.call('微信支付 result code: ${response.errCode} '); + if (response.errCode == 0) { + _params!.onToast?.call('支付成功'); + onSuccess?.call(); + } else if (response.errCode == -2) { + _params!.onToast?.call('已取消支付'); + } else { + _params!.onToast?.call('支付失败'); + } + } + }); + final result = await fluwx.pay(which: which); + if (!result) { + cancelable.cancel(); + _params!.onToast?.call('支付失败'); + } + return result; + } + + /// ****************** open ****************** /// + + Future open(OpenType target) async { + if (!await isInstalled) return false; + return fluwx.open(target: target); + } + + Future openWeChatApp({ + FlWeChatLaunchFromWXRequestCallback? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatLaunchFromWXRequest) onResponse?.call(response); + }); + final result = await fluwx.open(target: WeChatApp()); + if (!result) cancelable.cancel(); + return result; + } + + Future openBrowser( + String url, { + FlWeChatOpenBusinessWebViewResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatOpenBusinessWebviewResponse) { + onResponse?.call(response); + } + }); + final result = await fluwx.open(target: Browser(url)); + if (!result) cancelable.cancel(); + return result; + } + + Future openRankList() async { + if (!await isInstalled) return false; + return fluwx.open(target: RankList()); + } + + Future openBusinessView({ + required String businessType, + required String query, + FlWeChatOpenBusinessViewResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatOpenBusinessViewResponse) { + onResponse?.call(response); + } + }); + final result = await fluwx.open( + target: BusinessView(businessType: businessType, query: query), + ); + if (!result) cancelable.cancel(); + return result; + } + + Future openInvoice({ + required String appId, + required String cardType, + String locationId = '', + String cardId = '', + String canMultiSelect = '', + Function(WeChatOpenInvoiceResponse response)? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatOpenInvoiceResponse) onResponse?.call(response); + }); + final result = await fluwx.open( + target: Invoice( + appId: appId, + cardType: cardType, + locationId: locationId, + cardId: cardId, + canMultiSelect: canMultiSelect, + ), + ); + if (!result) cancelable.cancel(); + return result; + } + + Future openCustomerServiceChat({ + required String corpId, + required String url, + Function(WeChatOpenCustomerServiceChatResponse response)? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatOpenCustomerServiceChatResponse) { + onResponse?.call(response); + } + }); + final result = await fluwx.open( + target: CustomerServiceChat(corpId: corpId, url: url), + ); + if (!result) cancelable.cancel(); + return result; + } + + Future openMiniProgram( + String username, { + String? path, + Function(WeChatLaunchMiniProgramResponse response)? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatLaunchMiniProgramResponse) { + onResponse?.call(response); + } + }); + final result = await fluwx.open( + target: MiniProgram(username: username, path: path), + ); + if (!result) { + cancelable.cancel(); + _params?.onToast?.call('小程序打开失败'); + } + return result; + } + + Future openSubscribeMessage({ + required String appId, + required int scene, + required String templateId, + String? reserved, + Function(WeChatSubscribeMsgResponse response)? onResponse, + }) async { + if (!await isInstalled) return false; + final cancelable = _onListener((response) { + if (response is WeChatSubscribeMsgResponse) onResponse?.call(response); + }); + final result = await fluwx.open( + target: SubscribeMessage( + appId: appId, + scene: scene, + templateId: templateId, + reserved: reserved, + ), + ); + if (!result) cancelable.cancel(); + return result; + } + + /// ****************** share ****************** /// + + Future share( + WeChatShareModel what, { + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse(fluwx.share(what), onResponse: onResponse); + } + + Future shareText( + String source, { + String? title, + String? description, + WeChatScene scene = WeChatScene.session, + String? messageExt, + String? messageAction, + String? mediaTagName, + String? msgSignature, + Uint8List? thumbData, + String? thumbDataHash, + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse( + fluwx.share( + WeChatShareTextModel( + source, + title: title ?? _appName, + description: description ?? _appName, + scene: scene, + messageExt: messageExt, + messageAction: messageAction, + mediaTagName: mediaTagName, + msgSignature: msgSignature, + thumbData: thumbData ?? _shareThumbnail, + thumbDataHash: thumbDataHash, + ), + ), + onResponse: onResponse, + ); + } + + Future shareMiniProgram( + String webPage, { + required String webPageUrl, + WXMiniProgramType miniProgramType = WXMiniProgramType.release, + required String userName, + String path = "/", + String? title, + String? description, + bool withShareTicket = false, + String? messageExt, + String? messageAction, + String? mediaTagName, + String? msgSignature, + Uint8List? thumbData, + String? thumbDataHash, + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse( + fluwx.share( + WeChatShareMiniProgramModel( + webPageUrl: webPageUrl, + userName: userName, + title: title ?? _appName, + description: description ?? _appName, + messageExt: messageExt, + messageAction: messageAction, + mediaTagName: mediaTagName, + msgSignature: msgSignature, + thumbData: thumbData ?? _shareThumbnail, + thumbDataHash: thumbDataHash, + ), + ), + onResponse: onResponse, + ); + } + + Future shareImage( + WeChatImageToShare source, { + String? title, + String? description, + WeChatScene scene = WeChatScene.session, + String? messageExt, + String? messageAction, + String? mediaTagName, + String? msgSignature, + Uint8List? thumbData, + String? thumbDataHash, + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse( + fluwx.share( + WeChatShareImageModel( + source, + title: title ?? _appName, + description: description ?? _appName, + scene: scene, + messageExt: messageExt, + messageAction: messageAction, + mediaTagName: mediaTagName, + msgSignature: msgSignature, + thumbData: thumbData ?? _shareThumbnail, + thumbDataHash: thumbDataHash, + ), + ), + onResponse: onResponse, + ); + } + + Future shareMusic({ + String? musicUrl, + String? musicDataUrl, + String? musicLowBandUrl, + String? musicLowBandDataUrl, + String? title, + String? description, + WeChatScene scene = WeChatScene.session, + String? messageExt, + String? messageAction, + String? mediaTagName, + String? msgSignature, + Uint8List? thumbData, + String? thumbDataHash, + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse( + fluwx.share( + WeChatShareMusicModel( + musicUrl: musicUrl, + musicDataUrl: musicDataUrl, + musicLowBandUrl: musicLowBandUrl, + musicLowBandDataUrl: musicLowBandDataUrl, + title: title ?? _appName, + description: description ?? _appName, + scene: scene, + messageExt: messageExt, + messageAction: messageAction, + mediaTagName: mediaTagName, + msgSignature: msgSignature, + thumbData: thumbData ?? _shareThumbnail, + thumbDataHash: thumbDataHash, + ), + ), + onResponse: onResponse, + ); + } + + Future shareVideo( + String videoUrl, { + String? title, + String? description, + WeChatScene scene = WeChatScene.session, + String? messageExt, + String? messageAction, + String? mediaTagName, + String? msgSignature, + Uint8List? thumbData, + String? thumbDataHash, + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse( + fluwx.share( + WeChatShareVideoModel( + title: title ?? _appName, + videoUrl: videoUrl, + description: description ?? _appName, + scene: scene, + messageExt: messageExt, + messageAction: messageAction, + mediaTagName: mediaTagName, + msgSignature: msgSignature, + thumbData: thumbData ?? _shareThumbnail, + thumbDataHash: thumbDataHash, + ), + ), + onResponse: onResponse, + ); + } + + Future shareWebPage( + String webPage, { + String? title, + String? description, + WeChatScene scene = WeChatScene.session, + String? messageExt, + String? messageAction, + String? mediaTagName, + String? msgSignature, + Uint8List? thumbData, + String? thumbDataHash, + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse( + fluwx.share( + WeChatShareWebPageModel( + webPage, + title: title ?? _appName, + description: description ?? _appName, + scene: scene, + messageExt: messageExt, + messageAction: messageAction, + mediaTagName: mediaTagName, + msgSignature: msgSignature, + thumbData: thumbData ?? _shareThumbnail, + thumbDataHash: thumbDataHash, + ), + ), + onResponse: onResponse, + ); + } + + Future shareFile( + File source, { + required String suffix, + String? title, + String? description, + WeChatScene scene = WeChatScene.session, + String? messageExt, + String? messageAction, + String? mediaTagName, + String? msgSignature, + Uint8List? thumbData, + String? thumbDataHash, + FlWeChatShareResponseCallback? onResponse, + }) async { + if (!await isInstalled) return false; + return _shareResponse( + fluwx.share( + WeChatShareFileModel( + WeChatFile.file(source, suffix: suffix), + title: title ?? _appName, + description: description ?? _appName, + scene: scene, + messageExt: messageExt, + messageAction: messageAction, + mediaTagName: mediaTagName, + msgSignature: msgSignature, + thumbData: thumbData ?? _shareThumbnail, + thumbDataHash: thumbDataHash, + ), + ), + onResponse: onResponse, + ); + } + + Future _shareResponse( + Future share, { + FlWeChatShareResponseCallback? onResponse, + }) async { + final cancelable = _onListener((response) { + if (response is WeChatShareResponse) onResponse?.call(response); + }); + final result = await share; + if (!result) cancelable.cancel(); + return result; + } +} + +class FlWXCallbackParams { + FlWXCallbackParams({this.onToast, this.onLog, this.onHttp}); + + /// toast 显示 + final FlWXToastCallback? onToast; + + /// 日志 打印 + final FlWXLogCallback? onLog; + + /// http 请求 返回 json + final FlWXHTTPCallback? onHttp; +} + +class WXUserModel { + final String? openid; + final String? nickname; + final int? sex; + final String? language; + final String? city; + final String? province; + final String? country; + final String? headImgUrl; + final List? privilege; + final String? unionId; + + WXUserModel.fromJson(Map json) + : openid = json['openid'] as String?, + nickname = json['nickname'] as String?, + sex = json['sex'] as int?, + language = json['language'] as String?, + city = json['city'] as String?, + province = json['province'] as String?, + country = json['country'] as String?, + headImgUrl = json['headimgurl'] as String?, + privilege = json['privilege'] as List?, + unionId = json['unionid'] as String?; + + Map toMap() => { + 'openid': openid, + 'unionId': unionId, + 'nickname': nickname, + 'sex': sex, + 'headImgUrl': headImgUrl, + 'privilege': privilege, + 'language': language, + 'city': city, + 'province': province, + 'country': country, + }; +} + +class WXTokenModel { + final String? accessToken; + final int? expiresIn; + final String? refreshToken; + final String? openid; + final String? scope; + final String? unionId; + + WXTokenModel.fromJson(Map json) + : accessToken = json['access_token'] as String?, + expiresIn = json['expires_in'] as int?, + refreshToken = json['refresh_token'] as String?, + openid = json['openid'] as String?, + scope = json['scope'] as String?, + unionId = json['unionid'] as String?; + + Map toMap() => { + 'openid': openid, + 'unionId': unionId, + 'accessToken': accessToken, + 'expiresIn': expiresIn, + 'refreshToken': refreshToken, + 'scope': scope, + }; +} + +extension ExtensionMap on Map { + Payment? toPayment() { + try { + final appId = (this['appId'] ?? this['appid']); + final partnerId = (this['partnerId'] ?? + this['partnerid'] ?? + this['partner']) as String?; + final prepayId = (this['prepayId'] ?? this['prepayid'] ?? this['prepay']); + final packageValue = + (this['packageValue'] ?? this['packagevalue'] ?? this['package']); + final nonceStr = (this['nonceStr'] ?? this['noncestr'] ?? this['nonce']); + final timestamp = (this['timestamp'] ?? this['timeStamp']); + final sign = this['sign']; + if (appId != null && + partnerId != null && + prepayId != null && + packageValue != null && + nonceStr != null && + timestamp != null && + sign != null) { + return Payment( + appId: appId.toString(), + partnerId: partnerId.toString(), + prepayId: prepayId.toString(), + packageValue: packageValue.toString(), + nonceStr: nonceStr.toString(), + timestamp: int.parse(timestamp!.toString()), + sign: sign.toString(), + ); + } + } catch (e) { + FlWX()._params?.onLog?.call(e.toString()); + } + return null; + } +} diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 9fda142f7..58a707a37 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../controllers/login_controller.dart'; -import '../../../core/themes/app_theme.dart'; +import '../../common/webview_page.dart'; class LoginView extends GetView { const LoginView({Key? key}) : super(key: key); @@ -11,7 +11,7 @@ class LoginView extends GetView { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], body: SafeArea( @@ -33,11 +33,11 @@ class LoginView extends GetView { // 应用标志和标题 SizedBox(height: 32.h), _buildAppLogo(isDarkMode), - + // 欢迎文本 SizedBox(height: 32.h), _buildWelcomeText(isDarkMode), - + // 输入框和继续按钮 SizedBox(height: 24.h), _buildInputField(isDarkMode), @@ -45,11 +45,11 @@ class LoginView extends GetView { _buildVerificationInputField(isDarkMode), SizedBox(height: 16.h), _buildContinueButton(isDarkMode), - + // 社交媒体登录 SizedBox(height: 32.h), _buildSocialLoginSection(isDarkMode), - + // 用户协议 SizedBox(height: 40.h), _buildTermsAndPrivacy(isDarkMode), @@ -71,23 +71,23 @@ class LoginView extends GetView { children: [ // 返回按钮 _buildBackButton(isDarkMode), - + // 验证码标题 SizedBox(height: 24.h), _buildVerificationTitle(isDarkMode), - + // 验证码输入框 SizedBox(height: 32.h), _buildVerificationInputs(context, isDarkMode), - + // 重新发送验证码 SizedBox(height: 16.h), _buildResendButton(isDarkMode), - + // 登录按钮 SizedBox(height: 32.h), _buildLoginButton(isDarkMode), - + // 使用密码登录 SizedBox(height: 16.h), _buildUsePasswordButton(isDarkMode), @@ -128,7 +128,7 @@ class LoginView extends GetView { ), SizedBox(height: 16.h), Text( - 'DeepVoice', + 'DeepSound', style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold, @@ -137,7 +137,7 @@ class LoginView extends GetView { ), SizedBox(height: 8.h), Text( - '连接世界,超越声音', + 'slogan'.tr, // 连接世界,超越声音 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.black54, @@ -152,7 +152,7 @@ class LoginView extends GetView { return Column( children: [ Text( - '欢迎使用', + 'welcome'.tr, // 欢迎使用 style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w600, @@ -161,7 +161,7 @@ class LoginView extends GetView { ), SizedBox(height: 4.h), Text( - '请使用手机号或邮箱登录/注册', + 'loginTips'.tr, // 请使用手机号或邮箱登录/注册 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.black54, @@ -175,24 +175,62 @@ class LoginView extends GetView { Widget _buildInputField(bool isDarkMode) { return Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900]!.withOpacity(0.4) : Colors.grey.withOpacity(0.1), + color: isDarkMode + ? Colors.grey[900]!.withOpacity(0.4) + : Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(16.r), ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h), child: Row( children: [ - Icon( - Icons.person_outline, - color: isDarkMode ? Colors.grey[400] : Colors.black54, - size: 20.sp, + // 添加国家选择按钮 + InkWell( + onTap: () => _showCountryPicker(isDarkMode), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx(() => Text( + '${controller.selectedCountryFlag}${controller.selectedCountryCode}', + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + )), + Icon( + Icons.arrow_drop_down, + color: isDarkMode ? Colors.grey[400] : Colors.black54, + size: 20.sp, + ), + ], + ), + ), ), - SizedBox(width: 12.w), + // 分隔线 + Container( + height: 20.h, + width: 1, + color: isDarkMode ? Colors.grey[800] : Colors.grey[300], + margin: EdgeInsets.symmetric(horizontal: 8.w), + ), + // Icon( + // Icons.person_outline, + // color: isDarkMode ? Colors.grey[400] : Colors.black54, + // size: 20.sp, + // ), + //SizedBox(width: 12.w), Expanded( child: TextField( controller: controller.contactController, + // keyboardType: controller.selectedCountryFlag == '✉️' + // ? TextInputType.emailAddress + // : TextInputType.phone, decoration: InputDecoration( - hintText: '手机号码或邮箱', + hintText: controller.selectedCountryFlag == '✉️' + ? 'email'.tr // 需要在翻译文件中添加 + : 'phoneNumber'.tr, // 需要在翻译文件中添加 hintStyle: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[600] : Colors.black38, @@ -213,11 +251,78 @@ class LoginView extends GetView { ); } + // 添加国家选择底部弹窗 + void _showCountryPicker(bool isDarkMode) { + Get.bottomSheet( + Container( + height: 400.h, + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.r), + topRight: Radius.circular(20.r), + ), + ), + child: Column( + children: [ + Text( + 'selectCountry'.tr, // 选择国家/地区 + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 16.h), + Expanded( + child: ListView.builder( + itemCount: controller.countryCodes.length, + itemBuilder: (context, index) { + final country = controller.countryCodes[index]; + return ListTile( + onTap: () { + controller.updateCountryCode( + country['code']!, + country['flag']!, + ); + Get.back(); + }, + leading: Text( + country['flag']!, + style: TextStyle(fontSize: 24.sp), + ), + title: Text( + '${country['name']}'.tr, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + trailing: Text( + country['code']!, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ); + }, + ), + ), + ], + ), + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + ); + } + // 验证码输入框 Widget _buildVerificationInputField(bool isDarkMode) { return Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900]!.withOpacity(0.4) : Colors.grey.withOpacity(0.1), + color: isDarkMode + ? Colors.grey[900]!.withOpacity(0.4) + : Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(16.r), ), child: Padding( @@ -236,7 +341,7 @@ class LoginView extends GetView { keyboardType: TextInputType.number, maxLength: 6, decoration: InputDecoration( - hintText: '验证码', + hintText: 'verificationCode'.tr, // 验证码 hintStyle: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[600] : Colors.black38, @@ -253,25 +358,26 @@ class LoginView extends GetView { ), ), Obx(() => TextButton( - onPressed: controller.countdownSeconds > 0 - ? null - : controller.getVerificationCode, - style: TextButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 16.w), - foregroundColor: const Color(0xFF3B82F6), - backgroundColor: Colors.transparent, - disabledForegroundColor: isDarkMode ? Colors.grey[600] : Colors.grey[400], - ), - child: Text( - controller.countdownSeconds > 0 - ? '${controller.countdownSeconds}s后重新获取' - : '获取验证码', - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - ), - ), - )), + onPressed: controller.countdownSeconds > 0 + ? null + : controller.getVerificationCode, + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(horizontal: 16.w), + foregroundColor: const Color(0xFF3B82F6), + backgroundColor: Colors.transparent, + disabledForegroundColor: + isDarkMode ? Colors.grey[600] : Colors.grey[400], + ), + child: Text( + controller.countdownSeconds > 0 + ? '${controller.countdownSeconds}${'reacquireAfter'.tr}' // s后重新获取 + : 'getCode'.tr, // 获取验证码 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + )), ], ), ), @@ -283,35 +389,36 @@ class LoginView extends GetView { return SizedBox( width: double.infinity, child: Obx(() => ElevatedButton( - onPressed: controller.isLoading ? null : () => controller.login(), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0066CC), - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 14.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - elevation: 0, - disabledBackgroundColor: const Color(0xFF0066CC).withOpacity(0.7), - disabledForegroundColor: Colors.white, - ), - child: controller.isLoading - ? SizedBox( - height: 20.h, - width: 20.h, - child: CircularProgressIndicator( - strokeWidth: 2.w, - valueColor: const AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - '继续', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), + onPressed: controller.isLoading ? null : () => controller.login(), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066CC), + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), ), - )), + elevation: 0, + disabledBackgroundColor: const Color(0xFF0066CC).withOpacity(0.7), + disabledForegroundColor: Colors.white, + ), + child: controller.isLoading + ? SizedBox( + height: 20.h, + width: 20.h, + child: CircularProgressIndicator( + strokeWidth: 2.w, + valueColor: + const AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + 'continue'.tr, // 继续 + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + )), ); } @@ -321,18 +428,22 @@ class LoginView extends GetView { children: [ Row( children: [ - Expanded(child: Divider(color: isDarkMode ? Colors.grey[800] : Colors.grey[300])), + Expanded( + child: Divider( + color: isDarkMode ? Colors.grey[800] : Colors.grey[300])), Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: Text( - '或使用以下方式登录', + 'otherWays'.tr, // 或使用以下方式登录 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[500] : Colors.grey[600], ), ), ), - Expanded(child: Divider(color: isDarkMode ? Colors.grey[800] : Colors.grey[300])), + Expanded( + child: Divider( + color: isDarkMode ? Colors.grey[800] : Colors.grey[300])), ], ), SizedBox(height: 24.h), @@ -341,7 +452,14 @@ class LoginView extends GetView { children: [ _buildSocialButton(FontAwesomeIcons.google, Colors.red, isDarkMode), SizedBox(width: 24.w), - _buildSocialButton(FontAwesomeIcons.weixin, Colors.green, isDarkMode), + _buildSocialButton( + FontAwesomeIcons.weixin, Colors.green, isDarkMode), + SizedBox(width: 24.w), + _buildSocialButton( + FontAwesomeIcons.facebook, Colors.blue, isDarkMode), + SizedBox(width: 24.w), + _buildSocialButton( + FontAwesomeIcons.userSecret, Colors.black, isDarkMode),//添加游客登录按钮 ], ), ], @@ -356,31 +474,58 @@ class LoginView extends GetView { decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.white, borderRadius: BorderRadius.circular(24.r), - boxShadow: isDarkMode ? [] : [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + boxShadow: isDarkMode + ? [] + : [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), child: IconButton( - icon: FaIcon( - icon, - color: color, - size: 20.sp, - ), - onPressed: () {}, - ), + icon: FaIcon( + icon, + color: color, + size: 20.sp, + ), + onPressed: () => SocialLogin(icon)), ); } + Future SocialLogin(IconData icon) async { + switch (icon) { + case FontAwesomeIcons.facebook: + { + controller.loginType = 4; + break; + } + case FontAwesomeIcons.google: + { + controller.loginType = 3; + break; + } + case FontAwesomeIcons.weixin: + { + controller.loginType = 2; + break; + } + case FontAwesomeIcons.userSecret: // 处理游客登录 + { + controller.loginType = 6; + break; + } + } + controller.login(); + } + // 用户协议和隐私政策 Widget _buildTermsAndPrivacy(bool isDarkMode) { return Column( children: [ Text( - '登录/注册即表示你同意', + 'expressConsent'.tr, // 登录/注册即表示你同意 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[500] : Colors.grey[600], @@ -391,9 +536,19 @@ class LoginView extends GetView { mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( - onTap: () {}, + onTap: () { + String url = 'https://www.deepsoud.com/yhxy.html'; + print('----------${Get.locale?.toString()}'); + if (Get.locale?.toString() != 'zh_CN') { + url = 'https://www.deepsoud.com/yhxy_en.html'; + } + Get.to(() => WebViewPage( + url: url, + title: 'userAgreement'.tr, + )); + }, child: Text( - '《用户协议》', + 'userAgreement'.tr, // 《用户协议》 style: TextStyle( fontSize: 12.sp, color: const Color(0xFF3B82F6), @@ -401,16 +556,25 @@ class LoginView extends GetView { ), ), Text( - ' 和 ', + 'and'.tr, // 和 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[500] : Colors.grey[600], ), ), GestureDetector( - onTap: () {}, + onTap: () { + String url = 'https://www.deepsoud.com/ysxy.html'; + if (Get.locale?.toString() != 'zh_CN') { + url = 'https://www.deepsoud.com/ysxy_en.html'; + } + Get.to(() => WebViewPage( + url: url, + title: 'privacyPolicy'.tr, + )); + }, child: Text( - '《隐私政策》', + 'privacyPolicy'.tr, // 《隐私政策》 style: TextStyle( fontSize: 12.sp, color: const Color(0xFF3B82F6), @@ -458,12 +622,12 @@ class LoginView extends GetView { ), SizedBox(height: 8.h), Obx(() => Text( - '验证码已发送至 ${controller.displayContact}', - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - )), + '验证码已发送至 ${controller.displayContact}', + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + )), ], ); } @@ -472,17 +636,21 @@ class LoginView extends GetView { Widget _buildVerificationInputs(BuildContext context, bool isDarkMode) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(4, (index) => _buildVerificationInput(index, context, isDarkMode)), + children: List.generate( + 4, (index) => _buildVerificationInput(index, context, isDarkMode)), ); } // 单个验证码输入框 - Widget _buildVerificationInput(int index, BuildContext context, bool isDarkMode) { + Widget _buildVerificationInput( + int index, BuildContext context, bool isDarkMode) { return Container( width: 60.w, height: 60.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900]!.withOpacity(0.3) : Colors.grey.withOpacity(0.1), + color: isDarkMode + ? Colors.grey[900]!.withOpacity(0.3) + : Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(12.r), border: Border.all( color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, @@ -520,28 +688,28 @@ class LoginView extends GetView { Widget _buildResendButton(bool isDarkMode) { return Center( child: Obx(() => TextButton.icon( - onPressed: controller.countdownSeconds > 0 - ? null - : controller.resendVerificationCode, - icon: Icon( - Icons.refresh, - size: 14.sp, - color: controller.countdownSeconds > 0 - ? (isDarkMode ? Colors.grey[600] : Colors.grey[500]) - : const Color(0xFF3B82F6), - ), - label: Text( - controller.countdownSeconds > 0 - ? '重新发送验证码 (${controller.countdownSeconds}s)' - : '重新发送验证码', - style: TextStyle( - fontSize: 14.sp, - color: controller.countdownSeconds > 0 - ? (isDarkMode ? Colors.grey[600] : Colors.grey[500]) - : const Color(0xFF3B82F6), - ), - ), - )), + onPressed: controller.countdownSeconds > 0 + ? null + : controller.resendVerificationCode, + icon: Icon( + Icons.refresh, + size: 14.sp, + color: controller.countdownSeconds > 0 + ? (isDarkMode ? Colors.grey[600] : Colors.grey[500]) + : const Color(0xFF3B82F6), + ), + label: Text( + controller.countdownSeconds > 0 + ? '重新发送验证码 (${controller.countdownSeconds}s)' + : '重新发送验证码', + style: TextStyle( + fontSize: 14.sp, + color: controller.countdownSeconds > 0 + ? (isDarkMode ? Colors.grey[600] : Colors.grey[500]) + : const Color(0xFF3B82F6), + ), + ), + )), ); } @@ -550,35 +718,36 @@ class LoginView extends GetView { return SizedBox( width: double.infinity, child: Obx(() => ElevatedButton( - onPressed: controller.isLoading ? null : controller.login, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF3B82F6), - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 14.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - elevation: 0, - disabledBackgroundColor: const Color(0xFF3B82F6).withOpacity(0.7), - disabledForegroundColor: Colors.white, - ), - child: controller.isLoading - ? SizedBox( - height: 20.h, - width: 20.h, - child: CircularProgressIndicator( - strokeWidth: 2.w, - valueColor: const AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - '登录', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), + onPressed: controller.isLoading ? null : controller.login, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF3B82F6), + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), ), - )), + elevation: 0, + disabledBackgroundColor: const Color(0xFF3B82F6).withOpacity(0.7), + disabledForegroundColor: Colors.white, + ), + child: controller.isLoading + ? SizedBox( + height: 20.h, + width: 20.h, + child: CircularProgressIndicator( + strokeWidth: 2.w, + valueColor: + const AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + '登录', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + )), ); } @@ -591,10 +760,11 @@ class LoginView extends GetView { '使用密码登录', style: TextStyle( fontSize: 14.sp, - color: isDarkMode ? const Color(0xFF3B82F6) : const Color(0xFF3B82F6), + color: + isDarkMode ? const Color(0xFF3B82F6) : const Color(0xFF3B82F6), ), ), ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/meeting/bindings/meeting_binding.dart b/lib/modules/meeting/bindings/meeting_binding.dart new file mode 100644 index 000000000..82c4c9f5d --- /dev/null +++ b/lib/modules/meeting/bindings/meeting_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import '../controllers/meeting_controller.dart'; + +class MeetingBinding extends Bindings { + @override + void dependencies() { + // 注册控制器 + Get.lazyPut(() => MeetingController()); + } +} diff --git a/lib/modules/meeting/bindings/meeting_details_binding.dart b/lib/modules/meeting/bindings/meeting_details_binding.dart new file mode 100644 index 000000000..6676625bd --- /dev/null +++ b/lib/modules/meeting/bindings/meeting_details_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import '../controllers/meeting_details_controller.dart'; + +class MeetingDetailsBinding extends Bindings { + @override + void dependencies() { + // 注册控制器 + Get.lazyPut(() => MeetingDetailsController()); + } +} diff --git a/lib/modules/meeting/bindings/meeting_edit_binding.dart b/lib/modules/meeting/bindings/meeting_edit_binding.dart new file mode 100644 index 000000000..31d1443ee --- /dev/null +++ b/lib/modules/meeting/bindings/meeting_edit_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; + +import '../controllers/meeting_edit_controller.dart'; + +class MeetingEditBinding extends Bindings { + @override + void dependencies() { + // 注册控制器 + Get.lazyPut(() => MeetingEditController()); + } +} diff --git a/lib/modules/meeting/bindings/meeting_record_binding.dart b/lib/modules/meeting/bindings/meeting_record_binding.dart new file mode 100644 index 000000000..3fd92a57c --- /dev/null +++ b/lib/modules/meeting/bindings/meeting_record_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import '../controllers/meeting_record_controller.dart'; + +class MeetingRecordBinding extends Bindings { + @override + void dependencies() { + // 注册控制器 + Get.lazyPut(() => MeetingRecordController()); + } +} diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart new file mode 100644 index 000000000..e798e6f36 --- /dev/null +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../../data/services/db/sqflite_api.dart'; + +class MeetingController extends GetxController { + final ScrollController listController = ScrollController(); + + // 是否还有更多数据 + bool isLoadingMore = true; + int page = 1; + + TextEditingController titleController = TextEditingController(); // 录音标题 + + RxList dataList = [].obs; + + @override + void onInit() { + super.onInit(); + listController.addListener(_onScroll); + _getDataList(); + } + + @override + void onClose() { + listController.dispose(); + titleController.dispose(); + super.onClose(); + } + + void _onScroll() { + if (isLoadingMore && + listController.position.pixels >= + listController.position.maxScrollExtent - 100) { + page++; + _getDataList(); + } + } + + // 请求本地数据 + void _getDataList() async { + List list = await SqfliteApi.getMeetingList(page); + dataList.addAll(list); + if (list.length < 10) { + isLoadingMore = false; + } else { + isLoadingMore = true; + } + } + + // 修改标题 + void editMeetingTitle(int id, String title) async { + if (title.isEmpty) return; + SqfliteApi.editMeetingTitle(id, title); + int index = dataList.indexWhere((item) => item['id'] == id); + if (index != -1) { + final dataMap = Map.from(dataList[index]); + dataMap['title'] = title; + dataList[index] = dataMap; + } + Get.back(); + } + + // 删除数据 + void deleteMeeting(int id) { + dataList.removeWhere((item) => item['id'] == id); + // 删除数据库中的记录 + SqfliteApi.deleteMeeting(id); + } +} diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart new file mode 100644 index 000000000..c1319036d --- /dev/null +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; + +import '../../../data/services/db/sqflite_api.dart'; +import '../model/meeting_details_model.dart'; +import '../model/meeting_model.dart'; + +class MeetingDetailsController extends GetxController + with GetSingleTickerProviderStateMixin { + late AnimationController lottieController; + final AudioPlayer audioPlayer = AudioPlayer(); + + int id = 0; + + RxBool isLoading = true.obs; + Rx meetingData = MeetingModel().obs; + Rx dataDetails = MeetingDetailsModel().obs; + + int seconds = 0; + + RxBool isPlay = false.obs; + + @override + void onInit() { + super.onInit(); + lottieController = AnimationController(vsync: this); + final Map args = Get.arguments ?? {}; + id = args['id']; + _getDataDetails(); + } + + @override + void onClose() { + lottieController.dispose(); + audioPlayer.dispose(); + super.onClose(); + } + + void _getDataDetails() async { + final data = await SqfliteApi.getMeetingDetails(id); + meetingData.value = MeetingModel.fromJson(data); + dataDetails.value = MeetingDetailsModel.fromJson(data['details']); + audioPlayer.setAudioSource(AudioSource.file(dataDetails.value.audio)); + audioPlayer.playerStateStream.listen((state) async { + if (state.processingState == ProcessingState.completed) { + await audioPlayer.seek(Duration.zero); + audioPlayer.pause(); + lottieController.stop(); + isPlay.value = false; + } + }); + isLoading.value = false; + } + + void start() async { + if (isPlay.value) { + lottieController.stop(); + audioPlayer.pause(); + isPlay.value = false; + } else { + lottieController.repeat(); + audioPlayer.play(); + isPlay.value = true; + } + } +} diff --git a/lib/modules/meeting/controllers/meeting_edit_controller.dart b/lib/modules/meeting/controllers/meeting_edit_controller.dart new file mode 100644 index 000000000..d214735bc --- /dev/null +++ b/lib/modules/meeting/controllers/meeting_edit_controller.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; + +import '../../../data/services/ai_service.dart'; +import '../../../data/services/db/sqflite_api.dart'; +import '../../../data/services/volcano_ai_service.dart'; +import 'meeting_controller.dart'; +import 'meeting_details_controller.dart'; + +class MeetingEditController extends GetxController + with GetSingleTickerProviderStateMixin { + late AiService _aiService; + + final MeetingController meetingController = Get.find(); + final MeetingDetailsController meetingDetailsController = + Get.find(); + + late AnimationController lottieController; + + late TextEditingController titleController; + late TextEditingController contentController; + + final AudioPlayer audioPlayer = AudioPlayer(); + + int creationTime = 0; + + RxString address = ''.obs; + String _longitude = ''; + String _latitude = ''; + + int id = 0; + + RxBool isPlay = false.obs; + + @override + void onInit() { + super.onInit(); + lottieController = AnimationController(vsync: this); + final Map args = Get.arguments ?? {}; + id = args['id']; + _getDataDetails(); + } + + @override + void onClose() { + lottieController.dispose(); + titleController.dispose(); + contentController.dispose(); + audioPlayer.dispose(); + super.onClose(); + } + + void _getDataDetails() async { + titleController = TextEditingController( + text: meetingDetailsController.meetingData.value.title, + ); + contentController = TextEditingController( + text: meetingDetailsController.dataDetails.value.content, + ); + creationTime = meetingDetailsController.meetingData.value.creationtime; + address.value = meetingDetailsController.meetingData.value.address; + _longitude = meetingDetailsController.meetingData.value.longitude; + _latitude = meetingDetailsController.meetingData.value.latitude; + audioPlayer.setAudioSource( + AudioSource.file(meetingDetailsController.dataDetails.value.audio)); + audioPlayer.playerStateStream.listen((state) async { + if (state.processingState == ProcessingState.completed) { + await audioPlayer.seek(Duration.zero); + audioPlayer.pause(); + lottieController.stop(); + isPlay.value = false; + } + }); + } + + void start() async { + if (isPlay.value) { + lottieController.stop(); + audioPlayer.pause(); + isPlay.value = false; + } else { + lottieController.repeat(); + audioPlayer.play(); + isPlay.value = true; + } + } + + void confirm() async { + EasyLoading.show(); + _aiService = Get.find(); + String aiSummary = await _aiService.sendMessage( + systemPrompt: '', + messages: [ + {'role': 'user', 'content': contentController.text}, + {'role': 'user', 'content': '帮我总结一下这次会议'}, + ], + ); + String content = contentController.text; + String firstContent = + content.length >= 30 ? content.substring(0, 30) : content; + String title = titleController.text.isNotEmpty + ? titleController.text + : meetingDetailsController.meetingData.value.title; + Map data = { + 'title': title, + 'firstContent': firstContent.replaceAll("\n", ""), + 'seconds': meetingDetailsController.seconds, + 'content': contentController.text, + 'aisummary': aiSummary, + 'address': address.value, + 'longitude': _longitude, + 'latitude': _latitude, + }; + final result = await SqfliteApi.editMeeting(id, data); + if (result != null) { + int index = meetingController.dataList.indexWhere( + (item) => item['id'] == id, + ); + if (index != -1) { + final dataMap = + Map.from(meetingController.dataList[index]); + dataMap['title'] = title; + dataMap['content'] = firstContent.replaceAll("\n", ""); + dataMap['seconds'] = meetingDetailsController.seconds; + dataMap['address'] = address.value; + meetingController.dataList[index] = dataMap; + } + meetingDetailsController.meetingData.value.title = title; + meetingDetailsController.meetingData.value.address = address.value; + meetingDetailsController.meetingData.value.longitude = _longitude; + meetingDetailsController.meetingData.value.latitude = _latitude; + meetingDetailsController.dataDetails.value.content = + contentController.text; + meetingDetailsController.dataDetails.value.aisummary = aiSummary; + meetingDetailsController.meetingData.refresh(); + meetingDetailsController.dataDetails.refresh(); + Get.back(); + } + EasyLoading.dismiss(); + } +} diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart new file mode 100644 index 000000000..ba449a942 --- /dev/null +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -0,0 +1,312 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../../../core/utils/logger.dart'; +import '../../../data/services/ai_service.dart'; +import '../../../data/services/asr_service.dart'; +import '../../../data/services/db/sqflite_api.dart'; +import '../../../data/services/speech_impl/xunfei_asr_service.dart'; +import '../../../data/services/volcano_ai_service.dart'; +import 'meeting_controller.dart'; + +class MeetingRecordController extends GetxController + with GetSingleTickerProviderStateMixin { + late AiService _aiService; + + final MeetingController meetingController = Get.find(); + + late AnimationController lottieController; + late AsrService _asrService; + + Timer? timer; + + RxBool isRecording = false.obs; //是否录音中 + RxInt seconds = 0.obs; + + String title = ''; + int creationTime = 0; + + RxString address = '深圳市高新技术产业园区W2-A座'.obs; + String _longitude = ''; + String _latitude = ''; + + RxString intermediateContent = ''.obs; + RxString finalContent = ''.obs; + + StreamSubscription? _recognitionSubscription; + + BytesBuilder _originaBytes = BytesBuilder(); //原始音频 + + @override + void onInit() { + super.onInit(); + Permission.storage.request(); + lottieController = AnimationController(vsync: this); + Get.put(XunfeiAsrService()); + _asrService = Get.find(); + final Map args = Get.arguments ?? {}; + title = args['title']; + creationTime = DateTime.now().millisecondsSinceEpoch; + } + + @override + void onClose() { + lottieController.dispose(); + timer?.cancel(); + _asrService.dispose(); + _recognitionSubscription?.cancel(); + _originaBytes = BytesBuilder(); + super.onClose(); + } + + // 点击开始、暂停按钮 + void recorderHandle() { + if (isRecording.value) { + _pause(); + } else { + _start(); + } + } + + // 开始 + void _start() async { + await _startRecorder(); + lottieController.repeat(); + isRecording.value = true; + timer = Timer.periodic(const Duration(seconds: 1), (_) => seconds++); + } + + // 暂停 + Future _pause() async { + await _stopRecorder(); + lottieController.stop(); + isRecording.value = false; + timer?.cancel(); + } + + //开始录音 + Future _startRecorder() async { + final recognitionStream = await _asrService.startContinuousRecognition(); + _recognitionSubscription = recognitionStream.listen( + _handleRecognitionEvent, + ); + } + + //结束录音 + Future _stopRecorder() async { + await _asrService.stopContinuousRecognition(); + } + + // 处理语音识别事件 + void _handleRecognitionEvent(RecognitionEvent event) { + switch (event.type) { + case RecognitionEventType.intermediateResult: + intermediateContent.value = event.text; + break; + case RecognitionEventType.finalResult: + finalContent.value += _spliceText(event); + intermediateContent.value = ''; + _originaBytes.add(event.audio!); + break; + default: + // 不做任何处理 + break; + } + } + + String _spliceText(RecognitionEvent event) { + String eventText = event.text; + String text = ''; + if (finalContent.value.isNotEmpty) { + if (event.role != '') { + final punctuations = { + ',', + '。', + '!', + '?', + ';', + ':', + ',', + '.', + ':', + ';', + '!', + '?' + }; + final firstChar = eventText[0]; + if (punctuations.contains(firstChar)) { + text += '$firstChar\n'; + eventText = eventText.substring(1); + } else { + text += '\n'; + } + text += '说话人${event.role}:'; + } + } else { + text += '说话人${event.role}:'; + } + text += eventText; + return text; + } + + // 完成 + void complete() async { + if (await Permission.storage.request().isGranted) { + EasyLoading.show(); + await _pause(); + _aiSummary(); + } else { + Get.snackbar( + 'error'.tr, // 错误 + '请开启文件存储权限', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + } + + void _aiSummary() async { + _aiService = Get.find(); + String aiSummary = await _aiService.sendMessage( + systemPrompt: '', + messages: [ + {'role': 'user', 'content': finalContent.value}, + {'role': 'user', 'content': '帮我总结一下这次会议'}, + ], + ); + _save(aiSummary); + } + + void _save(String aiSummary) async { + String firstContent = finalContent.value.length >= 30 + ? finalContent.value.substring(0, 30) + : finalContent.value; + Uint8List audio = _originaBytes.toBytes(); + Uint8List wavHeader = generateWavHeader( + sampleRate: 16000, + numberOfChannels: 1, + bitsPerSample: 16, + dataLength: audio.length, + ); + Uint8List wavData = Uint8List.fromList([...wavHeader, ...audio]); + String audiopPath = + await _saveAudioToFile(wavData, 'meeting_$creationTime.wav'); + if (audiopPath.isNotEmpty) { + Map data = { + 'title': title, + 'firstContent': firstContent.replaceAll("\n", ""), + 'seconds': seconds.value, + 'creationTime': creationTime, + 'audio': audiopPath, + 'content': finalContent.value, + 'aisummary': aiSummary, + 'address': address.value, + 'longitude': _longitude, + 'latitude': _latitude, + }; + final result = await SqfliteApi.insertMeeting(data); + if (result != null) { + meetingController.dataList.insert(0, { + 'id': result, + 'title': title, + 'content': firstContent, + 'seconds': seconds.value, + 'creationtime': creationTime, + }); + Get.back(); + } + } + EasyLoading.dismiss(); + } + + Future _saveAudioToFile(Uint8List audioData, String fileName) async { + try { + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/$fileName'); + + // 写入文件 + await file.writeAsBytes(audioData); + return file.path; + } catch (e) { + Logger.error('保存失败: $e'); + return ''; + } + } + + Uint8List generateWavHeader({ + required int sampleRate, + required int numberOfChannels, + required int bitsPerSample, + required int dataLength, + }) { + final byteRate = sampleRate * numberOfChannels * (bitsPerSample ~/ 8); + final blockAlign = numberOfChannels * (bitsPerSample ~/ 8); + final riffSize = 36 + dataLength; + final header = Uint8List(44); + var pos = 0; + // RIFF 块 + header.setRange(pos, pos + 4, [0x52, 0x49, 0x46, 0x46]); // 'RIFF' + pos += 4; + // 写入 RIFF 大小(小端) + header.setRange(pos, pos + 4, [ + riffSize & 0xff, + (riffSize >> 8) & 0xff, + (riffSize >> 16) & 0xff, + (riffSize >> 24) & 0xff, + ]); + pos += 4; + // WAVE 标识 + header.setRange(pos, pos + 4, [0x57, 0x41, 0x56, 0x45]); // 'WAVE' + pos += 4; + // fmt 子块 + header.setRange(pos, pos + 4, [0x66, 0x6d, 0x74, 0x20]); // 'fmt ' + pos += 4; + header.setRange(pos, pos + 4, [16, 0, 0, 0]); // 块大小 + pos += 4; + header.setRange(pos, pos + 2, [1, 0]); // PCM 格式 + pos += 2; + header.setRange(pos, pos + 2, [numberOfChannels, 0]); // 声道数 + pos += 2; + // 采样率(小端) + header.setRange(pos, pos + 4, [ + sampleRate & 0xff, + (sampleRate >> 8) & 0xff, + (sampleRate >> 16) & 0xff, + (sampleRate >> 24) & 0xff, + ]); + pos += 4; + // 字节率(小端) + header.setRange(pos, pos + 4, [ + byteRate & 0xff, + (byteRate >> 8) & 0xff, + (byteRate >> 16) & 0xff, + (byteRate >> 24) & 0xff, + ]); + pos += 4; + // 块对齐 + header.setRange(pos, pos + 2, [blockAlign, 0]); + pos += 2; + // 位深度 + header.setRange(pos, pos + 2, [bitsPerSample, 0]); + pos += 2; + // data 子块 + header.setRange(pos, pos + 4, [0x64, 0x61, 0x74, 0x61]); // 'data' + pos += 4; + // 数据大小(小端) + header.setRange(pos, pos + 4, [ + dataLength & 0xff, + (dataLength >> 8) & 0xff, + (dataLength >> 16) & 0xff, + (dataLength >> 24) & 0xff, + ]); + return header; + } +} diff --git a/lib/modules/meeting/model/meeting_details_model.dart b/lib/modules/meeting/model/meeting_details_model.dart new file mode 100644 index 000000000..fa7e08f97 --- /dev/null +++ b/lib/modules/meeting/model/meeting_details_model.dart @@ -0,0 +1,27 @@ +class MeetingDetailsModel { + MeetingDetailsModel(); + + late int id; + late int meetingid; + late String audio; + late String content; + late String aisummary; + + factory MeetingDetailsModel.fromJson(Map map) => + MeetingDetailsModel() + ..id = map["id"] + ..meetingid = map["meetingid"] + ..audio = map["audio"] + ..content = map["content"] + ..aisummary = map["aisummary"]; + + Map toJson() { + final data = {}; + data['id'] = id; + data['meetingid'] = meetingid; + data['audio'] = audio; + data['content'] = content; + data['aisummary'] = aisummary; + return data; + } +} diff --git a/lib/modules/meeting/model/meeting_model.dart b/lib/modules/meeting/model/meeting_model.dart new file mode 100644 index 000000000..36f9d3940 --- /dev/null +++ b/lib/modules/meeting/model/meeting_model.dart @@ -0,0 +1,35 @@ +class MeetingModel { + MeetingModel(); + + late int id; + late String title; + late String content; + late int seconds; + late String address; + late String longitude; + late String latitude; + late int creationtime; + + factory MeetingModel.fromJson(Map map) => MeetingModel() + ..id = map["id"] + ..title = map["title"] + ..content = map["content"] + ..seconds = map["seconds"] + ..address = map["address"] + ..longitude = map["longitude"] + ..latitude = map["latitude"] + ..creationtime = map["creationtime"]; + + Map toJson() { + final data = {}; + data['id'] = id; + data['title'] = title; + data['content'] = content; + data['seconds'] = seconds; + data['address'] = address; + data['longitude'] = longitude; + data['latitude'] = latitude; + data['creationtime'] = creationtime; + return data; + } +} diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart new file mode 100644 index 000000000..9947c0b7c --- /dev/null +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -0,0 +1,326 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:lottie/lottie.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../../../routes/app_routes.dart'; +import '../controllers/meeting_details_controller.dart'; + +class MeetingDetailsView extends GetView { + const MeetingDetailsView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: Obx(() => !controller.isLoading.value + ? _buildBody(isDarkMode) + : const SizedBox()), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + '录音详情', + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + controller.meetingData.value.title, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 17.sp, + ), + ), + ), + 10.horizontalSpace, + Text( + DateUtil.formatDateMs( + controller.meetingData.value.creationtime, + format: DateFormats.y_mo_d_h_m, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Row( + children: [ + Icon( + Icons.location_on, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + Text( + controller.meetingData.value.address, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, + ), + ), + ], + ), + ), + _audioWave(isDarkMode), + _recordContent(isDarkMode), + _bottomButton(isDarkMode), + ], + ), + ); + } + + // 音波 + Widget _audioWave(bool isDarkMode) { + return Container( + padding: EdgeInsets.only(bottom: 10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + SizedBox( + height: 70.w, + child: Row( + children: [ + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + onLoaded: (composition) { + controller.lottieController.duration = + composition.duration; + }, + ), + ), + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + ), + ), + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + ), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + StreamBuilder( + stream: controller.audioPlayer.positionStream, + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + return Text( + DateUtil.formatDateMs( + position.inMilliseconds, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ); + }, + ), + GestureDetector( + onTap: controller.start, + child: Container( + width: 30.w, + height: 30.w, + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular( + 12.w, + ), + ), + child: Icon( + controller.isPlay.value ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 25.w, + ), + ), + ), + StreamBuilder( + stream: controller.audioPlayer.durationStream, + builder: (context, snapshot) { + final duration = snapshot.data ?? Duration.zero; + controller.seconds = duration.inSeconds; + return Text( + DateUtil.formatDateMs( + duration.inMilliseconds, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ); + }, + ), + ], + ), + ], + ), + ); + } + + // 记录内容 + Widget _recordContent(bool isDarkMode) { + return Expanded( + child: Container( + width: double.infinity, + padding: EdgeInsets.all(10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: DefaultTabController( + length: 2, + child: Column( + children: [ + SizedBox( + height: 40.w, + child: TabBar( + indicatorSize: TabBarIndicatorSize.tab, + indicatorColor: isDarkMode ? AppColors.primary : Colors.blue, + labelColor: isDarkMode ? AppColors.primary : Colors.blue, + labelStyle: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 14.sp, + ), + tabs: const [ + Tab(text: 'AI总结'), + Tab(text: '会议原文'), + ], + ), + ), + 10.verticalSpace, + Expanded( + child: TabBarView( + children: [ + SingleChildScrollView( + child: Text( + controller.dataDetails.value.aisummary, + ), + ), + SingleChildScrollView( + child: Text( + controller.dataDetails.value.content, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + // 底部按钮 + Widget _bottomButton(bool isDarkMode) { + return Container( + padding: EdgeInsets.only(top: 20.w, bottom: 10.w), + alignment: Alignment.center, + child: GestureDetector( + onTap: () { + Get.toNamed(Routes.meetingEdit, arguments: { + 'id': controller.id, + }); + }, + child: Container( + width: 130.w, + height: 48.w, + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular(12.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.edit_calendar, + color: Colors.white, + size: 20.w, + ), + 5.horizontalSpace, + Text( + '编辑', + style: TextStyle(color: Colors.white, fontSize: 16.sp), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/meeting_edit_view.dart b/lib/modules/meeting/views/meeting_edit_view.dart new file mode 100644 index 000000000..ce28c2fca --- /dev/null +++ b/lib/modules/meeting/views/meeting_edit_view.dart @@ -0,0 +1,298 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:lottie/lottie.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../controllers/meeting_edit_controller.dart'; + +class MeetingEditView extends GetView { + const MeetingEditView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + actions: [ + GestureDetector( + onTap: controller.confirm, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 6.w, + ), + child: Text( + '保存', + style: TextStyle( + color: isDarkMode ? AppColors.primary : Colors.blue, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: controller.titleController, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + decoration: InputDecoration( + hintText: '最多50个字符', + hintStyle: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 16.sp, + ), + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 10.w, + ), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Text( + DateUtil.formatDateMs( + controller.creationTime, + format: DateFormats.y_mo_d_h_m, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, + ), + ), + ), + Row( + children: [ + Icon( + Icons.location_on, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + Obx( + () => Text( + controller.address.value, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, + ), + ), + ), + ], + ), + _audioWave(isDarkMode), + _recordContent(isDarkMode), + ], + ), + ); + } + + // 音波 + Widget _audioWave(bool isDarkMode) { + return Container( + padding: EdgeInsets.only(bottom: 10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + SizedBox( + height: 70.w, + child: Row( + children: [ + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + onLoaded: (composition) { + controller.lottieController.duration = + composition.duration; + }, + ), + ), + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + ), + ), + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + ), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + StreamBuilder( + stream: controller.audioPlayer.positionStream, + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + return Text( + DateUtil.formatDateMs( + position.inMilliseconds, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ); + }, + ), + GestureDetector( + onTap: controller.start, + child: Container( + width: 30.w, + height: 30.w, + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular( + 12.w, + ), + ), + child: Obx( + () => Icon( + controller.isPlay.value ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 25.w, + ), + ), + ), + ), + StreamBuilder( + stream: controller.audioPlayer.durationStream, + builder: (context, snapshot) { + final duration = snapshot.data ?? Duration.zero; + return Text( + DateUtil.formatDateMs( + duration.inMilliseconds, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ); + }, + ), + ], + ), + ], + ), + ); + } + + // 记录内容 + Widget _recordContent(bool isDarkMode) { + return Expanded( + child: Container( + width: double.infinity, + padding: EdgeInsets.all(10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: controller.contentController, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 14.sp, + ), + maxLines: 50, + decoration: InputDecoration( + hintText: '编辑记录内容', + hintStyle: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 14.sp, + ), + fillColor: Colors.transparent, + border: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart new file mode 100644 index 000000000..726a1b2e1 --- /dev/null +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -0,0 +1,358 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:lottie/lottie.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../../../core/widgets/select_location_map.dart'; +import '../controllers/meeting_record_controller.dart'; + +class MeetingRecordView extends GetView { + const MeetingRecordView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + _showAlertDialog(isDarkMode); + }, + child: Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () { + _showAlertDialog(isDarkMode); + }, + ), + title: Padding( + padding: const EdgeInsets.only(right: 56), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Obx( + () => Icon( + Icons.keyboard_voice, + color: isDarkMode + ? controller.seconds.value > 0 + ? AppColors.primary + : Colors.white + : controller.seconds.value > 0 + ? Colors.blue + : Colors.black, + ), + ), + 5.horizontalSpace, + Obx( + () => Text( + DateUtil.formatDateMs( + controller.seconds.value * 1000, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode + ? controller.seconds.value > 0 + ? AppColors.primary + : Colors.white + : controller.seconds.value > 0 + ? Colors.blue + : Colors.black, + ), + ), + ), + ], + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 10.verticalSpace, + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Row( + children: [ + Expanded( + child: Text( + controller.title, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 17.sp, + ), + ), + ), + 10.horizontalSpace, + Text( + DateUtil.formatDateMs( + controller.creationTime, + format: DateFormats.y_mo_d_h_m, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () { + Get.to(() => const SelectLocationMap()); + }, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + child: Row( + children: [ + Icon( + Icons.location_on, + size: 16.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + Text( + controller.address.value, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, + ), + ), + ], + ), + ), + ), + _recordContent(isDarkMode), + _bottomButton(isDarkMode), + ], + ); + } + + // 记录内容 + Widget _recordContent(bool isDarkMode) { + return Expanded( + child: Container( + width: double.infinity, + padding: EdgeInsets.all(10.w), + margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Align( + alignment: Alignment.topLeft, + child: SingleChildScrollView( + reverse: true, + child: Obx( + () => Text( + '${controller.finalContent.value}${controller.intermediateContent.value}', + ), + ), + ), + ), + ), + ); + } + + // 底部按钮 + Widget _bottomButton(bool isDarkMode) { + return Container( + margin: EdgeInsets.only(top: 10.h), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 3, + offset: const Offset(0, -1), + ), + ], + ), + child: SafeArea( + child: SizedBox( + height: 80.h, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 语音录制按钮 + Container( + width: 60.r, + height: 60.r, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: Center( + child: Obx( + () => IconButton( + icon: Icon( + controller.isRecording.value + ? Icons.mic + : Icons.mic_none, + ), + color: controller.isRecording.value + ? AppColors.primary + : Colors.grey.shade400, + iconSize: 28.r, + padding: EdgeInsets.zero, + onPressed: controller.recorderHandle, + ), + ), + ), + ), + 10.horizontalSpace, + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + onLoaded: (composition) { + controller.lottieController.duration = + composition.duration; + }, + ), + ), + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + onLoaded: (composition) { + controller.lottieController.duration = + composition.duration; + }, + ), + ), + 10.horizontalSpace, + // 关闭按钮 + Container( + width: 60.r, + height: 60.r, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: Center( + child: IconButton( + icon: Icon( + Icons.close, + color: isDarkMode ? Colors.white : Colors.grey.shade600, + size: 28.r, + ), + padding: EdgeInsets.zero, + onPressed: () { + _showAlertDialog(isDarkMode); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + // 显示对话框 + void _showAlertDialog(bool isDarkMode) { + if (controller.seconds.value > 0) { + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + '注意', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + '你想结束录音吗?', + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + controller.complete(); + }, + child: Text( + 'confirm'.tr, // 确定 + style: TextStyle( + color: isDarkMode ? Colors.red[300] : Colors.red[600], + ), + ), + ), + ], + ), + ); + } else { + Get.back(); + } + } +} diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart new file mode 100644 index 000000000..263bdc6e7 --- /dev/null +++ b/lib/modules/meeting/views/meeting_view.dart @@ -0,0 +1,419 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../../../routes/app_routes.dart'; +import '../controllers/meeting_controller.dart'; +import '../model/meeting_model.dart'; + +class MeetingView extends GetView { + const MeetingView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(context, isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'conferenceAssistant'.tr, // 商务会议助手 + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(BuildContext context, bool isDarkMode) { + return Stack( + children: [ + Obx( + () => ListView.builder( + itemCount: controller.dataList.length, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + itemBuilder: (BuildContext context, int index) { + MeetingModel data = + MeetingModel.fromJson(controller.dataList[index]); + return _item(context, isDarkMode, data); + }, + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: GestureDetector( + onTap: () { + _bottomSheet(context, isDarkMode); + }, + child: Container( + width: 130.w, + height: 48.w, + margin: EdgeInsets.symmetric(vertical: 20.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular(12.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.keyboard_voice, color: Colors.white), + 5.horizontalSpace, + Text( + '开始会议', + style: TextStyle(color: Colors.white, fontSize: 16.sp), + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _item(BuildContext context, bool isDarkMode, MeetingModel data) { + return GestureDetector( + onTap: () { + Get.toNamed(Routes.meetingDetails, arguments: { + 'id': data.id, + }); + }, + onLongPressStart: (details) { + RenderBox? renderBox = + Overlay.of(context).context.findRenderObject() as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx + 110, // 菜单显示位置X轴坐标 + details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标 + ), + Offset.zero & renderBox.size, + ); + + showMenu( + context: context, + position: position, + color: isDarkMode ? AppColors.cardBackground : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + items: [ + _popupMenuEntry( + Icon( + Icons.edit_calendar, + color: isDarkMode ? Colors.white : Colors.black87, + ), + '编辑', + isDarkMode ? Colors.white : Colors.black87, + () { + controller.titleController.text = data.title; + _bottomSheet(context, isDarkMode, id: data.id); + }, + ), + _popupMenuEntry( + const Icon(Icons.delete_outline, color: Colors.red), + '删除', + isDarkMode ? Colors.white : Colors.black87, + () { + controller.deleteMeeting(data.id); + }, + ), + ], + ); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + data.title, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + overflow: TextOverflow.ellipsis, + ), + ), + 10.horizontalSpace, + Text( + DateUtil.formatDateMs( + data.creationtime, + format: DateFormats.y_mo_d_h_m, + ), + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 12.sp, + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.content.isNotEmpty ? data.content : '无内容', + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 12.sp, + ), + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Icon( + Icons.location_on, + size: 14.w, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + Expanded( + child: Text( + data.address, + style: TextStyle( + color: isDarkMode + ? Colors.white70 + : Colors.black54, + fontSize: 12.sp, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + Container( + width: 76.w, + height: 28.w, + margin: EdgeInsets.only(left: 10.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular(28.w), + ), + child: Text( + DateUtil.formatDateMs( + data.seconds * 1000, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle(color: Colors.white, fontSize: 14.sp), + ), + ), + ], + ), + ], + ), + ), + ); + } + + PopupMenuEntry _popupMenuEntry( + Icon icon, + String title, + Color color, + Function() onTap, + ) { + return PopupMenuItem( + onTap: onTap, + child: SizedBox( + width: 120.w, + child: Row( + children: [ + icon, + 8.horizontalSpace, + Text( + title, + style: TextStyle(color: color), + ), + ], + ), + ), + ); + } + + void _bottomSheet( + BuildContext context, + bool isDarkMode, { + int? id, + }) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + clipBehavior: Clip.hardEdge, + builder: (BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + color: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + id != null ? '修改会议标题(非必填)' : '会议标题(非必填)', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + ), + Container( + height: 48.w, + margin: EdgeInsets.only(top: 10.w, bottom: 20.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: controller.titleController, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + decoration: InputDecoration( + hintText: '最多50个字符', + hintStyle: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 16.sp, + ), + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 10.w), + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 30.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + Get.back(); + }, + child: Container( + width: 120.w, + height: 40.w, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border.all( + width: 2.w, + color: + isDarkMode ? AppColors.primary : Colors.blue, + ), + borderRadius: BorderRadius.circular(12.w), + ), + child: Text( + '取消', + style: TextStyle( + fontSize: 14.sp, + color: + isDarkMode ? AppColors.primary : Colors.blue, + ), + ), + ), + ), + GestureDetector( + onTap: () { + int time = DateTime.now().millisecondsSinceEpoch; + String title = controller.titleController.text; + if (id != null) { + controller.editMeetingTitle(id, title); + } else { + if (title.isEmpty) { + title = + '${DateUtil.formatDateMs(time, format: DateFormats.y_mo_d)} 会议'; + } + Get.back(); + Get.toNamed(Routes.meetingRecord, arguments: { + 'title': title, + }); + } + }, + child: Container( + width: 120.w, + height: 40.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular(12.w), + ), + child: Text( + '确定', + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ).then((value) { + controller.titleController.text = ''; + }); + } +} diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 29a4ab851..2501e26bd 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -1,3 +1,4 @@ +import 'package:app_settings/app_settings.dart'; import 'package:get/get.dart'; import 'dart:async'; import '../../../routes/app_routes.dart'; @@ -6,101 +7,102 @@ import '../../../core/utils/logger.dart'; import 'dart:io'; import '../../../data/services/voice_interaction_service.dart'; - /// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 class PairingController extends GetxController { static PairingController get to => Get.find(); - // 经典蓝牙服务 - final ClassicBluetoothService _bluetoothService = Get.find(); - - // 语音交互服务 - final VoiceInteractionService _voiceInteractionService = Get.find(); - + final ClassicBluetoothService _bluetoothService = + Get.find(); + + // 语音交互服务 + final VoiceInteractionService _voiceInteractionService = + Get.find(); // 是否开始搜索 final _isSearching = false.obs; bool get isSearching => _isSearching.value; - + // 设备连接状态 final _isConnecting = false.obs; bool get isConnecting => _isConnecting.value; - + // 蓝牙服务是否可用 final _isBluetoothServiceAvailable = true.obs; bool get isBluetoothServiceAvailable => _isBluetoothServiceAvailable.value; - + // 当前连接设备 Rx get currentDevice => _bluetoothService.currentDevice; - + // 连接设备列表 - RxList get connectedDevices => _bluetoothService.connectedDevices; - + RxList get connectedDevices => + _bluetoothService.connectedDevices; + // 蓝牙状态 RxString get bluetoothState => _bluetoothService.bluetoothState; - + // 错误信息 final errorMessage = ''.obs; - - // 状态描述(用于UI显示) - final statusDescription = '正在初始化...'.obs; - + final statusDescription = 'initializing'.tr.obs; // 正在初始化... + // 加载状态 final isLoading = false.obs; - + // 是否正在跳转到主页 bool _isNavigatingToHome = false; - + @override void onInit() { super.onInit(); _initialize(); } + /// 设置状态变化监听 void _setupStatusListeners() { if (!_isBluetoothServiceAvailable.value) return; - + // 监听蓝牙开关状态 ever(_bluetoothService.bluetoothState, (state) { _updateStatusDescription(); }); - + // 监听当前设备状态 ever(_bluetoothService.currentDevice, (device) { _updateStatusDescription(); - + // 如果有设备连接,自动跳转到主页 if (device != null) { _autoNavigateToHome(); } }); } - + /// 更新状态描述 void _updateStatusDescription() { if (_bluetoothService.bluetoothState.value == 'off') { - statusDescription.value = '蓝牙已关闭,请打开蓝牙'; + statusDescription.value = 'bluetoothTurnedOff'.tr;// 蓝牙已关闭, 请打开蓝牙 return; } - + if (_bluetoothService.currentDevice.value != null) { - statusDescription.value = '已连接到设备: ${_bluetoothService.currentDevice.value!.name}'; + statusDescription.value = + '已连接到设备: ${_bluetoothService.currentDevice.value!.name}'; return; } - + if (_bluetoothService.connectedDevices.isNotEmpty) { - statusDescription.value = '找到已连接的设备: ${_bluetoothService.connectedDevices.first.name}'; + statusDescription.value = + '找到已连接的设备: ${_bluetoothService.connectedDevices.first.name}'; return; } - + if (isLoading.value) { statusDescription.value = '正在搜索设备...'; return; } - + statusDescription.value = '未找到已连接的蓝牙设备'; } @@ -112,12 +114,12 @@ class PairingController extends GetxController { isLoading.value = true; statusDescription.value = '正在初始化...'; - - + // 确保蓝牙服务已初始化 if (!await _bluetoothService.initialize()) { Logger.error('蓝牙服务初始化失败'); - statusDescription.value = '蓝牙服务初始化失败'; + statusDescription.value = + 'bluetoothInitializationFailure'.tr; // 蓝牙服务初始化失败 _isBluetoothServiceAvailable.value = false; isLoading.value = false; return; @@ -133,76 +135,75 @@ class PairingController extends GetxController { return; } } - + // 刷新设备列表 statusDescription.value = '正在搜索设备...'; await refreshConnectedDevices(); - + // 更新状态描述 _updateStatusDescription(); - } catch (e) { Logger.error('初始化配对控制器失败: $e'); - statusDescription.value = '初始化失败'; + statusDescription.value = 'initializationFailure'.tr; // 初始化失败 _isBluetoothServiceAvailable.value = false; } finally { isLoading.value = false; } } - + /// 刷新设备 Future refreshConnectedDevices() async { try { isLoading.value = true; - statusDescription.value = '正在搜索设备...'; - + statusDescription.value = 'searchingDevice'.tr; // 正在搜索设备... + final hasDevices = await _bluetoothService.refreshConnectedDevices(); - + // 更新状态描述 _updateStatusDescription(); - } catch (e) { Logger.error('刷新设备失败: $e'); - statusDescription.value = '刷新设备失败'; + statusDescription.value = 'failedRefreshDevice'.tr; // 刷新设备失败 } finally { isLoading.value = false; } } - + /// 自动跳转到主页 void _autoNavigateToHome() { // 如果已经在跳转或设备列表为空,则不执行 - if (_isNavigatingToHome || _bluetoothService.connectedDevices.isEmpty) return; - + if (_isNavigatingToHome || _bluetoothService.connectedDevices.isEmpty) + return; + // 设置跳转标志 _isNavigatingToHome = true; - + Logger.info('已找到连接的设备,自动跳转到主页'); - + Future.delayed(const Duration(milliseconds: 500), () { Get.offAllNamed(Routes.home); }); } - + /// 打开系统蓝牙设置 void openBluetoothSettings() { try { // 使用app_settings插件打开蓝牙设置 - // AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); } catch (e) { Logger.error('打开蓝牙设置失败: $e'); } } - + /// 刷新设备(兼容旧代码) Future refreshDevices() async { await refreshConnectedDevices(); } - + /// 跳过连接 void skipConnecting() { // 设置跳转标志防止自动跳转再次触发 _isNavigatingToHome = true; Get.offAllNamed(Routes.home); } -} \ No newline at end of file +} diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index c3866a3b9..45facd71b 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -11,7 +11,7 @@ class PairingView extends GetView { @override Widget build(BuildContext context) { final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.white, body: SafeArea( @@ -35,32 +35,33 @@ class PairingView extends GetView { physics: BouncingScrollPhysics(), child: ConstrainedBox( constraints: BoxConstraints( - minHeight: MediaQuery.of(Get.context!).size.height - 100.h, // 减去底部导航和状态栏高度 + minHeight: + MediaQuery.of(Get.context!).size.height - 100.h, // 减去底部导航和状态栏高度 ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: 20.h), // 顶部间距 - + // 标题部分 _buildHeader(isDarkMode), - + SizedBox(height: 20.h), - + // 设备动画 - 更大尺寸 _buildDeviceAnimation(), - + SizedBox(height: 20.h), - + // 状态信息 _buildStatusInfo(isDarkMode), - + SizedBox(height: 40.h), - + // 底部按钮 _buildActionButtons(isDarkMode), - + SizedBox(height: 20.h), ], ), @@ -74,7 +75,7 @@ class PairingView extends GetView { return Column( children: [ Text( - 'DeepVoice', + 'DeepSound', style: TextStyle( fontSize: 30.sp, fontWeight: FontWeight.bold, @@ -83,7 +84,7 @@ class PairingView extends GetView { ), SizedBox(height: 8.h), Text( - '连接世界,超越声音', + 'homeSlogan'.tr, // 连接世界,超越声音 style: TextStyle( fontSize: 16.sp, color: isDarkMode ? Colors.white60 : Colors.black54, @@ -127,7 +128,7 @@ class PairingView extends GetView { // 重新开始动画 }, ), - + // 旋转边框 SizedBox( width: 100.w, @@ -140,7 +141,7 @@ class PairingView extends GetView { backgroundColor: Colors.grey.withOpacity(0.2), ), ), - + // 设备图标 Container( width: 120.w, @@ -166,13 +167,13 @@ class PairingView extends GetView { ), ); } - + // 状态信息 Widget _buildStatusInfo(bool isDarkMode) { return Obx(() { final status = controller.statusDescription.value; final isLoading = controller.isLoading.value; - + return Column( children: [ // 状态文本 - 更大更醒目 @@ -185,19 +186,19 @@ class PairingView extends GetView { ), textAlign: TextAlign.center, ), - + SizedBox(height: 10.h), - + // 子状态提示文本 Text( - '请打开耳机充电盒,将耳机放入您的耳朵', + 'turnEarphones'.tr, // 请打开耳机充电盒,将耳机放入您的耳朵 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.white60 : Colors.grey[600], ), textAlign: TextAlign.center, ), - + // 设备列表(有设备时才显示) if (controller.connectedDevices.isNotEmpty) ...[ SizedBox(height: 24.h), @@ -207,23 +208,23 @@ class PairingView extends GetView { ); }); } - + // 已连接设备列表 Widget _buildConnectedDevicesList(bool isDarkMode) { return Obx(() { final devices = controller.connectedDevices; - + if (devices.isEmpty) { return const SizedBox.shrink(); } - + return Container( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '当前已连接:', + 'currentlyConnected'.tr, // 当前已连接: style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -237,7 +238,7 @@ class PairingView extends GetView { ); }); } - + // 设备项 Widget _buildDeviceItem(device, bool isDarkMode) { return Container( @@ -287,7 +288,7 @@ class PairingView extends GetView { ), ); } - + // 操作按钮 Widget _buildActionButtons(bool isDarkMode) { return Container( @@ -308,16 +309,16 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 56.h), ), child: Text( - '去系统设置', + 'unsystemSetup'.tr, // 去系统设置 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, ), ), ), - + SizedBox(height: 12.h), - + // 次要按钮 TextButton( onPressed: () => controller.skipConnecting(), @@ -327,7 +328,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 48.h), ), child: Text( - '稍后再说', + 'talkLater'.tr, // 稍后再说 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -338,4 +339,4 @@ class PairingView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/profile/bindings/profile_binding.dart b/lib/modules/profile/bindings/profile_binding.dart index 6bfc58f45..ca7ef9b5d 100644 --- a/lib/modules/profile/bindings/profile_binding.dart +++ b/lib/modules/profile/bindings/profile_binding.dart @@ -6,4 +6,4 @@ class ProfileBinding extends Bindings { void dependencies() { Get.lazyPut(() => ProfileController()); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index f3d7a94eb..dc2863aa9 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -1,47 +1,65 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:flutter/material.dart'; +//import 'package:http/http.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:logger/logger.dart'; +import '../../../data/models/user_Info.dart'; +import '../../settings/controllers/settings_controller.dart'; +import 'dart:io'; +import 'dart:convert'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path_provider/path_provider.dart'; +import '../../../data/services/network/api.dart'; +import 'package:dio/dio.dart'; + class ProfileController extends GetxController { static ProfileController get to => Get.find(); - - final _userName = ''.obs; - final _email = ''.obs; - final _avatar = ''.obs; - final _isEditing = false.obs; - final _storage = GetStorage(); - + + // final _userName = ''.obs; + // final _email = ''.obs; + // final _avatar = ''.obs; + // final _isEditing = false.obs; + // final _storage = GetStorage(); + //用户信息 + static const String userKey = 'user_info'; //用户信息存储键 + final _storage = GetStorage(); //本地存储 + final ImagePicker _picker = ImagePicker(); //图片选择器 + final Rx userInfo = User.instance.obs; // 响应式用户信息对象,使用单例模式确保全局唯一性 + final RxString localAvatar = ''.obs; // 本地头像路径,便于响应式更新UI + // 使用响应式变量 final currentLanguage = 'zh'.obs; - - String get userName => _userName.value; - String get email => _email.value; - String get avatar => _avatar.value; - bool get isEditing => _isEditing.value; - + + // String get userName => _userName.value; + // String get email => _email.value; + // String get avatar => _avatar.value; + // bool get isEditing => _isEditing.value; + @override void onInit() { super.onInit(); - loadUserData(); + loadUserInfo(); loadLanguagePreference(); } - - void loadUserData() { - try { - _userName.value = _storage.read('userName') ?? 'User Name'; - _email.value = _storage.read('userEmail') ?? 'user@example.com'; - _avatar.value = _storage.read('userAvatar') ?? ''; - } catch (e) { - Get.snackbar('Error', 'Failed to load user data'); - } - } - + + // void loadUserData() { + // try { + // _userName.value = _storage.read('userName') ?? 'User Name'; + // _email.value = _storage.read('userEmail') ?? 'user@example.com'; + // _avatar.value = _storage.read('userAvatar') ?? ''; + // } catch (e) { + // Get.snackbar('Error', 'Failed to load user data'); + // } + // } + // 加载用户语言偏好设置 void loadLanguagePreference() { try { final String? languageCode = _storage.read('languageCode'); final String? countryCode = _storage.read('countryCode'); - + if (languageCode != null && countryCode != null) { Get.updateLocale(Locale(languageCode, countryCode)); currentLanguage.value = languageCode; @@ -50,7 +68,7 @@ class ProfileController extends GetxController { print('Failed to load language preference: $e'); } } - + // 保存用户语言偏好设置 Future saveLanguagePreference(Locale locale) async { try { @@ -60,7 +78,7 @@ class ProfileController extends GetxController { print('Failed to save language preference: $e'); } } - + // 切换应用语言 void changeLanguage(String languageCode, String countryCode) { // 使用Future.delayed来避免在构建过程中更新UI @@ -71,32 +89,298 @@ class ProfileController extends GetxController { currentLanguage.value = languageCode; }); } - - Future updateUserData({ - String? userName, - String? email, - String? avatar, - }) async { + + // Future updateUserData({ + // String? userName, + // String? email, + // String? avatar, + // }) async { + // try { + // if (userName != null) { + // await _storage.write('userName', userName); + // _userName.value = userName; + // } + // if (email != null) { + // await _storage.write('userEmail', email); + // _email.value = email; + // } + // if (avatar != null) { + // await _storage.write('userAvatar', avatar); + // _avatar.value = avatar; + // } + // Get.snackbar('Success', 'Profile updated successfully'); + // } catch (e) { + // Get.snackbar('Error', 'Failed to update profile'); + // } + // } + + // void toggleEditing() { + // _isEditing.value = !_isEditing.value; + // } + + // 验证base64图片数据 + // Future _verifyBase64Image(String base64String) async { + // try { + // // 解码base64数据 + // List imageBytes = base64Decode(base64String); + + // // 获取应用文档目录 + // final appDir = await getApplicationDocumentsDirectory(); + // final verifyPath = '${appDir.path}/verify_base64_${DateTime.now().millisecondsSinceEpoch}.jpg'; + + // // 将解码后的数据写入文件 + // final file = File(verifyPath); + // await file.writeAsBytes(imageBytes); + + // // 记录验证文件的路径 + // Logger().d('Base64验证图片已保存到: $verifyPath'); + + // // 显示图片预览对话框 + // Get.dialog( + // Dialog( + // child: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Padding( + // padding: const EdgeInsets.all(16.0), + // child: Text('Base64 图片验证', style: TextStyle(fontWeight: FontWeight.bold)), + // ), + // Image.file(File(verifyPath)), + // Padding( + // padding: const EdgeInsets.all(16.0), + // child: ElevatedButton( + // onPressed: () => Get.back(), + // child: Text('关闭'), + // ), + // ), + // ], + // ), + // ), + // ); + + // Get.snackbar('验证', '已将Base64转换回图片,请查看预览'); + // } catch (e) { + // Logger().e('验证Base64图片失败: $e'); + // Get.snackbar('错误', 'Base64验证失败: $e'); + // } + // } + + // 更新用户信息到后端 + Future _syncWithBackend() async { try { - if (userName != null) { - await _storage.write('userName', userName); - _userName.value = userName; + // 准备数据 + Map data = { + "image": "", // 默认空字符串 + "language": currentLanguage.value,//当前用户语言 + "name": userInfo.value.name,//当前用户名 + }; + + // 如果有头像,转换为base64 + if (localAvatar.value.isNotEmpty) { + //如果本地头像路径不为空,即代表有头像 + try { + File imageFile = File(localAvatar.value); + if (await imageFile.exists()) { + List imageBytes = await imageFile.readAsBytes(); + String base64Image = base64Encode(imageBytes); + data["image"] = base64Image; + + // // 验证base64数据 - 将base64转回图片并保存到本地 + // await _verifyBase64Image(base64Image); + } else { + Logger().e('头像文件不存在'); + } + } catch (e) { + Logger().e('读取头像文件失败: $e'); + } } - if (email != null) { - await _storage.write('userEmail', email); - _email.value = email; + + final dio = Dio(BaseOptions( + baseUrl: 'https://www.deapsound.com:443', + headers: { + 'accept': 'application/json', + 'Authorization': _storage.read("logintoken"), + 'Content-Type': 'application/json', + }, + )); + + // 调用API前打印token + Logger().d('当前用户token: ${_storage.read("logintoken")}'); + Logger().d('请求数据: ${{"user": data}}'); + final response = await dio.post( + '/api/home/user_setting', + data: data, + ); + + + // 调用API + //final response = await Api.updateUserSetting(data); + + if (response.statusCode == 200) { + if (response.data['code'] == 0) { + Logger().d('更新成功: ${response.data}'); + Get.snackbar('成功', '同步到服务器成功'); + userInfo.update((u) { + if (u != null) { + u.name = response.data['data']['name'] ?? ''; + u.avatar = response.data['data']['avatar'] ?? ''; + } + }); + Logger().d('更新后的用户信息: ${userInfo.value.toJson()}'); + } else { + throw '更新失败:${response.data['msg'] ?? '未知错误'}'; + } + } else { + throw '服务器响应错误: ${response.statusCode}'; } - if (avatar != null) { - await _storage.write('userAvatar', avatar); - _avatar.value = avatar; + } catch (e) { + Logger().e('同步到后端失败: $e'); + Get.snackbar('错误', '同步到服务器失败,请重试'); + throw e; + } + } + + void loadUserInfo() { + final userJson = _storage.read(userKey); + if (userJson != null) { + userInfo.value = User.instance; + Logger().d('从本地加载用户信息: $userJson'); + // 如果有远程头像地址,下载并保存到本地 + if (userInfo.value.avatar.isNotEmpty) { + Logger().d('远程头像地址: ${userInfo.value.avatar}'); + _downloadAndSaveAvatar(userInfo.value.avatar); + } + } + } + + // 下载并保存远程头像 + Future _downloadAndSaveAvatar(String url) async { + try { + Logger().d('开始下载头像: $url'); + + final dio = Dio(BaseOptions( + headers: { + 'accept': '*/*', + 'Authorization': _storage.read("logintoken"), + }, + )); + + final response = await dio.get( + url, + options: Options(responseType: ResponseType.bytes), + ); + + Logger().d('下载状态: ${response.statusCode}'); + + if (response.statusCode == 200) { + // 保存到应用文档目录 + final appDir = await getApplicationDocumentsDirectory(); + final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final localPath = '${appDir.path}/$fileName'; + + // 写入文件 + final file = File(localPath); + await file.writeAsBytes(response.data); + + // 更新本地头像路径 + localAvatar.value = localPath; + await _storage.write('local_avatar', localPath); + Logger().d('头像已下载到本地: $localPath'); + //Get.snackbar('头像', '头像已下载到本地'); } - Get.snackbar('Success', 'Profile updated successfully'); } catch (e) { - Get.snackbar('Error', 'Failed to update profile'); + Logger().e('下载头像失败: $e'); } } - - void toggleEditing() { - _isEditing.value = !_isEditing.value; + + Future saveUserInfo() async { + await _storage.write(userKey, userInfo.value.toJson()); + } + + // 更新用户头像 + Future updateUserAvatar() async { + final XFile? image = await _picker.pickImage( + source: ImageSource.gallery,// 从相册选择 + maxWidth: 800, + maxHeight: 800, + imageQuality: 85, + ); + + if (image != null) { + // 检查文件大小 + final file = File(image.path); + var fileSize = await file.length(); + var quality = 85; + + // 如果文件大于1MB,进行压缩 + while (fileSize > 1024 * 1024 && quality > 30) { + final compressedImage = await FlutterImageCompress.compressAndGetFile( + file.absolute.path, + file.absolute.path, // 覆盖原文件 + quality: quality, + minWidth: 800, + minHeight: 800, + ); + + if (compressedImage == null) { + Get.snackbar('错误', '图片压缩失败'); + return; + } + + //更新文件大小和质量 + fileSize = await compressedImage.length(); + quality -= 5; + Logger().d('压缩后文件大小:$fileSize'); + } + + userInfo.update((user) { + if (user != null) { + user.avatar = image.path; + } + }); + + // 更新本地头像路径 + localAvatar.value = image.path; + await _storage.write('local_avatar', image.path); + // await saveUserInfo(); //保存到本地 + //await _syncWithBackend(); //同步到后端 + + // 通知 SettingsController 更新 + // if (Get.isRegistered()) { + // Get.find().loadUserInfo(); + // } + } + } + + // 更新用户名 + Future updateUserNameAndAvatar(String name) async { + if (name.trim().isEmpty) { + Logger().e('无效名称: $name'); + return; + } + + try { + userInfo.update((user) { + if (user != null) { + user.name = name.trim(); + Logger().d('名称更新为: ${user.name}'); + } + }); + + //await saveUserInfo(); //保存到本地 + await _syncWithBackend(); //同步到后端 + + //同步更新 SettingsController + if (Get.isRegistered()) { + final settingsController = Get.find(); + settingsController.userInfo.value.name = userInfo.value.name; + settingsController.userInfo.value.avatar = '${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}'; + //settingsController.loadUserInfo(); + settingsController.userInfo.refresh(); + } + } catch (e) { + Logger().e('更新用户名和头像错误: $e'); + Get.snackbar('错误', '更新用户名和头像失败'); + } } -} \ No newline at end of file +} diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 84e8a25fa..ff1fc4ad7 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -4,7 +4,9 @@ import '../controllers/profile_controller.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../core/widgets/common_bottom_nav.dart'; import '../../../routes/app_routes.dart'; +import 'dart:io'; +// ProfileView: 用户设置页面 class ProfileView extends GetView { const ProfileView({Key? key}) : super(key: key); @@ -61,7 +63,6 @@ class ProfileView extends GetView { body: ListView( children: [ SizedBox(height: 12.h), - const Divider(), _buildMenuItem( title: '关于'.tr, @@ -70,7 +71,6 @@ class ProfileView extends GetView { // TODO: 实现关于页面 }, ), - const Divider(), _buildMenuItem( title: '火山语音合成测试'.tr, @@ -80,8 +80,6 @@ class ProfileView extends GetView { Get.toNamed(Routes.TTS_TEST); }, ), - - const Divider(), _buildMenuItem( title: '火山语音识别测试'.tr, @@ -100,7 +98,7 @@ class ProfileView extends GetView { Get.toNamed(Routes.AZURE_ASR_TEST); }, ), - const Divider(), + const Divider(), _buildMenuItem( title: 'Azure语音合成测试'.tr, icon: Icons.record_voice_over, @@ -132,4 +130,138 @@ class ProfileView extends GetView { bottomNavigationBar: const CommonBottomNav(currentIndex: 2), ); } -} \ No newline at end of file +} + +// EditProfileView: 编辑个人资料页面 +class EditProfileView extends GetView { + const EditProfileView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final nameController = + TextEditingController(text: controller.userInfo.value.name); + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + centerTitle: true, + elevation: 0, + backgroundColor: isDarkMode ? Colors.black : Colors.white, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.sp, + color: isDarkMode? Colors.white : Colors.black, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'editProfile'.tr, + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + body: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + // 头像编辑 + Center( + child: GestureDetector( + onTap: () => controller.updateUserAvatar(), + child: Stack( + children: [ + Obx(() => CircleAvatar( + radius: 50.r, + backgroundImage: controller.localAvatar.value.isNotEmpty + ? FileImage(File(controller.localAvatar.value)) + : (controller.userInfo.value.avatar.isNotEmpty + ? NetworkImage( + controller.userInfo.value.avatar) + : null) as ImageProvider?, + child: controller.userInfo.value.avatar.isEmpty + ? Icon(Icons.person, size: 50.r) + : null, + )), + Positioned( + right: 0, + bottom: 0, + child: Container( + padding: EdgeInsets.all(4.w), + decoration: const BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + ), + child: Icon( + Icons.camera_alt, + size: 20.w, + color: Colors.white, + )), + ) + ], + )), + ), + SizedBox(height: 20.h), + + // 昵称编辑 - 使用外部定义的controller而不是每次重建 + Row( + children: [ + Expanded( + child: TextField( + controller: nameController, + decoration: InputDecoration( + labelText: 'nickname'.tr, + border: const OutlineInputBorder(), + suffixIcon: nameController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + nameController.clear(); + }, + ) + : null, + ), + ), + ), + ], + ), + // ... 其他编辑项 ... + + SizedBox(height: 30.h), + // 添加一个大的保存按钮 + SizedBox( + width: double.infinity, + height: 50.h, + child: ElevatedButton( + onPressed: () { + // 保存用户名 + controller.updateUserNameAndAvatar(nameController.text); + // 返回上一级页面 + Get.back(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: isDarkMode ? Colors.blue[700] : Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Text( + 'save'.tr, + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 1ab68e829..ec8dcb43e 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -1,41 +1,237 @@ +import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; +import 'package:get_storage/get_storage.dart'; import '../../../core/themes/theme_service.dart'; import '../../../data/services/speech_factory.dart'; +import '../models/language_model.dart'; +import 'package:logger/logger.dart'; +import '../../../data/models/user_Info.dart'; +import '../../../modules/login/controllers/googleLogin.dart'; +import '../../../modules/login/controllers/facebookLogin.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; +import '../../../routes/app_routes.dart'; class SettingsController extends GetxController { + // 语言设置 + final List languageList = [ + LanguageModel( + name: 'followingSystem', // 跟随系统.tr + alias: 'followingSystem', // 跟随系统.tr + value: 'system'), + LanguageModel(name: 'chinese', alias: '中文', value: 'zh_CN'), // 中文.tr + LanguageModel(name: 'english', alias: 'English', value: 'en_US'), // 英语.tr + ]; + + Rx language = LanguageModel().obs; + + final GetStorage _storage = GetStorage(); + // 主题服务 final ThemeService _themeService = ThemeService(); - + // 是否是暗黑模式 final RxBool isDarkMode = false.obs; - + // 翻译语言对 final RxString sourceLanguage = '中文'.obs; final RxString targetLanguage = '英文'.obs; - final List availableLanguages = ['中文', '英文', '日文', '韩文', '法文', '德文', '西班牙文', '俄文']; - + final List availableLanguages = [ + '中文', + '英文', + '日文', + '韩文', + '法文', + '德文', + '西班牙文', + '俄文' + ]; + // 语音服务类型 final Rx speechServiceType = SpeechServiceType.azure.obs; final List> availableSpeechServices = [ - {'type': SpeechServiceType.azure, 'name': 'Azure 语音服务', 'description': '微软云端语音服务,支持高质量语音合成和识别'}, - {'type': SpeechServiceType.flutter, 'name': 'Flutter 本地语音服务', 'description': '设备本地语音服务,无需联网即可使用'}, + { + 'type': SpeechServiceType.azure, + 'name': 'azureVoiceService', // Azure 语音服务.tr + 'description': 'azureVoiceServiceInfo' // 微软云端语音服务,支持高质量语音合成和识别.tr + }, + { + 'type': SpeechServiceType.flutter, + 'name': 'flutterVoiceService', // Flutter 本地语音服务.tr + 'description': 'flutterVoiceServiceInfo' // 设备本地语音服务,无需联网即可使用.tr + }, ]; - + + RxString timbreName = 'defaultTimbre'.obs; + + //用户信息 + static const String userKey = 'user_info'; + //final _storage=GetStorage(); + //final ImagePicker _picker = ImagePicker(); + final Rx userInfo = User.instance.obs; //使用单例实例 + @override void onInit() { super.onInit(); - + + // 获取系统语言 + _getLanguage(); + + _getTimbre(); + // 从存储中加载上次保存的主题设置 final savedDarkMode = _themeService.isSavedDarkMode(); - + // 设置当前主题状态 isDarkMode.value = savedDarkMode; - + // 获取当前语音服务类型 _loadCurrentSpeechServiceType(); + + //获取用户信息 + loadUserInfo(); + } + + void loadUserInfo() { + final userJson = _storage.read(userKey); + Logger().d('加载用户信息: $userJson'); + + if (userJson != null) { + userInfo.value = User.instance; + Logger().d('用户信息已加载: ${userInfo.value.toJson()}'); + + if (userInfo.value.avatar.isNotEmpty) { + Logger().d('开始下载头像'); + _downloadAndSaveAvatar(userInfo.value.avatar); + } else { + Logger().d('用户没有头像'); + } + } else { + Logger().d('没有找到用户信息'); + } + } + + Future saveUserInfo() async { + await _storage.write(userKey, userInfo.value.toJson()); + } + + // 下载并保存远程头像 + Future _downloadAndSaveAvatar(String url) async { + try { + Logger().d('开始下载头像: $url'); + + final dio = Dio(BaseOptions( + headers: { + 'accept': '*/*', + 'Authorization': _storage.read("logintoken"), + }, + )); + + final response = await dio.get( + url, + options: Options(responseType: ResponseType.bytes), + ); + + Logger().d('下载状态: ${response.statusCode}'); + + if (response.statusCode == 200) { + // 保存到应用文档目录 + final appDir = await getApplicationDocumentsDirectory(); + final fileName = 'avatar_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final localPath = '${appDir.path}/$fileName'; + + // 写入文件 + final file = File(localPath); + await file.writeAsBytes(response.data); + + // 更新本地头像路径 + //localAvatar.value = localPath; + await _storage.write('local_avatar', localPath); + Logger().d('头像已下载到本地: $localPath'); + //Get.snackbar('头像', '头像已下载到本地'); + } + } catch (e) { + Logger().e('下载头像失败: $e'); + } } - + + // Future updateUserAvatar() async { + // final XFile? image = await _picker.pickImage( + // source: ImageSource.gallery, + // maxWidth: 800, + // maxHeight: 800, + // imageQuality: 85, + // ); + + // if (image != null) { + // userInfo.update((user) { + // if (user != null) { + // user.avatar = image.path; + // } + // }); + // await saveUserInfo(); + // } + // } + + Future updateUserName(String name) async { + if (name.trim().isNotEmpty) { + userInfo.update((user) { + if (user != null) { + user.name = name.trim(); + } + }); + await saveUserInfo(); + } + } + +// Future updateDeviceName(String deviceName) async { +// userInfo.update((user) { +// if (user != null) { +// user.deviceName = deviceName; +// } +// }); +// await saveUserInfo(); +// } +// } + + void _getLanguage() { + String? languageValue = _storage.read('language'); + LanguageModel data = languageList.firstWhere( + (element) => element.value == languageValue, + orElse: () => languageList[0], + ); + language.value = data; + } + + // 获取音色 + void _getTimbre() { + List? timbreList = _storage.read('timbreList'); + String? timbre = _storage.read('timbre'); + if (timbreList != null && timbre != null) { + Map data = timbreList.firstWhere( + (element) => element['type'] == timbre, + orElse: () => {}, + ); + if (data.isNotEmpty) { + timbreName.value = data['name']; + } + } + } + + void setLanguage(LanguageModel data) { + if (data.value != 'system') { + List languageList = data.value.split('_'); + Get.updateLocale(Locale(languageList[0], languageList[1])); + } else { + Get.updateLocale(Get.deviceLocale!); + } + language.value = data; + _storage.write('language', data.value); + Get.back(); + } + // 加载当前语音服务类型 void _loadCurrentSpeechServiceType() { try { @@ -45,19 +241,19 @@ class SettingsController extends GetxController { // 如果找不到SpeechFactory实例,使用默认值 } } - + // 切换主题 Future toggleTheme() async { // 切换暗黑模式状态 isDarkMode.value = !isDarkMode.value; - + // 更新全局主题 Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light); - + // 保存用户选择的主题设置 - 确保立即写入存储 await _themeService.saveThemeMode(isDarkMode.value); } - + // 设置语音服务类型 void setSpeechServiceType(SpeechServiceType type) { try { @@ -68,23 +264,64 @@ class SettingsController extends GetxController { // 处理错误 } } - + void setSourceLanguage(String language) { sourceLanguage.value = language; } - + void setTargetLanguage(String language) { targetLanguage.value = language; } - + void swapLanguages() { final temp = sourceLanguage.value; sourceLanguage.value = targetLanguage.value; targetLanguage.value = temp; } - - void signOut() { - // 在此实现退出登录逻辑 - Get.offAllNamed('/login'); + + Future signOut() async { + try { + //显示加载提示 + // Get.dialog( + // const Center(child: CircularProgressIndicator()), + // barrierDismissible: false, + // ); + + // 清理所有存储 + await _storage.erase(); + // 或者逐个清理关键存储项 + await _storage.write("logintoken", null); + await _storage.write("user_info", null); + await _storage.write("local_avatar", null); + print('清理存储成功'); + + // 第三方登录退出 + try { + final google = Google(); + final facebook = Facebook(); + await Future.wait([ + google.signOutGoogle(), + facebook.signOutFacebook(), + ]); + } catch (e) { + print('第三方登录退出错误: $e'); + // 继续执行,不中断退出流程 + } + + // 关闭加载提示 + //Get.back(); + + // 确保导航到登录页面 + if (Get.currentRoute != Routes.login) { + await Get.offAllNamed(Routes.login); + } + + Get.snackbar('成功', '已退出登录'); + print('退出登录成功'); + } catch (e) { + //Get.back(); // 关闭加载提示 + Get.snackbar('错误', '退出登录失败,请重试'); + print('退出登录错误: $e'); + } } -} \ No newline at end of file +} diff --git a/lib/modules/settings/models/language_model.dart b/lib/modules/settings/models/language_model.dart new file mode 100644 index 000000000..8b635e18c --- /dev/null +++ b/lib/modules/settings/models/language_model.dart @@ -0,0 +1,11 @@ +class LanguageModel { + final String name; + final String alias; + final String value; + + LanguageModel({ + this.name = '', + this.alias = '', + this.value = '', + }); +} diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index d66e6e9c9..ce4f7866a 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -1,26 +1,37 @@ +import 'package:app_settings/app_settings.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:ui'; +import '../../../core/theme/app_colors.dart'; import '../controllers/settings_controller.dart'; import '../../../core/widgets/common_bottom_nav.dart'; import '../../../routes/app_routes.dart'; +//import '../../../data/models/user_Info.dart'; +import 'dart:io'; class SettingsView extends GetView { const SettingsView({Key? key}) : super(key: key); + // 正确获取用户名方式 + // String get userName => User.getname(); + + // String get deviceInfo => User.getdeviceInfo(); + // String get userId => User.getuserId(); + // String get standing => User.getstanding(); + // String get avatar => User.getnavatar(); @override Widget build(BuildContext context) { // 直接使用 Theme.of(context) 获取当前主题 final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], appBar: AppBar( backgroundColor: isDarkMode ? Colors.black : Colors.white, elevation: 0.5, title: Text( - '设置', + 'settings'.tr, // 设置 style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, @@ -28,13 +39,14 @@ class SettingsView extends GetView { ), ), centerTitle: true, - ), body: ListView( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), children: [ _buildUserInfoCard(isDarkMode), SizedBox(height: 20.h), + _buildAISettingsSection(isDarkMode), + SizedBox(height: 20.h), _buildSystemSettingsSection(isDarkMode), SizedBox(height: 20.h), _buildAboutSection(isDarkMode), @@ -51,22 +63,19 @@ class SettingsView extends GetView { padding: EdgeInsets.all(16.w), decoration: BoxDecoration( gradient: LinearGradient( - colors: isDarkMode + colors: isDarkMode ? [ - const Color(0xFF1A1A1A).withOpacity(0.7), + const Color(0xFF1A1A1A).withOpacity(0.7), const Color(0xFF2A2A2A).withOpacity(0.7) ] - : [ - const Color(0xFFE3F2FD), - const Color(0xFFF3E5F5) - ], + : [const Color(0xFFE3F2FD), const Color(0xFFF3E5F5)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( - color: isDarkMode + color: isDarkMode ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), blurRadius: 10, @@ -75,109 +84,186 @@ class SettingsView extends GetView { ], ), child: ClipRRect( + //裁剪圆角矩形 borderRadius: BorderRadius.circular(16.r), child: BackdropFilter( - filter: isDarkMode ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Row( children: [ - Container( - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), + Obx(() => GestureDetector( + //onTap: ()=>controller.updateUserAvatar(), + child: Container( + //用户头像部分 + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + // 裁剪圆形头像 + borderRadius: BorderRadius.circular(30.r), + child: controller.userInfo.value.avatar.isEmpty + ? + // Image.network( + // 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80', + // fit: BoxFit.cover, // 图片填充方式 + // ) + Container( + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(30.r), + ), + child: Center( + child: Text( + controller.userInfo.value.name.isNotEmpty + ? controller.userInfo.value.name[0] + .toUpperCase() + : '?', + style: TextStyle( + color: Colors.white, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ) + : Image.network( + controller.userInfo.value.avatar, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + // 当图片加载失败时显示用户名首字母 + return Container( + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: + BorderRadius.circular(30.r), + ), + child: Center( + child: Text( + controller.userInfo.value.name + .isNotEmpty + ? controller + .userInfo.value.name[0] + .toUpperCase() + : '?', + style: TextStyle( + color: Colors.white, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }, + )), ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(30.r), - child: Image.network( - 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80', - fit: BoxFit.cover, - ), - ), - ), + )), SizedBox(width: 16.w), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '林美珍', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, + // 占据剩余空间 + child: Obx( + () => Column( + crossAxisAlignment: CrossAxisAlignment.start, // 左对齐 + children: [ + Text( + //用户名 + controller.userInfo.value.name, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 4.h), - Row( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), - decoration: BoxDecoration( - color: Colors.blue[600], - borderRadius: BorderRadius.circular(10.r), - ), - child: Text( - '高级会员', - style: TextStyle( - fontSize: 10.sp, - color: Colors.white, + SizedBox(height: 4.h), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, vertical: 2.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(10.r), ), + child: Obx(() { + final membershipMap = { + 0: 'freeUser', //'免费用户' + 1: 'standardUser', //'标准用户' + 2: 'proUser', //'专业用户' + }; + final membershipString = membershipMap[controller + .userInfo.value.membershipLevel] ?? + 'Unknown'; + return Text( + membershipString, + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + ), + ); + }), ), - ), - SizedBox(width: 8.w), - Flexible( - child: Text( - '已连接设备: DeepVoice Pro', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + SizedBox(width: 8.w), + Flexible( + child: Text( + '${'joinDevice'.tr} DeepSound Pro', // 已连接设备: + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode + ? Colors.white70 + : Colors.black54, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ), + ], + ), + SizedBox(height: 4.h), + Text( + controller.userInfo.value.uid, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, ), - ], - ), - SizedBox(height: 4.h), - Text( - '账号ID: deepvoice_834291', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, ), - ), - ], + ], + ), ), ), Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.white.withOpacity(0.7), - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 1), + // 编辑按钮 + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.white.withOpacity(0.7), + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: InkWell( + onTap: () => Get.toNamed(Routes.editProfile), + child: Icon( + Icons.edit, + size: 16.w, + color: isDarkMode ? Colors.white70 : Colors.grey[600], ), - ], - ), - child: Icon( - Icons.edit, - size: 16.w, - color: isDarkMode ? Colors.white70 : Colors.grey[600], - ), - ), + )), ], ), ), @@ -185,6 +271,82 @@ class SettingsView extends GetView { ); } + // AI设置 + Widget _buildAISettingsSection(bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + 'AI设置', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.blue[400] : Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: BackdropFilter( + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Column( + children: [ + // 语音服务选择 + _buildSpeechServiceSetting(isDarkMode), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + Obx( + () => _buildNavigationSetting( + title: 'timbreService'.tr, // 音色服务 + subtitle: 'timbreServiceInfo'.tr, // 选择音色服务 + icon: Icons.photo_camera_front_outlined, + iconBgColor: isDarkMode + ? Colors.cyan[900]!.withOpacity(0.3) + : Colors.cyan[100]!, + iconColor: + isDarkMode ? Colors.cyan[300]! : Colors.cyan[600]!, + value: controller.timbreName.value.tr, + valueStyle: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[500], + ), + onTap: () { + Get.toNamed(Routes.timbre); + }, + isDarkMode: isDarkMode, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + // 系统设置部分 Widget _buildSystemSettingsSection(bool isDarkMode) { return Column( @@ -193,7 +355,7 @@ class SettingsView extends GetView { Padding( padding: EdgeInsets.only(left: 4.w, bottom: 8.h), child: Text( - '系统设置', + 'systemSetting'.tr, // 系统设置 style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w500, @@ -208,7 +370,7 @@ class SettingsView extends GetView { borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( - color: isDarkMode + color: isDarkMode ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), blurRadius: 10, @@ -219,115 +381,149 @@ class SettingsView extends GetView { child: ClipRRect( borderRadius: BorderRadius.circular(16.r), child: BackdropFilter( - filter: isDarkMode ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Column( children: [ - // 通知设置 - _buildSimpleNavigationSetting( - title: '通知设置', - subtitle: '管理应用推送和提醒', - icon: Icons.notifications_none, - iconBgColor: isDarkMode ? Colors.indigo[900]!.withOpacity(0.3) : Colors.indigo[100]!, - iconColor: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - // 打开通知设置页面 - }, - isDarkMode: isDarkMode, + // 语言设置 + Obx( + () => _buildSimpleNavigationSetting( + title: 'languageSetting'.tr, // 语言设置 + subtitle: controller.language.value.name.tr, + icon: Icons.language, + iconBgColor: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + iconColor: isDarkMode + ? Colors.indigo[300]! + : Colors.indigo[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + _showLanguageSelectionDialog(isDarkMode); + }, + isDarkMode: isDarkMode, + ), ), - Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 主题设置 _buildToggleSetting( - title: '暗黑模式', - subtitle: '切换应用的明暗主题', + title: 'darkMode'.tr, // 暗黑模式 + subtitle: 'darkModeInfo'.tr, // 切换应用的明暗主题 icon: isDarkMode ? Icons.dark_mode : Icons.light_mode, - iconBgColor: isDarkMode - ? Colors.indigo[900]!.withOpacity(0.3) + iconBgColor: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) : Colors.amber[100]!, - iconColor: isDarkMode - ? Colors.indigo[300]! - : Colors.amber[600]!, + iconColor: + isDarkMode ? Colors.indigo[300]! : Colors.amber[600]!, value: isDarkMode, onChanged: (value) { controller.toggleTheme(); }, isDarkMode: isDarkMode, ), - Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 备份与恢复 _buildNavigationSetting( - title: '备份与恢复', - subtitle: '备份设置和AI互动历史', + title: 'backups'.tr, // 备份与恢复 + subtitle: 'backupsInfo'.tr, // 备份设置和AI互动历史 icon: Icons.cloud_upload_outlined, - iconBgColor: isDarkMode ? Colors.cyan[900]!.withOpacity(0.3) : Colors.cyan[100]!, - iconColor: isDarkMode ? Colors.cyan[300]! : Colors.cyan[600]!, - value: '上次备份: 今天', + iconBgColor: isDarkMode + ? Colors.cyan[900]!.withOpacity(0.3) + : Colors.cyan[100]!, + iconColor: + isDarkMode ? Colors.cyan[300]! : Colors.cyan[600]!, + value: '${'lastBackup'.tr} 今天', // 上次备份: valueStyle: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.grey[500], ), onTap: () { // 打开备份设置页面 + print( + '---------用户头像-${controller.userInfo.value.avatar}'); + print('---------用户名-${controller.userInfo.value.name}'); + //print('user${User.instance.value}'); }, isDarkMode: isDarkMode, ), - Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - - // 语音服务选择 - _buildSpeechServiceSetting(isDarkMode), - - Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), // 隐私设置 _buildSimpleNavigationSetting( - title: '隐私设置', - subtitle: '管理数据收集和权限', + title: 'privacySettings'.tr, // 隐私设置 + subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限 icon: Icons.shield_outlined, - iconBgColor: isDarkMode ? Colors.purple[900]!.withOpacity(0.3) : Colors.purple[100]!, - iconColor: isDarkMode ? Colors.purple[300]! : Colors.purple[600]!, + iconBgColor: isDarkMode + ? Colors.purple[900]!.withOpacity(0.3) + : Colors.purple[100]!, + iconColor: + isDarkMode ? Colors.purple[300]! : Colors.purple[600]!, titleColor: isDarkMode ? Colors.white : null, subtitleColor: isDarkMode ? Colors.white70 : null, onTap: () { - // 打开隐私设置页面 + AppSettings.openAppSettings(); }, isDarkMode: isDarkMode, ), - Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 退出登录 _buildSimpleNavigationSetting( - title: '退出登录', + title: 'logout'.tr, // 退出登录 icon: Icons.logout, - iconBgColor: isDarkMode ? Colors.red[900]!.withOpacity(0.3) : Colors.red[50]!, + iconBgColor: isDarkMode + ? Colors.red[900]!.withOpacity(0.3) + : Colors.red[50]!, iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, - titleColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, + titleColor: + isDarkMode ? Colors.red[300]! : Colors.red[600]!, onTap: () { // 退出登录确认对话框 Get.dialog( AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + backgroundColor: + isDarkMode ? Colors.grey[900] : Colors.white, title: Text( - '退出登录', + 'logout'.tr, // 退出登录 style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, ), ), content: Text( - '确定要退出当前账号吗?', + 'logoutTips'.tr, // 确定要退出当前账号吗? style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, + color: + isDarkMode ? Colors.white70 : Colors.black54, ), ), actions: [ TextButton( onPressed: () => Get.back(), child: Text( - '取消', + 'cancel'.tr, // 取消 style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, + color: isDarkMode + ? Colors.white70 + : Colors.black54, ), ), ), @@ -337,9 +533,11 @@ class SettingsView extends GetView { controller.signOut(); }, child: Text( - '确定', + 'confirm'.tr, // 确定 style: TextStyle( - color: isDarkMode ? Colors.red[300] : Colors.red[600], + color: isDarkMode + ? Colors.red[300] + : Colors.red[600], ), ), ), @@ -366,7 +564,7 @@ class SettingsView extends GetView { Padding( padding: EdgeInsets.only(left: 4.w, bottom: 8.h), child: Text( - '关于应用', + 'aboutApplication'.tr, // 关于应用 style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w500, @@ -382,7 +580,7 @@ class SettingsView extends GetView { borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( - color: isDarkMode + color: isDarkMode ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), blurRadius: 10, @@ -393,7 +591,9 @@ class SettingsView extends GetView { child: ClipRRect( borderRadius: BorderRadius.circular(16.r), child: BackdropFilter( - filter: isDarkMode ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -405,8 +605,10 @@ class SettingsView extends GetView { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - const Color(0xFF2196F3).withOpacity(isDarkMode ? 0.7 : 1.0), - const Color(0xFF9C27B0).withOpacity(isDarkMode ? 0.7 : 1.0) + const Color(0xFF2196F3) + .withOpacity(isDarkMode ? 0.7 : 1.0), + const Color(0xFF9C27B0) + .withOpacity(isDarkMode ? 0.7 : 1.0) ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -424,7 +626,7 @@ class SettingsView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'DeepVoice', + 'DeepSound', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -433,10 +635,11 @@ class SettingsView extends GetView { ), SizedBox(height: 2.h), Text( - '版本 2.4.1 (Build 87)', + '${'versions'.tr} 2.4.1 (Build 87)', // 版本 style: TextStyle( fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + color: + isDarkMode ? Colors.white70 : Colors.black54, ), ), ], @@ -444,13 +647,16 @@ class SettingsView extends GetView { ], ), Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), decoration: BoxDecoration( - color: isDarkMode ? Colors.blue[900]!.withOpacity(0.3) : Colors.blue[100], + color: isDarkMode + ? Colors.blue[900]!.withOpacity(0.3) + : Colors.blue[100], borderRadius: BorderRadius.circular(16.r), ), child: Text( - '检查更新', + 'checkUpdates'.tr, // 检查更新 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.blue[300] : Colors.blue[600], @@ -467,7 +673,7 @@ class SettingsView extends GetView { child: Column( children: [ Text( - '© 2023 DeepVoice Technologies', + '© 2025 DeepSound Technologies', style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[500], @@ -475,7 +681,7 @@ class SettingsView extends GetView { ), SizedBox(height: 4.h), Text( - '连接世界,超越声音', + 'homeSlogan'.tr, // 连接世界,超越声音 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[500], @@ -534,8 +740,7 @@ class SettingsView extends GetView { ), overflow: TextOverflow.ellipsis, ), - if (subtitle != null) - SizedBox(height: 2.h), + if (subtitle != null) SizedBox(height: 2.h), if (subtitle != null) Text( subtitle, @@ -610,14 +815,14 @@ class SettingsView extends GetView { ), overflow: TextOverflow.ellipsis, ), - if (subtitle != null) - SizedBox(height: 2.h), + if (subtitle != null) SizedBox(height: 2.h), if (subtitle != null) Text( subtitle, style: TextStyle( fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + color: + isDarkMode ? Colors.white70 : Colors.black54, ), overflow: TextOverflow.ellipsis, ), @@ -633,10 +838,11 @@ class SettingsView extends GetView { Flexible( child: Text( value, - style: valueStyle ?? TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black87, - ), + style: valueStyle ?? + TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black87, + ), overflow: TextOverflow.ellipsis, ), ), @@ -645,7 +851,9 @@ class SettingsView extends GetView { width: 32.w, height: 32.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), ), child: Icon( @@ -707,18 +915,21 @@ class SettingsView extends GetView { style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, - color: titleColor ?? (isDarkMode ? Colors.white : Colors.black87), + color: titleColor ?? + (isDarkMode ? Colors.white : Colors.black87), ), overflow: TextOverflow.ellipsis, ), - if (subtitle != null) - SizedBox(height: 2.h), + if (subtitle != null) SizedBox(height: 2.h), if (subtitle != null) Text( subtitle, style: TextStyle( fontSize: 12.sp, - color: subtitleColor ?? (isDarkMode ? Colors.white70 : Colors.black54), + color: subtitleColor ?? + (isDarkMode + ? Colors.white70 + : Colors.black54), ), overflow: TextOverflow.ellipsis, ), @@ -732,7 +943,9 @@ class SettingsView extends GetView { width: 32.w, height: 32.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), ), child: Icon( @@ -765,7 +978,9 @@ class SettingsView extends GetView { width: 32.w, height: 32.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.teal[900]!.withOpacity(0.3) : Colors.teal[100]!, + color: isDarkMode + ? Colors.teal[900]!.withOpacity(0.3) + : Colors.teal[100]!, borderRadius: BorderRadius.circular(16.r), ), child: Icon( @@ -780,7 +995,7 @@ class SettingsView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '语音服务', + 'voiceService'.tr, // 语音服务 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -789,7 +1004,7 @@ class SettingsView extends GetView { ), SizedBox(height: 2.h), Text( - '选择语音识别和合成服务', + 'voiceServiceInfo'.tr, // 选择语音识别和合成服务 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.black54, @@ -808,11 +1023,11 @@ class SettingsView extends GetView { Obx(() { final serviceType = controller.speechServiceType.value; final serviceName = controller.availableSpeechServices - .firstWhere((service) => service['type'] == serviceType)['name']; - + .firstWhere( + (service) => service['type'] == serviceType)['name']; return Flexible( child: Text( - serviceName, + '$serviceName'.tr, style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.grey[600], @@ -826,7 +1041,9 @@ class SettingsView extends GetView { width: 32.w, height: 32.w, decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), ), child: Icon( @@ -842,7 +1059,7 @@ class SettingsView extends GetView { ), ); } - + // 显示语音服务选择对话框 void _showSpeechServiceSelectionDialog(bool isDarkMode) { Get.dialog( @@ -858,7 +1075,7 @@ class SettingsView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '选择语音服务', + 'selectVoiceService'.tr, // 选择语音服务 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, @@ -868,8 +1085,8 @@ class SettingsView extends GetView { SizedBox(height: 16.h), ...controller.availableSpeechServices.map((service) { return Obx(() { - final isSelected = controller.speechServiceType.value == service['type']; - + final isSelected = + controller.speechServiceType.value == service['type']; return InkWell( onTap: () { controller.setSpeechServiceType(service['type']); @@ -880,12 +1097,18 @@ class SettingsView extends GetView { padding: EdgeInsets.all(12.w), decoration: BoxDecoration( color: isSelected - ? (isDarkMode ? Colors.teal[900]!.withOpacity(0.3) : Colors.teal[50]!) - : (isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100]!), + ? (isDarkMode + ? Colors.teal[900]!.withOpacity(0.3) + : Colors.teal[50]!) + : (isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100]!), borderRadius: BorderRadius.circular(12.r), border: Border.all( color: isSelected - ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) + ? (isDarkMode + ? Colors.teal[300]! + : Colors.teal[600]!) : Colors.transparent, width: 1, ), @@ -897,8 +1120,12 @@ class SettingsView extends GetView { height: 24.w, decoration: BoxDecoration( color: isSelected - ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) - : (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[300]), + ? (isDarkMode + ? Colors.teal[300] + : Colors.teal[600]) + : (isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[300]), shape: BoxShape.circle, ), child: isSelected @@ -915,20 +1142,24 @@ class SettingsView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - service['name'], + '${service['name']}'.tr, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, + color: isDarkMode + ? Colors.white + : Colors.black87, ), overflow: TextOverflow.ellipsis, ), SizedBox(height: 4.h), Text( - service['description'], + '${service['description']}'.tr, style: TextStyle( fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + color: isDarkMode + ? Colors.white70 + : Colors.black54, ), overflow: TextOverflow.ellipsis, maxLines: 2, @@ -949,7 +1180,7 @@ class SettingsView extends GetView { TextButton( onPressed: () => Get.back(), child: Text( - '取消', + 'cancel'.tr, // 取消 style: TextStyle( color: isDarkMode ? Colors.white70 : Colors.black54, ), @@ -963,4 +1194,86 @@ class SettingsView extends GetView { ), ); } -} \ No newline at end of file + + // 显示语言选择对话框 + void _showLanguageSelectionDialog(bool isDarkMode) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24.r), + ), + insetPadding: EdgeInsets.symmetric(horizontal: 20.w), + backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, + child: Container( + width: 0.9.sw, + padding: EdgeInsets.only(top: 32.h, bottom: 16.h), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Text( + 'selectLanguage'.tr, // 选择语言 + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + SizedBox(height: 16.h), + Container( + constraints: BoxConstraints(maxHeight: 0.5.sh), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: controller.languageList.map((language) { + return ListTile( + title: Text( + language.name.tr, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + trailing: + controller.language.value.value == language.value + ? Icon( + Icons.check_circle, + color: isDarkMode + ? AppColors.primary + : Colors.blue, + ) + : const SizedBox.shrink(), + onTap: () { + controller.setLanguage(language); + }, + ); + }).toList(), + ), + ), + ), + SizedBox(height: 16.h), + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only(right: 24.w), + child: TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode ? AppColors.primary : Colors.blue, + fontSize: 16.sp, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index b12ae7050..b5dd9d823 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -1,87 +1,116 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import '../../../data/models/user_Info.dart'; import '../../../routes/app_routes.dart'; import '../../../core/themes/theme_service.dart'; +import '../../../data/services/network/api.dart'; class SplashController extends GetxController { + final GetStorage _storage = GetStorage(); // 进度条动画控制 final _loadingProgress = 0.0.obs; double get loadingProgress => _loadingProgress.value; - - // 版本号 - final String version = 'v1.0.0'; - + // 资源加载状态 final _isResourceLoaded = false.obs; bool get isResourceLoaded => _isResourceLoaded.value; - + + // 版本号 + final String version = 'v1.0.0'; + // 主题服务 final ThemeService _themeService = ThemeService(); - + @override void onInit() { super.onInit(); - - // 确保页面先渲染,再执行其他操作 WidgetsBinding.instance.addPostFrameCallback((_) { - // 启动进度条动画 _startLoadingAnimation(); - - // 异步加载应用资源 _loadAppResources(); }); } - - // 进度条动画 + + // 进度条动画(持续2秒) void _startLoadingAnimation() { - // 模拟0.5秒的加载进度 - Timer.periodic(const Duration(milliseconds: 5), (timer) { + const totalDuration = 2000; // 动画总时长2秒 + const steps = totalDuration ~/ 50; // 40次更新(50ms间隔) + final stepValue = 1.0 / steps; + + Timer.periodic(const Duration(milliseconds: 50), (timer) { if (_loadingProgress.value < 1.0) { - _loadingProgress.value += 0.01; + _loadingProgress.value += stepValue; } else { timer.cancel(); - // 当进度条动画完成且资源加载完毕时,跳转到登录页面 - if (_isResourceLoaded.value) { - _navigateToLogin(); - } + _checkNavigateCondition(); } }); } - + // 异步加载应用资源 Future _loadAppResources() async { try { - // 在这里可以添加应用初始化需要的资源加载 - // 例如:预加载图片、初始化数据库、加载配置等 - - // 模拟耗时操作 - await Future.delayed(const Duration(milliseconds: 100)); - - // TODO: 在此处添加实际的资源加载代码 - - // 标记资源加载完成 + // 模拟资源加载(如API配置、本地缓存等) + await Future.delayed(const Duration(milliseconds: 1500)); + + // 实际资源加载代码 + // await _loadImages(); + // await _initDatabase(); + _isResourceLoaded.value = true; - - // 如果进度条已经走完,则跳转到登录页面 - if (_loadingProgress.value >= 1.0) { - _navigateToLogin(); - } + _checkNavigateCondition(); } catch (e) { - // 处理资源加载错误 - // 即使加载失败,也应该允许用户继续使用应用 - _isResourceLoaded.value = true; - - // 记录错误日志 + Get.snackbar('错误', '资源加载失败,请检查网络连接'); + _isResourceLoaded.value = true; // 允许用户继续使用 print('资源加载错误: $e'); } } - - // 跳转到登录页面 - void _navigateToLogin() { - // 添加一个小延迟,确保UI完全渲染 - Future.delayed(const Duration(milliseconds: 200), () { - Get.offAllNamed(Routes.login); - }); + + // 检查跳转条件 + void _checkNavigateCondition() { + if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { + _navigateToLogin(); + } + } + + // 获取登录Token并跳转 + Future _navigateToLogin() async { + bool isLoggedIn = false; + final token = _storage.read('logintoken'); + print('getLoginToken:进来了${token}'); + if (token != null && token.isNotEmpty) { + isLoggedIn = await getLoginToken(); + } + if (isLoggedIn) { + Get.offAllNamed(Routes.pairing); + } else { + Get.offAllNamed(Routes.login); // 跳转到登录页 + } + } + + Future getLoginToken() async { + try { + print('getLoginToken:进来了'); + //if (User.getname() != "") { + final response = await Api.tokenlogin(); + print('getLoginToken: $response'); + if (response == null) return false; + + if (response['user'] != '') { + print('getLoginToken:进来了'); + // 根据API设计调整 + final user = User.fromJson(response); + // 存储用户信息(示例) + await _storage.write('user_info', user.toJson()); + return true; + } + //return true; + //} + return false; + } catch (e) { + print('Token获取失败: $e'); + return false; + } } -} \ No newline at end of file +} diff --git a/lib/modules/splash/views/splash_view.dart b/lib/modules/splash/views/splash_view.dart index 0f2fb0e4f..900c5af3e 100644 --- a/lib/modules/splash/views/splash_view.dart +++ b/lib/modules/splash/views/splash_view.dart @@ -9,7 +9,7 @@ class SplashView extends GetView { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF5F5F7), body: Stack( @@ -21,24 +21,24 @@ class SplashView extends GetView { children: [ // Logo动画 _buildAnimatedLogo(isDarkMode), - + // 应用名称 _buildAppTitle(isDarkMode), - + // 标语 _buildSlogan(isDarkMode), - + // 加载进度条 SizedBox(height: 40.h), _buildLoadingBar(isDarkMode), - + // 额外信息 SizedBox(height: 16.h), _buildExtraInfo(isDarkMode), ], ), ), - + // 版本号 _buildVersionInfo(isDarkMode), ], @@ -55,7 +55,7 @@ class SplashView extends GetView { builder: (context, value, child) { // 确保opacity值不超过1.0 final opacity = value <= 1.0 ? value : 1.0; - + return Transform.scale( scale: value, child: AnimatedOpacity( @@ -105,7 +105,7 @@ class SplashView extends GetView { child: Padding( padding: EdgeInsets.only(top: 24.h), child: Text( - 'DeepVoice', + 'DeepSound', style: TextStyle( fontSize: 28.sp, fontWeight: FontWeight.bold, @@ -130,7 +130,7 @@ class SplashView extends GetView { child: Padding( padding: EdgeInsets.only(top: 8.h), child: Text( - '连接世界,岂止声音', + 'slogan'.tr, // 连接世界,岂止声音 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -189,7 +189,7 @@ class SplashView extends GetView { return Opacity( opacity: value, child: Text( - 'AI语音专家 · 随时随地', + 'sloganInfo'.tr, // AI语音专家 · 随时随地 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.grey[500] : Colors.grey[400], @@ -217,4 +217,4 @@ class SplashView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/timbre/bindings/timbre_binding.dart b/lib/modules/timbre/bindings/timbre_binding.dart new file mode 100644 index 000000000..6883836f7 --- /dev/null +++ b/lib/modules/timbre/bindings/timbre_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/timbre_controller.dart'; + +class TimbreBinding extends Bindings { + @override + void dependencies() { + Get.put(TimbreController()); + } +} diff --git a/lib/modules/timbre/controllers/timbre_controller.dart b/lib/modules/timbre/controllers/timbre_controller.dart new file mode 100644 index 000000000..62d2cec06 --- /dev/null +++ b/lib/modules/timbre/controllers/timbre_controller.dart @@ -0,0 +1,292 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:record/record.dart'; + +import '../../../data/services/speech_factory.dart'; +import '../../settings/controllers/settings_controller.dart'; + +class TimbreController extends GetxController { + final SettingsController settingsController = Get.find(); + + final GetStorage _storage = GetStorage(); + final _record = AudioRecorder(); + + final String _appId = "43d87d22"; + final String _appKey = "2bd01ce06ac35957fc0c082f9b6284ec"; + + RxList dataList = [].obs; + RxString type = '1'.obs; + + RxString text = ''.obs; + + String _token = ''; + + int _textId = 0; + int _segId = 0; + String _taskId = ''; + + @override + void onInit() { + super.onInit(); + _getDataList(); + } + + @override + void onClose() { + _record.dispose(); + super.onClose(); + } + + void _getDataList() { + List? timbreList = _storage.read('timbreList'); + String? timbre = _storage.read('timbre'); + if (timbreList != null) { + dataList.value = timbreList; + } else { + dataList.add({ + 'type': '1', + 'name': 'defaultTimbre', + 'description': 'microsoftCloudTimbreService', + 'isUsable': true + }); + _storage.write('timbreList', [ + { + 'type': '1', + 'name': 'defaultTimbre', + 'description': 'microsoftCloudTimbreService', + 'isUsable': true + } + ]); + } + if (timbre != null) { + type.value = timbre; + } + } + + // 录制声音 + void startRecorder() async { + if (_token.isEmpty) { + await _getToken(); + } + await _getTraintext(); + await _addTask(); + final directory = await getApplicationDocumentsDirectory(); + final path = + '${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav'; + await _record.start( + const RecordConfig( + encoder: AudioEncoder.wav, + sampleRate: 24000, + numChannels: 1, + ), + path: path, + ); + } + + // 获取token + Future _getToken() async { + int timeStamp = DateTime.now().millisecondsSinceEpoch; + String data = + '{"base":{"appid":"$_appId","version":"v1","timestamp":"$timeStamp"},"model":"remote"}'; + String keySignInput = _appKey + timeStamp.toString(); + List keySignBytes = utf8.encode(keySignInput); + Digest keySignDigest = md5.convert(keySignBytes); + String keySign = keySignDigest.toString(); + String signInput = keySign + data; + List signBytes = utf8.encode(signInput); + Digest signDigest = md5.convert(signBytes); + String sign = signDigest.toString(); + + final response = await http.post( + Uri.parse('http://avatar-hci.xfyousheng.com/aiauth/v1/token'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': sign, + }, + body: data, + ); + if (response.statusCode == 200) { + final body = json.decode(response.body); + if (body['retcode'] == '000000') { + _token = body['accesstoken']; + } + } + } + + // 获取训练文本 + Future _getTraintext() async { + String data = '{"textId":"5001"}'; + Map sign = _sign(data); + + final response = await http.post( + Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/traintext'), + headers: { + 'Content-Type': 'application/json', + 'X-Sign': sign['sign'], + 'X-Token': _token, + 'X-AppId': _appId, + 'X-Time': sign['timeStamp'], + }, + body: data, + ); + if (response.statusCode == 200) { + final body = json.decode(response.body); + if (body['code'] == 0) { + _textId = body['data']['textId']; + Map textSegs = body['data']['textSegs'].first; + _segId = textSegs['segId']; + text.value = _decodeCorruptedText(textSegs['segText']); + } + } + } + + // 创建训练任务 + Future _addTask() async { + String data = '{"resourceType":"12"}'; + Map sign = _sign(data); + + final response = await http.post( + Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/add'), + headers: { + 'Content-Type': 'application/json', + 'X-Sign': sign['sign'], + 'X-Token': _token, + 'X-AppId': _appId, + 'X-Time': sign['timeStamp'], + }, + body: data, + ); + if (response.statusCode == 200) { + final body = json.decode(response.body); + if (body['code'] == 0) { + _taskId = body['data']; + } + } + } + + Map _sign(String data) { + int timeStamp = DateTime.now().millisecondsSinceEpoch; + String keySignInput = _appKey + timeStamp.toString(); + List keySignBytes = utf8.encode(keySignInput); + Digest keySignDigest = md5.convert(keySignBytes); + String keySign = keySignDigest.toString(); + String signInput = keySign + data; + List signBytes = utf8.encode(signInput); + Digest signDigest = md5.convert(signBytes); + String sign = signDigest.toString(); + return {'sign': sign, 'timeStamp': timeStamp.toString()}; + } + + String _decodeCorruptedText(String corruptedText) { + List latin1Bytes = + corruptedText.codeUnits.map((ch) => ch & 0xFF).toList(); + return utf8.decode(latin1Bytes); + } + + // 完成录制 + void completeRecorder() async { + final audioPath = await _record.stop(); + String data = + '{"taskId":"$_taskId","textId":"$_textId","textSegId":"$_segId"}'; + Map sign = _sign(data); + + final uri = Uri.parse( + 'http://opentrain.xfyousheng.com/voice_train/task/submitWithAudio'); + final request = http.MultipartRequest('POST', uri); + request.files.add(await http.MultipartFile.fromPath( + 'file', + audioPath!, + filename: path.basename(audioPath), + )); + request.fields.addAll({ + 'taskId': _taskId, + 'textId': '$_textId', + 'textSegId': '$_segId', + }); + request.headers.addAll({ + "X-Sign": sign['sign'], + "X-Token": _token, + "X-AppId": _appId, + "X-Time": sign['timeStamp'], + }); + final response = await request.send(); + if (response.statusCode == 200) { + final responseBody = await response.stream.bytesToString(); + final body = json.decode(responseBody); + if (body['code'] == 0) { + text.value = ''; + dataList.add({ + 'type': _taskId, + 'name': '声音复刻${dataList.length}', + 'description': '复刻本人音色服务', + 'isUsable': false + }); + await _getTaskResult(_taskId); + } + } + } + + // 查询训练任务状态 + Future _getTaskResult(String id) async { + String data = '{"taskId":"$id"}'; + Map sign = _sign(data); + + final response = await http.post( + Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/result'), + headers: { + 'Content-Type': 'application/json', + 'X-Sign': sign['sign'], + 'X-Token': _token, + 'X-AppId': _appId, + 'X-Time': sign['timeStamp'], + }, + body: data, + ); + if (response.statusCode == 200) { + final body = json.decode(response.body); + if (body['code'] == 0) { + if (body['data']['trainStatus'] == -1) { + await _getTaskResult(id); + } + if (body['data']['trainStatus'] == 1) { + Map? dataMap = + dataList.firstWhereOrNull((item) => item['type'] == id); + if (dataMap != null) { + dataMap['type'] = body['data']['assetId']; + dataMap['isUsable'] = true; + dataList.refresh(); + _storage.write('timbreList', dataList); + } + } + if (body['data']['trainStatus'] == 0) { + Get.snackbar( + 'error'.tr, // 错误 + _decodeCorruptedText(body['data']['failedDesc']), + snackPosition: SnackPosition.TOP, + ); + } + } + } + } + + // 取消 + void cancel() { + text.value = ''; + _record.stop(); + Get.back(); + } + + // 选择音色 + void selectTone(Map data) { + if (!data['isUsable'] || type.value == data['type']) return; + type.value = data['type']; + settingsController.timbreName.value = data['name']; + _storage.write('timbre', data['type']); + } +} diff --git a/lib/modules/timbre/views/timbre_view.dart b/lib/modules/timbre/views/timbre_view.dart new file mode 100644 index 000000000..9073b1dcd --- /dev/null +++ b/lib/modules/timbre/views/timbre_view.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../../core/theme/app_colors.dart'; +import '../controllers/timbre_controller.dart'; + +class TimbreView extends GetView { + const TimbreView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + '选择音色', + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Column( + children: [ + Expanded( + child: Obx( + () => ListView.builder( + itemCount: controller.dataList.length, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + itemBuilder: (BuildContext context, int index) { + Map data = controller.dataList[index]; + return _item(isDarkMode, data); + }, + ), + ), + ), + GestureDetector( + onTap: () { + controller.startRecorder(); + Get.dialog( + barrierDismissible: false, + AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + '请朗读', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Obx( + () => controller.text.value.isNotEmpty + ? Text( + controller.text.value, + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ) + : Container( + width: 40.w, + height: 40.w, + alignment: Alignment.center, + child: CircularProgressIndicator( + strokeWidth: 2.w, + valueColor: const AlwaysStoppedAnimation( + Colors.blue), + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + controller.cancel(); + }, + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + Obx(() => controller.text.value.isNotEmpty + ? TextButton( + onPressed: () { + Get.back(); + controller.completeRecorder(); + }, + child: Text( + '完成录制', + style: TextStyle( + color: isDarkMode + ? Colors.red[300] + : Colors.red[600], + ), + ), + ) + : const SizedBox()), + ], + ), + ); + }, + child: Container( + width: 130.w, + height: 48.w, + margin: EdgeInsets.symmetric(vertical: 20.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.primary : Colors.blue, + borderRadius: BorderRadius.circular(12.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.keyboard_voice, color: Colors.white), + 5.horizontalSpace, + Text( + 'voiceClone'.tr, + style: TextStyle(color: Colors.white, fontSize: 16.sp), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _item(bool isDarkMode, Map data) { + return InkWell( + onTap: () { + controller.selectTone(data); + }, + child: Obx( + () => Container( + margin: EdgeInsets.symmetric(vertical: 6.h), + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: controller.type.value == data['type'] + ? (isDarkMode + ? Colors.teal[900]!.withOpacity(0.3) + : Colors.teal[50]!) + : (isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100]!), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: controller.type.value == data['type'] + ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) + : Colors.transparent, + width: 1, + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${data['name']}'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + Text( + '${data['description']}'.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], + ), + ), + data['isUsable'] + ? Container( + width: 24.w, + height: 24.w, + margin: EdgeInsets.only(left: 12.w), + decoration: BoxDecoration( + color: controller.type.value == data['type'] + ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) + : (isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[300]), + shape: BoxShape.circle, + ), + child: controller.type.value == data['type'] + ? Icon( + Icons.check, + size: 16.w, + color: Colors.white, + ) + : null, + ) + : SizedBox( + width: 24.w, + height: 24.w, + child: CircularProgressIndicator( + strokeWidth: 2.w, + valueColor: + const AlwaysStoppedAnimation(Colors.blue), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index eb606da19..8c9d404ec 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -13,7 +13,7 @@ class TranslationView extends GetView { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], appBar: _buildAppBar(isDarkMode), @@ -47,22 +47,25 @@ class TranslationView extends GetView { child: Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), ), child: Obx(() => Text( - controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - overflow: TextOverflow.ellipsis, - )), + controller.sourceLanguage.value + .split(' ')[0], // Just show the Chinese part + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + overflow: TextOverflow.ellipsis, + )), ), ), ), - + // 语言互换按钮 GestureDetector( onTap: controller.swapLanguages, @@ -70,7 +73,9 @@ class TranslationView extends GetView { margin: EdgeInsets.symmetric(horizontal: 8.w), padding: EdgeInsets.all(4.r), decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary.withOpacity(0.2) : Colors.blue[50], + color: isDarkMode + ? AppColors.primary.withOpacity(0.2) + : Colors.blue[50], shape: BoxShape.circle, ), child: Icon( @@ -80,7 +85,7 @@ class TranslationView extends GetView { ), ), ), - + // 目标语言选择 Flexible( child: GestureDetector( @@ -88,18 +93,21 @@ class TranslationView extends GetView { child: Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), ), child: Obx(() => Text( - controller.targetLanguage.value.split(' ')[0], // Just show the English part - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - overflow: TextOverflow.ellipsis, - )), + controller.targetLanguage.value + .split(' ')[0], // Just show the English part + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + overflow: TextOverflow.ellipsis, + )), ), ), ), @@ -109,15 +117,17 @@ class TranslationView extends GetView { actions: [ // TTS toggle button Obx(() => IconButton( - icon: Icon( - controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, - color: isDarkMode ? Colors.white : Colors.black, - size: 24.sp, - ), - onPressed: controller.toggleTts, - tooltip: '自动播放翻译结果', - )), - + icon: Icon( + controller.isTtsEnabled.value + ? Icons.volume_up + : Icons.volume_off, + color: isDarkMode ? Colors.white : Colors.black, + size: 24.sp, + ), + onPressed: controller.toggleTts, + tooltip: '自动播放翻译结果', + )), + // 历史记录按钮 IconButton( icon: Icon( @@ -129,10 +139,10 @@ class TranslationView extends GetView { // 停止语音识别和TTS,释放资源 await controller.stopAll(); await Get.to( - () => const TranslationHistoryView(), + () => const TranslationHistoryView(), binding: TranslationHistoryBinding(), ); - + // 返回时不自动开始翻译,只需重新设置滚动监听器 controller.setupScrollListener(); }, @@ -156,46 +166,49 @@ class TranslationView extends GetView { Widget _buildTranslationContent(bool isDarkMode) { return Obx(() { final itemCount = controller.translationHistory.length; - return ListView.builder( controller: controller.scrollController, padding: EdgeInsets.only( - left: 16.w, - right: 16.w, - top: 16.h, + left: 16.w, + right: 16.w, + top: 16.h, bottom: 16.h, ), itemCount: itemCount, itemBuilder: (context, index) { // 获取当前项 final historyItem = controller.translationHistory[index]; - + // 构建项目widget - return _buildTranslationItemWidget( - context, - historyItem - ); + return _buildTranslationItemWidget(context, historyItem); }, ); }); } - Widget _buildTranslationItemWidget(BuildContext context, TranslationItem item) { + Widget _buildTranslationItemWidget( + BuildContext context, TranslationItem item) { return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: item.isIntermediate - ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色 - : (item.sourceLanguageCode == controller.sourceLanguageCode.value - ? Theme.of(context).colorScheme.primary.withOpacity(0.08) // 源语言匹配时使用主题色底色 - : Theme.of(context).colorScheme.secondary.withOpacity(0.08)), // 否则使用次要色底色 + color: item.isIntermediate + ? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色 + : (item.sourceLanguageCode == controller.sourceLanguageCode.value + ? Theme.of(context) + .colorScheme + .primary + .withOpacity(0.08) // 源语言匹配时使用主题色底色 + : Theme.of(context) + .colorScheme + .secondary + .withOpacity(0.08)), // 否则使用次要色底色 borderRadius: BorderRadius.circular(12), border: Border.all( - color: item.isIntermediate - ? Colors.grey.withOpacity(0.2) - : Colors.transparent, + color: item.isIntermediate + ? Colors.grey.withOpacity(0.2) + : Colors.transparent, width: 1, ), ), @@ -253,16 +266,17 @@ class TranslationView extends GetView { Widget _buildControlBar(bool isDarkMode) { // 使用GlobalKey来获取控制栏的高度 final controlBarKey = GlobalKey(); - + // 在构建完成后获取控制栏的高度并设置到控制器中 WidgetsBinding.instance.addPostFrameCallback((_) { if (controlBarKey.currentContext != null) { - final RenderBox renderBox = controlBarKey.currentContext!.findRenderObject() as RenderBox; + final RenderBox renderBox = + controlBarKey.currentContext!.findRenderObject() as RenderBox; final height = renderBox.size.height; controller.setBottomBarHeight(height); } }); - + return Container( key: controlBarKey, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), @@ -279,8 +293,6 @@ class TranslationView extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - - // 主要控制按钮 - 开始/暂停语音识别 Obx(() { final isRecognizing = controller.isRecognizing.value; @@ -296,8 +308,8 @@ class TranslationView extends GetView { width: 60.w, height: 60.w, decoration: BoxDecoration( - color: isRecognizing - ? (isDarkMode ? Colors.red[700] : Colors.red) + color: isRecognizing + ? (isDarkMode ? Colors.red[700] : Colors.red) : (isDarkMode ? AppColors.primary : Colors.blue), shape: BoxShape.circle, ), @@ -333,7 +345,9 @@ class TranslationView extends GetView { Padding( padding: EdgeInsets.symmetric(horizontal: 24.w), child: Text( - isSource ? '选择源语言' : '选择目标语言', + isSource + ? 'selectSourceLanguage'.tr // 选择源语言 + : 'selectTargetLanguage'.tr, // 选择目标语言 style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, @@ -347,7 +361,8 @@ class TranslationView extends GetView { child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, - children: controller.supportedLanguages.keys.map((language) { + children: + controller.supportedLanguages.keys.map((language) { return ListTile( title: Text( language, @@ -356,13 +371,15 @@ class TranslationView extends GetView { ), ), trailing: Obx(() { - final currentLanguage = isSource - ? controller.sourceLanguage.value + final currentLanguage = isSource + ? controller.sourceLanguage.value : controller.targetLanguage.value; return currentLanguage == language ? Icon( Icons.check_circle, - color: isDarkMode ? AppColors.primary : Colors.blue, + color: isDarkMode + ? AppColors.primary + : Colors.blue, ) : const SizedBox.shrink(); }), @@ -387,7 +404,7 @@ class TranslationView extends GetView { child: TextButton( onPressed: () => Get.back(), child: Text( - '取消', + 'cancel'.tr, // 取消 style: TextStyle( color: isDarkMode ? AppColors.primary : Colors.blue, fontSize: 16.sp, @@ -402,4 +419,4 @@ class TranslationView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index acb1f1e8b..8a2466fda 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -3,11 +3,21 @@ import '../modules/home/views/home_view.dart'; import '../modules/home/bindings/home_binding.dart'; import '../modules/explore/views/explore_view.dart'; import '../modules/explore/bindings/explore_binding.dart'; +import '../modules/meeting/bindings/meeting_binding.dart'; +import '../modules/meeting/bindings/meeting_details_binding.dart'; +import '../modules/meeting/bindings/meeting_edit_binding.dart'; +import '../modules/meeting/bindings/meeting_record_binding.dart'; +import '../modules/meeting/views/meeting_details_view.dart'; +import '../modules/meeting/views/meeting_edit_view.dart'; +import '../modules/meeting/views/meeting_record_view.dart'; +import '../modules/meeting/views/meeting_view.dart'; import '../modules/profile/views/profile_view.dart'; import '../modules/profile/bindings/profile_binding.dart'; import '../modules/chat/bindings/chat_binding.dart'; import '../modules/chat/views/chat_view.dart'; +import '../modules/timbre/bindings/timbre_binding.dart'; +import '../modules/timbre/views/timbre_view.dart'; import '../modules/translation/views/translation_view.dart'; import '../modules/translation/bindings/translation_binding.dart'; import '../modules/translation/views/translation_history_view.dart'; @@ -56,7 +66,7 @@ abstract class AppPages { ), GetPage( name: Routes.chat, - page: () => ChatView(), + page: () => const ChatView(), binding: ChatBinding(), ), GetPage( @@ -64,6 +74,11 @@ abstract class AppPages { page: () => const ProfileView(), binding: ProfileBinding(), ), + GetPage( + name: Routes.editProfile, + page: () => const EditProfileView(), + binding: ProfileBinding(), + ), GetPage( name: Routes.translation, page: () => const TranslationView(), @@ -74,11 +89,35 @@ abstract class AppPages { page: () => const TranslationHistoryView(), binding: TranslationHistoryBinding(), ), + GetPage( + name: Routes.meeting, + page: () => const MeetingView(), + binding: MeetingBinding(), + ), + GetPage( + name: Routes.meetingRecord, + page: () => const MeetingRecordView(), + binding: MeetingRecordBinding(), + ), + GetPage( + name: Routes.meetingDetails, + page: () => const MeetingDetailsView(), + binding: MeetingDetailsBinding(), + ), + GetPage( + name: Routes.meetingEdit, + page: () => const MeetingEditView(), + binding: MeetingEditBinding(), + ), + GetPage( + name: Routes.timbre, + page: () => const TimbreView(), + binding: TimbreBinding(), + ), GetPage( name: Routes.settings, page: () => const SettingsView(), binding: SettingsBinding(), ), - ]; -} \ No newline at end of file +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 1d6c8a1ea..4488f63db 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -7,6 +7,11 @@ abstract class Routes { static const explore = '/explore'; static const chat = '/chat'; static const profile = '/profile'; + static const meeting = '/meeting'; + static const meetingRecord = '/meetingRecord'; + static const meetingDetails = '/meetingDetails'; + static const meetingEdit = '/meetingEdit'; + static const timbre = '/timbre'; static const settings = '/settings'; static const speechDemo = '/speech_demo'; static const TTS_TEST = '/tts_test'; @@ -18,4 +23,5 @@ abstract class Routes { static const translationHistory = '/translation_history'; static const FLUTTER_TTS_TEST = '/flutter_tts_test'; static const FLUTTER_ASR_TEST = '/flutter_asr_test'; -} \ No newline at end of file + static const editProfile = '/edit_profile'; +} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 24b0c8f03..05e1258a9 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "deep_voice") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.deep_voice") +set(APPLICATION_ID "com.yunqiinnovation.deepsound") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index fab21e7ca..f443e8bbe 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6915aceb9..346ccb75e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + flutter_secure_storage_linux record_linux url_launcher_linux window_size diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a5036d1d0..4658cacbc 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -479,7 +479,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/deep_voice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/deep_voice"; @@ -494,7 +494,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/deep_voice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/deep_voice"; @@ -509,7 +509,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/deep_voice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/deep_voice"; diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 99aa41de4..34460e77c 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = deep_voice // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice +PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/pubspec.yaml b/pubspec.yaml index 32eb78d63..e4b2f4e6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.6 get: ^4.6.6 get_storage: ^2.1.1 @@ -28,7 +30,8 @@ dependencies: logger: ^2.0.2+1 flutter_blue_plus: ^1.31.16 just_audio: ^0.9.36 - + webview_flutter: ^4.10.0 + flutter_easyloading: ^3.0.5 window_size: git: @@ -44,7 +47,19 @@ dependencies: audio_waveforms: ^1.3.0 font_awesome_flutter: ^10.8.0 lottie: ^3.3.1 + app_settings: 5.2.0 speech_to_text: ^7.0.0 + fluwx: ^5.4.1 + firebase_core: ^3.12.1 + firebase_auth: ^5.5.1 + google_sign_in: ^6.2.2 + flutter_facebook_auth: ^7.1.1 + common_utils: ^2.1.0 + flutter_image_compress: ^2.4.0 + # image_cropper: ^9.0.0 + dio: ^5.8.0+1 + sqflite: ^2.4.2 + path: ^1.9.1 dev_dependencies: flutter_test: @@ -57,6 +72,11 @@ flutter: assets: - assets/images/headphone_light.png - assets/images/headphone_dark.png + - assets/images/robot_light.png + - assets/images/glasses_light.png - assets/images/agents/ - assets/images/explore.png + - assets/images/explore1.png + - assets/images/explore2.jpg + - assets/lottie/ - .env diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index eb676d5f0..74bd2623b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,9 @@ #include "generated_plugin_registrant.h" #include +#include +#include +#include #include #include #include @@ -17,6 +20,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); FlutterTtsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterTtsPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 47ae460e0..e548eecfe 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + firebase_auth + firebase_core + flutter_secure_storage_windows flutter_tts permission_handler_windows record_windows