113 changed files with 10434 additions and 1774 deletions
@ -0,0 +1,36 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>CLIENT_ID</key> |
||||
|
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string> |
||||
|
<key>REVERSED_CLIENT_ID</key> |
||||
|
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string> |
||||
|
<key>ANDROID_CLIENT_ID</key> |
||||
|
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string> |
||||
|
<key>API_KEY</key> |
||||
|
<string>AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808</string> |
||||
|
<key>GCM_SENDER_ID</key> |
||||
|
<string>907754492821</string> |
||||
|
<key>PLIST_VERSION</key> |
||||
|
<string>1</string> |
||||
|
<key>BUNDLE_ID</key> |
||||
|
<string>com.yunqiinnovation.deepsound</string> |
||||
|
<key>PROJECT_ID</key> |
||||
|
<string>deepsound-e0e3d</string> |
||||
|
<key>STORAGE_BUCKET</key> |
||||
|
<string>deepsound-e0e3d.firebasestorage.app</string> |
||||
|
<key>IS_ADS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_ANALYTICS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_APPINVITE_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_GCM_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_SIGNIN_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>GOOGLE_APP_ID</key> |
||||
|
<string>1:907754492821:ios:246794fc44b23174145c9c</string> |
||||
|
</dict> |
||||
|
</plist> |
||||
@ -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" |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
storePassword=13071376716 |
||||
|
keyPassword=13071376716 |
||||
|
keyAlias=DeepSound |
||||
|
storeFile=sign/DeepSound.keystore |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
<string name="facebook_app_id">3829022037361855</string> |
||||
|
<string name="fb_login_protocol_scheme">fb3829022037361855</string> |
||||
|
<string name="facebook_client_token">193d0a2b1757c41cfa557ea8e8f21c33</string> |
||||
|
</resources> |
||||
|
After Width: | Height: | Size: 592 KiB |
|
After Width: | Height: | Size: 653 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 110 KiB |
File diff suppressed because one or more lines are too long
@ -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"}}}}}} |
||||
@ -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 |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>CLIENT_ID</key> |
||||
|
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string> |
||||
|
<key>REVERSED_CLIENT_ID</key> |
||||
|
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string> |
||||
|
<key>ANDROID_CLIENT_ID</key> |
||||
|
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string> |
||||
|
<key>API_KEY</key> |
||||
|
<string>AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808</string> |
||||
|
<key>GCM_SENDER_ID</key> |
||||
|
<string>907754492821</string> |
||||
|
<key>PLIST_VERSION</key> |
||||
|
<string>1</string> |
||||
|
<key>BUNDLE_ID</key> |
||||
|
<string>com.yunqiinnovation.deepsound</string> |
||||
|
<key>PROJECT_ID</key> |
||||
|
<string>deepsound-e0e3d</string> |
||||
|
<key>STORAGE_BUCKET</key> |
||||
|
<string>deepsound-e0e3d.firebasestorage.app</string> |
||||
|
<key>IS_ADS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_ANALYTICS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_APPINVITE_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_GCM_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_SIGNIN_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>GOOGLE_APP_ID</key> |
||||
|
<string>1:907754492821:ios:246794fc44b23174145c9c</string> |
||||
|
</dict> |
||||
|
</plist> |
||||
@ -0,0 +1,10 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>keychain-access-groups</key> |
||||
|
<array> |
||||
|
<string>$(AppIdentifierPrefix)com.yunqiinnovation.deepsound</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
</plist> |
||||
@ -0,0 +1,93 @@ |
|||||
|
const Map<String, String> 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?", |
||||
|
}; |
||||
@ -0,0 +1,91 @@ |
|||||
|
const Map<String, String> 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": "你想结束录音吗?", |
||||
|
}; |
||||
@ -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<String, Map<String, String>> get keys => {'en_US': enUS, 'zh_CN': zhCN}; |
||||
|
} |
||||
@ -1,81 +1,29 @@ |
|||||
|
import 'dart:ui'; |
||||
|
|
||||
import 'package:get/get.dart'; |
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 { |
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 |
@override |
||||
Map<String, Map<String, String>> get keys => { |
Map<String, Map<String, String>> get keys => {'en_US': enUS, 'zh_CN': zhCN}; |
||||
'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', |
|
||||
}, |
|
||||
}; |
|
||||
} |
|
||||
|
|||||
@ -0,0 +1,285 @@ |
|||||
|
const Map<String, String> 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", |
||||
|
}; |
||||
@ -0,0 +1,281 @@ |
|||||
|
const Map<String, String> 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": "保存", |
||||
|
|
||||
|
//国家列表 |
||||
|
"邮箱": "邮箱", |
||||
|
"中国": "中国", |
||||
|
"美国": "美国", |
||||
|
"加拿大": "加拿大", |
||||
|
"俄罗斯": "俄罗斯", |
||||
|
"英国": "英国", |
||||
|
"德国": "德国", |
||||
|
"意大利": "意大利", |
||||
|
"法国": "法国", |
||||
|
"西班牙": "西班牙", |
||||
|
"南非": "南非", |
||||
|
"沙特阿拉伯": "沙特阿拉伯", |
||||
|
"土耳其": "土耳其", |
||||
|
"日本": "日本", |
||||
|
"韩国": "韩国", |
||||
|
"新加坡": "新加坡", |
||||
|
"马来西亚": "马来西亚", |
||||
|
"泰国": "泰国", |
||||
|
"越南": "越南", |
||||
|
"印度": "印度", |
||||
|
"巴基斯坦": "巴基斯坦", |
||||
|
"菲律宾": "菲律宾", |
||||
|
"印度尼西亚": "印度尼西亚", |
||||
|
"阿联酋": "阿联酋", |
||||
|
"墨西哥": "墨西哥", |
||||
|
"巴西": "巴西", |
||||
|
"阿根廷": "阿根廷", |
||||
|
"澳大利亚": "澳大利亚", |
||||
|
"新西兰": "新西兰", |
||||
|
"中国香港": "中国香港", |
||||
|
"中国澳门": "中国澳门", |
||||
|
"中国台湾": "中国台湾", |
||||
|
"埃及": "埃及", |
||||
|
"希腊": "希腊", |
||||
|
"荷兰": "荷兰", |
||||
|
"比利时": "比利时", |
||||
|
"匈牙利": "匈牙利", |
||||
|
"罗马尼亚": "罗马尼亚", |
||||
|
"瑞士": "瑞士", |
||||
|
"奥地利": "奥地利", |
||||
|
"丹麦": "丹麦", |
||||
|
"瑞典": "瑞典", |
||||
|
"挪威": "挪威", |
||||
|
"波兰": "波兰", |
||||
|
"秘鲁": "秘鲁", |
||||
|
"智利": "智利", |
||||
|
"哥伦比亚": "哥伦比亚", |
||||
|
"委内瑞拉": "委内瑞拉", |
||||
|
"葡萄牙": "葡萄牙", |
||||
|
"卢森堡": "卢森堡", |
||||
|
"爱尔兰": "爱尔兰", |
||||
|
"冰岛": "冰岛", |
||||
|
"阿尔巴尼亚": "阿尔巴尼亚", |
||||
|
"马耳他": "马耳他", |
||||
|
"塞浦路斯": "塞浦路斯", |
||||
|
"芬兰": "芬兰", |
||||
|
"保加利亚": "保加利亚", |
||||
|
"立陶宛": "立陶宛", |
||||
|
"拉脱维亚": "拉脱维亚", |
||||
|
"爱沙尼亚": "爱沙尼亚", |
||||
|
"乌克兰": "乌克兰", |
||||
|
"塞尔维亚": "塞尔维亚", |
||||
|
"克罗地亚": "克罗地亚", |
||||
|
"斯洛文尼亚": "斯洛文尼亚", |
||||
|
"捷克": "捷克", |
||||
|
"斯洛伐克": "斯洛伐克", |
||||
|
"危地马拉": "危地马拉", |
||||
|
"萨尔瓦多": "萨尔瓦多", |
||||
|
"洪都拉斯": "洪都拉斯", |
||||
|
"尼加拉瓜": "尼加拉瓜", |
||||
|
"哥斯达黎加": "哥斯达黎加", |
||||
|
"巴拿马": "巴拿马", |
||||
|
"海地": "海地", |
||||
|
"玻利维亚": "玻利维亚", |
||||
|
"厄瓜多尔": "厄瓜多尔", |
||||
|
"巴拉圭": "巴拉圭", |
||||
|
"乌拉圭": "乌拉圭", |
||||
|
"摩洛哥": "摩洛哥", |
||||
|
"阿尔及利亚": "阿尔及利亚", |
||||
|
"突尼斯": "突尼斯", |
||||
|
"尼日利亚": "尼日利亚", |
||||
|
"加纳": "加纳", |
||||
|
"埃塞俄比亚": "埃塞俄比亚", |
||||
|
"肯尼亚": "肯尼亚", |
||||
|
"坦桑尼亚": "坦桑尼亚", |
||||
|
"乌干达": "乌干达", |
||||
|
"孟加拉国": "孟加拉国", |
||||
|
"以色列": "以色列", |
||||
|
"黎巴嫩": "黎巴嫩", |
||||
|
"约旦": "约旦", |
||||
|
"伊拉克": "伊拉克", |
||||
|
"科威特": "科威特", |
||||
|
"巴林": "巴林", |
||||
|
"卡塔尔": "卡塔尔", |
||||
|
"阿曼": "阿曼", |
||||
|
"阿富汗": "阿富汗", |
||||
|
"斯里兰卡": "斯里兰卡", |
||||
|
"缅甸": "缅甸", |
||||
|
"尼泊尔": "尼泊尔", |
||||
|
"蒙古": "蒙古", |
||||
|
"柬埔寨": "柬埔寨", |
||||
|
"老挝": "老挝", |
||||
|
"文莱": "文莱", |
||||
|
"东帝汶": "东帝汶", |
||||
|
"斐济": "斐济", |
||||
|
"所罗门群岛": "所罗门群岛", |
||||
|
}; |
||||
@ -0,0 +1,15 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
|
||||
|
class SelectLocationMap extends StatefulWidget { |
||||
|
const SelectLocationMap({super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<SelectLocationMap> createState() => _SelectLocationMapState(); |
||||
|
} |
||||
|
|
||||
|
class _SelectLocationMapState extends State<SelectLocationMap> { |
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return const Placeholder(); |
||||
|
} |
||||
|
} |
||||
@ -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<String, dynamic> json) { |
||||
|
// 修复1:安全获取嵌套的 user 字段 |
||||
|
final userData = json['user'] as Map<String, dynamic>?; // 允许空值 |
||||
|
//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<String, dynamic> json) { |
||||
|
// 修复1:安全获取嵌套的 user 字段 |
||||
|
final userData = json['user'] as Map<String, dynamic>?; // 允许空值 |
||||
|
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<String, dynamic> 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, |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
/// AI回复服务接口 |
||||
|
abstract class AiService { |
||||
|
/// 非流式输出方法 |
||||
|
Future<String> sendMessage({ |
||||
|
required List<Map<String, String>> messages, |
||||
|
required String systemPrompt, |
||||
|
}); |
||||
|
|
||||
|
/// 流式输出方法 |
||||
|
Stream<String> sendMessageStream({ |
||||
|
required List<Map<String, String>> messages, |
||||
|
required String systemPrompt, |
||||
|
}); |
||||
|
} |
||||
@ -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<Database> get database async { |
||||
|
if (_database != null) return _database!; |
||||
|
_database = await _initDatabase(); |
||||
|
return _database!; |
||||
|
} |
||||
|
|
||||
|
Future<Database> _initDatabase() async { |
||||
|
String path = join( |
||||
|
await getDatabasesPath(), |
||||
|
'database.db', |
||||
|
); |
||||
|
return await openDatabase(path, version: 1, onCreate: _createDB); |
||||
|
} |
||||
|
|
||||
|
Future<void> _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(); |
||||
|
} |
||||
|
} |
||||
@ -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 <String, dynamic>{ |
||||
|
...meetingList.first, |
||||
|
'details': detailsList.first |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// 插入商务会议助手 |
||||
|
static Future insertMeeting(Map<String, Object?> 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<String, Object?> 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]); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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<String> sendMessage({ |
||||
|
required List<Map<String, String>> messages, |
||||
|
required String systemPrompt, |
||||
|
}) async { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Stream<String> sendMessageStream({ |
||||
|
required List<Map<String, String>> 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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<String> sendMessage({ |
||||
|
required List<Map<String, String>> messages, |
||||
|
required String systemPrompt, |
||||
|
}) async { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Stream<String> sendMessageStream({ |
||||
|
required List<Map<String, String>> 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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<void> _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'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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<String>("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); |
||||
|
} |
||||
|
} |
||||
@ -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<T>( |
||||
|
NWMethod method, |
||||
|
String path, { |
||||
|
Map? params, |
||||
|
Map<String, dynamic>? queryParameters, |
||||
|
}) async { |
||||
|
Response response = await dio.request( |
||||
|
path, |
||||
|
data: params, |
||||
|
queryParameters: queryParameters, |
||||
|
options: Options(method: nwMethodValues[method]), |
||||
|
); |
||||
|
return response.data; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import 'entity_factory.dart'; |
||||
|
|
||||
|
class BaseEntity<T> { |
||||
|
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<T>(json["data"]), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
class EntityFactory { |
||||
|
static T? generateOBJ<T>(json) { |
||||
|
if (json == null) { |
||||
|
return null; |
||||
|
} else { |
||||
|
return json as T; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
class NWApi { |
||||
|
// static const baseApi = "https://www.deapsound.com"; |
||||
|
static const baseApi = "https://www.deapsound.com"; |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
enum NWMethod { get, post, delete, put } |
||||
|
|
||||
|
const nwMethodValues = { |
||||
|
NWMethod.get: "get", |
||||
|
NWMethod.post: "post", |
||||
|
NWMethod.delete: "delete", |
||||
|
NWMethod.put: "put" |
||||
|
}; |
||||
@ -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<TtsEvent> _eventController = |
||||
|
StreamController<TtsEvent>.broadcast(); |
||||
|
|
||||
|
@override |
||||
|
Stream<TtsEvent> get onEvent => _eventController.stream; |
||||
|
|
||||
|
bool _isInitialized = false; |
||||
|
String _resId = ''; |
||||
|
|
||||
|
// 语音合成队列 |
||||
|
final List _textQueue = []; |
||||
|
bool _isProcessingQueue = false; |
||||
|
|
||||
|
BytesBuilder _audioBytes = BytesBuilder(); |
||||
|
|
||||
|
Completer<void> _completer = Completer(); |
||||
|
|
||||
|
final isEnabled = true.obs; |
||||
|
|
||||
|
// 流式文本缓冲区 |
||||
|
String _streamBuffer = ''; |
||||
|
|
||||
|
@override |
||||
|
Future<bool> initialize({ |
||||
|
List<String>? 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<bool> setVoice(String voiceName) async { |
||||
|
if (!_isInitialized) await initialize(); |
||||
|
try { |
||||
|
Logger.info('设置TTS语音: $voiceName'); |
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
Logger.error('设置TTS语音失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 语音合成 |
||||
|
Future<void> _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<void> _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<bool> 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<bool> 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<String> 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<bool> 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<void> _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<bool> 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<void> dispose() async { |
||||
|
if (!_isInitialized) return; |
||||
|
try { |
||||
|
await stop(); |
||||
|
_player.dispose(); |
||||
|
// 关闭事件流控制器 |
||||
|
await _eventController.close(); |
||||
|
_isInitialized = false; |
||||
|
} catch (e) { |
||||
|
Logger.error('释放资源失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<String> _defaultSupportedLanguages = ['zh-CN']; |
||||
|
String _languageStr = 'cn'; |
||||
|
|
||||
|
// 连续识别相关 |
||||
|
bool _isContinuousRecognitionActive = false; |
||||
|
StreamController<RecognitionEvent>? _eventStreamController; |
||||
|
|
||||
|
@override |
||||
|
List<String> get supportedLanguages => _defaultSupportedLanguages; |
||||
|
|
||||
|
Uint8List _buff = Uint8List(0); //音频缓存区 |
||||
|
BytesBuilder _originaBytes = BytesBuilder(); //原始音频 |
||||
|
bool isRecording = false; |
||||
|
|
||||
|
@override |
||||
|
Future<bool> initialize({ |
||||
|
List<String>? supportedLanguages, |
||||
|
}) async { |
||||
|
try { |
||||
|
final List<String> 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<RecognitionEvent> recognizeOnce() async { |
||||
|
if (!_isInitialized) { |
||||
|
await initialize(); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
throw Exception('无效的识别结果格式'); |
||||
|
} catch (e) { |
||||
|
Logger.error('语音识别失败: $e'); |
||||
|
return RecognitionEvent.error(e.toString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { |
||||
|
if (!_isInitialized) { |
||||
|
await initialize(); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
_eventStreamController = StreamController<RecognitionEvent>.broadcast(); |
||||
|
|
||||
|
await recorder?.dispose(); |
||||
|
recorder = AudioRecorder(); |
||||
|
Stream<Uint8List>? 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<void> _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<bool> 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<void> 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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', |
||||
|
); |
||||
|
} |
||||
@ -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, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -1,9 +1,13 @@ |
|||||
|
import '../../explore/controllers/explore_controller.dart'; |
||||
import 'package:get/get.dart'; |
import 'package:get/get.dart'; |
||||
|
import '../../explore/controllers/explore_controller.dart'; |
||||
import '../controllers/home_controller.dart'; |
import '../controllers/home_controller.dart'; |
||||
|
|
||||
class HomeBinding extends Bindings { |
class HomeBinding extends Bindings { |
||||
@override |
@override |
||||
void dependencies() { |
void dependencies() { |
||||
Get.lazyPut<HomeController>(() => HomeController()); |
Get.lazyPut<HomeController>(() => HomeController()); |
||||
|
Get.lazyPut<ExploreController>( |
||||
|
() => ExploreController()); // 注册ExploreController |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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<bool> 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<UserCredential> 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<void> signOutFacebook() async { |
||||
|
await FacebookAuth.instance.logOut(); |
||||
|
} |
||||
|
} |
||||
@ -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<bool> 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<UserCredential> 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<void> signOutGoogle() async { |
||||
|
await FirebaseAuth.instance.signOut(); |
||||
|
await GoogleSignIn().signOut(); |
||||
|
} |
||||
|
} |
||||
@ -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<String?> 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<bool> 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<Map<String, dynamic>> fetchData(String Url) async { |
||||
|
try { |
||||
|
final response = await http.get(Uri.parse(Url)); |
||||
|
|
||||
|
if (response.statusCode == 200) { |
||||
|
final data = jsonDecode(response.body) as Map<String, dynamic>; |
||||
|
print('GET响应数据: $data'); |
||||
|
|
||||
|
return data; |
||||
|
} else { |
||||
|
print('GET请求失败,状态码: ${response.statusCode}'); |
||||
|
return {}; |
||||
|
} |
||||
|
} catch (e) { |
||||
|
print('GET请求异常: $e'); |
||||
|
return {}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// POST请求示例 |
||||
|
Future<bool> 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<bool> get isInstalled async { |
||||
|
assert(_isRegister, '请先调用 FlWX().register()'); |
||||
|
final installed = await fluwx.isWeChatInstalled; |
||||
|
if (!installed) _params!.onToast?.call('该服务需要先安装微信'); |
||||
|
return installed; |
||||
|
} |
||||
|
|
||||
|
/// isSupportOpenBusinessView |
||||
|
Future<bool> 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<String?> getExtMsg() async { |
||||
|
if (!await isInstalled) return null; |
||||
|
return await fluwx.getExtMsg(); |
||||
|
} |
||||
|
|
||||
|
Future<void> 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<bool> authBy( |
||||
|
AuthType which, { |
||||
|
FlWeChatAuthResponseCallback? onResponse, |
||||
|
FlWeChatAuthResponseTokenCallback? onToken, |
||||
|
FlWeChatAuthResponseUserinfoCallback? onUserinfo, |
||||
|
bool getUserInfo = false, |
||||
|
}) async { |
||||
|
if (!await isInstalled) { |
||||
|
_params?.onToast?.call('微信未安装'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
final completer = Completer<bool>(); // 创建 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<int> 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<bool> 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<bool> 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<bool> open(OpenType target) async { |
||||
|
if (!await isInstalled) return false; |
||||
|
return fluwx.open(target: target); |
||||
|
} |
||||
|
|
||||
|
Future<bool> 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<bool> 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<bool> openRankList() async { |
||||
|
if (!await isInstalled) return false; |
||||
|
return fluwx.open(target: RankList()); |
||||
|
} |
||||
|
|
||||
|
Future<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> share( |
||||
|
WeChatShareModel what, { |
||||
|
FlWeChatShareResponseCallback? onResponse, |
||||
|
}) async { |
||||
|
if (!await isInstalled) return false; |
||||
|
return _shareResponse(fluwx.share(what), onResponse: onResponse); |
||||
|
} |
||||
|
|
||||
|
Future<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> _shareResponse( |
||||
|
Future<bool> 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<dynamic>? privilege; |
||||
|
final String? unionId; |
||||
|
|
||||
|
WXUserModel.fromJson(Map<String, dynamic> 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<dynamic>?, |
||||
|
unionId = json['unionid'] as String?; |
||||
|
|
||||
|
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import '../controllers/meeting_controller.dart'; |
||||
|
|
||||
|
class MeetingBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
// 注册控制器 |
||||
|
Get.lazyPut<MeetingController>(() => MeetingController()); |
||||
|
} |
||||
|
} |
||||
@ -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>(() => MeetingDetailsController()); |
||||
|
} |
||||
|
} |
||||
@ -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>(() => MeetingEditController()); |
||||
|
} |
||||
|
} |
||||
@ -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>(() => MeetingRecordController()); |
||||
|
} |
||||
|
} |
||||
@ -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<String, dynamic>.from(dataList[index]); |
||||
|
dataMap['title'] = title; |
||||
|
dataList[index] = dataMap; |
||||
|
} |
||||
|
Get.back(); |
||||
|
} |
||||
|
|
||||
|
// 删除数据 |
||||
|
void deleteMeeting(int id) { |
||||
|
dataList.removeWhere((item) => item['id'] == id); |
||||
|
// 删除数据库中的记录 |
||||
|
SqfliteApi.deleteMeeting(id); |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingModel> meetingData = MeetingModel().obs; |
||||
|
Rx<MeetingDetailsModel> dataDetails = MeetingDetailsModel().obs; |
||||
|
|
||||
|
int seconds = 0; |
||||
|
|
||||
|
RxBool isPlay = false.obs; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
lottieController = AnimationController(vsync: this); |
||||
|
final Map<String, dynamic> 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingController>(); |
||||
|
final MeetingDetailsController meetingDetailsController = |
||||
|
Get.find<MeetingDetailsController>(); |
||||
|
|
||||
|
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<String, dynamic> 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<VolcanoAIService>(); |
||||
|
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<String, Object?> 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<String, dynamic>.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(); |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingController>(); |
||||
|
|
||||
|
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<XunfeiAsrService>(); |
||||
|
final Map<String, dynamic> 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<void> _pause() async { |
||||
|
await _stopRecorder(); |
||||
|
lottieController.stop(); |
||||
|
isRecording.value = false; |
||||
|
timer?.cancel(); |
||||
|
} |
||||
|
|
||||
|
//开始录音 |
||||
|
Future<void> _startRecorder() async { |
||||
|
final recognitionStream = await _asrService.startContinuousRecognition(); |
||||
|
_recognitionSubscription = recognitionStream.listen( |
||||
|
_handleRecognitionEvent, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
//结束录音 |
||||
|
Future<void> _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<VolcanoAIService>(); |
||||
|
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<String, Object?> 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<String> _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; |
||||
|
} |
||||
|
} |
||||
@ -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<String, dynamic> map) => |
||||
|
MeetingDetailsModel() |
||||
|
..id = map["id"] |
||||
|
..meetingid = map["meetingid"] |
||||
|
..audio = map["audio"] |
||||
|
..content = map["content"] |
||||
|
..aisummary = map["aisummary"]; |
||||
|
|
||||
|
Map<String, dynamic> toJson() { |
||||
|
final data = <String, dynamic>{}; |
||||
|
data['id'] = id; |
||||
|
data['meetingid'] = meetingid; |
||||
|
data['audio'] = audio; |
||||
|
data['content'] = content; |
||||
|
data['aisummary'] = aisummary; |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
@ -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<String, dynamic> 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<String, dynamic> toJson() { |
||||
|
final data = <String, dynamic>{}; |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingDetailsController> { |
||||
|
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<Duration>( |
||||
|
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<Duration?>( |
||||
|
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), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingEditController> { |
||||
|
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<Duration>( |
||||
|
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<Duration?>( |
||||
|
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, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingRecordController> { |
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<MeetingController> { |
||||
|
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 = ''; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
class LanguageModel { |
||||
|
final String name; |
||||
|
final String alias; |
||||
|
final String value; |
||||
|
|
||||
|
LanguageModel({ |
||||
|
this.name = '', |
||||
|
this.alias = '', |
||||
|
this.value = '', |
||||
|
}); |
||||
|
} |
||||
File diff suppressed because it is too large
@ -1,87 +1,116 @@ |
|||||
import 'dart:async'; |
import 'dart:async'; |
||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.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 '../../../routes/app_routes.dart'; |
||||
import '../../../core/themes/theme_service.dart'; |
import '../../../core/themes/theme_service.dart'; |
||||
|
import '../../../data/services/network/api.dart'; |
||||
|
|
||||
class SplashController extends GetxController { |
class SplashController extends GetxController { |
||||
|
final GetStorage _storage = GetStorage(); |
||||
// 进度条动画控制 |
// 进度条动画控制 |
||||
final _loadingProgress = 0.0.obs; |
final _loadingProgress = 0.0.obs; |
||||
double get loadingProgress => _loadingProgress.value; |
double get loadingProgress => _loadingProgress.value; |
||||
|
|
||||
// 版本号 |
|
||||
final String version = 'v1.0.0'; |
|
||||
|
|
||||
// 资源加载状态 |
// 资源加载状态 |
||||
final _isResourceLoaded = false.obs; |
final _isResourceLoaded = false.obs; |
||||
bool get isResourceLoaded => _isResourceLoaded.value; |
bool get isResourceLoaded => _isResourceLoaded.value; |
||||
|
|
||||
|
// 版本号 |
||||
|
final String version = 'v1.0.0'; |
||||
|
|
||||
// 主题服务 |
// 主题服务 |
||||
final ThemeService _themeService = ThemeService(); |
final ThemeService _themeService = ThemeService(); |
||||
|
|
||||
@override |
@override |
||||
void onInit() { |
void onInit() { |
||||
super.onInit(); |
super.onInit(); |
||||
|
|
||||
// 确保页面先渲染,再执行其他操作 |
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) { |
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||
// 启动进度条动画 |
|
||||
_startLoadingAnimation(); |
_startLoadingAnimation(); |
||||
|
|
||||
// 异步加载应用资源 |
|
||||
_loadAppResources(); |
_loadAppResources(); |
||||
}); |
}); |
||||
} |
} |
||||
|
|
||||
// 进度条动画 |
// 进度条动画(持续2秒) |
||||
void _startLoadingAnimation() { |
void _startLoadingAnimation() { |
||||
// 模拟0.5秒的加载进度 |
const totalDuration = 2000; // 动画总时长2秒 |
||||
Timer.periodic(const Duration(milliseconds: 5), (timer) { |
const steps = totalDuration ~/ 50; // 40次更新(50ms间隔) |
||||
|
final stepValue = 1.0 / steps; |
||||
|
|
||||
|
Timer.periodic(const Duration(milliseconds: 50), (timer) { |
||||
if (_loadingProgress.value < 1.0) { |
if (_loadingProgress.value < 1.0) { |
||||
_loadingProgress.value += 0.01; |
_loadingProgress.value += stepValue; |
||||
} else { |
} else { |
||||
timer.cancel(); |
timer.cancel(); |
||||
// 当进度条动画完成且资源加载完毕时,跳转到登录页面 |
_checkNavigateCondition(); |
||||
if (_isResourceLoaded.value) { |
|
||||
_navigateToLogin(); |
|
||||
} |
|
||||
} |
} |
||||
}); |
}); |
||||
} |
} |
||||
|
|
||||
// 异步加载应用资源 |
// 异步加载应用资源 |
||||
Future<void> _loadAppResources() async { |
Future<void> _loadAppResources() async { |
||||
try { |
try { |
||||
// 在这里可以添加应用初始化需要的资源加载 |
// 模拟资源加载(如API配置、本地缓存等) |
||||
// 例如:预加载图片、初始化数据库、加载配置等 |
await Future.delayed(const Duration(milliseconds: 1500)); |
||||
|
|
||||
// 模拟耗时操作 |
// 实际资源加载代码 |
||||
await Future.delayed(const Duration(milliseconds: 100)); |
// await _loadImages(); |
||||
|
// await _initDatabase(); |
||||
// TODO: 在此处添加实际的资源加载代码 |
|
||||
|
|
||||
// 标记资源加载完成 |
|
||||
_isResourceLoaded.value = true; |
_isResourceLoaded.value = true; |
||||
|
_checkNavigateCondition(); |
||||
// 如果进度条已经走完,则跳转到登录页面 |
|
||||
if (_loadingProgress.value >= 1.0) { |
|
||||
_navigateToLogin(); |
|
||||
} |
|
||||
} catch (e) { |
} catch (e) { |
||||
// 处理资源加载错误 |
Get.snackbar('错误', '资源加载失败,请检查网络连接'); |
||||
// 即使加载失败,也应该允许用户继续使用应用 |
_isResourceLoaded.value = true; // 允许用户继续使用 |
||||
_isResourceLoaded.value = true; |
|
||||
|
|
||||
// 记录错误日志 |
|
||||
print('资源加载错误: $e'); |
print('资源加载错误: $e'); |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
// 跳转到登录页面 |
// 检查跳转条件 |
||||
void _navigateToLogin() { |
void _checkNavigateCondition() { |
||||
// 添加一个小延迟,确保UI完全渲染 |
if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { |
||||
Future.delayed(const Duration(milliseconds: 200), () { |
_navigateToLogin(); |
||||
Get.offAllNamed(Routes.login); |
} |
||||
}); |
} |
||||
|
|
||||
|
// 获取登录Token并跳转 |
||||
|
Future<void> _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<bool> 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; |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,9 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import '../controllers/timbre_controller.dart'; |
||||
|
|
||||
|
class TimbreBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
Get.put<TimbreController>(TimbreController()); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue