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,
......
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!