Commit e1a297a1 by Administrator

Merge branch 'feature-2511-opt' into develop

2 parents e53ceeae 4c4434af
Showing 93 changed files with 1572 additions and 1337 deletions
...@@ -45,3 +45,5 @@ app.*.map.json ...@@ -45,3 +45,5 @@ app.*.map.json
/android/app/release /android/app/release
assets/static assets/static
pubspec.lock pubspec.lock
assets/dist.zip
macos/Podfile.lock
...@@ -9,3 +9,78 @@ flutter run -d macos ...@@ -9,3 +9,78 @@ flutter run -d macos
flutter run -d 00008030-001C75810E42402E --release flutter run -d 00008030-001C75810E42402E --release
flutter run -d 00008140-001068C93AB8801C --release
flutter build ipa --export-method ad-hoc
flutter build ipa --export-method ad-hoc --build-name=1.0.0 --build-number=1
### 未曾测试
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
修改 Flutter 工程的 ios/Podfile 文件,显式声明更高版本的 iOS 平台。
platform :ios, '15.0'
cd ios
pod deintegrate
pod repo update
pod install
cd ..
flutter build ios --build-number=2511061
#### test
cd ios
rm -rf Pods Podfile.lock
pod deintegrate
pod install
# 1. 回到项目根目录
flutter clean
# 2. 进入 ios 目录
cd ios
# 3. 删除构建残留和 Pod 锁文件 (必须删 Lock 文件)
rm -rf Pods
rm -rf Podfile.lock
rm -rf .symlinks
rm -rf Flutter/Flutter.framework
rm -rf Flutter/App.framework
# 4. 清理 CocoaPods 本地缓存 (这一步很重要,ffmpeg 包很大,容易缓存损坏)
pod cache clean --all
# 5. 重新安装 Pods 并更新仓库索引
# 注意:这一步可能比较慢,因为 ffmpeg 库非常大(几百MB),请保持网络通畅
pod install --repo-update
#显示安装日志
pod install --verbose
# 6. 回到根目录
cd ..
flutter build ios --build-number=2511061
### iOS 包下载问题
# 先手动下载到本地
cd ~/Downloads
git clone https://gitee.com/mirrors/DKImagePickerController.git --branch 4.3.9
# 然后在 Podfile 中修改
# 在 ios/Podfile 中添加:
pod 'DKImagePickerController', :path => '~/Downloads/DKImagePickerController'
...@@ -38,6 +38,11 @@ android { ...@@ -38,6 +38,11 @@ android {
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
manifestPlaceholders["VIVO_APPKEY"] = "f7e37b20f48234c425d9e7f82fdd16c0"
manifestPlaceholders["VIVO_APPID"] = "105993977"
// manifestPlaceholders["HONOR_APPID"] = "您应用分配的证书 APPID"
} }
// 添加签名配置 // 添加签名配置
...@@ -84,3 +89,23 @@ android { ...@@ -84,3 +89,23 @@ android {
flutter { flutter {
source = "../.." source = "../.."
} }
dependencies {
// implementation("com.tencent.timpush:timpush:8.7.7201")
// implementation("com.tencent.liteav.tuikit:tuicore:8.7.7201")
// 版本号 "VERSION" 请前往 更新日志 中获取配置。
// Huawei
//implementation("com.tencent.timpush:huawei:8.7.7201")
// XiaoMi
//implementation("com.tencent.timpush:xiaomi:8.7.7201")
// OPPO
//implementation("com.tencent.timpush:oppo:8.7.7201")
// vivo
implementation("com.tencent.timpush:vivo:8.7.7201")
// Honor
//implementation("com.tencent.timpush:honor:8.7.7201")
// Meizu
//implementation("com.tencent.timpush:meizu:8.7.7201")
// Google Firebase Cloud Messaging (Google FCM)
//implementation("com.tencent.timpush:fcm:8.7.7201")
}
\ No newline at end of file \ No newline at end of file
...@@ -16,5 +16,13 @@ ...@@ -16,5 +16,13 @@
*; *;
} }
# tencent push
-keep class com.tencent.qcloud.** {
*;
}
-keep class com.tencent.timpush.** {
*;
}
# 可选:如果遇到兼容性问题,可以忽略警告 # 可选:如果遇到兼容性问题,可以忽略警告
-dontwarn com.tencent.mm.** -dontwarn com.tencent.mm.**
\ No newline at end of file \ No newline at end of file
...@@ -21,8 +21,8 @@ ...@@ -21,8 +21,8 @@
<application <application
android:label="班小二" android:label="班小二"
android:name="${applicationName}" android:name="cn.banxe.bxe.MyApplication"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/launcher_icon"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
......
package cn.banxe.bxe;
import com.tencent.chat.flutter.push.tencent_cloud_chat_push.application.TencentCloudChatPushApplication;
public class MyApplication extends TencentCloudChatPushApplication {
@Override
public void onCreate() {
super.onCreate();
}
}
\ No newline at end of file \ No newline at end of file
No preview for this file type
...@@ -13,16 +13,16 @@ ...@@ -13,16 +13,16 @@
<button onclick="startRecord()">开始录音</button> <button onclick="startRecord()">开始录音</button>
<button onclick="">暂停录音</button> <button onclick="pauseRecord()">暂停录音</button>
<button onclick="">唤醒录音</button> <button onclick="resumeRecord()">唤醒录音</button>
<button onclick="stopRecord()">停止录音</button> <button onclick="stopRecord()">停止录音</button>
<br> <br>
<br> <br>
<input type="hidden" id="url" value=""> <input type="hidden" id="url" value="">
<button onclick="startPlay()">开始播放</button> <button onclick="startPlay()">开始播放</button>
<button onclick="">暂停播放</button> <button onclick="pausePlay()">暂停播放</button>
<button onclick="">唤醒播放</button> <button onclick="resumePlay()">唤醒播放</button>
<button onclick="stopPlay()">停止播放</button> <button onclick="stopPlay()">停止播放</button>
<button onclick="openlink()">打开新链接</button> <button onclick="openlink()">打开新链接</button>
...@@ -36,16 +36,28 @@ ...@@ -36,16 +36,28 @@
<script> <script>
function xeJsBridgeCallback(message) { function xeJsBridgeCallback(message) {
let jsonData = JSON.parse(message); document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + message + '</p>';
document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + jsonData.data + '</p>';
// 设置id为url的input的值 // 设置id为url的input的值
document.getElementById('url').value = jsonData.data; let jsonData = JSON.parse(message);
if(jsonData.cmd=="audioRecorderStop"){
document.getElementById('url').value = jsonData.data.tempFilePath;
}
} }
function startRecord() { function startRecord() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioRecorderStart", "params": {} }'; let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioRecorderStart", "params": {"duration":3} }';
xeJsBridge.postMessage(message);
}
function pauseRecord() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioRecorderPause", "params": {} }';
xeJsBridge.postMessage(message);
}
function resumeRecord() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioRecorderResume", "params": {} }';
xeJsBridge.postMessage(message); xeJsBridge.postMessage(message);
} }
...@@ -57,10 +69,21 @@ ...@@ -57,10 +69,21 @@
function startPlay() { function startPlay() {
let url = document.getElementById('url').value; let url = document.getElementById('url').value;
//let url = 'https://files-cos.banxiaoer.net/txbb/res/mp3/d2-d560e952-4029-11eb-928a-acde48001122/wd-db0b7502-4029-11eb-928a-acde48001122_h.mp3';
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioPlay", "params": {"url":"' + url + '"} }'; let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioPlay", "params": {"url":"' + url + '"} }';
xeJsBridge.postMessage(message); xeJsBridge.postMessage(message);
} }
function pausePlay() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioPause", "params": {} }';
xeJsBridge.postMessage(message);
}
function resumePlay() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioResume", "params": {} }';
xeJsBridge.postMessage(message);
}
function stopPlay() { function stopPlay() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioStop", "params": {} }'; let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioStop", "params": {} }';
xeJsBridge.postMessage(message); xeJsBridge.postMessage(message);
......
{"version":"1.0.1","cn.banxe.bxe":{"manifestPlaceholders":{"VIVO_APPKEY":"f7e37b20f48234c425d9e7f82fdd16c0","VIVO_APPID":"105993977","HONOR_APPID":""},"vivoPushBussinessId":"44580"}}
\ No newline at end of file \ No newline at end of file
cd /Users/ethanlam/works/gitlab/flutter_pros/appframe.git
# 1. 回到项目根目录
flutter clean
# 2. 进入 ios 目录
cd ios
# 3. 删除构建残留和 Pod 锁文件 (必须删 Lock 文件)
rm -rf Pods
rm -rf Podfile.lock
rm -rf .symlinks
rm -rf Flutter/Flutter.framework
rm -rf Flutter/App.framework
flutter pub get
#pod deintegrate
# 4. 清理 CocoaPods 本地缓存 (这一步很重要,ffmpeg 包很大,容易缓存损坏)
pod cache clean --all
# 5. 重新安装 Pods 并更新仓库索引
# 注意:这一步可能比较慢,因为 ffmpeg 库非常大(几百MB),请保持网络通畅
pod install --repo-update
# 6. 回到根目录
cd ..
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
#platform :ios, '12.0' #platform :ios, '12.0'
#source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' #source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'
#platform :ios, '13.0' platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
...@@ -26,15 +26,33 @@ end ...@@ -26,15 +26,33 @@ end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
pod 'SDWebImage', :git => 'https://gitee.com/mirrors/SDWebImage.git', :branch => '5.21.3' # pod 'TOCropViewController', :path => '/Users/ethanlam/works/base_env/flutter_envirs/extlibs/TOCropViewController.git/2.8.0' , :podspec => '/Users/ethanlam/works/base_env/flutter_envirs/extlibs/TOCropViewController.git/2.8.0/TOCropViewController.podspec'
pod 'SDWebImage', :git => 'https://gitee.com/mirrors/SDWebImage.git', :branch => '5.21.3', :modular_headers => true
pod 'SDWebImageWebPCoder', :git => 'https://gitee.com/mirrors_SDWebImage/SDWebImageWebPCoder.git', :branch => '0.13.0', :modular_headers => true
# pod 'Mantle', :git => 'https://gitee.com/mirrors/Mantle.git' , :tag => '2.0.2'
pod 'DKImagePickerController', :path => '/Users/ethanlam/works/base_env/flutter_envirs/extlibs/DKImagePickerController.git/4.3.9'
flutter_ios_podfile_setup flutter_ios_podfile_setup
target 'Runner' do target 'Runner' do
use_frameworks! #use_frameworks!
use_frameworks! :linkage => :static
use_modular_headers!
# pod 'SDWebImage', :modular_headers => true # pod 'SDWebImage', :modular_headers => true
# pod 'SDWebImageWebPCoder', :modular_headers => true # pod 'SDWebImageWebPCoder', :modular_headers => true
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# ================= 添加下面这一行 =================
# 强制引入 FFmpeg 原生库 (版本 6.0 对应 ffmpeg_kit_flutter_new 3.2.0)
# 这会自动下载包含 FFmpegKitConfig.h 的 Framework
# pod 'ffmpeg-kit-ios-min-gpl', '~> 6.0'
# =================================================
target 'RunnerTests' do target 'RunnerTests' do
inherit! :search_paths inherit! :search_paths
end end
...@@ -42,6 +60,32 @@ end ...@@ -42,6 +60,32 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
# if target.name == 'CropViewController'
# target.remove_from_project
# puts "已移除重复目标: #{target.name}"
# end
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
# 解决有些库构建版本不一致的问题
target.build_configurations.each do |config|
# 强制统一所有第三方库的 iOS 版本为 13.0,防止某些库仍然用旧版本编译导致头文件路径错误
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
# 解决 "signing for xxx requires a development team" 的报错
# 解决 M系列芯片(M4 Pro)可能的签名问题
config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_CAMERA=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_PHOTOS=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_MICROPHONE=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_LOCATION=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_BLUETOOTH=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_LOCAL_NETWORK=1'
end
end end
end end
PODS:
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_sound (9.28.0):
- Flutter
- flutter_sound_core (= 9.28.0)
- flutter_sound_core (9.28.0)
- fluwx (0.0.1):
- Flutter
- fluwx/pay (= 0.0.1)
- fluwx/pay (0.0.1):
- Flutter
- WechatOpenSDK-XCFramework (~> 2.0.5)
- gallery_saver_plus (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- video_thumbnail (0.0.1):
- Flutter
- libwebp
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- WechatOpenSDK-XCFramework (2.0.5)
DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_sound (from `.symlinks/plugins/flutter_sound/ios`)
- fluwx (from `.symlinks/plugins/fluwx/ios`)
- gallery_saver_plus (from `.symlinks/plugins/gallery_saver_plus/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- SDWebImage (from `https://gitee.com/mirrors/SDWebImage.git`, branch `5.21.3`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- flutter_sound_core
- libwebp
- Mantle
- SDWebImageWebPCoder
- SwiftyGif
- WechatOpenSDK-XCFramework
EXTERNAL SOURCES:
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_sound:
:path: ".symlinks/plugins/flutter_sound/ios"
fluwx:
:path: ".symlinks/plugins/fluwx/ios"
gallery_saver_plus:
:path: ".symlinks/plugins/gallery_saver_plus/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
SDWebImage:
:branch: 5.21.3
:git: https://gitee.com/mirrors/SDWebImage.git
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
video_thumbnail:
:path: ".symlinks/plugins/video_thumbnail/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
CHECKOUT OPTIONS:
SDWebImage:
:commit: 7312c9ce5e1987cb6cbf11213b5fc1fc03abe3a8
:git: https://gitee.com/mirrors/SDWebImage.git
SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_sound: b9236a5875299aaa4cef1690afd2f01d52a3f890
flutter_sound_core: 427465f72d07ab8c3edbe8ffdde709ddacd3763c
fluwx: 620a35d7ba14842304dc2b19fed6b4e279da9d0f
gallery_saver_plus: 61c8a831d5975e2bf57f87bcd64379dcc7d29ff2
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
SDWebImage: 22f3e17ef402a1cabed97a37a36397ec1f36e435
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
PODFILE CHECKSUM: 796b297eb3180da7b164556770b43e60cda83073
COCOAPODS: 1.16.2

295 Bytes | W: | H:

416 Bytes | W: | H:

ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  • 2-up
  • Swipe
  • Onion skin

406 Bytes | W: | H:

869 Bytes | W: | H:

ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  • 2-up
  • Swipe
  • Onion skin

282 Bytes | W: | H:

607 Bytes | W: | H:

ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  • 2-up
  • Swipe
  • Onion skin

406 Bytes | W: | H:

869 Bytes | W: | H:

ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -7,15 +7,15 @@ ...@@ -7,15 +7,15 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Appframe</string> <string>班小二App</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>cn.banxe.appframe</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>appframe</string> <string>banxiaoer</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
...@@ -89,5 +89,11 @@ ...@@ -89,5 +89,11 @@
</array> </array>
<key>io.flutter.embedded_views_preview</key> <key>io.flutter.embedded_views_preview</key>
<true/> <true/>
<key>NSLocalNetworkUsageDescription</key>
<string>此应用需要访问本地网络以发现和连接智能设备</string>
<key>NSBonjourServices</key>
<array>
<string>_dartobservatory._tcp</string>
</array>
</dict> </dict>
</plist> </plist>
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/ethanlam/works/gitlab/flutter_pros/appframe.git/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=a5693d2b86d2bd745700c225bc3c49cb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
\ No newline at end of file \ No newline at end of file
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/ethanlam/works/gitlab/flutter_pros/appframe.git/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=14ee682db2378d556925c69eae52a50e_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
\ No newline at end of file \ No newline at end of file
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/ethanlam/works/gitlab/flutter_pros/appframe.git/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=1ac66dbb2b848fb24d56375428d6412e_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
\ No newline at end of file \ No newline at end of file
...@@ -16,16 +16,18 @@ class App extends StatelessWidget { ...@@ -16,16 +16,18 @@ class App extends StatelessWidget {
routerConfig: router, routerConfig: router,
title: '班小二', title: '班小二',
theme: const CupertinoThemeData(primaryColor: CupertinoColors.systemBlue), theme: const CupertinoThemeData(primaryColor: CupertinoColors.systemBlue),
// === 为 iOS 添加本地化配置 === localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: const [ supportedLocales: AppLocalizations.supportedLocales,
GlobalMaterialLocalizations.delegate, // 为Material组件提供本地化 // // === 为 iOS 添加本地化配置 ===
GlobalCupertinoLocalizations.delegate, // 为Cupertino组件提供本地化 // localizationsDelegates: const [
GlobalWidgetsLocalizations.delegate, // 定义文本方向等 // GlobalMaterialLocalizations.delegate, // 为Material组件提供本地化
], // GlobalCupertinoLocalizations.delegate, // 为Cupertino组件提供本地化
supportedLocales: const [ // GlobalWidgetsLocalizations.delegate, // 定义文本方向等
Locale('zh', 'CN'), // 中文(中国) // ],
Locale('en', 'US'), // 英语(美国) // supportedLocales: const [
], // Locale('zh', 'CN'), // 中文(中国)
// Locale('en', 'US'), // 英语(美国)
// ],
) )
: MaterialApp.router( : MaterialApp.router(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
......
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/services/im_service.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart';
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
class ImState extends Equatable {
final List<String> messages;
const ImState({this.messages = const []});
ImState copyWith({List<String>? messages}) {
return ImState(
messages: messages ?? this.messages,
);
}
@override
List<Object?> get props => [messages];
}
class ImCubit extends Cubit<ImState> {
late ImService imService;
late V2TimAdvancedMsgListener _msgListener;
ImCubit() : super(const ImState()) {
imService = getIt.get<ImService>();
_msgListener = V2TimAdvancedMsgListener(
onRecvNewMessage: _onMessageReceived,
);
init();
}
void init() async {
await imService.removeMsgListener(imService.msgListener);
await imService.addMsgListener(_msgListener);
}
void addMessage(String message) {
emit(state.copyWith(messages: [...state.messages, message]));
}
void goWeb() {
router.go('/web');
}
Future<void> _onMessageReceived(V2TimMessage message) async {
// 处理文本消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) {
print("page 收到IM消息-------- ${message.textElem?.text}");
// 处理接收到的消息
emit(state.copyWith(messages: [...state.messages, message.textElem?.text ?? '']));
} else {
print("page 忽略非文本消息-------- ${message.textElem?.text}");
}
}
@override
Future<void> close() async {
await imService.removeMsgListener(_msgListener);
await imService.addMsgListener(imService.msgListener);
super.close();
}
}
import 'package:appframe/config/constant.dart'; import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart'; import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart'; import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
...@@ -6,24 +7,33 @@ import 'package:flutter_bloc/flutter_bloc.dart'; ...@@ -6,24 +7,33 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluwx/fluwx.dart'; import 'package:fluwx/fluwx.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../config/locator.dart';
class LoginMainState extends Equatable { class LoginMainState extends Equatable {
final bool agreed; final bool agreed;
final bool showAgreed; final bool showAgreed;
final bool loading;
const LoginMainState({this.agreed = false, this.showAgreed = false}); const LoginMainState({
this.agreed = false,
this.showAgreed = false,
this.loading = false,
});
@override @override
List<Object?> get props => [agreed, showAgreed]; List<Object?> get props => [
agreed,
showAgreed,
loading,
];
LoginMainState copyWith({ LoginMainState copyWith({
bool? agreed, bool? agreed,
bool? showAgreed, bool? showAgreed,
bool? loading,
}) { }) {
return LoginMainState( return LoginMainState(
agreed: agreed ?? this.agreed, agreed: agreed ?? this.agreed,
showAgreed: showAgreed ?? this.showAgreed, showAgreed: showAgreed ?? this.showAgreed,
loading: loading ?? this.loading,
); );
} }
} }
...@@ -64,6 +74,9 @@ class LoginMainCubit extends Cubit<LoginMainState> { ...@@ -64,6 +74,9 @@ class LoginMainCubit extends Cubit<LoginMainState> {
if (!result) { if (!result) {
throw Exception('微信授权处理失败'); throw Exception('微信授权处理失败');
} }
// 控制显示加载框
emit(state.copyWith(loading: true));
} }
void goLoginPhone() { void goLoginPhone() {
...@@ -73,6 +86,7 @@ class LoginMainCubit extends Cubit<LoginMainState> { ...@@ -73,6 +86,7 @@ class LoginMainCubit extends Cubit<LoginMainState> {
void _responseListener(WeChatResponse response) async { void _responseListener(WeChatResponse response) async {
if (response is WeChatAuthResponse) { if (response is WeChatAuthResponse) {
if (response.code == null || response.code == '') { if (response.code == null || response.code == '') {
emit(state.copyWith(loading: false));
return; return;
} }
...@@ -93,12 +107,10 @@ class LoginMainCubit extends Cubit<LoginMainState> { ...@@ -93,12 +107,10 @@ class LoginMainCubit extends Cubit<LoginMainState> {
sharedPreferences.setString('auth_classCode', classCode); sharedPreferences.setString('auth_classCode', classCode);
sharedPreferences.setInt('auth_userType', userType); sharedPreferences.setInt('auth_userType', userType);
sharedPreferences.setString('auth_stuId', stuId ?? ''); sharedPreferences.setString('auth_stuId', stuId ?? '');
sharedPreferences.setString('auth_ip', Constant.h5Server);
router.go( router.go(
'/web', '/web',
extra: { extra: {
'ip': Constant.h5Server,
'sessionCode': sessionCode, 'sessionCode': sessionCode,
'userCode': userCode, 'userCode': userCode,
'classCode': classCode, 'classCode': classCode,
...@@ -108,4 +120,10 @@ class LoginMainCubit extends Cubit<LoginMainState> { ...@@ -108,4 +120,10 @@ class LoginMainCubit extends Cubit<LoginMainState> {
); );
} }
} }
@override
Future<void> close() {
_fluwx.removeSubscriber(_responseListener);
return super.close();
}
} }
import 'package:appframe/config/db.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/services/mqtt_service.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mqtt_client/mqtt_client.dart';
class MqttState extends Equatable {
final List<String> messages;
const MqttState({this.messages = const []});
MqttState copyWith({List<String>? messages}) {
return MqttState(
messages: messages ?? this.messages,
);
}
@override
List<Object?> get props => [messages];
}
class MqttCubit extends Cubit<MqttState> with WidgetsBindingObserver {
late MqttService _mqttService;
MqttCubit() : super(MqttState()) {
_mqttService = getIt.get<MqttService>();
_mqttService.listen(_onMessageReceived);
WidgetsBinding.instance.addObserver(this);
}
void goWeb() {
router.go('/web');
}
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
// print('----------------------didChangeAppLifecycleState------------------------------');
if (state == AppLifecycleState.resumed) {
// print('----------------------resumed------------------------------');
if (!_mqttService.isConnected()) {
// print('----------------------reconnected------------------------------');
await _mqttService.initConn();
_mqttService.listen(_onMessageReceived);
}
}
}
Future<void> _onMessageReceived(List<MqttReceivedMessage<MqttMessage?>>? c) async {
final recMess = c![0].payload as MqttPublishMessage;
final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
print('-------------------------> $pt');
// Map<String, dynamic> data = {"id": 1, "content": pt};
// bxeDb.insert('msg', data);
//
// var msgList = await bxeDb.query('msg');
// for (var msg in msgList) {
// print('-------------------------> $msg');
// }
// 处理接收到的消息
emit(state.copyWith(messages: [...state.messages, pt]));
}
@override
Future<void> close() {
// 关闭时移除观察者
WidgetsBinding.instance.removeObserver(this);
return super.close();
}
}
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_sound/public/flutter_sound_recorder.dart';
class RecorderState {
final bool recorderIsInit;
final String path;
RecorderState({this.recorderIsInit = false, this.path = ''});
}
class RecorderCubit extends Cubit<String> {
late FlutterSoundRecorder _recorder;
RecorderCubit(super.initialState, this._recorder) {
_recorder.openRecorder();
}
@override
Future<void> close() {
try {
_recorder.closeRecorder();
} catch (e) {
print(e);
}
return super.close();
}
}
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluwx/fluwx.dart';
import 'package:shared_preferences/shared_preferences.dart';
class WechatAuthState extends Equatable {
final String ip;
final String? result;
final String? sessionCode;
final String? userCode;
final String? classCode;
final int? userType;
final String? stuId;
const WechatAuthState({
this.ip = Constant.localServerHost,
this.result,
this.sessionCode,
this.userCode,
this.classCode,
this.userType,
this.stuId,
});
WechatAuthState copyWith({
String? ip,
String? result,
String? sessionCode,
String? userCode,
String? classCode,
int? userType,
String? stuId,
}) {
return WechatAuthState(
ip: ip ?? this.ip,
result: result ?? this.result,
sessionCode: sessionCode ?? this.sessionCode,
userCode: userCode ?? this.userCode,
classCode: classCode ?? this.classCode,
userType: userType ?? this.userType,
stuId: stuId ?? this.stuId,
);
}
@override
List<Object?> get props => [ip, result, sessionCode, userCode, classCode, userType, stuId];
}
class WechatAuthCubit extends Cubit<WechatAuthState> {
late final WechatAuthRepository _wechatAuthRepository;
late final Fluwx _fluwx;
late final TextEditingController _textEditingController;
TextEditingController get textEditingController => _textEditingController;
WechatAuthCubit(super.initialState) {
_fluwx = getIt.get<Fluwx>();
_fluwx.addSubscriber(_responseListener);
_textEditingController = TextEditingController()..text = Constant.h5Server;
_wechatAuthRepository = getIt<WechatAuthRepository>();
}
void _responseListener(response) async {
if (response is WeChatAuthResponse) {
dynamic resultData = await _wechatAuthRepository.codeToSk(response.code!);
var data = resultData['data'];
var role = data['roles'][0];
final sessionCode = data['sessionCode'];
final userCode = data['userCode'];
final classCode = role['classCode'];
final userType = role['userType'];
final stuId = role['stuId'];
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.setString('auth_sessionCode', sessionCode);
sharedPreferences.setString('auth_userCode', userCode);
sharedPreferences.setString('auth_classCode', classCode);
sharedPreferences.setInt('auth_userType', userType);
sharedPreferences.setString('auth_stuId', stuId ?? '');
sharedPreferences.setString('auth_ip', Constant.h5Server);
router.go(
'/web',
extra: {
'ip': state.ip,
'sessionCode': sessionCode,
'userCode': userCode,
'classCode': classCode,
'userType': userType,
'stuId': stuId,
},
);
}
}
void auth() async {
emit(state.copyWith(ip: _textEditingController.text));
var result = await _fluwx.authBy(
which: NormalAuth(scope: 'snsapi_userinfo', state: 'wechat_sdk_test'),
);
if (!result) {
throw Exception('微信授权处理失败');
}
}
void goIndex() {
router.go('/web');
}
@override
Future<void> close() async {
_fluwx.removeSubscriber(_responseListener);
return super.close();
}
}
class Constant { class Constant {
/// 应用内部 http 服务 /// 应用内部 http 服务
static const int localServerPort = 35982; static const int localServerPort = 35982;
// static const String localServerHost = 'appdev-xj.banxiaoer.net';
static const String localServerHost = '127.0.0.1'; static const String localServerHost = '127.0.0.1';
static const String localServerUrl = 'http://$localServerHost:$localServerPort'; static const String localServerUrl = 'http://$localServerHost:$localServerPort';
static const String localFileUrl = 'http://127.0.0.1:$localServerPort';
static const String localServerTemp = '/temp'; static const String localServerTemp = '/temp';
static const String localServerTempFileUrl = '$localServerUrl$localServerTemp'; static const String localServerTempFileUrl = '$localFileUrl$localServerTemp';
static const String localServerTest = '/test'; static const String localServerTest = '/test';
static const String localServerTestFileUrl = '$localServerUrl$localServerTest'; static const String localServerTestFileUrl = '$localFileUrl$localServerTest';
/// obs文件分片上传的分片大小:5M /// obs文件分片上传的分片大小:5M
static const int obsUploadChunkSize = 1024 * 1024 * 5; static const int obsUploadChunkSize = 1024 * 1024 * 5;
/// 测试阶段使用的 h5 服务地址 /// 版本号
// static const String h5Server = 'appdev-xj.banxiaoer.net'; static const String appVersion = '1.0.0';
static const String h5Server = 'appdev-th.banxiaoer.net'; static const String h5Version = '1.0.0';
// static const String h5Server = '192.168.1.136';
/// H5版本号配置文件地址
static const String configUrl = 'https://bxe-obs.banxiaoer.com/conf/xeapp_conf_dev.json';
/// 内部 H5 dist 目录
static const String h5DistDir = 'http_dist_assets';
/// IM SDK
static const int imSdkAppId = 1400310691;
static const String imClientSecure = 'kM4yqbehB3io9UiLvH6eHvM7xAhfYxoyyaO1tLoHgKltcaI7MZXkUbpFaWdeQIqe';
/// 测试阶段使用
static const bool needIM = true;
} }
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
late final Database bxeDb;
Future<void> initDatabase() async {
// 获取数据库存储的默认路径
String databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'bxe_database.db');
// 打开数据库,如果不存在则创建
Database database = await openDatabase(
path,
version: 1, // 数据库版本
onCreate: (Database db, int version) async {
// 创建表
await db.execute(
// 'CREATE TABLE msg(id INTEGER PRIMARY KEY, content TEXT)',
'CREATE TABLE msg(id INTEGER, content TEXT)',
);
},
);
bxeDb = database;
}
import 'dart:io' show Platform;
import 'package:appframe/data/repositories/message/app_info_handler.dart'; import 'package:appframe/data/repositories/message/app_info_handler.dart';
import 'package:appframe/data/repositories/message/audio_player_handler.dart'; import 'package:appframe/data/repositories/message/audio_player_handler.dart';
import 'package:appframe/data/repositories/message/audio_recorder_handler.dart'; import 'package:appframe/data/repositories/message/audio_recorder_handler.dart';
...@@ -20,8 +22,9 @@ import 'package:appframe/data/repositories/message/orientation_handler.dart'; ...@@ -20,8 +22,9 @@ import 'package:appframe/data/repositories/message/orientation_handler.dart';
import 'package:appframe/data/repositories/message/save_file_to_disk_handler.dart'; import 'package:appframe/data/repositories/message/save_file_to_disk_handler.dart';
import 'package:appframe/data/repositories/message/save_to_album_handler.dart'; import 'package:appframe/data/repositories/message/save_to_album_handler.dart';
import 'package:appframe/data/repositories/message/scan_code_handler.dart'; import 'package:appframe/data/repositories/message/scan_code_handler.dart';
import 'package:appframe/data/repositories/message/set_title_handler.dart'; import 'package:appframe/data/repositories/message/share_to_wx_handler.dart';
import 'package:appframe/data/repositories/message/storage_handler.dart'; import 'package:appframe/data/repositories/message/storage_handler.dart';
import 'package:appframe/data/repositories/message/title_bar_handler.dart';
import 'package:appframe/data/repositories/message/upload_file.dart'; import 'package:appframe/data/repositories/message/upload_file.dart';
import 'package:appframe/data/repositories/message/vibrate_short_handler.dart'; import 'package:appframe/data/repositories/message/vibrate_short_handler.dart';
import 'package:appframe/data/repositories/message/video_info_handler.dart'; import 'package:appframe/data/repositories/message/video_info_handler.dart';
...@@ -30,10 +33,13 @@ import 'package:appframe/data/repositories/message/window_info_handler.dart'; ...@@ -30,10 +33,13 @@ import 'package:appframe/data/repositories/message/window_info_handler.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart'; import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:appframe/services/api_service.dart'; import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/im_service.dart';
import 'package:appframe/services/local_server_service.dart';
import 'package:appframe/services/player_service.dart';
import 'package:appframe/services/recorder_service.dart';
import 'package:fluwx/fluwx.dart'; import 'package:fluwx/fluwx.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'dart:io' show Platform;
final getIt = GetIt.instance; final getIt = GetIt.instance;
...@@ -43,7 +49,10 @@ Future<void> setupLocator() async { ...@@ -43,7 +49,10 @@ Future<void> setupLocator() async {
await (() async { await (() async {
Fluwx fluwx = Fluwx(); Fluwx fluwx = Fluwx();
if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid || Platform.isIOS) {
await fluwx.registerApi(appId: "wx8c32ea248f0c7765", universalLink: "https://dev.banxiaoer.net/path/to/wechat/"); await fluwx.registerApi(
appId: "wx8c32ea248f0c7765",
universalLink: "https://dev.banxiaoer.net/path/to/wechat/",
);
} }
return fluwx; return fluwx;
})(), })(),
...@@ -59,6 +68,9 @@ Future<void> setupLocator() async { ...@@ -59,6 +68,9 @@ Future<void> setupLocator() async {
/// 打开小程序 /// 打开小程序
getIt.registerLazySingleton<MessageHandler>(() => OpenWeappHandler(), instanceName: 'openWeapp'); getIt.registerLazySingleton<MessageHandler>(() => OpenWeappHandler(), instanceName: 'openWeapp');
/// 分享微信会话
getIt.registerLazySingleton<MessageHandler>(() => ShareToWxHandler(), instanceName: 'sharetowx');
/// 设备信息 /// 设备信息
getIt.registerLazySingleton<MessageHandler>(() => DeviceInfoHandler(), instanceName: 'getDeviceInfo'); getIt.registerLazySingleton<MessageHandler>(() => DeviceInfoHandler(), instanceName: 'getDeviceInfo');
...@@ -161,7 +173,7 @@ Future<void> setupLocator() async { ...@@ -161,7 +173,7 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<MessageHandler>(() => DownloadFileHandler(), instanceName: 'downloadFile'); getIt.registerLazySingleton<MessageHandler>(() => DownloadFileHandler(), instanceName: 'downloadFile');
/// 设置标题和返回按钮 /// 设置标题和返回按钮
getIt.registerLazySingleton<MessageHandler>(() => SetTitleHandler(), instanceName: 'setTitle'); getIt.registerLazySingleton<MessageHandler>(() => TitleBarHandler(), instanceName: 'setTitlebar');
/// 新路由打开链接 /// 新路由打开链接
getIt.registerLazySingleton<MessageHandler>(() => OpenLinkHandler(), instanceName: 'openlink'); getIt.registerLazySingleton<MessageHandler>(() => OpenLinkHandler(), instanceName: 'openlink');
...@@ -170,8 +182,28 @@ Future<void> setupLocator() async { ...@@ -170,8 +182,28 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<MessageHandler>(() => GoLoginHandler(), instanceName: 'goLogin'); getIt.registerLazySingleton<MessageHandler>(() => GoLoginHandler(), instanceName: 'goLogin');
/// service /// service
getIt.registerLazySingleton<ApiService>(() => ApiService(baseUrl: 'https://dev.banxiaoer.net')); ///
/// local server
getIt.registerSingleton<LocalServerService>(LocalServerService());
/// apiService
getIt.registerLazySingleton<ApiService>(
() => ApiService(baseUrl: 'https://dev.banxiaoer.net'),
instanceName: "bxeApiService",
);
getIt.registerLazySingleton<ApiService>(
() => ApiService(baseUrl: 'https://iotapp-dev.banxiaoer.com/iotapp'),
instanceName: "appApiService",
);
/// imService
getIt.registerSingleton<ImService>(ImService());
/// 播放
getIt.registerSingleton<PlayerService>(PlayerService());
/// 录音
getIt.registerSingleton<RecorderService>(RecorderService());
/// repository /// repository
getIt.registerLazySingleton<WechatAuthRepository>(() => WechatAuthRepository()); getIt.registerLazySingleton<WechatAuthRepository>(() => WechatAuthRepository());
......
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/mqtt_service.dart';
/// 此处暂时测试
/// 正常需要在登录状态下,查询host和jwt
Future<void> registerMqtt() async {
String mqttHost = '58.87.99.45';
int mqttPort = 1883;
String mqttClientId = 'asdfasdf';
int keepAlive = 60;
String mqttUsername = 'user';
String mqttPassword =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjIzOTY5MDQsInJzIjoib2sifQ.tsouW-uHTe6ndiEqSE4wcR_oSScuoS0qVvqWtNSaXgWkX2yEja9ImnMYr6jNKxJY8_qox9UdXRuCept5cTM4-8ZaA0BBlYXwr3LbC3yPmniFhs-tAMFbZqG2-3r0sc5NMbE3M1fXMi3dmvQc2AlyazheL98EmRNILFsz6pZ-x5rR1gFulZ53PXa7fX60XRlXg8jc3-89gdUJcS0MOMU7yyU0Sv3QBBplLr3CmWCoYX99a_3QHvq3o7aUTJ_Ed5Ms9_3k4QHgawsfdTWjZkyBouAvLMgVkt4D0PJbhgsjzAaA01Q7jEJ_SokNojOFfQYuHNScczmlOLXocJCCD4189A';
/// 初始化MQTT客户端
var mqttService = MqttService(mqttHost, mqttPort, mqttClientId, keepAlive, mqttUsername, mqttPassword);
await mqttService.initConn();
/// 设置到getIt,用于获取使用
getIt.registerSingleton(mqttService);
}
import 'package:appframe/ui/pages/adv_page.dart'; import 'package:appframe/ui/pages/adv_page.dart';
import 'package:appframe/ui/pages/im_page.dart';
import 'package:appframe/ui/pages/link_page.dart'; import 'package:appframe/ui/pages/link_page.dart';
import 'package:appframe/ui/pages/login_main_page.dart'; import 'package:appframe/ui/pages/login_main_page.dart';
import 'package:appframe/ui/pages/login_phone_page.dart'; import 'package:appframe/ui/pages/login_phone_page.dart';
import 'package:appframe/ui/pages/mqtt_page.dart';
import 'package:appframe/ui/pages/scan_code_page.dart'; import 'package:appframe/ui/pages/scan_code_page.dart';
import 'package:appframe/ui/pages/web_page.dart'; import 'package:appframe/ui/pages/web_page.dart';
import 'package:appframe/ui/pages/wechat_auth_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
final GoRouter router = GoRouter( final GoRouter router = GoRouter(
initialLocation: '/adv', initialLocation: '/web',
routes: <RouteBase>[ routes: <RouteBase>[
GoRoute( GoRoute(
path: '/web', path: '/web',
...@@ -19,12 +18,6 @@ final GoRouter router = GoRouter( ...@@ -19,12 +18,6 @@ final GoRouter router = GoRouter(
}, },
), ),
GoRoute( GoRoute(
path: '/wechatAuth',
builder: (BuildContext context, GoRouterState state) {
return const WechatAuthPage();
},
),
GoRoute(
path: '/scanCode', path: '/scanCode',
builder: (BuildContext context, GoRouterState state) { builder: (BuildContext context, GoRouterState state) {
return const ScanCodePage(); return const ScanCodePage();
...@@ -55,9 +48,9 @@ final GoRouter router = GoRouter( ...@@ -55,9 +48,9 @@ final GoRouter router = GoRouter(
}, },
), ),
GoRoute( GoRoute(
path: '/mqtt', path: '/im',
builder: (BuildContext context, GoRouterState state) { builder: (BuildContext context, GoRouterState state) {
return const MqttPage(); return const ImPage();
}, },
), ),
], ],
......
import 'package:appframe/bloc/web_cubit.dart'; import 'package:appframe/config/locator.dart';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/player_service.dart';
import 'package:uuid/uuid.dart';
class AudioPlayHandler extends MessageHandler { class AudioPlayHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
try { if (params is! Map<String, dynamic>) {
if (params is! Map<String, dynamic>) { throw Exception('参数错误');
throw Exception('参数错误'); }
}
final url = params['url'] as String;
final seek = params['seek'] as int? ?? 0;
// 暂时忽略
// final isBg = params['isBg'] as bool ?? false;
var playId = params['playId'] as String? ?? '';
if (playId.isEmpty) {
playId = Uuid().v4();
}
final url = params['url'] as String; var playerService = getIt.get<PlayerService>();
var result = await playerService.playAudio(url, seek, playId);
bool result = await _webCubit!.playAudio(url); if (!result) {
return result; throw Exception('播放错误');
} finally {
_unfollowCubit();
} }
return {'playId': playId};
} }
} }
class AudioPauseHandler extends MessageHandler { class AudioPauseHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.pauseAudio(); return await getIt.get<PlayerService>().pauseAudio();
return result;
} }
} }
class AudioResumeHandler extends MessageHandler { class AudioResumeHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.resumeAudio(); return await getIt.get<PlayerService>().resumeAudio();
return result;
} }
} }
class AudioSeekHandler extends MessageHandler { class AudioSeekHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) { if (params is! Map<String, dynamic>) {
...@@ -87,45 +51,21 @@ class AudioSeekHandler extends MessageHandler { ...@@ -87,45 +51,21 @@ class AudioSeekHandler extends MessageHandler {
} }
final seek = params['seek'] as int; final seek = params['seek'] as int;
bool result = await _webCubit!.seekAudio(seek); return await getIt.get<PlayerService>().seekAudio(seek);
return result;
} }
} }
class AudioStopHandler extends MessageHandler { class AudioStopHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.stopAudio(); return await getIt.get<PlayerService>().stopAudio();
return result;
} }
} }
class AudioClearHandler extends MessageHandler { class AudioClearHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
await _webCubit!.clearAudio(); await getIt.get<PlayerService>().clearAudio();
return true; return true;
} }
} }
import 'package:appframe/bloc/web_cubit.dart'; import 'package:appframe/config/locator.dart';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/recorder_service.dart';
class AudioRecorderStartHandler extends MessageHandler { class AudioRecorderStartHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
try { // 0代表不限制时长
bool result = await _webCubit!.startRecording(); final duration = params['duration'] as int? ?? 0;
return result;
} finally { return await getIt.get<RecorderService>().startRecording(duration);
_unfollowCubit();
}
} }
} }
class AudioRecorderPauseHandler extends MessageHandler { class AudioRecorderPauseHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
try { return await getIt.get<RecorderService>().pauseRecording();
bool result = await _webCubit!.pauseRecording();
return result;
} finally {
_unfollowCubit();
}
} }
} }
class AudioRecorderResumeHandler extends MessageHandler { class AudioRecorderResumeHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
try { return await getIt.get<RecorderService>().resumeRecording();
bool result = await _webCubit!.resumeRecording();
return result;
} finally {
_unfollowCubit();
}
} }
} }
class AudioRecorderStopHandler extends MessageHandler { class AudioRecorderStopHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
try { return await getIt.get<RecorderService>().stopRecording();
return await _webCubit!.stopRecording();
} finally {
_unfollowCubit();
}
} }
} }
class AudioRecorderClearHandler extends MessageHandler { class AudioRecorderClearHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override @override
Future<dynamic> handleMessage(dynamic params) async { Future<dynamic> handleMessage(dynamic params) async {
try { return await getIt.get<RecorderService>().clearRecording();
return await _webCubit!.clearRecording();
} finally {
_unfollowCubit();
}
} }
} }
import 'dart:io'; import 'dart:io';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart';
import 'package:appframe/utils/video_util.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:video_compress/video_compress.dart'; import 'package:uuid/uuid.dart';
///
/// 压缩图片 /// 压缩图片
/// ///
class CompressImageHandler extends MessageHandler { class CompressImageHandler extends MessageHandler {
...@@ -24,13 +27,7 @@ class CompressImageHandler extends MessageHandler { ...@@ -24,13 +27,7 @@ class CompressImageHandler extends MessageHandler {
throw Exception('参数错误'); throw Exception('参数错误');
} }
var compressedWidth = params['compressedWidth'] as int?; var compressedWidth = params['compressedWidth'] as int?;
// if (compressedWidth == null) {
// throw Exception('参数错误');
// }
var compressedHeight = params['compressedHeight'] as int?; var compressedHeight = params['compressedHeight'] as int?;
// if (compressedHeight == null) {
// throw Exception('参数错误');
// }
if (compressedWidth == null && compressedHeight == null) { if (compressedWidth == null && compressedHeight == null) {
throw Exception('参数错误'); throw Exception('参数错误');
...@@ -38,8 +35,8 @@ class CompressImageHandler extends MessageHandler { ...@@ -38,8 +35,8 @@ class CompressImageHandler extends MessageHandler {
if (compressedWidth == null) { if (compressedWidth == null) {
compressedWidth = compressedHeight; compressedWidth = compressedHeight;
} else if (compressedHeight == null) { } else {
compressedHeight = compressedWidth; compressedHeight ??= compressedWidth;
} }
// 获取后缀名 // 获取后缀名
...@@ -84,6 +81,7 @@ class CompressImageHandler extends MessageHandler { ...@@ -84,6 +81,7 @@ class CompressImageHandler extends MessageHandler {
} }
} }
///
/// 压缩视频 /// 压缩视频
/// ///
class CompressVideoHandler extends MessageHandler { class CompressVideoHandler extends MessageHandler {
...@@ -105,11 +103,11 @@ class CompressVideoHandler extends MessageHandler { ...@@ -105,11 +103,11 @@ class CompressVideoHandler extends MessageHandler {
throw Exception('参数错误'); throw Exception('参数错误');
} }
Directory tempDir = await getTemporaryDirectory();
String srcPath; String srcPath;
if (url.startsWith('http')) { if (url.startsWith('http')) {
String ext = path.extension(url); String ext = path.extension(url);
final Directory tempDir = await getTemporaryDirectory(); srcPath = '${tempDir.path}/${Uuid().v4()}$ext';
srcPath = path.join(tempDir.path, '${DateTime.now().millisecondsSinceEpoch}$ext');
// Dio 下载文件 // Dio 下载文件
final resp = await Dio().download(url, srcPath); final resp = await Dio().download(url, srcPath);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
...@@ -119,28 +117,33 @@ class CompressVideoHandler extends MessageHandler { ...@@ -119,28 +117,33 @@ class CompressVideoHandler extends MessageHandler {
srcPath = url; srcPath = url;
} }
VideoQuality videoQuality; var originFile = File(srcPath);
switch (quality) { if (!originFile.existsSync()) {
case 'low': throw Exception('文件不存在');
videoQuality = VideoQuality.LowQuality; }
break; print('原视频大小:${originFile.lengthSync()}');
case 'middle':
videoQuality = VideoQuality.MediumQuality; String? mimeType = await FileTypeUtil.getMimeType(originFile);
break; if (!(mimeType?.startsWith('video/') ?? false)) {
case 'high': throw Exception('非视频文件');
videoQuality = VideoQuality.HighestQuality; }
break;
default: final outputPath = '${tempDir.path}/${Uuid().v4()}.mp4';
throw Exception('参数错误');
} bool result;
if (mimeType != 'video/mp4') {
final mediaInfo = await VideoCompress.compressVideo( result = await VideoUtil.convertToMp4(srcPath, outputPath);
srcPath, } else {
quality: videoQuality, result = await VideoUtil.compressVideo(srcPath, outputPath, quality);
deleteOrigin: false, }
includeAudio: true, if (!result) {
); throw Exception('视频压缩失败');
}
print('压缩后视频大小:${File(outputPath).lengthSync()}');
return {"tempFilePath": "/temp${mediaInfo!.path}", "size": mediaInfo.filesize}; return {
"tempFilePath": '/temp$outputPath',
"size": File(outputPath).lengthSync(),
};
} }
} }
...@@ -37,8 +37,8 @@ class CropImageHandler extends MessageHandler { ...@@ -37,8 +37,8 @@ class CropImageHandler extends MessageHandler {
} }
Future<String> _cropImageByRatio(String url, String cropScale) async { Future<String> _cropImageByRatio(String url, String cropScale) async {
if (url.startsWith(Constant.localServerUrl)) { if (url.startsWith(Constant.localFileUrl)) {
url = url.replaceFirst(Constant.localServerUrl, ''); url = url.replaceFirst(Constant.localFileUrl, '');
} }
if (url.startsWith(Constant.localServerTemp)) { if (url.startsWith(Constant.localServerTemp)) {
......
...@@ -15,8 +15,8 @@ class OpenDocumentHandler extends MessageHandler { ...@@ -15,8 +15,8 @@ class OpenDocumentHandler extends MessageHandler {
throw Exception('参数错误'); throw Exception('参数错误');
} }
if (url.startsWith(Constant.localServerUrl)) { if (url.startsWith(Constant.localFileUrl)) {
url = url.replaceFirst(Constant.localServerUrl, ''); url = url.replaceFirst(Constant.localFileUrl, '');
} }
if (url.startsWith(Constant.localServerTemp)) { if (url.startsWith(Constant.localServerTemp)) {
......
...@@ -28,7 +28,11 @@ class OpenWeappHandler extends MessageHandler { ...@@ -28,7 +28,11 @@ class OpenWeappHandler extends MessageHandler {
try { try {
return await _fluwx.open( return await _fluwx.open(
target: MiniProgram(username: appid, path: path, miniProgramType: _getWXMiniProgramType(envVersion)), target: MiniProgram(
username: appid,
path: path,
miniProgramType: _getWXMiniProgramType(envVersion),
),
); );
} catch (e) { } catch (e) {
print(e); print(e);
......
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:fluwx/fluwx.dart';
class ShareToWxHandler extends MessageHandler {
@override
Future<bool> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
String text = params['text'] as String? ?? '';
String fileUrl = params['fileUrl'] as String? ?? '';
String fileName = params['fileName'] as String? ?? '';
try {
Fluwx fluwx = getIt.get<Fluwx>();
if (text.isNotEmpty) {
return await fluwx.share(WeChatShareTextModel(text));
} else if (fileUrl.isNotEmpty && fileName.isNotEmpty) {
return await fluwx.share(WeChatShareFileModel(WeChatFile.network(fileUrl), title: fileName));
} else {
return false;
}
} catch (e) {
print(e);
return false;
}
}
}
import 'package:appframe/bloc/web_cubit.dart'; import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
class SetTitleHandler extends MessageHandler { class TitleBarHandler extends MessageHandler {
late WebCubit? _webCubit; late WebCubit? _webCubit;
@override @override
...@@ -21,9 +21,11 @@ class SetTitleHandler extends MessageHandler { ...@@ -21,9 +21,11 @@ class SetTitleHandler extends MessageHandler {
} }
final String title = params['title'] as String; final String title = params['title'] as String;
final bool showBack = params['showBack'] as bool; final String color = params['color'] as String;
final String bgColor = params['bgColor'] as String;
final String icon = params['icon'] as String;
return _webCubit!.setTitle(title, showBack); return _webCubit!.setTitleBar(title, color, bgColor, icon);
} finally { } finally {
_unfollowCubit(); _unfollowCubit();
} }
......
...@@ -3,9 +3,11 @@ import 'dart:io'; ...@@ -3,9 +3,11 @@ import 'dart:io';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart'; import 'package:appframe/utils/file_type_util.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:ffmpeg_kit_flutter_new/ffprobe_kit.dart';
import 'package:ffmpeg_kit_flutter_new/media_information_session.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:video_compress/video_compress.dart';
class VideoInfoHandler extends MessageHandler { class VideoInfoHandler extends MessageHandler {
@override @override
...@@ -15,8 +17,26 @@ class VideoInfoHandler extends MessageHandler { ...@@ -15,8 +17,26 @@ class VideoInfoHandler extends MessageHandler {
} }
final url = params['url'] as String; final url = params['url'] as String;
final filePath = await _getFilePath(url);
final file = File(filePath);
if (!file.existsSync()) {
throw Exception('视频文件不存在');
}
// 使用 ffmpeg_kit_flutter_new 获取视频信息
final mediaInfoSession = await FFprobeKit.getMediaInformation(filePath);
final returnCode = await mediaInfoSession.getReturnCode();
if (!ReturnCode.isSuccess(returnCode)) {
throw Exception('获取视频信息失败');
}
final result = await _extractVideoInfo(mediaInfoSession, file, filePath);
return result;
}
String filePath; /// 根据URL获取文件路径,如果URL是网络地址则下载到本地
Future<String> _getFilePath(String url) async {
if (url.startsWith('http')) { if (url.startsWith('http')) {
// 获取后缀名 // 获取后缀名
String ext = path.extension(url); String ext = path.extension(url);
...@@ -30,18 +50,41 @@ class VideoInfoHandler extends MessageHandler { ...@@ -30,18 +50,41 @@ class VideoInfoHandler extends MessageHandler {
throw Exception('文件下载失败'); throw Exception('文件下载失败');
} }
filePath = targetPath; return targetPath;
} else { } else {
filePath = url; return url;
} }
}
final file = File(filePath); /// 提取视频信息
if (!file.existsSync()) { Future<Map<String, dynamic>> _extractVideoInfo(
throw Exception('视频文件不存在'); MediaInformationSession mediaInfoSession, File file, String filePath) async {
final mediaInformation = mediaInfoSession.getMediaInformation();
if (mediaInformation == null) {
throw Exception('获取视频信息失败');
}
// 获取视频时长
final durationStr = mediaInformation.getDuration();
if (durationStr == null) {
throw Exception('获取视频信息失败');
}
var duration = (double.tryParse(durationStr) ?? 0).ceil();
// 获取视频流信息
final videoStreams = mediaInformation
.getStreams()
.where((stream) => stream.getAllProperties() != null && stream.getAllProperties()!['codec_type'] == 'video')
.toList();
if (videoStreams.isEmpty) {
throw Exception('获取视频信息失败');
} }
// 使用video_compress获取视频信息 final videoStream = videoStreams[0];
final mediaInfo = await VideoCompress.getMediaInfo(filePath); final properties = videoStream.getAllProperties();
int width = properties!['width'] ?? 0;
int height = properties['height'] ?? 0;
// 获取文件大小 // 获取文件大小
final size = await file.length(); final size = await file.length();
...@@ -52,10 +95,10 @@ class VideoInfoHandler extends MessageHandler { ...@@ -52,10 +95,10 @@ class VideoInfoHandler extends MessageHandler {
return { return {
'tempFilePath': '/temp$filePath', 'tempFilePath': '/temp$filePath',
'width': mediaInfo.width ?? 0, 'width': width,
'height': mediaInfo.height ?? 0, 'height': height,
'type': fileExtension, 'type': fileExtension,
'duration': (mediaInfo.duration ?? 0) / 1000, // 转换为秒 'duration': duration, // 已经是秒单位
'size': size, 'size': size,
}; };
} }
......
...@@ -6,14 +6,17 @@ class WechatAuthRepository { ...@@ -6,14 +6,17 @@ class WechatAuthRepository {
late final ApiService _apiService; late final ApiService _apiService;
WechatAuthRepository() { WechatAuthRepository() {
_apiService = getIt<ApiService>(); _apiService = getIt<ApiService>(instanceName: 'bxeApiService');
} }
Future<dynamic> codeToSk(String code) async { Future<dynamic> codeToSk(String code) async {
Response resp = await _apiService.post('/login/applet/wkbxe/codeToSkByApp?version=1.0.0', { Response resp = await _apiService.post(
"appCode": "bxeapp", '/login/applet/wkbxe/codeToSkByApp?version=1.0.0',
"params": {"code": code}, {
}); "appCode": "bxeapp",
"params": {"code": code},
},
);
print('登录结果: $resp'); print('登录结果: $resp');
......
import 'package:appframe/config/db.dart'; import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart'; import 'package:appframe/config/locator.dart';
import 'package:appframe/config/mqtt.dart'; import 'package:appframe/services/im_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'app.dart'; import 'app.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await setupLocator(); await setupLocator();
await registerMqtt(); // IM SDK 初始化
await initDatabase(); if (Constant.needIM) {
await getIt.get<ImService>().initSdk();
}
runApp(const App()); runApp(const App());
} }
...@@ -54,8 +54,7 @@ class MessageDispatcher { ...@@ -54,8 +54,7 @@ class MessageDispatcher {
h5Message.cmd == "chooseImage" || h5Message.cmd == "chooseImage" ||
h5Message.cmd == "chooseVideo" || h5Message.cmd == "chooseVideo" ||
h5Message.cmd == "goLogin" || h5Message.cmd == "goLogin" ||
h5Message.cmd.startsWith("audio") || h5Message.cmd.startsWith("setTitlebar")) {
h5Message.cmd.startsWith("setTitle")) {
handler.setCubit(webCubit!); handler.setCubit(webCubit!);
handler.setMessage(message); handler.setMessage(message);
} }
......
import 'dart:io'; import 'dart:io';
import 'package:appframe/config/constant.dart'; import 'package:appframe/config/constant.dart';
import 'package:archive/archive.dart'; import 'package:appframe/config/locator.dart';
import 'package:dio/dio.dart'; import 'package:appframe/utils/zip_util.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LocalServerService { class LocalServerService {
String? _httpDirectory; String? _httpDirectory;
...@@ -12,7 +13,7 @@ class LocalServerService { ...@@ -12,7 +13,7 @@ class LocalServerService {
// 启动本地HTTP服务器 // 启动本地HTTP服务器
Future<HttpServer> startLocalServer() async { Future<HttpServer> startLocalServer() async {
// 测试情况下, 每次启动服务,先解压dist文件 // 测试情况下, 每次启动服务,先解压dist文件
_extractDist(); await _extractDist();
HttpServer server = await HttpServer.bind(InternetAddress.loopbackIPv4, Constant.localServerPort); HttpServer server = await HttpServer.bind(InternetAddress.loopbackIPv4, Constant.localServerPort);
print('本地服务器启动在端口: ${server.port}'); print('本地服务器启动在端口: ${server.port}');
...@@ -43,6 +44,10 @@ class LocalServerService { ...@@ -43,6 +44,10 @@ class LocalServerService {
return server; return server;
} }
void resetHttpDirectory() {
_httpDirectory = null;
}
// 目录下的文件 // 目录下的文件
Future<void> _serveTempFile(HttpRequest request, String requestPath) async { Future<void> _serveTempFile(HttpRequest request, String requestPath) async {
try { try {
...@@ -77,7 +82,7 @@ class LocalServerService { ...@@ -77,7 +82,7 @@ class LocalServerService {
Future<void> _serveHttpFile(HttpRequest request, String requestPath) async { Future<void> _serveHttpFile(HttpRequest request, String requestPath) async {
try { try {
var httpDirectory = await getHttpDirectory(); var httpDirectory = await _getHttpDirectory();
final String filePath = '$httpDirectory$requestPath'; final String filePath = '$httpDirectory$requestPath';
// 检查文件是否存在 // 检查文件是否存在
...@@ -136,7 +141,7 @@ class LocalServerService { ...@@ -136,7 +141,7 @@ class LocalServerService {
return 'application/octet-stream'; return 'application/octet-stream';
} }
Future<String> getHttpDirectory() async { Future<String> _getHttpDirectory() async {
if (_httpDirectory == null) { if (_httpDirectory == null) {
await _initHttpDirectory(); await _initHttpDirectory();
} }
...@@ -144,66 +149,26 @@ class LocalServerService { ...@@ -144,66 +149,26 @@ class LocalServerService {
} }
Future<void> _initHttpDirectory() async { Future<void> _initHttpDirectory() async {
if (Platform.isAndroid) { var version = getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version;
var direct = await getExternalStorageDirectory(); var direct = await getApplicationSupportDirectory();
_httpDirectory = '${direct?.path}/http_dist_assets'; _httpDirectory = '${direct.path}/${Constant.h5DistDir}/$version';
} else if (Platform.isIOS || Platform.isMacOS) {
var direct = await getApplicationSupportDirectory();
_httpDirectory = '${direct.path}/http_dist_assets';
}
} }
Future<void> _extractDist() async { Future<void> _extractDist() async {
var outputDirectory = await getHttpDirectory(); var outputDirectory = await _getHttpDirectory();
// 判断目录存在则不需要再解压 // 判断目录存在则不需要再解压
if (Directory(outputDirectory).existsSync()) { // if (Directory(outputDirectory).existsSync()) {
return; // return;
} // }
var zipFilePath = "assets/dist.zip"; // 判断H5打包文件是否存在,不存在则从assets中解压
final ByteData data = await rootBundle.load(zipFilePath); var version = getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version;
final bytes = data.buffer.asUint8List(); var dir = await getApplicationSupportDirectory();
var distFilePath = '${dir.path}/${Constant.h5DistDir}/$version.zip';
// 解码 ZIP 文件 if (!File(distFilePath).existsSync()) {
final archive = ZipDecoder().decodeBytes(bytes); distFilePath = 'assets/dist.zip';
// 提取文件到指定目录
for (final file in archive) {
final filename = file.name;
if (file.isFile) {
final data = file.content as List<int>;
final outputFile = File('$outputDirectory/$filename');
outputFile.createSync(recursive: true);
outputFile.writeAsBytesSync(data);
} else {
// 创建目录
Directory('$outputDirectory/$filename').createSync(recursive: true);
}
} }
} await ZipUtil.extractZipFile(distFilePath, outputDirectory);
Future<void> _clearDist() async {
var outputDirectory = await getHttpDirectory();
Directory(outputDirectory).deleteSync(recursive: true);
}
void _downloadDist() async {
var httpDirectory = await getHttpDirectory();
var distUrl = "https://github.com/xinxin-wu/flutter_web_dist/releases/download/v1.0.0/dist.zip";
// Dio进行下载
var dio = Dio();
dio.download(
distUrl,
'$httpDirectory/dist.zip',
onReceiveProgress: (received, total) {
if (total != -1) {
print((received / total * 100).toStringAsFixed(0) + "%");
}
},
).then((_) {
_extractDist();
});
dio.close();
} }
} }
import 'dart:async';
import 'dart:io';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
class MqttService {
/// MQTT服务器信息
final String _host;
final int _port;
final String _clientId;
final int _keepAlive;
final String _username;
/// 重连的时候要重新获取一下 token
final String _password;
/// MQTT客户端
late MqttServerClient _client;
StreamSubscription? _subscription;
/// MQTT重连相关变量
int _reconnectAttempts = 0;
final int _maxReconnectAttempts = 5;
Timer? _reconnectTimer;
MqttService(this._host, this._port, this._clientId, this._keepAlive, this._username, this._password);
Future<void> initConn() async {
_client = MqttServerClient(_host, _clientId);
_client.logging(on: false);
_client.port = _port;
_client.keepAlivePeriod = _keepAlive;
_client.onDisconnected = onDisconnected;
_client.onConnected = onConnected;
_client.onSubscribed = onSubscribed;
_client.pongCallback = pong;
final connMess = MqttConnectMessage()
.withWillTopic('willtopic')
.withWillMessage('My Will message')
.startClean()
.withWillQos(MqttQos.atLeastOnce);
_client.connectionMessage = connMess;
try {
print('MQTT 开始连接......');
await _client.connect(_username, _password);
} on NoConnectionException catch (e) {
print('MQTT客户端连接失败: $e');
_client.disconnect();
} on SocketException catch (e) {
print('MQTT客户端连接失败: $e');
_client.disconnect();
}
// 订阅主题
subscribe("\$q/bxe/abc");
// 监听消息
listen(onMessageReceived);
}
void onConnected() {
print('MQTT客户端连接成功');
_reconnectAttempts = 0;
_reconnectTimer?.cancel();
}
void onDisconnected() {
print('MQTT客户端断开连接');
// if (_reconnectAttempts < _maxReconnectAttempts) {
// handleReconnect(_host, _port, _clientId, '');
// }
}
void onSubscribed(String topic) {
print('MQTT客户端订阅主题: $topic');
}
void onMessageReceived(List<MqttReceivedMessage<MqttMessage?>>? c) {
final recMess = c![0].payload as MqttPublishMessage;
final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
print('MQTT消息接收: topic is ${c[0].topic}, payload is $pt');
// 处理接收到的消息
}
void subscribe(String topic) {
_client.subscribe(topic, MqttQos.atLeastOnce);
}
void listen(Function(List<MqttReceivedMessage<MqttMessage?>>? c) onMessageReceivedCallback) {
_subscription?.cancel();
_subscription = _client.updates?.listen(onMessageReceivedCallback);
}
void disconnect() {
_client.disconnect();
}
bool isConnected() {
return _client.connectionStatus?.state == MqttConnectionState.connected;
}
void pong() {
print('Ping response client callback invoked');
}
void onMqttMessageReceived(List<MqttReceivedMessage<MqttMessage?>>? c) {
final recMess = c![0].payload as MqttPublishMessage;
final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
print('MQTT消息接收: topic is ${c[0].topic}, payload is $pt');
// 处理接收到的消息
}
void handleReconnect(String host, int port, String username, String password) {
if (_reconnectAttempts >= _maxReconnectAttempts) {
print('已达到最大重连次数,停止重连');
return;
}
_reconnectAttempts++;
print('第 $_reconnectAttempts 次重连尝试,${_maxReconnectAttempts - _reconnectAttempts} 次机会剩余');
// 取消之前的重连定时器(如果有)
_reconnectTimer?.cancel();
// 设置延迟重连,每次重连间隔递增
final delay = Duration(seconds: _reconnectAttempts * 5);
_reconnectTimer = Timer(delay, () async {
print('正在尝试重新连接...');
await initConn();
if (!isConnected()) {
disconnect();
}
});
}
}
import 'dart:async';
import 'package:flutter_sound/flutter_sound.dart';
class PlayerService {
/// 播放器
FlutterSoundPlayer? _player;
///
StreamSubscription? _playerSubscription;
/// 播放器是否初始化
bool? _playerIsInit;
/// 播放指令传递的 playId
String? _playId;
/// 当前播放文件的时长
int? _playDuration;
Function? _sendResponse;
set sendResponse(Function value) {
_sendResponse = value;
}
Future<bool> _initPlayer() async {
// 打开播放器
try {
final player = FlutterSoundPlayer();
_player = (await player.openPlayer())!;
await _player!.setSpeed(1); // 播放速度,默认1
// 播放进度回调
_player!.setSubscriptionDuration(Duration(seconds: 1));
_playerSubscription = _player!.onProgress!.listen((event) {
_playDuration = event.duration.inSeconds;
var playPosition = event.position.inSeconds;
var h5Cmd = {
'unique': '',
'cmd': 'audioProgress',
'data': {'playId': _playId, 'duration': _playDuration, 'currentTime': playPosition},
'errMsg': '',
};
_sendResponse!(h5Cmd);
});
_playerIsInit = true;
return true;
} catch (e) {
throw Exception('打开播放器失败!');
}
}
Future<bool> playAudio(String url, int seek, String playId) async {
if (!(_playerIsInit ?? false)) {
final initResult = await _initPlayer();
if (!initResult) {
return false;
}
}
_playId = playId;
await _player!.startPlayer(
fromURI: url,
whenFinished: () async {
await _player!.stopPlayer();
// 补发一下全部进度
var h5Cmd = {
'unique': '',
'cmd': 'audioProgress',
'data': {'playId': _playId, 'duration': _playDuration, 'currentTime': _playDuration},
'errMsg': '',
};
_sendResponse!(h5Cmd);
// 播放结束后,发送消息给客户端
h5Cmd = {
'unique': '',
'cmd': 'audioEnd',
'data': {'playId': _playId},
'errMsg': '',
};
_sendResponse!(h5Cmd);
},
);
if (seek != 0) {
await seekAudio(seek);
}
return true;
}
Future<bool> pauseAudio() async {
if (!(_playerIsInit ?? false)) {
throw Exception("播放器未初始化");
}
if (!_player!.isPlaying) {
throw Exception("播放器状态错误");
}
await _player!.pausePlayer();
return true;
}
Future<bool> resumeAudio() async {
if (!(_playerIsInit ?? false)) {
throw Exception("播放器未初始化");
}
if (!_player!.isPaused) {
throw Exception("播放器状态错误");
}
await _player!.resumePlayer();
return true;
}
Future<bool> seekAudio(int seek) async {
if (!(_playerIsInit ?? false)) {
throw Exception("播放器未初始化");
}
await _player!.seekToPlayer(Duration(seconds: seek));
return true;
}
Future<bool> stopAudio() async {
if (!(_playerIsInit ?? false)) {
throw Exception("播放器未初始化");
}
if (!_player!.isPlaying && !_player!.isPaused) {
throw Exception("播放器状态错误");
}
await _player!.stopPlayer();
return true;
}
Future<bool> clearAudio() async {
try {
await _player?.closePlayer();
_player = null;
await _playerSubscription?.cancel();
_playerSubscription = null;
} catch (e) {
print(e);
}
_playerIsInit = false;
_playId = '';
_playDuration = null;
return true;
}
///
/// 清理
///
Future<void> close() async {
try {
await _player?.closePlayer();
_player = null;
await _playerSubscription?.cancel();
_playerSubscription = null;
} catch (e) {
print(e);
}
_playerIsInit = false;
_playId = '';
_playDuration = null;
}
}
import 'dart:async';
import 'package:appframe/config/constant.dart';
import 'package:appframe/utils/audio_util.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';
class RecorderService {
FlutterSoundRecorder? _recorder;
StreamSubscription? _recorderSubscription;
bool? _recorderIsInit;
String? _recordPath;
Future<bool> _initRecorder(int maxDuration) async {
// 请求麦克风权限
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('no auth');
}
final directory = await getTemporaryDirectory();
String recordPath = '${directory.path}/${Uuid().v4()}_record.mp4';
// 打开录音器
try {
final recorder = FlutterSoundRecorder();
_recorder = (await recorder.openRecorder())!;
if (maxDuration > 0) {
// 设置进度回调间隔
await _recorder!.setSubscriptionDuration(Duration(seconds: 1));
// 监听录制进度
_recorderSubscription = _recorder!.onProgress!.listen((event) {
// event.duration 包含当前录制时长
// event.decibels 包含当前音量级别
print('录制进度: ${event.duration.inSeconds}秒, 音量: ${event.decibels}');
/*if (event.duration.inSeconds >= maxDuration) {
stopRecording();
}*/
});
}
_recorderIsInit = true;
_recordPath = recordPath;
return true;
} catch (e) {
throw Exception('打开录音器失败!');
}
}
Future<bool> startRecording(int maxDuration) async {
if (_recorderIsInit ?? false) {
return false;
}
if (_recorder != null && !_recorder!.isStopped) {
return false;
}
final initResult = await _initRecorder(maxDuration);
if (!initResult) {
return false;
}
await _recorder!.startRecorder(toFile: _recordPath, codec: Codec.aacMP4);
return true;
}
Future<bool> pauseRecording() async {
if (!(_recorderIsInit ?? false)) {
return false;
}
if (!_recorder!.isRecording) {
return false;
}
await _recorder!.pauseRecorder();
return true;
}
Future<bool> resumeRecording() async {
if (!(_recorderIsInit ?? false)) {
return false;
}
if (!_recorder!.isPaused) {
return false;
}
await _recorder!.resumeRecorder();
return true;
}
Future<Map<String, dynamic>> stopRecording() async {
if (!(_recorderIsInit ?? false)) {
throw Exception("录音器未初始化");
}
if (!_recorder!.isRecording && !_recorder!.isPaused) {
throw Exception("录音器状态错误");
}
var url = await _recorder!.stopRecorder();
await _recorder!.closeRecorder();
_recorder = null;
_recorderIsInit = false;
_recordPath = '';
if (url == null || url.isEmpty) {
throw Exception("录音失败");
}
var tempDir = await getTemporaryDirectory();
String fileName = path.basenameWithoutExtension(url);
String mp3Path = '${tempDir.path}/$fileName.mp3';
// var convertResult = await compute(AudioUtil.convertAacToMp3, {'accPath': url, 'mp3Path': mp3Path});
var convertResult = await AudioUtil.convertAacToMp3({'accPath': url, 'mp3Path': mp3Path});
if (!convertResult) {
throw Exception("录音转码失败");
}
// 时长
// var duration = await AudioUtil.getAudioDuration(mp3Path);
var duration = await AudioUtil.getAudioDuration(url);
return {
'tempFilePath': '${Constant.localServerTempFileUrl}$mp3Path',
'duration': duration.inSeconds,
};
}
Future<bool> clearRecording() async {
// await _recorder!.stopRecorder();
try {
await _recorder?.closeRecorder();
_recorder = null;
await _recorderSubscription?.cancel();
_recorderSubscription = null;
} catch (e) {
print(e);
}
_recorderIsInit = false;
_recordPath = '';
return true;
}
Future<void> close() async {
try {
await _recorder?.closeRecorder();
_recorder = null;
await _recorderSubscription?.cancel();
_recorderSubscription = null;
} catch (e) {
print(e);
}
_recorderIsInit = false;
_recordPath = '';
}
}
import 'package:dio/dio.dart';
class ParallelUploader {
final Dio dio = Dio();
final int maxConcurrentUploads;
ParallelUploader({this.maxConcurrentUploads = 3});
// 并行上传多个分片
Future<List<UploadResult>> uploadChunksParallel(
File file,
List<ChunkInfo> chunks,
String uploadUrl,
) async {
final results = <UploadResult>[];
final completed = <bool>[];
// 使用队列控制并发数量
for (int i = 0; i < chunks.length; i += maxConcurrentUploads) {
final currentBatch = chunks.sublist(
i,
i + maxConcurrentUploads > chunks.length ? chunks.length : i + maxConcurrentUploads
);
// 并行上传当前批次的分片
final batchFutures = currentBatch.map((chunk) {
return compute(_uploadSingleChunk, UploadTask(
filePath: file.path,
chunk: chunk,
uploadUrl: uploadUrl,
));
}).toList();
final batchResults = await Future.wait(batchFutures);
results.addAll(batchResults);
// 更新进度
_updateProgress(results.length, chunks.length);
}
return results;
}
void _updateProgress(int completed, int total) {
final progress = (completed / total * 100).toInt();
print('上传进度: $progress%');
}
}
// 上传任务数据类(必须是可序列化的)
class UploadTask {
final String filePath;
final ChunkInfo chunk;
final String uploadUrl;
UploadTask({
required this.filePath,
required this.chunk,
required this.uploadUrl,
});
}
// 上传结果类
class UploadResult {
final int chunkIndex;
final bool success;
final String? error;
final String? chunkId;
UploadResult({
required this.chunkIndex,
required this.success,
this.error,
this.chunkId,
});
}
// 在后台线程上传单个分片
Future<UploadResult> _uploadSingleChunk(UploadTask task) async {
try {
final file = File(task.filePath);
final chunk = task.chunk;
// 读取分片数据
final raf = file.openSync();
raf.setPositionSync(chunk.start);
final chunkData = raf.readSync(chunk.end - chunk.start);
raf.closeSync();
// 上传分片
final dio = Dio();
final formData = FormData.fromMap({
'file': MultipartFile.fromBytes(
chunkData,
filename: 'chunk-${chunk.index}'
),
'chunkIndex': chunk.index,
'totalChunks': chunk.totalChunks,
'chunkSize': chunk.end - chunk.start,
});
final response = await dio.post(
task.uploadUrl,
data: formData,
options: Options(
sendTimeout: Duration(seconds: 30),
receiveTimeout: Duration(seconds: 30),
),
);
return UploadResult(
chunkIndex: chunk.index,
success: response.statusCode == 200,
chunkId: response.data['chunkId'],
);
} catch (e) {
return UploadResult(
chunkIndex: task.chunk.index,
success: false,
error: e.toString(),
);
}
}
\ No newline at end of file \ No newline at end of file
class MultiThreadedFileUploader {
static const int CHUNK_SIZE = 1024 * 1024; // 1MB
static const int MAX_CONCURRENT_UPLOADS = 3;
final ParallelUploader _uploader = ParallelUploader(
maxConcurrentUploads: MAX_CONCURRENT_UPLOADS
);
// 主上传方法
Future<UploadSummary> uploadFile(
File file,
String uploadUrl,
String mergeUrl,
) async {
final stopwatch = Stopwatch()..start();
try {
print('开始准备分片...');
// 在后台线程计算分片
final chunks = await FileUploader.prepareChunks(file, CHUNK_SIZE);
print('文件分片完成,共 ${chunks.length} 个分片');
// 并行上传所有分片
print('开始并行上传分片...');
final results = await _uploader.uploadChunksParallel(
file, chunks, uploadUrl
);
// 检查上传结果
final failedChunks = results.where((r) => !r.success).toList();
if (failedChunks.isNotEmpty) {
throw Exception('部分分片上传失败: ${failedChunks.length}');
}
// 通知服务端合并文件
print('所有分片上传完成,开始合并...');
await _notifyMerge(file, chunks, mergeUrl);
stopwatch.stop();
return UploadSummary(
success: true,
totalChunks: chunks.length,
fileSize: await file.length(),
duration: stopwatch.elapsed,
);
} catch (e) {
stopwatch.stop();
return UploadSummary(
success: false,
error: e.toString(),
duration: stopwatch.elapsed,
);
}
}
Future<void> _notifyMerge(
File file,
List<ChunkInfo> chunks,
String mergeUrl
) async {
final dio = Dio();
await dio.post(mergeUrl, data: {
'fileName': file.path.split('/').last,
'totalChunks': chunks.length,
'fileSize': await file.length(),
});
}
}
class UploadSummary {
final bool success;
final int? totalChunks;
final int? fileSize;
final Duration duration;
final String? error;
UploadSummary({
required this.success,
this.totalChunks,
this.fileSize,
required this.duration,
this.error,
});
}
\ No newline at end of file \ No newline at end of file
class FileUploadScreen extends StatefulWidget {
@override
_FileUploadScreenState createState() => _FileUploadScreenState();
}
class _FileUploadScreenState extends State<FileUploadScreen> {
final _uploader = MultiThreadedFileUploader();
double _progress = 0.0;
bool _isUploading = false;
String _status = '准备就绪';
Future<void> _uploadFile() async {
setState(() {
_isUploading = true;
_progress = 0.0;
_status = '开始上传...';
});
// 选择文件
final file = await _pickFile();
if (file == null) return;
try {
final summary = await _uploader.uploadFile(
file,
'https://api.example.com/upload-chunk',
'https://api.example.com/merge-file',
);
setState(() {
_status = summary.success
? '上传成功! 耗时: ${summary.duration.inSeconds}秒'
: '上传失败: ${summary.error}';
_progress = 1.0;
});
} catch (e) {
setState(() {
_status = '上传异常: $e';
});
} finally {
setState(() {
_isUploading = false;
});
}
}
Future<File?> _pickFile() async {
// 实现文件选择逻辑
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('多线程文件上传')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LinearProgressIndicator(value: _progress),
SizedBox(height: 20),
Text(_status),
SizedBox(height: 20),
ElevatedButton(
onPressed: _isUploading ? null : _uploadFile,
child: Text(_isUploading ? '上传中...' : '选择文件并上传'),
),
],
),
),
);
}
}
\ No newline at end of file \ No newline at end of file
import 'package:flutter/foundation.dart';
import 'dart:io';
// 分片信息类
class ChunkInfo {
final int index;
final int start;
final int end;
final int totalChunks;
ChunkInfo(this.index, this.start, this.end, this.totalChunks);
}
// 在后台线程计算文件分片信息
List<ChunkInfo> calculateChunks(String filePath, int chunkSize) {
final file = File(filePath);
final fileSize = file.lengthSync();
final totalChunks = (fileSize / chunkSize).ceil();
final chunks = <ChunkInfo>[];
for (int i = 0; i < totalChunks; i++) {
final start = i * chunkSize;
final end = (i + 1) * chunkSize > fileSize ? fileSize : (i + 1) * chunkSize;
chunks.add(ChunkInfo(i, start, end, totalChunks));
}
return chunks;
}
class FileUploader {
static Future<List<ChunkInfo>> prepareChunks(File file, int chunkSize) async {
// 在后台线程计算分片信息,避免阻塞UI
return await compute(calculateChunks, file.path, chunkSize);
}
}
\ No newline at end of file \ No newline at end of file
import 'package:appframe/bloc/mqtt_cubit.dart'; import 'package:appframe/bloc/im_cubit.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class MqttPage extends StatelessWidget { class ImPage extends StatelessWidget {
const MqttPage({super.key}); const ImPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => MqttCubit(), create: (context) => ImCubit(),
child: BlocConsumer<MqttCubit, MqttState>( child: BlocConsumer<ImCubit, ImState>(
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('推送测试'), title: Text('消息测试'),
centerTitle: true, centerTitle: true,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: IconButton( leading: IconButton(
icon: Icon(Icons.arrow_back), icon: Icon(Icons.arrow_back),
onPressed: () { onPressed: () {
context.read<MqttCubit>().goWeb(); context.read<ImCubit>().goWeb();
}, },
), ),
), ),
......
...@@ -14,41 +14,61 @@ class LoginMainPage extends StatelessWidget { ...@@ -14,41 +14,61 @@ class LoginMainPage extends StatelessWidget {
create: (context) => LoginMainCubit(LoginMainState()), create: (context) => LoginMainCubit(LoginMainState()),
child: BlocConsumer<LoginMainCubit, LoginMainState>(builder: (context, state) { child: BlocConsumer<LoginMainCubit, LoginMainState>(builder: (context, state) {
var loginMainCubit = context.read<LoginMainCubit>(); var loginMainCubit = context.read<LoginMainCubit>();
return Scaffold( return Stack(
resizeToAvoidBottomInset: true, children: [
backgroundColor: Colors.white, Scaffold(
body: SafeArea( resizeToAvoidBottomInset: true,
top: false, backgroundColor: Colors.white,
child: SingleChildScrollView( body: SafeArea(
child: Column( top: false,
children: [ child: SingleChildScrollView(
LoginPageImageWidget(),
Transform.translate(
offset: Offset(0, -40), // 向上移动40像素
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
color: Colors.white, // 设置背景色
),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, children: [
crossAxisAlignment: CrossAxisAlignment.start, LoginPageImageWidget(),
children: [ Transform.translate(
SizedBox(height: 30), offset: Offset(0, -40), // 向上移动40像素
// _buildHeader(), child: Container(
LoginPageHeaderWidget(), decoration: BoxDecoration(
SizedBox(height: 25), borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
_buildLoginButtons(context, loginMainCubit, state.agreed), color: Colors.white, // 设置背景色
SizedBox(height: 20), ),
_buildAgreement(context, loginMainCubit, state.agreed), padding: const EdgeInsets.symmetric(horizontal: 24.0),
], child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 30),
LoginPageHeaderWidget(),
SizedBox(height: 25),
_buildLoginButtons(context, loginMainCubit, state.agreed),
SizedBox(height: 20),
_buildAgreement(context, loginMainCubit, state.agreed),
],
),
),
), ),
), ],
), )),
], ),
)), ),
), state.loading
? Container(
color: Colors.black54,
width: MediaQuery.of(context).size.width,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Color(0xFF7691FA),
),
],
),
),
)
: SizedBox(),
],
); );
}, listener: (context, state) { }, listener: (context, state) {
if (state.showAgreed) { if (state.showAgreed) {
...@@ -58,31 +78,6 @@ class LoginMainPage extends StatelessWidget { ...@@ -58,31 +78,6 @@ class LoginMainPage extends StatelessWidget {
); );
} }
Widget _buildHeader() {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'欢迎登录',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 6),
Text(
'班小二,班级群必备效率工具',
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
],
);
}
Widget _buildLoginButtons(BuildContext context, LoginMainCubit loginMainCubit, bool agreed) { Widget _buildLoginButtons(BuildContext context, LoginMainCubit loginMainCubit, bool agreed) {
return Column( return Column(
children: [ children: [
......
import 'package:appframe/bloc/wechat_auth_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class WechatAuthPage extends StatelessWidget {
const WechatAuthPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WechatAuthCubit(WechatAuthState()),
child: BlocConsumer<WechatAuthCubit, WechatAuthState>(
builder: (context, state) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
context.read<WechatAuthCubit>().goIndex();
},
child: Scaffold(
appBar: AppBar(title: Text('微信授权')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 200,
child: TextField(
controller: context.read<WechatAuthCubit>().textEditingController,
decoration: InputDecoration(hintText: '请输入UI端IP', border: OutlineInputBorder()),
),
),
SizedBox(height: 20),
Text(state.result ?? '点击拉取微信授权'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.read<WechatAuthCubit>().auth();
},
child: const Text('微信授权'),
),
],
),
),
),
);
},
listener: (context, state) {},
),
);
}
}
import 'dart:convert'; import 'dart:convert';
import 'package:ffmpeg_kit_flutter_new_audio/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new_audio/ffprobe_kit.dart'; import 'package:ffmpeg_kit_flutter_new/ffprobe_kit.dart';
import 'package:ffmpeg_kit_flutter_new_audio/return_code.dart'; import 'package:ffmpeg_kit_flutter_new/return_code.dart';
class AudioUtil { class AudioUtil {
/// 转码 /// 转码
......
import 'dart:io'; import 'dart:io';
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:video_compress/video_compress.dart';
/// 缩略图工具类 /// 缩略图工具类
/// ///
...@@ -28,28 +29,29 @@ class ThumbnailUtil { ...@@ -28,28 +29,29 @@ class ThumbnailUtil {
/// 返回缩略图路径 /// 返回缩略图路径
static Future<String?> genVideoThumbnail(String videoPath, Directory dir) async { static Future<String?> genVideoThumbnail(String videoPath, Directory dir) async {
try { try {
var fileThumbnail = await VideoCompress.getFileThumbnail(videoPath, quality: 50, position: -1); final thumbnailPath = '${dir.path}/video_thumb_${DateTime.now().millisecondsSinceEpoch}.jpg';
return fileThumbnail.path;
// 使用 ffmpeg_kit_flutter_new 生成视频缩略图
// 构建FFmpeg命令行参数
String cmd = '-i "$videoPath" ' // 指定输入文件路径
'-ss 1 ' // 从视频第1秒处截取画面
'-vframes 1 ' // 只截取一帧画面
'-vf scale=128:-1 ' // 设置缩略图宽度为128像素,高度按比例缩放
'-y ' // 覆盖已存在的输出文件
'"$thumbnailPath"'; // 指定输出文件路径
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
return thumbnailPath;
} else {
print('生成视频缩略图失败: ${await session.getFailStackTrace()}');
return null;
}
} catch (e) { } catch (e) {
print('生成视频缩略图出错: $e'); print('生成视频缩略图出错: $e');
return null; return null;
} }
// try {
// final thumbnailPath = '${dir.path}/video_thumb_${DateTime.now().millisecondsSinceEpoch}.jpg';
//
// final thumbPath = await VideoThumbnail.thumbnailFile(
// video: videoPath,
// thumbnailPath: thumbnailPath,
// imageFormat: ImageFormat.JPEG,
// maxWidth: 128, // 缩略图最大宽度
// quality: 75, // 图片质量
// );
//
// return thumbPath;
// } catch (e) {
// print('生成视频缩略图出错: $e');
// return null;
// }
} }
} }
import 'dart:io';
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
class VideoUtil {
///
/// 将视频格式转换为mp4
/// 转码的同时,进行压缩
///
static Future<bool> convertToMp4(String inputPath, String outputPath) async {
String cmd;
if (Platform.isIOS) {
cmd = '-i "$inputPath" '
'-c:v h264_videotoolbox ' // 启用 iOS 硬件加速
'-b:v 1500k ' // 限制视频码率为 1.5Mbps (体积小,手机看足够)
'-vf scale=1280:-2 ' // 缩放到 720p (保持比例)
'-c:a aac ' // 音频转为 AAC (兼容性最好)
'-b:a 128k ' // 音频码率
'"$outputPath"';
} else {
cmd = '-i "$inputPath" ' // 指定输入文件路径
'-c:v libx264 ' // 设置视频编码器为libx264(H.264)
'-crf 28 ' // 设置恒定速率因子CRF为28(中等压缩质量)
'-c:a aac ' // 设置音频编码器为AAC
'-b:a 128k ' // 设置音频比特率为128kbps
'-strict experimental ' // 允许使用实验性编解码器功能
'-movflags faststart ' // 优化MP4文件结构,使视频可以快速启动播放
'-f mp4 ' // 指定输出格式为MP4
'"$outputPath"'; // 指定输出文件路径
}
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode);
}
///
/// 通过 ffmpeg 压缩视频
///
static Future<bool> compressVideo(String inputPath, String outputPath, String quality) async {
// 使用CRF模式进行压缩,值范围0-51,建议值18-28
// 高质量: CRF 18-20
// 中等质量: CRF 23-26
// 低质量: CRF 28-32
int crf;
switch (quality) {
case 'low':
crf = 32;
break;
case 'middle':
crf = 26;
break;
case 'high':
crf = 20;
break;
default:
throw Exception('参数错误');
}
String cmd = '-i "$inputPath" ' // 输入文件
'-c:v libx264 ' // 视频编码器
'-crf $crf ' // 恒定速率因子(质量控制)
'-c:a aac ' // 音频编码器
'-b:a 128k ' // 音频比特率
'-preset medium ' // 编码预设
'-movflags faststart ' // 优化MP4文件结构
'"$outputPath"'; // 输出文件
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode);
}
}
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/services.dart';
class ZipUtil {
static Future<bool> extractZipFile(String zipFilePath, String extractPath) async {
try {
// 读取zip文件
Uint8List bytes;
if (zipFilePath.startsWith('assets/')) {
// 读取 assets 中的文件
final ByteData data = await rootBundle.load(zipFilePath);
bytes = data.buffer.asUint8List();
} else {
// 读取本地文件
final zipFile = File(zipFilePath);
bytes = await zipFile.readAsBytes();
}
// 解压ZIP文件
final archive = ZipDecoder().decodeBytes(bytes);
// 创建解压目录
final directory = Directory(extractPath);
if (!(await directory.exists())) {
await directory.create(recursive: true);
}
// 遍历并提取文件
for (final file in archive) {
final filename = file.name;
if (file.isFile) {
final data = file.content as List<int>;
final outputFile = File('$extractPath/$filename');
// 创建父目录
await outputFile.create(recursive: true);
// 写入文件内容
await outputFile.writeAsBytes(data);
} else {
// 创建目录
final dir = Directory('$extractPath/$filename');
await dir.create(recursive: true);
}
}
print('文件解压成功: $extractPath');
return true;
} catch (e) {
print('文件解压失败: $e');
return false;
}
}
}
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_localization/flutter_localization_plugin.h> #include <flutter_localization/flutter_localization_plugin.h>
#include <flutter_sound/flutter_sound_plugin.h>
#include <open_file_linux/open_file_linux_plugin.h> #include <open_file_linux/open_file_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
...@@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { ...@@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_localization_registrar = g_autoptr(FlPluginRegistrar) flutter_localization_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin");
flutter_localization_plugin_register_with_registrar(flutter_localization_registrar); flutter_localization_plugin_register_with_registrar(flutter_localization_registrar);
g_autoptr(FlPluginRegistrar) flutter_sound_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSoundPlugin");
flutter_sound_plugin_register_with_registrar(flutter_sound_registrar);
g_autoptr(FlPluginRegistrar) open_file_linux_registrar = g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_localization flutter_localization
flutter_sound
open_file_linux open_file_linux
url_launcher_linux url_launcher_linux
) )
......
...@@ -7,10 +7,11 @@ import Foundation ...@@ -7,10 +7,11 @@ import Foundation
import connectivity_plus import connectivity_plus
import device_info_plus import device_info_plus
import ffmpeg_kit_flutter_new_audio import ffmpeg_kit_flutter_new
import file_picker import file_picker
import flutter_image_compress_macos import flutter_image_compress_macos
import flutter_localization import flutter_localization
import flutter_sound
import geolocator_apple import geolocator_apple
import mobile_scanner import mobile_scanner
import network_info_plus import network_info_plus
...@@ -19,9 +20,8 @@ import package_info_plus ...@@ -19,9 +20,8 @@ import package_info_plus
import path_provider_foundation import path_provider_foundation
import photo_manager import photo_manager
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import tencent_cloud_chat_sdk
import url_launcher_macos import url_launcher_macos
import video_compress
import video_player_avfoundation import video_player_avfoundation
import webview_flutter_wkwebview import webview_flutter_wkwebview
...@@ -32,6 +32,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ...@@ -32,6 +32,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
FlutterSoundPlugin.register(with: registry.registrar(forPlugin: "FlutterSoundPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
...@@ -40,9 +41,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ...@@ -40,9 +41,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin")) PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) TencentCloudChatSdkPlugin.register(with: registry.registrar(forPlugin: "TencentCloudChatSdkPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
} }
PODS:
- connectivity_plus (0.0.1):
- FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- ffmpeg_kit_flutter_new_audio (7.1.1):
- ffmpeg_kit_flutter_new_audio/audio (= 7.1.1)
- FlutterMacOS
- ffmpeg_kit_flutter_new_audio/audio (7.1.1):
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- flutter_image_compress_macos (1.0.0):
- FlutterMacOS
- flutter_localization (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- network_info_plus (0.0.1):
- FlutterMacOS
- open_file_mac (0.0.1):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- photo_manager (3.7.1):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- video_compress (0.3.0):
- FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- ffmpeg_kit_flutter_new_audio (from `Flutter/ephemeral/.symlinks/plugins/ffmpeg_kit_flutter_new_audio/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`)
- flutter_localization (from `Flutter/ephemeral/.symlinks/plugins/flutter_localization/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`)
- network_info_plus (from `Flutter/ephemeral/.symlinks/plugins/network_info_plus/macos`)
- open_file_mac (from `Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- photo_manager (from `Flutter/ephemeral/.symlinks/plugins/photo_manager/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
EXTERNAL SOURCES:
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
ffmpeg_kit_flutter_new_audio:
:path: Flutter/ephemeral/.symlinks/plugins/ffmpeg_kit_flutter_new_audio/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
flutter_image_compress_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos
flutter_localization:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_localization/macos
FlutterMacOS:
:path: Flutter/ephemeral
geolocator_apple:
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin
network_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/network_info_plus/macos
open_file_mac:
:path: Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
photo_manager:
:path: Flutter/ephemeral/.symlinks/plugins/photo_manager/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_compress:
:path: Flutter/ephemeral/.symlinks/plugins/video_compress/macos
video_player_avfoundation:
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
webview_flutter_wkwebview:
:path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin
SPEC CHECKSUMS:
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
ffmpeg_kit_flutter_new_audio: d3c98512bcf22c530b56262ac7969a62f3dd60b6
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
flutter_image_compress_macos: e68daf54bb4bf2144c580fd4d151c949cbf492f0
flutter_localization: 3cda759884c7dab4466fe963d97fba723a492666
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
network_info_plus: 21d1cd6a015ccb2fdff06a1fbfa88d54b4e92f61
open_file_mac: 01874b6d6a2c1485ac9b126d7105b99102dea2cf
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
video_compress: 752b161da855df2492dd1a8fa899743cc8fe9534
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
COCOAPODS: 1.16.2
name: appframe name: appframe
description: "app frame project." description: "app frame project."
# The following line prevents the package from being accidentally published to publish_to: 'none'
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ">=3.5.0 <4.0.0" sdk: ">=3.5.0 <4.0.0"
# 1. 移除无效的 fluwx 配置块,改用原生配置
fluwx:
app_id: wx8c32ea248f0c7765
debug_logging: true # Logging in debug mode.
ios:
universal_link: https://dev.banxiaoer.net/path/to/wechat/
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations: # 确保存在这一行 flutter_localizations:
sdk: flutter sdk: flutter
# --- 核心工具类 ---
archive: ^4.0.7 archive: ^4.0.7
connectivity_plus: ^7.0.0 connectivity_plus: ^7.0.0
device_info_plus: ^11.5.0 device_info_plus: ^11.5.0
dio: ^5.9.0 dio: ^5.9.0
equatable: ^2.0.7 equatable: ^2.0.7
exif: ^3.3.0 get_it: ^8.2.0
ffmpeg_kit_flutter_new_audio: ^1.1.0 logger: ^2.6.2
path: ^1.9.1
path_provider: ^2.1.5
shared_preferences: ^2.5.3
uuid: ^4.5.1
# --- 路由与权限 ---
go_router: ^16.2.1
permission_handler: ^12.0.1
url_launcher: ^6.3.2
# --- UI 与 媒体 ---
file_picker: ^10.3.2 file_picker: ^10.3.2
flutter_bloc: ^9.1.1 flutter_bloc: ^9.1.1
flutter_localization: ^0.3.3 flutter_localization: ^0.3.3
flutter_image_compress: ^2.4.0 flutter_image_compress: ^2.4.0
flutter_sound: ^9.28.0
fluwx: ^5.7.2 # 建议:如果项目中不需要 image_picker(被 wechat_assets_picker 替代),则保持注释或删除
gallery_saver_plus: ^3.2.9 # image_picker: ^1.2.0
get_it: ^8.2.0
geolocator: ^14.0.2
go_router: ^16.2.1
# image_picker: ^1.2.0
image: ^4.5.4 image: ^4.5.4
image_size_getter: ^2.4.1 image_size_getter: ^2.4.1
json_annotation: ^4.9.0
mime: ^2.0.0 mime: ^2.0.0
mobile_scanner: ^7.0.1
mqtt_client: ^10.11.1 # --- 微信生态 ---
network_info_plus: ^7.0.0 fluwx: ^5.7.2
open_file: ^3.5.10 # 这里的 Asset picker 和 Camera picker 需要在 Info.plist 配置相册/相机权限描述
path: ^1.9.1
path_provider: ^2.1.5
permission_handler: ^12.0.1
shared_preferences: ^2.5.3
sqflite: ^2.4.2
# video_thumbnail: ^0.5.6
url_launcher: ^6.3.2
uuid: ^4.5.1
vibration: ^3.1.3
video_compress: ^3.1.4
video_player: ^2.10.0
webview_flutter: ^4.13.0
wechat_assets_picker: ^9.8.0 wechat_assets_picker: ^9.8.0
wechat_camera_picker: ^4.4.0 wechat_camera_picker: ^4.4.0
# The following adds the Cupertino Icons font to your application. # --- 音视频与直播 (重灾区) ---
# Use with the CupertinoIcons class for iOS style icons. # 确保 ffmpeg_kit 版本与你的架构兼容。
# 如果只是为了压缩视频,建议评估是否移除 video_compress,直接用 ffmpeg
ffmpeg_kit_flutter_new: ^4.1.0
# video_compress: ^3.1.4
video_player: ^2.10.0
# video_thumbnail 已被注释,确认是否需要生成缩略图,如果需要,ffmpeg_kit 也能做
flutter_sound: ^9.28.0
# --- 腾讯云服务 ---
tencent_cloud_chat_sdk: ^8.7.7201+2
tencent_cloud_chat_push: ^8.7.7201
# --- 其他硬件功能 ---
geolocator: ^14.0.2
mobile_scanner: ^7.0.1
vibration: ^3.1.3
webview_flutter: ^4.13.0
# --- 杂项 ---
exif: ^3.3.0
gallery_saver_plus: ^3.2.9
json_annotation: ^4.9.0
network_info_plus: ^7.0.0
open_file: ^3.5.10
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
dev_dependencies: dev_dependencies:
...@@ -89,56 +84,14 @@ dev_dependencies: ...@@ -89,56 +84,14 @@ dev_dependencies:
sdk: flutter sdk: flutter
build_runner: ^2.7.0 build_runner: ^2.7.0
json_serializable: ^6.11.0 json_serializable: ^6.11.0
flutter_launcher_icons: ^0.14.4
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets: assets:
- assets/ - assets/
- assets/dist.zip # 建议:不要直接引用 assets/ 根目录,这可能会导致打包进不必要的文件
# 最好精确到子文件夹
# - assets/dist.zip <-- 确认 zip 文件是否必须在运行时解压,这会增加包体积
- assets/images/login/ - assets/images/login/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
...@@ -8,8 +8,10 @@ ...@@ -8,8 +8,10 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <flutter_localization/flutter_localization_plugin_c_api.h> #include <flutter_localization/flutter_localization_plugin_c_api.h>
#include <flutter_sound/flutter_sound_plugin_c_api.h>
#include <geolocator_windows/geolocator_windows.h> #include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <tencent_cloud_chat_sdk/tencent_cloud_chat_sdk_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
...@@ -17,10 +19,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { ...@@ -17,10 +19,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FlutterLocalizationPluginCApiRegisterWithRegistrar( FlutterLocalizationPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi")); registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi"));
FlutterSoundPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSoundPluginCApi"));
GeolocatorWindowsRegisterWithRegistrar( GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows")); registry->GetRegistrarForPlugin("GeolocatorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
TencentCloudChatSdkPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TencentCloudChatSdkPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus connectivity_plus
flutter_localization flutter_localization
flutter_sound
geolocator_windows geolocator_windows
permission_handler_windows permission_handler_windows
tencent_cloud_chat_sdk
url_launcher_windows url_launcher_windows
) )
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!