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_storage/get_storage.dart'; |
|||
|
|||
import 'language/en_us.dart'; |
|||
import 'language/zh_cn.dart'; |
|||
|
|||
class AppTranslations extends Translations { |
|||
static const fallbackLocale = Locale('zh', 'CN'); |
|||
static final supportedLocales = [ |
|||
const Locale('zh', 'CN'), |
|||
const Locale('en', 'US'), |
|||
]; |
|||
|
|||
static Locale? deviceLocale() { |
|||
final GetStorage storage = GetStorage(); |
|||
Locale? locale = Get.deviceLocale; |
|||
String? language = storage.read('language'); |
|||
if (language != null && language != 'system') { |
|||
List languageList = language.split('_'); |
|||
locale = Locale(languageList[0], languageList[1]); |
|||
} |
|||
return locale; |
|||
} |
|||
|
|||
@override |
|||
Map<String, Map<String, String>> get keys => { |
|||
'zh_CN': { |
|||
// 通用 |
|||
'app_name': 'i深音', |
|||
'confirm': '确认', |
|||
'cancel': '取消', |
|||
'retry': '重试', |
|||
'error': '错误', |
|||
'success': '成功', |
|||
|
|||
// 主页 |
|||
'add_device': '添加设备', |
|||
'searching': '搜索中', |
|||
'searching_hint': '请将设备放在手机附近', |
|||
'connected': '已连接', |
|||
'device_ready': '设备已就绪', |
|||
'connection_failed': '连接失败', |
|||
'connection_failed_hint': '未找到设备。请确保蓝牙已开启且设备在5米范围内。尝试重启设备。', |
|||
'disconnect': '断开连接', |
|||
|
|||
// 聊天页面 |
|||
'clear_chat': '清空对话', |
|||
'thinking': '正在思考...', |
|||
'type_message': '发消息...', |
|||
'voice_enabled': '语音已开启', |
|||
'voice_disabled': '语音已关闭', |
|||
|
|||
// 设置页面 |
|||
'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', |
|||
}, |
|||
}; |
|||
} |
|||
Map<String, Map<String, String>> get keys => {'en_US': enUS, 'zh_CN': zhCN}; |
|||
} |
|||
|
|||
@ -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 '../../explore/controllers/explore_controller.dart'; |
|||
import '../controllers/home_controller.dart'; |
|||
|
|||
class HomeBinding extends Bindings { |
|||
@override |
|||
void dependencies() { |
|||
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 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
import '../../../data/models/user_Info.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../../../core/themes/theme_service.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
|
|||
class SplashController extends GetxController { |
|||
final GetStorage _storage = GetStorage(); |
|||
// 进度条动画控制 |
|||
final _loadingProgress = 0.0.obs; |
|||
double get loadingProgress => _loadingProgress.value; |
|||
|
|||
// 版本号 |
|||
final String version = 'v1.0.0'; |
|||
|
|||
|
|||
// 资源加载状态 |
|||
final _isResourceLoaded = false.obs; |
|||
bool get isResourceLoaded => _isResourceLoaded.value; |
|||
|
|||
|
|||
// 版本号 |
|||
final String version = 'v1.0.0'; |
|||
|
|||
// 主题服务 |
|||
final ThemeService _themeService = ThemeService(); |
|||
|
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
|
|||
// 确保页面先渲染,再执行其他操作 |
|||
WidgetsBinding.instance.addPostFrameCallback((_) { |
|||
// 启动进度条动画 |
|||
_startLoadingAnimation(); |
|||
|
|||
// 异步加载应用资源 |
|||
_loadAppResources(); |
|||
}); |
|||
} |
|||
|
|||
// 进度条动画 |
|||
|
|||
// 进度条动画(持续2秒) |
|||
void _startLoadingAnimation() { |
|||
// 模拟0.5秒的加载进度 |
|||
Timer.periodic(const Duration(milliseconds: 5), (timer) { |
|||
const totalDuration = 2000; // 动画总时长2秒 |
|||
const steps = totalDuration ~/ 50; // 40次更新(50ms间隔) |
|||
final stepValue = 1.0 / steps; |
|||
|
|||
Timer.periodic(const Duration(milliseconds: 50), (timer) { |
|||
if (_loadingProgress.value < 1.0) { |
|||
_loadingProgress.value += 0.01; |
|||
_loadingProgress.value += stepValue; |
|||
} else { |
|||
timer.cancel(); |
|||
// 当进度条动画完成且资源加载完毕时,跳转到登录页面 |
|||
if (_isResourceLoaded.value) { |
|||
_navigateToLogin(); |
|||
} |
|||
_checkNavigateCondition(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
|
|||
// 异步加载应用资源 |
|||
Future<void> _loadAppResources() async { |
|||
try { |
|||
// 在这里可以添加应用初始化需要的资源加载 |
|||
// 例如:预加载图片、初始化数据库、加载配置等 |
|||
|
|||
// 模拟耗时操作 |
|||
await Future.delayed(const Duration(milliseconds: 100)); |
|||
|
|||
// TODO: 在此处添加实际的资源加载代码 |
|||
|
|||
// 标记资源加载完成 |
|||
// 模拟资源加载(如API配置、本地缓存等) |
|||
await Future.delayed(const Duration(milliseconds: 1500)); |
|||
|
|||
// 实际资源加载代码 |
|||
// await _loadImages(); |
|||
// await _initDatabase(); |
|||
|
|||
_isResourceLoaded.value = true; |
|||
|
|||
// 如果进度条已经走完,则跳转到登录页面 |
|||
if (_loadingProgress.value >= 1.0) { |
|||
_navigateToLogin(); |
|||
} |
|||
_checkNavigateCondition(); |
|||
} catch (e) { |
|||
// 处理资源加载错误 |
|||
// 即使加载失败,也应该允许用户继续使用应用 |
|||
_isResourceLoaded.value = true; |
|||
|
|||
// 记录错误日志 |
|||
Get.snackbar('错误', '资源加载失败,请检查网络连接'); |
|||
_isResourceLoaded.value = true; // 允许用户继续使用 |
|||
print('资源加载错误: $e'); |
|||
} |
|||
} |
|||
|
|||
// 跳转到登录页面 |
|||
void _navigateToLogin() { |
|||
// 添加一个小延迟,确保UI完全渲染 |
|||
Future.delayed(const Duration(milliseconds: 200), () { |
|||
Get.offAllNamed(Routes.login); |
|||
}); |
|||
|
|||
// 检查跳转条件 |
|||
void _checkNavigateCondition() { |
|||
if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { |
|||
_navigateToLogin(); |
|||
} |
|||
} |
|||
|
|||
// 获取登录Token并跳转 |
|||
Future<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