Commit 4c4434af by Administrator

Merge branch 'feature-2511-opt-update-test' into feature-2511-opt

1、验证ios的编译问题

 Please enter a commit message to explain why this merge is necessary,
2 parents e76f21a1 81b73f5b
......@@ -11,6 +11,10 @@ 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
......@@ -62,6 +66,21 @@ pod cache clean --all
# 注意:这一步可能比较慢,因为 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'
......@@ -26,12 +26,17 @@ end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
# 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
target 'Runner' do
......@@ -55,6 +60,11 @@ end
post_install do |installer|
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)
# 解决有些库构建版本不一致的问题
......
......@@ -11,7 +11,7 @@
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>cn.banxe.appframe</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
......
......@@ -2,29 +2,38 @@ 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:appframe/services/im_service.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluwx/fluwx.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LoginMainState extends Equatable {
final bool agreed;
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
List<Object?> get props => [agreed, showAgreed];
List<Object?> get props => [
agreed,
showAgreed,
loading,
];
LoginMainState copyWith({
bool? agreed,
bool? showAgreed,
bool? loading,
}) {
return LoginMainState(
agreed: agreed ?? this.agreed,
showAgreed: showAgreed ?? this.showAgreed,
loading: loading ?? this.loading,
);
}
}
......@@ -32,13 +41,11 @@ class LoginMainState extends Equatable {
class LoginMainCubit extends Cubit<LoginMainState> {
late final Fluwx _fluwx;
late final WechatAuthRepository _wechatAuthRepository;
late final ImService _imService;
LoginMainCubit(super.initialState) {
_fluwx = getIt.get<Fluwx>();
_fluwx.addSubscriber(_responseListener);
_wechatAuthRepository = getIt<WechatAuthRepository>();
_imService = getIt.get<ImService>();
}
void toggleAgreed(bool value) {
......@@ -67,6 +74,9 @@ class LoginMainCubit extends Cubit<LoginMainState> {
if (!result) {
throw Exception('微信授权处理失败');
}
// 控制显示加载框
emit(state.copyWith(loading: true));
}
void goLoginPhone() {
......@@ -76,6 +86,7 @@ class LoginMainCubit extends Cubit<LoginMainState> {
void _responseListener(WeChatResponse response) async {
if (response is WeChatAuthResponse) {
if (response.code == null || response.code == '') {
emit(state.copyWith(loading: false));
return;
}
......@@ -96,23 +107,10 @@ class LoginMainCubit extends Cubit<LoginMainState> {
sharedPreferences.setString('auth_classCode', classCode);
sharedPreferences.setInt('auth_userType', userType);
sharedPreferences.setString('auth_stuId', stuId ?? '');
sharedPreferences.setString('auth_ip', Constant.h5Server);
if(Constant.needIM){
// IM登录, 正式使用时,需要从服务端获取用户签名
var loginResult = await _imService.login(userCode);
if (loginResult) {
print("微信登录处,IM 登录成功");
await _imService.registerPush();
} else {
print("微信登录处,IM 登录失败");
}
}
router.go(
'/web',
extra: {
'ip': Constant.h5Server,
'sessionCode': sessionCode,
'userCode': userCode,
'classCode': classCode,
......
......@@ -10,42 +10,39 @@ 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:appframe/utils/zip_util.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluwx/fluwx.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
class WebState extends Equatable {
final int selectedIndex;
final bool loaded;
final bool isUpgrading;
final bool suggestUpgrade;
final String title;
final int titleColor;
final int bgColor;
final String opIcon;
final bool showBottomNavBar;
final String? ip;
final String? sessionCode;
final String? userCode;
final String? classCode;
final int? userType;
final String? stuId;
/// 录音
// final bool recorderIsInit;
//final int recordState;
// final String recordPath;
/// 播放
// final bool playerIsInit;
//final int playState;
// final String playId;
/// getOrientationCmd
final bool orientationCmdFlag;
final String orientationCmdMessage;
......@@ -65,23 +62,18 @@ class WebState extends Equatable {
const WebState({
this.selectedIndex = 0,
this.loaded = false,
this.title = '界面加载中...',
this.isUpgrading = false,
this.suggestUpgrade = false,
this.title = '',
this.titleColor = 0xFFFFFFFF,
this.bgColor = 0xFF7691FA,
this.opIcon = 'none',
this.showBottomNavBar = false,
this.ip,
this.sessionCode,
this.userCode,
this.classCode,
this.userType,
this.stuId,
// this.recorderIsInit = false,
// this.recordState = 0,
// this.recordPath = '',
// this.playerIsInit = false,
// this.playState = 0,
// this.playId = '',
this.orientationCmdFlag = false,
this.orientationCmdMessage = '',
this.windowInfoCmdFlag = false,
......@@ -95,24 +87,19 @@ class WebState extends Equatable {
WebState copyWith({
int? selectedIndex,
bool? loaded,
bool? isUpgrading,
bool? suggestUpgrade,
String? title,
int? titleColor,
int? bgColor,
String? opIcon,
bool? showNavBar,
bool? showBottomNavBar,
String? ip,
String? sessionCode,
String? userCode,
String? classCode,
int? userType,
String? stuId,
// bool? recorderIsInit,
// int? recordState,
// String? recordPath,
// bool? playerIsInit,
// int? playState,
// String? playId,
bool? orientationCmdFlag,
String? orientationCmdMessage,
bool? windowInfoCmdFlag,
......@@ -125,23 +112,18 @@ class WebState extends Equatable {
return WebState(
selectedIndex: selectedIndex ?? this.selectedIndex,
loaded: loaded ?? this.loaded,
isUpgrading: isUpgrading ?? this.isUpgrading,
suggestUpgrade: suggestUpgrade ?? this.suggestUpgrade,
title: title ?? this.title,
titleColor: titleColor ?? this.titleColor,
bgColor: bgColor ?? this.bgColor,
opIcon: opIcon ?? this.opIcon,
showBottomNavBar: showBottomNavBar ?? this.showBottomNavBar,
ip: ip ?? this.ip,
sessionCode: sessionCode ?? this.sessionCode,
userCode: userCode ?? this.userCode,
classCode: classCode ?? this.classCode,
userType: userType ?? this.userType,
stuId: stuId ?? this.stuId,
// recorderIsInit: recorderIsInit ?? this.recorderIsInit,
// recordState: recordState ?? this.recordState,
// recordPath: recordPath ?? this.recordPath,
// playerIsInit: playerIsInit ?? this.playerIsInit,
// playState: playState ?? this.playState,
// playId: playId ?? this.playId,
orientationCmdFlag: orientationCmdFlag ?? this.orientationCmdFlag,
orientationCmdMessage: orientationCmdMessage ?? this.orientationCmdMessage,
windowInfoCmdFlag: windowInfoCmdFlag ?? this.windowInfoCmdFlag,
......@@ -157,23 +139,18 @@ class WebState extends Equatable {
List<Object?> get props => [
selectedIndex,
loaded,
isUpgrading,
suggestUpgrade,
title,
titleColor,
bgColor,
opIcon,
showBottomNavBar,
ip,
sessionCode,
userCode,
classCode,
userType,
stuId,
// recorderIsInit,
// recordState,
// recordPath,
// playerIsInit,
// playState,
// playId,
orientationCmdFlag,
orientationCmdMessage,
windowInfoCmdFlag,
......@@ -188,102 +165,204 @@ class WebState extends Equatable {
class WebCubit extends Cubit<WebState> {
late final MessageDispatcher _dispatcher;
late final WebViewController _controller;
late final HttpServer _server;
late final Fluwx _fluwx;
late final PlayerService _playerService;
late final PlayerService _recorderService;
// FlutterSoundRecorder? _recorder;
// StreamSubscription? _recorderSubscription;
// FlutterSoundPlayer? _player;
// StreamSubscription? _playerSubscription;
// int? _playDuration;
HttpServer? _server;
PlayerService? _playerService;
RecorderService? _recorderService;
WebViewController get controller => _controller;
// FlutterSoundRecorder? get recorder => _recorder;
WebCubit(super.initialState) {
// 没有登录数据,跳转到登录页面
if (state.sessionCode == null || state.sessionCode == '') {
WidgetsBinding.instance.addPostFrameCallback((_) {
router.go('/loginMain');
});
} else {
_init();
}
}
// FlutterSoundPlayer? get player => _player;
Future<void> _init() async {
try {
// 获取版本信息
var versionConfig = await _getVersionConfig();
var correctVersion = versionConfig['version'] as String;
var downloadUrl = versionConfig['zip'] as String;
var force = versionConfig['force'] as String;
// 当前使用的H5版本
var curVersion = getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version;
// 版本不一致则需要升级
if (curVersion != correctVersion) {
if (force == "1") {
// 一直等待升级完成
// 遮罩界面
emit(state.copyWith(isUpgrading: true));
await _upgrade(correctVersion, downloadUrl);
// 升级完成后取消遮罩,继续初始化其它数据
emit(state.copyWith(isUpgrading: false));
} else {
// 后台下载,完成后提示用户
_upgrade(correctVersion, downloadUrl).then(
(value) {
emit(state.copyWith(suggestUpgrade: true));
},
);
}
}
} catch (e) {
print('升级检测处理失败');
print(e);
}
WebCubit(super.initialState) {
// 消息处理器
_dispatcher = MessageDispatcher();
// WebView控制器
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onUrlChange: (UrlChange url) {},
onPageStarted: (String url) async {
// 进行新页面加载时,关闭录音器和播放器,(如果有打开过)
// closeLocalRecorder();
// closeLocalPlayer();
await _playerService.close();
await _recorderService.close();
},
onPageFinished: (String url) async {
print('onPageFinished--------------------------------->');
print(url);
if (url == '${Constant.localServerTestFileUrl}/login.html') {
return;
}
// 页面加载完成时,清空录音和音频(如果有打开过)
// await clearRecording();
// await clearAudio();
_controller.runJavaScript(
'document.querySelector("meta[name=viewport]").setAttribute("content", "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no")',
);
finishLoading();
},
),
)
..addJavaScriptChannel("xeJsBridge", onMessageReceived: _onMessageReceived);
// 启动本地服务器
await _startLocalServer();
// 启动本地服务器,并加载HTML
_startLocalServerAndLoadHtml();
// 创建WebView控制器
await _createWebViewController();
// 加载H5页面
_loadHtml();
// 登录IM
_loginIM();
// 初始化其它一些属性
_fluwx = getIt.get<Fluwx>();
_playerService = getIt.get<PlayerService>();
_playerService.sendResponse = _sendResponse;
_recorderService = getIt.get<PlayerService>();
_playerService?.sendResponse = _sendResponse;
_recorderService = getIt.get<RecorderService>();
}
void _startLocalServerAndLoadHtml() async {
// 启动本地服务器
_server = await getIt.get<LocalServerService>().startLocalServer();
Future<Map<String, String>> _getVersionConfig() async {
Dio dio = Dio();
try {
Response response = await dio.get(
'${Constant.configUrl}?t=${DateTime.now().millisecondsSinceEpoch}',
options: Options(responseType: ResponseType.json),
);
if (response.statusCode != 200) {
throw Exception('获取版本信息失败');
}
final String serverUrl;
if (state.sessionCode == null || state.sessionCode == '') {
// serverUrl = '${Constant.localServerUrl}/index.html';
serverUrl = '${Constant.localServerTestFileUrl}/login.html';
// serverUrl = '${Constant.localServerTestFileUrl}/test2.html';
} else {
// serverUrl =
// 'http://${state.ip}:${_server.port}/index.html#/h5/login/pages/applogin?sessionCode=${state.sessionCode}&userCode=${state.userCode}&classCode=${state.classCode}&userType=${state.userType}&stuId=${state.stuId}';
// IM 登录
if (Constant.needIM) {
var imService = getIt.get<ImService>();
var loginResult = await imService.login(state.userCode!);
if (loginResult) {
print("缓存自动登录处,IM 登录成功");
await imService.registerPush();
} else {
print("缓存自动登录处,IM 登录失败");
}
String version = response.data['version'] as String;
String force = response.data['force'] as String;
String zip = response.data['zip'] as String;
return {
'version': version,
'force': force,
// 'zip': 'http://192.168.2.177/1.0.0.zip',
'zip': '$zip$version.zip',
};
} finally {
dio.close(force: true);
}
}
Future<void> _upgrade(String version, String zipUrl) async {
Dio dio = Dio();
try {
// 下载zip文件
var tempDir = await getTemporaryDirectory();
var tempFilePath = '${tempDir.path}/${Uuid().v4()}.zip';
Response response = await dio.download(zipUrl, tempFilePath);
if (response.statusCode != 200) {
throw Exception('文件下载失败');
}
serverUrl =
'${Constant.localServerUrl}/index.html#/h5/login/pages/applogin?sessionCode=${state.sessionCode}&userCode=${state.userCode}&classCode=${state.classCode}&userType=${state.userType}&stuId=${state.stuId}';
var dir = await getApplicationSupportDirectory();
String httpDirPath = '${dir.path}/${Constant.h5DistDir}';
var httpDir = Directory(httpDirPath);
if (!httpDir.existsSync()) {
await httpDir.create(recursive: true);
}
var tempZipFile = File(tempFilePath);
var saveZipFilePath = '$httpDirPath/$version.zip';
// 复制zip文件,保留备用
await tempZipFile.copy(saveZipFilePath);
// 删除临时文件
await tempZipFile.delete();
// 解压zip文件
String targetDir = '$httpDirPath/$version';
var result = await ZipUtil.extractZipFile(saveZipFilePath, targetDir);
if (!result) {
throw Exception('文件解压失败');
}
var sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setString('h5_version', version);
} finally {
dio.close(force: true);
}
}
Future<void> _startLocalServer() async {
// 启动本地服务器
_server = await getIt.get<LocalServerService>().startLocalServer();
}
Future<void> _createWebViewController() async {
_controller = WebViewController();
await _controller.setJavaScriptMode(JavaScriptMode.unrestricted);
await _controller.setNavigationDelegate(
NavigationDelegate(
onUrlChange: (UrlChange url) {},
onPageStarted: (String url) async {
// 进行新页面加载时,关闭录音器和播放器,(如果有打开过)
await _playerService?.close();
await _recorderService?.close();
},
onPageFinished: (String url) async {
print('onPageFinished--------------------------------->');
print(url);
_controller.runJavaScript(
'document.querySelector("meta[name=viewport]").setAttribute("content", "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no")',
);
finishLoading();
},
),
);
await _controller.addJavaScriptChannel("xeJsBridge", onMessageReceived: _onMessageReceived);
}
void _loadHtml() {
// 构造函数中已拦截判断未登录的情况进行了处理,所以这里不再处理未登录的情况
final String serverUrl = '${Constant.localServerUrl}/index.html'
'#/h5/login/pages/applogin?'
'sessionCode=${state.sessionCode}&'
'userCode=${state.userCode}&'
'classCode=${state.classCode}&'
'userType=${state.userType}&'
'stuId=${state.stuId}';
// final String serverUrl = '${Constant.localServerUrl}/test/test2.html';
_controller.loadRequest(Uri.parse(serverUrl));
}
Future<void> _loginIM() async {
if (Constant.needIM) {
var imService = getIt.get<ImService>();
var loginResult = await imService.login(state.userCode!);
if (loginResult) {
print("缓存自动登录处,IM 登录成功");
await imService.registerPush();
} else {
print("缓存自动登录处,IM 登录失败");
}
}
}
void _onMessageReceived(JavaScriptMessage message) async {
try {
_dispatcher.dispatch(message.message, (response) {
......@@ -303,18 +382,7 @@ class WebCubit extends Cubit<WebState> {
}
void finishLoading() {
// emit(state.copyWith(loaded: true, title: '班小二测试', opIcon: 'none'));
emit(state.copyWith(loaded: true, title: '班小二测试'));
}
// 测试
void resetLoading() {
emit(state.copyWith(loaded: false, title: '界面加载中...'));
}
//测试
void goWechatAuth() {
router.go('/wechatAuth');
emit(state.copyWith(loaded: true));
}
void goLogin() {
......@@ -327,9 +395,7 @@ class WebCubit extends Cubit<WebState> {
//测试
void goAuth() {
// String serverUrl = 'http://${state.ip}:${_server.port}/index.html';
String serverUrl = '${Constant.localServerUrl}/index.html';
// String serverUrl = 'http://localdev.banxiaoer.net';
_controller.loadRequest(Uri.parse(serverUrl));
}
......@@ -407,11 +473,57 @@ class WebCubit extends Cubit<WebState> {
}
Future<void> clearStorage() async {
await getIt.get<SharedPreferences>().clear();
// 1 清理非 h5_version 的缓存
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.getKeys().forEach((key) {
if (!key.startsWith('h5_version')) {
sharedPreferences.remove(key);
}
});
// 2 清理 http_dist_assets 下的非当前版本号的文件和目录
var dir = await getApplicationSupportDirectory();
var httpDir = Directory('${dir.path}/${Constant.h5DistDir}');
if (httpDir.existsSync()) {
var version = sharedPreferences.getString('h5_version') ?? Constant.h5Version;
await for (final FileSystemEntity entity in httpDir.list()) {
if (entity is Directory) {
// 删除目录
if (!entity.path.endsWith(version)) {
await entity.delete(recursive: true);
}
} else if (entity is File) {
// 删除文件
if (!entity.path.endsWith('$version.zip')) {
await entity.delete();
}
}
}
}
// 3 清理临时目录下的所有文件和目录
var tempDir = await getTemporaryDirectory();
if (tempDir.existsSync()) {
await for (final FileSystemEntity entity in tempDir.list()) {
if (entity is Directory) {
await entity.delete(recursive: true);
} else {
await entity.delete();
}
}
}
}
Future<void> logout() async {
await clearStorage();
// 删除所有auth_开头的key
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.getKeys().forEach((key) {
if (key.startsWith('auth_')) {
sharedPreferences.remove(key);
}
});
// IM 登出
await getIt.get<ImService>().logout();
......@@ -431,6 +543,41 @@ class WebCubit extends Cubit<WebState> {
}
///
/// 升级提示
///
void suggestUpgrade(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('温馨提示'),
content: Text('资源已更新为最新版本,是否现在进行加载?'),
actions: <Widget>[
TextButton(
child: Text('取消'),
onPressed: () {
emit(state.copyWith(suggestUpgrade: false));
Navigator.of(context).pop();
},
),
TextButton(
child: Text('确定'),
onPressed: () {
emit(state.copyWith(suggestUpgrade: false));
getIt.get<LocalServerService>().resetHttpDirectory();
_controller.reload();
// _loadHtml();
Navigator.of(context).pop();
},
),
],
);
},
);
}
///
///
///
void setChooseImageCmdFlag(bool chooseImageCmdFlag, String chooseImageCmdMessage) {
......@@ -739,353 +886,13 @@ class WebCubit extends Cubit<WebState> {
_sendResponse(resp);
}
/// 录音初始化
// Future<bool> _initRecorder(int maxDuration) async {
// // 请求麦克风权限
// var status = await Permission.microphone.request();
// if (status != PermissionStatus.granted) {
// throw RecordingPermissionException('no auth');
// }
//
// // if (state.recordState != 0) {
// // return false;
// // }
//
// final directory = await getTemporaryDirectory();
// // String recordPath = '${directory.path}/${Uuid().v5(Namespace.url.value, 'www.banxiaoer.com')}_record.aac';
// // String recordPath = '${directory.path}/${Uuid().v4()}_record.aac';
// 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));
// // 监听录制进度
// _recorder!.onProgress!.listen((event) {
// // event.duration 包含当前录制时长
// // event.decibels 包含当前音量级别
// print('录制进度: ${event.duration.inSeconds}秒, 音量: ${event.decibels}');
// /*if (event.duration.inSeconds >= maxDuration) {
// stopRecording();
// }*/
// });
// }
//
// emit(state.copyWith(recorderIsInit: true, recordPath: recordPath));
// return true;
// } catch (e) {
// throw Exception('打开录音器失败!');
// }
// }
//
// /// 开始录音
// Future<bool> startRecording(int maxDuration) async {
// if (state.recorderIsInit) {
// return false;
// }
//
// // if (state.recordState != 0) {
// // return false;
// // }
//
// if (_recorder != null && !_recorder!.isStopped) {
// return false;
// }
//
// final initResult = await _initRecorder(maxDuration);
// if (!initResult) {
// return false;
// }
//
// await _recorder!.startRecorder(toFile: state.recordPath, codec: Codec.aacMP4);
// // emit(state.copyWith(recordState: 1));
// return true;
// }
//
// /// 暂停录音
// Future<bool> pauseRecording() async {
// if (!state.recorderIsInit) {
// return false;
// }
//
// // if (state.recordState != 1) {
// // return false;
// // }
//
// if (!_recorder!.isRecording) {
// return false;
// }
//
// await _recorder!.pauseRecorder();
// // emit(state.copyWith(recordState: 2));
// return true;
// }
//
// /// 恢复录音
// Future<bool> resumeRecording() async {
// if (!state.recorderIsInit) {
// return false;
// }
//
// // if (state.recordState != 2) {
// // return false;
// // }
//
// if (!_recorder!.isPaused) {
// return false;
// }
//
// await _recorder!.resumeRecorder();
// // emit(state.copyWith(recordState: 1));
// return true;
// }
//
// /// 停止录音
// Future<Map<String, dynamic>> stopRecording() async {
// if (!state.recorderIsInit) {
// throw Exception("录音器未初始化");
// }
//
// // if (state.recordState != 1 && state.recordState != 2) {
// // throw Exception("录音器状态错误");
// // }
//
// if (!_recorder!.isRecording && !_recorder!.isPaused) {
// throw Exception("录音器状态错误");
// }
//
// var url = await _recorder!.stopRecorder();
// await _recorder!.closeRecorder();
// _recorder = null;
// // emit(state.copyWith(recorderIsInit: false, recordState: 0, recordPath: ''));
// emit(state.copyWith(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;
// } catch (e) {
// print(e);
// }
// // emit(state.copyWith(recorderIsInit: false, recordState: 0, recordPath: ''));
// emit(state.copyWith(recorderIsInit: false, recordPath: ''));
// return true;
// }
/// 播放初始化
// Future<bool> _initPlayer(String playId) async {
// // 打开播放器
// try {
// final player = FlutterSoundPlayer();
// _player = (await player.openPlayer())!;
// _player!.setSpeed(2); // 播放速度,默认1
//
// // 播放进度回调
// _player!.setSubscriptionDuration(Duration(seconds: 1));
// _playerSubscription = _player!.onProgress!.listen((event) {
// print('播放回调--------- ${event.duration.inSeconds} ${event.position.inSeconds}');
//
// _playDuration = event.duration.inSeconds;
// var playPosition = event.position.inSeconds;
//
// var data = {'playId': state.playId, 'duration': _playDuration, 'currentTime': playPosition};
// var h5Cmd = {
// 'unique': '',
// 'cmd': 'audioProgress',
// 'data': data,
// 'errMsg': '',
// };
// _sendResponse(h5Cmd);
// });
//
// emit(state.copyWith(playerIsInit: true, playId: playId));
// return true;
// } catch (e) {
// throw Exception('打开播放器失败!');
// }
// }
//
// /// 播放音频
// Future<bool> playAudio(String url, int seek, String playId) async {
// if (!state.playerIsInit) {
// final initResult = await _initPlayer(playId);
// if (!initResult) {
// return false;
// }
// }
//
// await _player!.startPlayer(
// fromURI: url,
// whenFinished: () async {
// await _player!.stopPlayer();
// // emit(state.copyWith(playState: 0));
//
// // 补发一下全部进度
// var h5Cmd = {
// 'unique': '',
// 'cmd': 'audioProgress',
// 'data': {'playId': state.playId, 'duration': _playDuration, 'currentTime': _playDuration},
// 'errMsg': '',
// };
// _sendResponse(h5Cmd);
//
// // 播放结束后,发送消息给客户端
// h5Cmd = {
// 'unique': '',
// 'cmd': 'audioEnd',
// 'data': {'playId': state.playId},
// 'errMsg': '',
// };
// _sendResponse(h5Cmd);
// },
// );
// if (seek != 0) {
// await seekAudio(seek);
// }
// // emit(state.copyWith(playState: 1));
// return true;
// }
//
// /// 暂停播放
// Future<bool> pauseAudio() async {
// if (!state.playerIsInit) {
// throw Exception("播放器未初始化");
// }
//
// // if (state.playState != 1) {
// // throw Exception("播放器状态错误");
// // }
//
// if (!_player!.isPlaying) {
// throw Exception("播放器状态错误");
// }
//
// await _player!.pausePlayer();
// // emit(state.copyWith(playState: 2));
// return true;
// }
//
// /// 恢复播放
// Future<bool> resumeAudio() async {
// if (!state.playerIsInit) {
// throw Exception("播放器未初始化");
// }
//
// // if (state.playState != 2) {
// // throw Exception("播放器状态错误");
// // }
//
// if (!_player!.isPaused) {
// throw Exception("播放器状态错误");
// }
//
// await _player!.resumePlayer();
// // emit(state.copyWith(playState: 1));
// return true;
// }
//
// /// 跳转播放
// Future<bool> seekAudio(int seek) async {
// if (!state.playerIsInit) {
// throw Exception("播放器未初始化");
// }
//
// await _player!.seekToPlayer(Duration(seconds: seek));
// // emit(state.copyWith(playState: 1));
// return true;
// }
//
// /// 停止播放
// Future<bool> stopAudio() async {
// if (!state.playerIsInit) {
// throw Exception("播放器未初始化");
// }
//
// // if (state.playState != 1 && state.playState != 2) {
// // throw Exception("播放器状态错误");
// // }
//
// if (!_player!.isPlaying && !_player!.isPaused) {
// throw Exception("播放器状态错误");
// }
//
// await _player!.stopPlayer();
// // emit(state.copyWith(playState: 0));
// return true;
// }
//
// /// 清空播放
// Future<bool> clearAudio() async {
// try {
// await _player?.closePlayer();
// _player = null;
// await _playerSubscription?.cancel();
// _playerSubscription = null;
// _playDuration = null;
// } catch (e) {
// print(e);
// }
// // emit(state.copyWith(playerIsInit: false, playState: 0, playId: ''));
// emit(state.copyWith(playerIsInit: false, playId: ''));
// return true;
// }
@override
Future<void> close() async {
_server.close();
_server?.close();
// _fluwx.removeSubscriber(_responseListener);
// closeLocalRecorder();
// closeLocalPlayer();
await _playerService.close();
await _recorderService.close();
// try {
// await _recorder?.closeRecorder();
// _recorder = null;
// } catch (e) {
// print(e);
// }
// try {
// await _player?.closePlayer();
// _player = null;
// await _playerSubscription?.cancel();
// _playerSubscription = null;
// _playDuration = null;
// } catch (e) {
// print(e);
// }
await _playerService?.close();
await _recorderService?.close();
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();
}
}
......@@ -2,8 +2,9 @@ class Constant {
/// 应用内部 http 服务
static const int localServerPort = 35982;
static const String localServerHost = '127.0.0.1';
// static const String localServerHost = 'appdev-xj.banxiaoer.net';
static const String localServerHost = '127.0.0.1';
static const String localServerUrl = 'http://$localServerHost:$localServerPort';
static const String localFileUrl = 'http://127.0.0.1:$localServerPort';
......@@ -19,16 +20,16 @@ class Constant {
static const String appVersion = '1.0.0';
static const String h5Version = '1.0.0';
/// 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';
/// 测试阶段使用的 h5 服务地址
static const String h5Server = 'appdev-xj.banxiaoer.net';
// static const String h5Server = 'appdev-th.banxiaoer.net';
// static const String h5Server = '192.168.1.136';
/// 测试阶段使用
static const bool needIM = true;
}
......@@ -22,6 +22,7 @@ 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_to_album_handler.dart';
import 'package:appframe/data/repositories/message/scan_code_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/title_bar_handler.dart';
import 'package:appframe/data/repositories/message/upload_file.dart';
......@@ -67,6 +68,9 @@ Future<void> setupLocator() async {
/// 打开小程序
getIt.registerLazySingleton<MessageHandler>(() => OpenWeappHandler(), instanceName: 'openWeapp');
/// 分享微信会话
getIt.registerLazySingleton<MessageHandler>(() => ShareToWxHandler(), instanceName: 'sharetowx');
/// 设备信息
getIt.registerLazySingleton<MessageHandler>(() => DeviceInfoHandler(), instanceName: 'getDeviceInfo');
......@@ -169,7 +173,6 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<MessageHandler>(() => DownloadFileHandler(), instanceName: 'downloadFile');
/// 设置标题和返回按钮
// getIt.registerLazySingleton<MessageHandler>(() => SetTitleHandler(), instanceName: 'setTitle');
getIt.registerLazySingleton<MessageHandler>(() => TitleBarHandler(), instanceName: 'setTitlebar');
/// 新路由打开链接
......
import 'package:appframe/config/locator.dart';
/// 此处暂时测试
/// 正常需要在登录状态下,查询host和jwt
Future<void> registerMqtt() async {
// String mqttHost = '58.87.99.45';
// int mqttPort = 1883;
// // 获取 mac 地址
// String mqttMac = '';
// String mqttClientId = 'tanghuan_phone';
// int keepAlive = 60;
// String mqttUsername = 'user';
// String mqttPassword =
// 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjI2NTgzOTEsInJzIjoib2sifQ.AoU0MfSKflC1VB0abi7BVr3g4MDah1uLlg01ZTFQgTxfolu28IfZ4BaGhRF9qy7yAQH2Efmdf2cs2iwKrdcHRSHzJhwTC44beX6viRhCCiCxe51AB8NVv72l2TmsNxIvACfXOhDLjKH6QE38EaKC486aS_L-QpakvDOQP_IPjq5ZvH68JwwhOwhLTgaCgOR3xde2H-NgRDK2BQ-FyDTXi1RX8hDGvKMw8pi6WiVBjR1ENTO5A7yvMioJS9qwdjs_7_5c6n5GXSjCHTtdQ7746hlId2uwP_41G5Ug3DYWiZ5aWIuvGRH6ZxKmbC32wN62ys_XkLGzhBw8wsQ-KhETvQ ';
//
// /// 初始化MQTT客户端
// var mqttService = MqttService(mqttHost, mqttPort, mqttClientId, keepAlive, mqttUsername, mqttPassword);
// await mqttService.initConn();
//
// /// 设置到getIt,用于获取使用
// getIt.registerSingleton(mqttService);
// MqttIsolateManager mqttIsolateManager = MqttIsolateManager();
// mqttIsolateManager.start();
// await mqttIsolateManager.connect('server', 'clientId');
// // 暂停3秒
// // await Future.delayed(Duration(seconds: 2));
// mqttIsolateManager.subscribe('bxe/abc');
//
// getIt.registerSingleton(mqttIsolateManager);
}
......@@ -5,7 +5,6 @@ import 'package:appframe/ui/pages/login_main_page.dart';
import 'package:appframe/ui/pages/login_phone_page.dart';
import 'package:appframe/ui/pages/scan_code_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:go_router/go_router.dart';
......@@ -19,12 +18,6 @@ final GoRouter router = GoRouter(
},
),
GoRoute(
path: '/wechatAuth',
builder: (BuildContext context, GoRouterState state) {
return const WechatAuthPage();
},
),
GoRoute(
path: '/scanCode',
builder: (BuildContext context, GoRouterState state) {
return const ScanCodePage();
......
import 'dart:io';
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:flutter_image_compress/flutter_image_compress.dart';
......@@ -26,13 +27,7 @@ class CompressImageHandler extends MessageHandler {
throw Exception('参数错误');
}
var compressedWidth = params['compressedWidth'] as int?;
// if (compressedWidth == null) {
// throw Exception('参数错误');
// }
var compressedHeight = params['compressedHeight'] as int?;
// if (compressedHeight == null) {
// throw Exception('参数错误');
// }
if (compressedWidth == null && compressedHeight == null) {
throw Exception('参数错误');
......@@ -40,8 +35,8 @@ class CompressImageHandler extends MessageHandler {
if (compressedWidth == null) {
compressedWidth = compressedHeight;
} else if (compressedHeight == null) {
compressedHeight = compressedWidth;
} else {
compressedHeight ??= compressedWidth;
}
// 获取后缀名
......@@ -128,8 +123,19 @@ class CompressVideoHandler extends MessageHandler {
}
print('原视频大小:${originFile.lengthSync()}');
String? mimeType = await FileTypeUtil.getMimeType(originFile);
if (!(mimeType?.startsWith('video/') ?? false)) {
throw Exception('非视频文件');
}
final outputPath = '${tempDir.path}/${Uuid().v4()}.mp4';
var result = await VideoUtil.compressVideo(srcPath, outputPath, quality);
bool result;
if (mimeType != 'video/mp4') {
result = await VideoUtil.convertToMp4(srcPath, outputPath);
} else {
result = await VideoUtil.compressVideo(srcPath, outputPath, quality);
}
if (!result) {
throw Exception('视频压缩失败');
}
......@@ -139,32 +145,5 @@ class CompressVideoHandler extends MessageHandler {
"tempFilePath": '/temp$outputPath',
"size": File(outputPath).lengthSync(),
};
// VideoQuality videoQuality;
// switch (quality) {
// case 'low':
// videoQuality = VideoQuality.LowQuality;
// break;
// case 'middle':
// videoQuality = VideoQuality.MediumQuality;
// break;
// case 'high':
// videoQuality = VideoQuality.HighestQuality;
// break;
// default:
// throw Exception('参数错误');
// }
//
// final mediaInfo = await VideoCompress.compressVideo(
// srcPath,
// quality: videoQuality,
// deleteOrigin: false,
// includeAudio: true,
// );
//
// return {
// "tempFilePath": "/temp${mediaInfo!.path}",
// "size": mediaInfo.filesize,
// };
}
}
// import 'package:appframe/bloc/web_cubit.dart';
// import 'package:appframe/services/dispatcher.dart';
//
// class SetTitleHandler extends MessageHandler {
// late WebCubit? _webCubit;
//
// @override
// void setCubit(WebCubit cubit) {
// this._webCubit = cubit;
// }
//
// void _unfollowCubit() {
// this._webCubit = null;
// }
//
// @override
// Future<dynamic> handleMessage(params) async {
// try {
// if (params is! Map<String, dynamic>) {
// throw Exception('参数错误');
// }
//
// final String title = params['title'] as String;
// final bool showBack = params['showBack'] as bool;
//
// return _webCubit!.setTitle(title, showBack);
// } finally {
// _unfollowCubit();
// }
// }
// }
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 'dart:convert';
import 'dart:io';
import 'package:appframe/config/constant.dart';
......@@ -12,29 +13,12 @@ import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
class UploadFileHandler extends MessageHandler {
// late Dio _dio;
// UploadFile5Handler() : _dio = Dio() {
// // _dio.httpClientAdapter = Http2Adapter(
// // ConnectionManager(idleTimeout: Duration(seconds: 10)),
// // );
//
// int connectTimeout = 30000;
// int receiveTimeout = 30000;
//
// _dio.options = BaseOptions(
// baseUrl: '',
// connectTimeout: Duration(milliseconds: connectTimeout),
// receiveTimeout: Duration(milliseconds: receiveTimeout),
// headers: {'Content-Type': '', 'Accept': ''},
// );
// }
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final String? tempFilePath = params['tempFilePath'] as String?;
if (tempFilePath == null || tempFilePath.isEmpty) {
throw Exception('参数错误');
......@@ -50,18 +34,6 @@ class UploadFileHandler extends MessageHandler {
throw Exception('参数错误');
}
// _dio = Dio()
// ..options = BaseOptions(
// baseUrl: '',
// connectTimeout: Duration(milliseconds: 30000),
// receiveTimeout: Duration(milliseconds: 30000),
// headers: {'Content-Type': '', 'Accept': ''},
// )
// /*..httpClientAdapter = Http2Adapter(
// ConnectionManager(idleTimeout: Duration(seconds: 10)),
// )*/
// ;
final startTime = DateTime.now();
final result = await _handle(tempFilePath, busi, subBusi);
final endTime = DateTime.now();
......@@ -205,7 +177,6 @@ class UploadFileHandler extends MessageHandler {
futures.clear();
await randomAccessFile.close();
dio.close(force: true);
///
/// 7 合并
......@@ -215,12 +186,21 @@ class UploadFileHandler extends MessageHandler {
var endTime2 = DateTime.now();
print('====================>合并签名耗时:${endTime2.millisecondsSinceEpoch - startTime2.millisecondsSinceEpoch} 毫秒');
///
/// 8 针对视频生成封面
///
if (mimeType?.startsWith('video/') ?? false) {
await _genHwVideoCover(dio, objectKey);
}
dio.close(force: true);
bxeApiService.close();
return {'url': _addPreUrl(location)};
}
static const _bxeBaseUrl = 'https://iotapp-dev.banxiaoer.com/iotapp';
static const _genBaseUrl = 'https://dev.banxiaoer.net';
static const _signatureNewUrl = '/api/v1/obs/multipart/signaturenew';
static const _signatureNextUrl = '/api/v1/obs/multipart/signaturenext';
static const _completeUrl = '/api/v1/obs/multipart/complete';
......@@ -234,12 +214,12 @@ class UploadFileHandler extends MessageHandler {
/// 每次上传前,请求后端获取签名信息
Future<Map<String, dynamic>> _next(
ApiService bxeApiService,
String objectKey,
String bucket,
String uploadId,
int partNum,
) async {
ApiService bxeApiService,
String objectKey,
String bucket,
String uploadId,
int partNum,
) async {
var endpoint = '$_signatureNextUrl?objectKey=$objectKey&bucket=$bucket&uploadId=$uploadId&partNum=$partNum';
final resp = await bxeApiService.get(endpoint);
return resp.data;
......@@ -247,12 +227,12 @@ class UploadFileHandler extends MessageHandler {
/// 上传段,按照最大重试次数进行上传重试
Future<Map<String, dynamic>> _uploadChunkWithRetry(
Dio dio,
String signUrl,
int chunkIndex,
Uint8List chunk, {
int maxRetries = 3,
}) async {
Dio dio,
String signUrl,
int chunkIndex,
Uint8List chunk, {
int maxRetries = 3,
}) async {
//print('====================> 分片$chunkIndex , 开始上传 ${DateTime.now()}');
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
......@@ -280,7 +260,7 @@ class UploadFileHandler extends MessageHandler {
}
/// 上传段
Future<Response> _uploadChunk(Dio dio,String signUrl, Uint8List chunk, int chunkIndex) async {
Future<Response> _uploadChunk(Dio dio, String signUrl, Uint8List chunk, int chunkIndex) async {
var url = signUrl.replaceFirst('AWSAccessKeyId=', 'AccessKeyId=').replaceFirst(':443', '');
try {
// Response response = await _put(url, chunk);
......@@ -301,12 +281,12 @@ class UploadFileHandler extends MessageHandler {
/// 请求合并文件
Future<String> _merge(
ApiService bxeApiService,
String objectKey,
String bucket,
String uploadId,
Map<int, String> tagsMap,
) async {
ApiService bxeApiService,
String objectKey,
String bucket,
String uploadId,
Map<int, String> tagsMap,
) async {
final parts = [];
for (int i = 1; i <= tagsMap.length; i++) {
parts.add({'partNumber': i, 'etag': tagsMap[i]});
......@@ -348,4 +328,27 @@ class UploadFileHandler extends MessageHandler {
}
}
/// 生成封面
Future<void> _genHwVideoCover(Dio dio, String keys) async {
try {
var headers = {
"api-key": 'FJ9qv53Bxp',
};
var params = {
"videoKeys": [keys],
"outputSuffix": "_p1",
};
await dio.post(
'$_genBaseUrl/go/mpc/create_covers',
data: jsonEncode(params),
options: Options(
headers: headers,
contentType: 'application/json',
responseType: ResponseType.json,
),
);
} catch (e) {
print(e);
}
}
}
......@@ -3,9 +3,11 @@ import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.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_provider/path_provider.dart';
import 'package:video_compress/video_compress.dart';
class VideoInfoHandler extends MessageHandler {
@override
......@@ -15,8 +17,26 @@ class VideoInfoHandler extends MessageHandler {
}
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')) {
// 获取后缀名
String ext = path.extension(url);
......@@ -30,18 +50,41 @@ class VideoInfoHandler extends MessageHandler {
throw Exception('文件下载失败');
}
filePath = targetPath;
return targetPath;
} else {
filePath = url;
return url;
}
}
final file = File(filePath);
if (!file.existsSync()) {
throw Exception('视频文件不存在');
/// 提取视频信息
Future<Map<String, dynamic>> _extractVideoInfo(
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 mediaInfo = await VideoCompress.getMediaInfo(filePath);
final videoStream = videoStreams[0];
final properties = videoStream.getAllProperties();
int width = properties!['width'] ?? 0;
int height = properties['height'] ?? 0;
// 获取文件大小
final size = await file.length();
......@@ -52,10 +95,10 @@ class VideoInfoHandler extends MessageHandler {
return {
'tempFilePath': '/temp$filePath',
'width': mediaInfo.width ?? 0,
'height': mediaInfo.height ?? 0,
'width': width,
'height': height,
'type': fileExtension,
'duration': (mediaInfo.duration ?? 0) / 1000, // 转换为秒
'duration': duration, // 已经是秒单位
'size': size,
};
}
......
......@@ -7,7 +7,9 @@ import 'app.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await setupLocator();
// IM SDK 初始化
if (Constant.needIM) {
await getIt.get<ImService>().initSdk();
}
......
......@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/upgrade_service.dart';
import 'package:appframe/utils/zip_util.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
......@@ -13,11 +12,8 @@ class LocalServerService {
// 启动本地HTTP服务器
Future<HttpServer> startLocalServer() async {
// 检测和升级
// await getIt.get<UpgradeService>().upgrade();
// 测试情况下, 每次启动服务,先解压dist文件
_extractDist();
await _extractDist();
HttpServer server = await HttpServer.bind(InternetAddress.loopbackIPv4, Constant.localServerPort);
print('本地服务器启动在端口: ${server.port}');
......@@ -48,6 +44,10 @@ class LocalServerService {
return server;
}
void resetHttpDirectory() {
_httpDirectory = null;
}
// 目录下的文件
Future<void> _serveTempFile(HttpRequest request, String requestPath) async {
try {
......@@ -150,13 +150,8 @@ class LocalServerService {
Future<void> _initHttpDirectory() async {
var version = getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version;
if (Platform.isAndroid) {
var direct = await getExternalStorageDirectory();
_httpDirectory = '${direct?.path}/http_dist_assets_/$version';
} else if (Platform.isIOS || Platform.isMacOS) {
var direct = await getApplicationSupportDirectory();
_httpDirectory = '${direct.path}/http_dist_assets_/$version';
}
var direct = await getApplicationSupportDirectory();
_httpDirectory = '${direct.path}/${Constant.h5DistDir}/$version';
}
Future<void> _extractDist() async {
......@@ -167,7 +162,13 @@ class LocalServerService {
// return;
// }
var zipFilePath = "assets/dist.zip";
ZipUtil.extractZipFile(zipFilePath, outputDirectory);
// 判断H5打包文件是否存在,不存在则从assets中解压
var version = getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version;
var dir = await getApplicationSupportDirectory();
var distFilePath = '${dir.path}/${Constant.h5DistDir}/$version.zip';
if (!File(distFilePath).existsSync()) {
distFilePath = 'assets/dist.zip';
}
await ZipUtil.extractZipFile(distFilePath, outputDirectory);
}
}
......@@ -29,7 +29,7 @@ class PlayerService {
try {
final player = FlutterSoundPlayer();
_player = (await player.openPlayer())!;
_player!.setSpeed(1); // 播放速度,默认1
await _player!.setSpeed(1); // 播放速度,默认1
// 播放进度回调
_player!.setSubscriptionDuration(Duration(seconds: 1));
......
import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/utils/zip_util.dart';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
class UpgradeService {
String configUrl = "https://bxe-obs.banxiaoer.com/conf/xeapp_conf_dev.json";
Future<void> upgrade() async {
await _handleUpgrade();
}
Future<bool> _handleUpgrade() async {
Dio dio = Dio();
try {
Response response = await dio.get('$configUrl?t=${DateTime.now().millisecondsSinceEpoch}');
if (response.statusCode != 200) {
return false;
}
String version = response.data['version'] as String;
String zip = response.data['zip'] as String;
// 版本号相同,则不需要升级
if (version == (getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version)) {
print('版本号相同,不需要升级');
return true;
}
// 下载zip文件
var tempDir = await getTemporaryDirectory();
var saveFilePath = '${tempDir.path}/${Uuid().v4()}.zip';
var downloadResult = await _downloadFile(dio, '$zip$version.zip', saveFilePath);
if (!downloadResult) {
return false;
}
// 解压zip文件
String? httpDirect;
if (Platform.isAndroid) {
var direct = await getExternalStorageDirectory();
httpDirect = '${direct?.path}/http_dist_assets_/$version';
} else if (Platform.isIOS || Platform.isMacOS) {
var direct = await getApplicationSupportDirectory();
httpDirect = '${direct.path}/http_dist_assets_/$version';
}
var result = await ZipUtil.extractZipFile(saveFilePath, httpDirect!);
if (!result) {
return false;
}
// 设置版本标识
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.setString('h5_version', version);
return true;
} catch (e) {
print('升级请求失败: $e');
return false;
} finally {
// 请求结束
dio.close(force: true);
}
}
Future<bool> _downloadFile(Dio dio, String url, String savePath) async {
try {
Response response = await dio.download(url, savePath);
if (response.statusCode == 200) {
print('文件下载成功: $savePath');
return true;
} else {
print('文件下载失败: ${response.statusCode}');
return false;
}
} catch (e) {
print('文件下载失败: $e');
return false;
}
}
}
......@@ -14,41 +14,61 @@ class LoginMainPage extends StatelessWidget {
create: (context) => LoginMainCubit(LoginMainState()),
child: BlocConsumer<LoginMainCubit, LoginMainState>(builder: (context, state) {
var loginMainCubit = context.read<LoginMainCubit>();
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: SingleChildScrollView(
child: Column(
children: [
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),
return Stack(
children: [
Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 30),
// _buildHeader(),
LoginPageHeaderWidget(),
SizedBox(height: 25),
_buildLoginButtons(context, loginMainCubit, state.agreed),
SizedBox(height: 20),
_buildAgreement(context, loginMainCubit, state.agreed),
],
children: [
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(
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) {
if (state.showAgreed) {
......@@ -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) {
return Column(
children: [
......
......@@ -14,7 +14,6 @@ class WebPage extends StatelessWidget {
Widget build(BuildContext buildContext) {
final Map<String, dynamic>? extraData = GoRouterState.of(buildContext).extra as Map<String, dynamic>?;
var ip = extraData?['ip'] ?? Constant.localServerHost;
var sessionCode = extraData?['sessionCode'];
var userCode = extraData?['userCode'];
var classCode = extraData?['classCode'];
......@@ -28,13 +27,11 @@ class WebPage extends StatelessWidget {
classCode = sharedPreferences.getString('auth_classCode');
userType = sharedPreferences.getInt('auth_userType');
stuId = sharedPreferences.getString('auth_stuId');
ip = sharedPreferences.getString('auth_ip');
}
return BlocProvider(
create: (context) => WebCubit(
WebState(
ip: ip,
sessionCode: sessionCode,
userCode: userCode,
classCode: classCode,
......@@ -56,14 +53,6 @@ class WebPage extends StatelessWidget {
automaticallyImplyLeading: false,
backgroundColor: Color(state.bgColor),
actionsIconTheme: IconThemeData(color: Colors.white),
// leading: state.beBack
// ? IconButton(
// icon: const Icon(Icons.arrow_back, color: Colors.white),
// onPressed: () {
// ctx.read<WebCubit>().handleBack();
// },
// )
// : null,
leading: state.opIcon == 'back'
? IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
......@@ -93,14 +82,6 @@ class WebPage extends StatelessWidget {
fontSize: 24,
color: Colors.white,
))),
/*ListTile(
tileColor: Color(0xFF7691FA),
title: Text('设置',
style: TextStyle(
color: Theme.of(ctx).colorScheme.onPrimary,
fontSize: 24,
)),
),*/
ListTile(
leading: const Icon(Icons.chat_bubble_outline),
title: const Text('消息测试'),
......@@ -110,22 +91,6 @@ class WebPage extends StatelessWidget {
},
),
ListTile(
leading: const Icon(Icons.login),
title: const Text('登录界面'),
onTap: () {
Navigator.pop(ctx);
ctx.read<WebCubit>().goLogin();
},
),
ListTile(
leading: const Icon(Icons.chat_outlined),
title: const Text('微信授权'),
onTap: () {
Navigator.pop(ctx);
ctx.read<WebCubit>().goWechatAuth();
},
),
ListTile(
leading: const Icon(Icons.accessibility_new),
title: const Text('身份认证'),
onTap: () {
......@@ -167,12 +132,40 @@ class WebPage extends StatelessWidget {
},
),
])),
body: state.loaded
? SizedBox(
height: MediaQuery.of(ctx).size.height - 60, // 减去100像素留空
child: WebViewWidget(controller: ctx.read<WebCubit>().controller),
)
: const Center(child: CircularProgressIndicator()),
body: Stack(
children: [
state.loaded
? SizedBox(
height: MediaQuery.of(ctx).size.height - 60, // 减去100像素留空
child: WebViewWidget(controller: ctx.read<WebCubit>().controller),
)
: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('加载中...'),
],
),
),
// 添加升级遮罩层
if (state.isUpgrading)
Container(
color: Colors.black54,
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('资源更新中...', style: TextStyle(color: Colors.white)),
],
),
),
),
],
),
bottomNavigationBar: state.showBottomNavBar
? BottomNavigationBar(
type: BottomNavigationBarType.fixed,
......@@ -208,7 +201,9 @@ class WebPage extends StatelessWidget {
);
},
listener: (context, state) {
if (state.orientationCmdFlag) {
if(state.suggestUpgrade){
context.read<WebCubit>().suggestUpgrade(context);
} else if (state.orientationCmdFlag) {
context.read<WebCubit>().getOrientation(context);
} else if (state.windowInfoCmdFlag) {
context.read<WebCubit>().getWindowInfo(context);
......
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: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:video_compress/video_compress.dart';
/// 缩略图工具类
///
......@@ -28,28 +29,29 @@ class ThumbnailUtil {
/// 返回缩略图路径
static Future<String?> genVideoThumbnail(String videoPath, Directory dir) async {
try {
var fileThumbnail = await VideoCompress.getFileThumbnail(videoPath, quality: 50, position: -1);
return fileThumbnail.path;
final thumbnailPath = '${dir.path}/video_thumb_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 使用 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) {
print('生成视频缩略图出错: $e');
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;
// }
}
}
......@@ -9,25 +9,25 @@ class VideoUtil {
/// 转码的同时,进行压缩
///
static Future<bool> convertToMp4(String inputPath, String outputPath) async {
String cmd;
if(Platform.isIOS) {
// 构建命令
// 1. -c:v h264_videotoolbox : 启用 iOS 硬件加速
// 2. -b:v 1500k : 限制视频码率为 1.5Mbps (体积小,手机看足够)
// 3. -vf scale=1280:-2 : 缩放到 720p (保持比例)
// 4. -c:a aac : 音频转为 AAC (兼容性最好)
// 5. -b:a 128k : 音频码率
cmd =
'-i "$inputPath" '
'-c:v h264_videotoolbox -b:v 1500k '
'-vf scale=1280:-2 '
'-c:a aac -b:a 128k '
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"';
print("开始极速转码: $cmd");
} else {
cmd = '-i "$inputPath" -c:v libx264 -crf 28 -c:a aac -b:a 128k -strict experimental -movflags faststart -f mp4 "$outputPath"';
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();
......@@ -56,8 +56,15 @@ class VideoUtil {
default:
throw Exception('参数错误');
}
final session = await FFmpegKit.execute(
'-i "$inputPath" -c:v libx264 -crf $crf -c:a aac -b:a 128k -preset medium -movflags faststart "$outputPath"');
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);
}
......
......@@ -22,7 +22,6 @@ import photo_manager
import shared_preferences_foundation
import tencent_cloud_chat_sdk
import url_launcher_macos
import video_compress
import video_player_avfoundation
import webview_flutter_wkwebview
......@@ -44,7 +43,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
TencentCloudChatSdkPlugin.register(with: registry.registrar(forPlugin: "TencentCloudChatSdkPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
}
......@@ -37,8 +37,7 @@ dependencies:
flutter_bloc: ^9.1.1
flutter_localization: ^0.3.3
flutter_image_compress: ^2.4.0
ffmpeg_kit_flutter_new: ^4.1.0
# 建议:如果项目中不需要 image_picker(被 wechat_assets_picker 替代),则保持注释或删除
# image_picker: ^1.2.0
......@@ -55,8 +54,8 @@ dependencies:
# --- 音视频与直播 (重灾区) ---
# 确保 ffmpeg_kit 版本与你的架构兼容。
# 如果只是为了压缩视频,建议评估是否移除 video_compress,直接用 ffmpeg
#ffmpeg_kit_flutter_new: ^3.2.0
video_compress: ^3.1.4
ffmpeg_kit_flutter_new: ^4.1.0
# video_compress: ^3.1.4
video_player: ^2.10.0
# video_thumbnail 已被注释,确认是否需要生成缩略图,如果需要,ffmpeg_kit 也能做
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!