Commit 60f5ffe5 by Administrator

Merge branch 'develop'

# Conflicts:
#	lib/bloc/wechat_auth_cubit.dart
#	macos/Flutter/GeneratedPluginRegistrant.swift
#	pubspec.lock
#	pubspec.yaml
2 parents d6cb8851 ba18c03b
Showing 73 changed files with 3500 additions and 491 deletions
......@@ -5,10 +5,20 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<application
android:label="班小二"
android:name="${applicationName}"
......
......@@ -4,5 +4,12 @@
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">127.0.0.1</domain>
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">192.168.2.59</domain>
<domain includeSubdomains="false">192.168.2.215</domain>
<domain includeSubdomains="false">192.168.2.177</domain>
<domain includeSubdomains="false">192.168.1.136</domain>
<domain includeSubdomains="false">localdev.banxiaoer.net</domain>
<domain includeSubdomains="false">appdev-th.banxiaoer.net</domain>
<domain includeSubdomains="false">appdev-xj.banxiaoer.net</domain>
</domain-config>
</network-security-config>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebView Communication</title>
</head>
<body>
<h2>登录跳转</h2>
</body>
<script>
window.onload = function () {
let message = '{ "timestamp": 1, "unique": "1", "cmd": "goLogin", "params": {} }';
xeJsBridge.postMessage(message);
}
</script>
</html>
\ No newline at end of file
......@@ -9,6 +9,15 @@
<div id="resp"></div>
<input type="file" />
<audio controls>
<source src="https://files-obs.banxiaoer.com/d2/pridel/user/20250918/bxe/220387141757177856/f/comm/bxe_homework/audio/fmp3t1758184802425n5zldhtvw.mp3">
</audio>
<img id="testImg" src="" alt="">
<button onclick="clearResp()">清除响应数据</button>
<br>
......@@ -20,23 +29,43 @@
<button onclick="setStorageSync()">设置本地缓存</button>
<button onclick="getStorageSync()">获取本地缓存</button>
<button onclick="removeStorageSync()">删除本地缓存</button>
<button onclick="clearStorageSync()">清空本地缓存</button>
<br>
<br>
<button onclick="chooseImage()">选择单个图片</button>
<button onclick="chooseMultipleImage()">选择多个图片</button>
<button onclick="chooseImage(1)">选择单个图片</button>
<button onclick="chooseMultipleImage(1)">选择多个图片</button>
<br>
<br>
<button onclick="scanCode()">扫码测试</button>
<button onclick="camera()">录像测试</button>
<br>
<br>
<button onclick="openWeapp()">打开小程序</button>
<br>
<br>
<a href="/test/test2.html">跳转测试2</a>
<br>
<br>
<a href="/test/test3.html">跳转测试3</a>
<br>
<br>
<a href="/test/test4.html">跳转测试4</a>
<br>
<br>
<a href="/index.html">iOS跳转</a>
<br>
<br>
<script src="/test/test.js"></script>
......
......@@ -11,6 +11,12 @@ function clearResp() {
// 接收Flutter响应数据
function xeJsBridgeCallback(message) {
document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + message + '</p>';
/*let jsonObj=JSON.parse(message);
if(jsonObj.cmd=='chooseImage'){
document.getElementById('testImg').src='/temp'+jsonObj.data[0].tempFilePath;
}*/
}
// 测试获取设备信息
......@@ -29,6 +35,11 @@ function getStorageSync() {
xeJsBridge.postMessage(message);
}
function removeStorageSync() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "removeStorageSync", "params": "test1" }';
xeJsBridge.postMessage(message);
}
function clearStorageSync() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "clearStorageSync", "params": {} }';
xeJsBridge.postMessage(message);
......@@ -47,11 +58,11 @@ function chooseImage(sourceType) {
}
function chooseMultipleImage() {
function chooseMultipleImage(sourceType) {
let params = {
"timestamp": 1, "unique": "123", "cmd": "chooseImage", "params": {
"sourceType": "album",
"sourceType": sourceType == 1 ? "album" : "camera",
"count": 9,
"sizeType": ["original", "compressed"],
}
......@@ -60,3 +71,22 @@ function chooseMultipleImage() {
}
function scanCode() {
let params = {
"timestamp": 1, "unique": "123", "cmd": "scanCode", "params": {}
};
xeJsBridge.postMessage(JSON.stringify(params));
}
function openWeapp(){
let params = {
"timestamp": 1, "unique": "123", "cmd": "openWeapp", "params": {
appid:'gh_9a8d84445828',
path:'/pages/index/index?classCode=needswitch',
envVersion:'trial'
}
};
xeJsBridge.postMessage(JSON.stringify(params));
}
......@@ -9,6 +9,8 @@
<div id="resp"></div>
<input type="text" id="text" value="">
<button onclick="startRecord()">开始录音</button>
<button onclick="">暂停录音</button>
......@@ -23,6 +25,8 @@
<button onclick="">唤醒播放</button>
<button onclick="stopPlay()">停止播放</button>
<button onclick="openlink()">打开新链接</button>
<br>
<br>
<a href="/test/test.html">跳转测试1</a>
......@@ -62,6 +66,12 @@
xeJsBridge.postMessage(message);
}
function openlink() {
let url = 'https://xw.qq.com';
let message = '{ "timestamp": 1, "unique": "123", "cmd": "openlink", "params": {"url":"' + url + '"} }';
xeJsBridge.postMessage(message);
}
</script>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebView Communication</title>
</head>
<body>
<h2>接口测试3</h2>
<div id="resp"></div>
<button onclick="selectFile()">选择文件</button>
<button onclick="saveImg()">保存图片视频</button>
<br>
<br>
<button onclick="setClipboard()">设置剪贴板</button>
<button onclick="getClipboard()">获取剪贴板</button>
<br>
<br>
<a href="/test/test.html">跳转测试1</a>
</body>
<script>
function xeJsBridgeCallback(message) {
document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + message + '</p>';
}
function selectFile() {
let params = {
timestamp: 1,
unique: '123',
cmd: 'chooseFile',
params: {
count: 2,
fileTypes: ['docx', 'xlsx', 'pdf', 'mp4','csv', 'png', 'm4a']
}
};
xeJsBridge.postMessage(JSON.stringify(params));
}
function saveImg() {
let params = {
timestamp: 1,
unique: '123',
cmd: 'saveToAlbum',
params: {
// filePath: 'http://www.people.com.cn/NMediaFile/2025/0910/MAIN175747975880516PN6WLU10.jpg'
// filePath: '/data/user/0/cn.banxe.bxe/cache/1757576655307_1000019720.jpg'
filePath: '/data/user/0/cn.banxe.bxe/cache/1757577249083_VID_20250906_111114.mp4'
}
};
xeJsBridge.postMessage(JSON.stringify(params));
}
function setClipboard() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "setClipboardData", "params": "此为测试剪贴板的内容" }';
xeJsBridge.postMessage(message);
}
function getClipboard() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "getClipboardData", "params": {} }';
xeJsBridge.postMessage(message);
}
</script>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebView Communication</title>
</head>
<body>
<h2>接口测试4</h2>
<div id="resp"></div>
<button onclick="clearResp()">清除响应数据</button>
<br>
<br>
<button onclick="compressImage()">测试图片压缩</button>
<br>
<br>
<button onclick="upload()">测试文件上传</button>
<br>
<br>
<a href="/test/test.html">跳转测试1</a>
</body>
<script>
// 清空响应数据
function clearResp() {
document.getElementById('resp').innerHTML = '';
}
function xeJsBridgeCallback(message) {
document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + message + '</p>';
}
function compressImage() {
let params = {
timestamp: 1,
unique: '123',
cmd: 'compressImage',
params: {
url: '/data/user/0/cn.banxe.bxe/cache/1757649757566_1000019639.jpg',
quality: 50,
compressedWidth: 800,
compressedHeight: 600,
}
};
xeJsBridge.postMessage(JSON.stringify(params));
}
function upload() {
let params = {
timestamp: 1,
unique: '123',
cmd: 'uploadFile',
params: {
tempFilePath: '/data/user/0/cn.banxe.bxe/cache/1758248263516_test.csv',
}
};
xeJsBridge.postMessage(JSON.stringify(params));
}
</script>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebView Communication</title>
</head>
<body>
<h2>前端测试</h2>
<form>
<input type="text" id="cliIp" value="">
<input type="submit">
</form>
<br>
<br>
<a href="/test/test.html">跳转测试1</a>
</body>
<script>
window.aaa = function (a, b, c) {
console.log(a, b, c);
}
function test(term, ) {
}
</script>
</html>
\ No newline at end of file
import 'dart:io';
import 'package:appframe/l10n/gen/app_localizations.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // 添加这行
......@@ -27,6 +28,8 @@ class App extends StatelessWidget {
],
)
: MaterialApp.router(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
routerConfig: router,
title: '班小二',
theme: ThemeData(primarySwatch: Colors.blue),
......
import 'package:appframe/config/routes.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:webview_flutter/webview_flutter.dart';
class LinkState extends Equatable {
final bool loaded;
final String url;
const LinkState({this.loaded = false, this.url = ''});
LinkState copyWith({bool? loaded, String? url}) {
return LinkState(loaded: loaded ?? this.loaded, url: url ?? this.url);
}
@override
// TODO: implement props
List<Object?> get props => [loaded, url];
}
class LinkCubit extends Cubit<LinkState> {
late final WebViewController _controller;
WebViewController get controller => _controller;
LinkCubit(super.initialState) {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onUrlChange: (UrlChange url) {},
onPageStarted: (String url) {},
onPageFinished: (String 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();
},
),
)
..loadRequest(Uri.parse(state.url));
}
void _finishLoading() {
emit(state.copyWith(loaded: true));
}
Future<void> handleBack(BuildContext context) async {
if (await _controller.canGoBack()) {
_controller.goBack();
} else {
// context.pop(true);
router.pop('ok');
}
}
}
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_sound/public/flutter_sound_recorder.dart';
class RecorderState {
final bool recorderIsInit;
final String path;
RecorderState({this.recorderIsInit = false, this.path = ''});
}
class RecorderCubit extends Cubit<String> {
late FlutterSoundRecorder _recorder;
RecorderCubit(super.initialState, this._recorder) {
_recorder.openRecorder();
}
@override
Future<void> close() {
try {
_recorder.closeRecorder();
} catch (e) {
print(e);
}
return super.close();
}
}
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluwx/fluwx.dart';
import 'package:shared_preferences/shared_preferences.dart';
class WechatAuthState extends Equatable {
final String ip;
final String? result;
final String? sessionCode;
final String? userCode;
final String? classCode;
final int? userType;
final String? stuId;
final String? result;
const WechatAuthState(this.sessionCode, this.userCode, this.classCode, this.userType, this.stuId, this.result);
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,
String? result,
}) {
return WechatAuthState(
sessionCode ?? this.sessionCode,
userCode ?? this.userCode,
classCode ?? this.classCode,
userType ?? this.userType,
stuId ?? this.stuId,
result ?? this.result,
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 => [sessionCode, userCode, classCode, userType, stuId, result];
List<Object?> get props => [ip, result, sessionCode, userCode, classCode, userType, stuId];
}
class WechatAuthCubit extends Cubit<WechatAuthState> {
late final Fluwx _fluwx;
late final WechatAuthRepository _wechatAuthRepository;
late final Fluwx _fluwx;
late final TextEditingController _textEditingController;
TextEditingController get textEditingController => _textEditingController;
WechatAuthCubit(super.initialState) {
_fluwx = Fluwx();
_register();
_subscribe();
_fluwx = getIt.get<Fluwx>();
_fluwx.addSubscriber(_responseListener);
_textEditingController = TextEditingController()..text = Constant.h5Server;
_wechatAuthRepository = getIt<WechatAuthRepository>();
}
......@@ -51,44 +71,60 @@ class WechatAuthCubit extends Cubit<WechatAuthState> {
Future<void> _register() async {
await _fluwx.registerApi(appId: "wx8c32ea248f0c7765", universalLink: "https://dev.banxiaoer.net/path/to/wechat/");
}
void _responseListener(response) async {
if (response is WeChatAuthResponse) {
dynamic resultData = await _wechatAuthRepository.codeToSk(response.code!);
var data = resultData['data'];
var role = data['roles'][0];
void _subscribe() {
_fluwx.addSubscriber((response) async {
if (response is WeChatAuthResponse) {
var result = 'state :${response.state} \n code:${response.code}';
// emit(state.copyWith(result: result));
dynamic data = await _wechatAuthRepository.codeToSk(response.code!);
print("===============================================");
print(data.toString());
var dd = data['data'];
var role = dd['roles'][0];
print(dd['sessionCode']);
print(dd['userCode']);
print(role['classCode']);
print(role['userType']);
print(role['stuId']);
emit(
state.copyWith(
sessionCode: dd['sessionCode'],
userCode: dd['userCode'],
classCode: role['classCode'],
userType: role['userType'],
stuId: role['stuId'],
),
);
}
});
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'),
);
print("++++++++++++++++++++++++++++++++++++++++++++++++++++++");
print(result);
if (!result) {
throw Exception('微信授权处理失败');
}
}
void goIndex() {
router.go('/web');
}
@override
Future<void> close() async {
_fluwx.removeSubscriber(_responseListener);
return super.close();
}
}
class Constant {
/// 应用内部 http 服务
static const int localServerPort = 35982;
static const String localServerHost = '127.0.0.1';
static const String localServerUrl = 'http://$localServerHost:$localServerPort';
static const String localServerTemp = '/temp';
static const String localServerTempFileUrl = '$localServerUrl$localServerTemp';
static const String localServerTest = '/test';
static const String localServerTestFileUrl = '$localServerUrl$localServerTest';
/// obs文件分片上传的分片大小:5M
static const int obsUploadChunkSize = 1024 * 1024 * 5;
/// 测试阶段使用的 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';
}
......@@ -7,7 +7,7 @@ late HttpServer localServer;
Future<void> startLocalServer() async {
HttpServer localServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
print('本地服务器启动在端口: ${localServer.port}');
// print('本地服务器启动在端口: ${localServer.port}');
localServer.listen((HttpRequest request) async {
final String requestPath = request.uri.path == '/' ? '/index.html' : request.uri.path;
......
import 'package:appframe/data/repositories/message/app_info_handler.dart';
import 'package:appframe/data/repositories/message/audio_player_handler.dart';
import 'package:appframe/data/repositories/message/audio_recorder_handler.dart';
import 'package:appframe/data/repositories/message/choose_file_handler.dart';
import 'package:appframe/data/repositories/message/choose_image_handler.dart';
import 'package:appframe/data/repositories/message/choose_video_handler.dart';
import 'package:appframe/data/repositories/message/clipboard_data_handler.dart';
import 'package:appframe/data/repositories/message/compress_handler.dart';
import 'package:appframe/data/repositories/message/crop_image_handler.dart';
import 'package:appframe/data/repositories/message/device_info_handler.dart';
import 'package:appframe/data/repositories/message/player_handler.dart';
import 'package:appframe/data/repositories/message/recorder_handler.dart';
import 'package:appframe/data/repositories/message/download_file_handler.dart';
import 'package:appframe/data/repositories/message/go_login_handler.dart';
import 'package:appframe/data/repositories/message/image_info_handler.dart';
import 'package:appframe/data/repositories/message/location_handler.dart';
import 'package:appframe/data/repositories/message/network_type_handler.dart';
import 'package:appframe/data/repositories/message/open_document_handler.dart';
import 'package:appframe/data/repositories/message/open_link_handler.dart';
import 'package:appframe/data/repositories/message/open_weapp_handler.dart';
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/services/dispatcher.dart';
import 'package:appframe/data/repositories/message/scan_code_handler.dart';
import 'package:appframe/data/repositories/message/set_title_handler.dart';
import 'package:appframe/data/repositories/message/storage_handler.dart';
import 'package:appframe/data/repositories/message/upload_file.dart';
import 'package:appframe/data/repositories/message/vibrate_short_handler.dart';
import 'package:appframe/data/repositories/message/video_info_handler.dart';
import 'package:appframe/data/repositories/message/wifi_info_handler.dart';
import 'package:appframe/data/repositories/message/window_info_handler.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:fluwx/fluwx.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
final getIt = GetIt.instance;
Future<void> setupLocator() async {
/// Fluwx
getIt.registerSingleton<Fluwx>(
await (() async {
Fluwx fluwx = Fluwx();
await fluwx.registerApi(appId: "wx8c32ea248f0c7765", universalLink: "https://univerallink.banxe.cn/link/");
return fluwx;
})(),
);
///SharedPreferences
getIt.registerSingleton<SharedPreferences>(await SharedPreferences.getInstance());
/// 按指令名称注册 message handler
/// 视情况,对大概率会执行的指令直接加载,对概率较低的指令使用懒加载
/// 使用懒加载,用户实际用到了才会进行加载
/// 打开小程序
getIt.registerLazySingleton<MessageHandler>(() => OpenWeappHandler(), instanceName: 'openWeapp');
/// 设备信息
getIt.registerLazySingleton<MessageHandler>(() => DeviceInfoHandler(), instanceName: 'getDeviceInfoSync');
getIt.registerLazySingleton<MessageHandler>(() => DeviceInfoHandler(), instanceName: 'getDeviceInfo');
/// 位置信息
getIt.registerLazySingleton<MessageHandler>(() => LocationHandler(), instanceName: 'getLocation');
/// 网络信息
getIt.registerLazySingleton<MessageHandler>(() => NetworkTypeHandler(), instanceName: 'getNetworkType');
/// wifi信息
getIt.registerLazySingleton<MessageHandler>(() => WifiInfoHandler(), instanceName: 'getWifiInfo');
/// 设备方向
getIt.registerLazySingleton<MessageHandler>(() => OrientationHandler(), instanceName: 'getOrientation');
/// 应用信息
getIt.registerLazySingleton<MessageHandler>(() => AppInfoHandler(), instanceName: 'getAppInfo');
/// 窗口信息
getIt.registerLazySingleton<MessageHandler>(() => WindowInfoHandler(), instanceName: 'getWindowInfo');
/// 本地缓存
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => GetStorageSyncHandler(), instanceName: 'getStorageSync');
getIt.registerLazySingleton<MessageHandler>(() => GetStorageHandler(), instanceName: 'getStorage');
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => SetStorageSyncHandler(), instanceName: 'setStorageSync');
getIt.registerLazySingleton<MessageHandler>(() => SetStorageHandler(), instanceName: 'setStorage');
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => ClearStorageSyncHandler(), instanceName: 'clearStorageSync');
getIt.registerLazySingleton<MessageHandler>(() => RemoveStorageHandler(), instanceName: 'removeStorage');
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => ClearStorageHandler(), instanceName: 'clearStorage');
/// 剪贴板
// Android已测试通过
......@@ -42,14 +98,35 @@ Future<void> setupLocator() async {
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => ChooseImageHandler(), instanceName: 'chooseImage');
/// 选择视频
getIt.registerLazySingleton<MessageHandler>(() => ChooseVideoHandler(), instanceName: 'chooseVideo');
/// 选择文件
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => ChooseFileHandler(), instanceName: 'chooseFile');
/// 图片信息
getIt.registerLazySingleton<MessageHandler>(() => ImageInfoHandler(), instanceName: 'getImageInfo');
/// 获取视频信息
getIt.registerLazySingleton<MessageHandler>(() => VideoInfoHandler(), instanceName: 'getVideoInfo');
/// 保存文件到客户端文件系统
getIt.registerLazySingleton<MessageHandler>(() => SaveFileToDisKHandler(), instanceName: 'saveFileToDisk');
/// 保存文件/视频到相册
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => SaveToAlbumHandler(), instanceName: 'saveToAlbum');
/// 图片压缩
getIt.registerLazySingleton<MessageHandler>(() => CompressImageHandler(), instanceName: 'compressImage');
/// 视频压缩
getIt.registerLazySingleton<MessageHandler>(() => CompressVideoHandler(), instanceName: 'compressVideo');
/// 打开文档
getIt.registerLazySingleton<MessageHandler>(() => OpenDocumentHandler(), instanceName: 'openDocument');
/// 录音
getIt.registerLazySingleton<MessageHandler>(() => AudioRecorderStartHandler(), instanceName: 'audioRecorderStart');
getIt.registerLazySingleton<MessageHandler>(() => AudioRecorderPauseHandler(), instanceName: 'audioRecorderPause');
......@@ -61,9 +138,34 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<MessageHandler>(() => AudioPlayHandler(), instanceName: 'audioPlay');
getIt.registerLazySingleton<MessageHandler>(() => AudioPauseHandler(), instanceName: 'audioPause');
getIt.registerLazySingleton<MessageHandler>(() => AudioResumeHandler(), instanceName: 'audioResume');
getIt.registerLazySingleton<MessageHandler>(() => AudioSeekHandler(), instanceName: 'audioSeek');
getIt.registerLazySingleton<MessageHandler>(() => AudioStopHandler(), instanceName: 'audioStop');
getIt.registerLazySingleton<MessageHandler>(() => AudioClearHandler(), instanceName: 'audioClear');
/// 裁剪图片
getIt.registerLazySingleton<MessageHandler>(() => CropImageHandler(), instanceName: 'cropImage');
/// 震动
getIt.registerLazySingleton<MessageHandler>(() => VibrateShortHandler(), instanceName: 'vibrateShort');
/// 扫码
getIt.registerLazySingleton<MessageHandler>(() => ScanCodeHandler(), instanceName: 'scanCode');
/// 上传文件
getIt.registerLazySingleton<MessageHandler>(() => UploadFileHandler(), instanceName: 'uploadFile');
/// 下载文件
getIt.registerLazySingleton<MessageHandler>(() => DownloadFileHandler(), instanceName: 'downloadFile');
/// 设置标题和返回按钮
getIt.registerLazySingleton<MessageHandler>(() => SetTitleHandler(), instanceName: 'setTitle');
/// 新路由打开链接
getIt.registerLazySingleton<MessageHandler>(() => OpenLinkHandler(), instanceName: 'openlink');
/// 登录
getIt.registerLazySingleton<MessageHandler>(() => GoLoginHandler(), instanceName: 'goLogin');
/// service
getIt.registerLazySingleton<ApiService>(() => ApiService(baseUrl: 'https://dev.banxiaoer.net'));
......
import 'package:appframe/ui/pages/link_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';
......@@ -18,5 +20,17 @@ final GoRouter router = GoRouter(
return const WechatAuthPage();
},
),
GoRoute(
path: '/scanCode',
builder: (BuildContext context, GoRouterState state) {
return const ScanCodePage();
},
),
GoRoute(
path: '/link',
builder: (BuildContext context, GoRouterState state) {
return const LinkPage();
},
),
],
);
import 'package:fluwx/fluwx.dart';
final Fluwx fluwx = Fluwx();
Future<void> registerWechatApi() async {
await fluwx.registerApi(appId: "wx8c32ea248f0c7765", universalLink: "https://dev.banxiaoer.net/path/to/wechat/");
}
import 'package:appframe/services/dispatcher.dart';
class AppInfoHandler extends MessageHandler {
@override
Future<Map<String, dynamic>> handleMessage(params) async {
return {"version": "0.1", "theme": "light"};
}
}
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart';
class AudioPlayHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final url = params['url'] as String;
bool result = await _webCubit!.playAudio(url);
return result;
}
}
class AudioPauseHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.pauseAudio();
return result;
}
}
class AudioResumeHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.resumeAudio();
return result;
}
}
class AudioSeekHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final seek = params['seek'] as int;
bool result = await _webCubit!.seekAudio(seek);
return result;
}
}
class AudioStopHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.stopAudio();
return result;
}
}
class AudioClearHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
await _webCubit!.clearAudio();
return true;
}
}
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart';
class AudioRecorderStartHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.startRecording();
return result;
}
}
class AudioRecorderPauseHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.pauseRecording();
return result;
}
}
class AudioRecorderResumeHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await _webCubit!.resumeRecording();
return result;
}
}
class AudioRecorderStopHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
return await _webCubit!.stopRecording();
}
}
class AudioRecorderClearHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
return await _webCubit!.clearRecording();
}
}
import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart';
import 'package:appframe/utils/thumbnail_util.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_size_getter/file_input.dart';
import 'package:image_size_getter/image_size_getter.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';
class ChooseFileHandler extends MessageHandler {
@override
......@@ -16,8 +17,12 @@ class ChooseFileHandler extends MessageHandler {
throw Exception('参数错误');
}
var count = params['count'] as int;
var fileTypes = params['fileTypes'] as List<dynamic>;
var count = 1;
if (params.containsKey('count')) {
count = params['count'] as int;
}
final fileTypes = params['fileTypes'] as List<dynamic>;
FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.custom,
......@@ -27,7 +32,7 @@ class ChooseFileHandler extends MessageHandler {
// 用户取消选择,返回空数组
if (filePickerResult == null) {
return [];
throw Exception('cancel');
}
// 限制最多count个文件
......@@ -40,46 +45,56 @@ class ChooseFileHandler extends MessageHandler {
result.add(await _handleFile(file));
}
return result;
return {'tempFiles': result};
}
Future<Map<String, dynamic>> _handleFile(PlatformFile file) async {
// 获取临时目录
final tempDir = await getTemporaryDirectory();
// 临时文件路径
final uniqueFileName = '${DateTime.now().millisecondsSinceEpoch}_${file.name}';
final tempFilePath = path.join(tempDir.path, uniqueFileName);
// 复制
// 所选中的文件已经被插件复制到临时目录,所以不需要再复制
final originalFile = File(file.path!);
final copiedFile = await originalFile.copy(tempFilePath);
bool isImage = await FileTypeUtil.isImage(copiedFile);
bool isImage = await FileTypeUtil.isImage(originalFile);
bool isVideo = false;
if (!isImage) {
isVideo = await FileTypeUtil.isVideo(copiedFile);
isVideo = await FileTypeUtil.isVideo(originalFile);
}
// 通过image_size_getter获取图片尺寸
SizeResult? sizeResult;
String? thumbTempFilePath;
int? imgWidth, imgHeight;
String? imgThumbFilePath;
if (isImage) {
sizeResult = ImageSizeGetter.getSizeResult(FileInput(copiedFile));
thumbTempFilePath = await ThumbnailUtil.genTempThumbnail(copiedFile, tempDir);
// 通过image_size_getter获取图片尺寸
var sizeResult = ImageSizeGetter.getSizeResult(FileInput(originalFile));
var size = sizeResult.size;
imgWidth = sizeResult.size.width;
imgHeight = sizeResult.size.height;
final tempDir = await getTemporaryDirectory();
imgThumbFilePath = await ThumbnailUtil.genTempThumbnail(originalFile, tempDir);
}
double? videoWidth, videoHeight;
String? videoThumbFilePath;
if (isVideo) {
thumbTempFilePath = await ThumbnailUtil.genVideoThumbnail(copiedFile.path, tempDir);
// 获取视频尺寸
VideoPlayerController controller = VideoPlayerController.file(originalFile);
await controller.initialize();
var size = controller.value.size;
videoWidth = size.width;
videoHeight = size.height;
controller.dispose();
final tempDir = await getTemporaryDirectory();
videoThumbFilePath = await ThumbnailUtil.genVideoThumbnail(originalFile.path, tempDir);
}
// 返回临时文件信息
return {
'tempFilePath': tempFilePath,
'tempFilePath': '${Constant.localServerTempFileUrl}${file.path}',
'size': file.size,
'width': sizeResult != null ? sizeResult.size.width : '',
'height': sizeResult != null ? sizeResult.size.height : '',
'thumbTempFilePath': thumbTempFilePath ?? '',
'fileType': copiedFile.path.split('/').last.split('.').last,
'width': isImage ? imgWidth : (isVideo ? videoWidth : ''),
'height': isImage ? imgHeight : (isVideo ? videoHeight : ''),
'thumbTempFilePath': isImage
? '${Constant.localServerTempFileUrl}$imgThumbFilePath'
: (isVideo ? '${Constant.localServerTempFileUrl}$videoThumbFilePath' : ''),
'fileType': file.extension,
};
}
}
import 'dart:io';
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_size_getter/file_input.dart';
import 'package:image_size_getter/image_size_getter.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class ChooseImageHandler implements MessageHandler {
class ChooseImageHandler extends MessageHandler {
late WebCubit? _webCubit;
late String? _message;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
void setMessage(String message) {
this._message = message;
}
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
......@@ -19,142 +29,22 @@ class ChooseImageHandler implements MessageHandler {
throw Exception('参数错误');
}
// 暂时忽略对此参数的处理
var sizeType = params['sizeType'] as List<dynamic>;
if (sizeType.isEmpty || sizeType.length > 2) {
throw Exception('参数错误');
}
var count = params['count'] as int;
if (count < 1 || count > 9) {
throw Exception('参数错误');
}
// 从相册选择
if (sourceType == 'album') {
if (count == 1) {
return await _selectSingle();
} else {
return await _selectMulti(count);
List<dynamic>? sizeType;
if (params.containsKey('sizeType')) {
sizeType = params['sizeType'] as List<dynamic>;
if (sizeType.isEmpty || sizeType.length > 2) {
throw Exception('参数错误');
}
}
// 拍照
else {
return await _cameraSingle();
}
}
///
/// 选择单张图片
///
/// 将选择的图片放到应用的临时目录中,并在路径前面添加“/temp”,返回给调用方后,调用方可通过http方式访问图片
///
Future<List<Map<String, dynamic>>?> _selectSingle() async {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
// 用户取消选择,返回空数组
if (pickedFile == null) {
return [];
}
// 获取临时目录
final Directory tempDir = await getTemporaryDirectory();
return [await _handleOne(pickedFile, tempDir)];
}
///
/// 选择多张图片
///
/// 将选择的图片放到应用的临时目录中,并在每个文件路径前面添加“/temp”,返回给调用方后,调用方可通过http方式访问图片
///
Future<List<Map<String, dynamic>>?> _selectMulti(int limit) async {
final ImagePicker picker = ImagePicker();
final List<XFile> pickedFileList = await picker.pickMultiImage(limit: limit);
// 用户取消选择,返回空数组
if (pickedFileList.isEmpty) {
return [];
}
// 限制最多limit张
if(pickedFileList.length > limit) {
pickedFileList.removeRange(limit, pickedFileList.length);
}
// 获取临时目录
final Directory tempDir = await getTemporaryDirectory();
final List<Map<String, dynamic>> result = [];
for (final XFile? file in pickedFileList) {
if (file != null) {
result.add(await _handleOne(file, tempDir));
int count = 9;
if (params.containsKey('count')) {
count = params['count'] as int;
if (count < 1 || count > 9) {
throw Exception('参数错误');
}
}
return result;
}
///
/// 拍照
///
Future<List<Map<String, dynamic>>?> _cameraSingle() async {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(source: ImageSource.camera);
// 用户取消选择,返回空数组
if (pickedFile == null) {
return [];
}
// 获取临时目录
final Directory tempDir = await getTemporaryDirectory();
return [await _handleOne(pickedFile, tempDir)];
}
Future<Map<String, dynamic>> _handleOne(XFile pickedFile, Directory tempDir) async {
// 生成唯一文件名
final String fileName = path.basename(pickedFile.path);
final String uniqueFileName = '${DateTime.now().millisecondsSinceEpoch}_$fileName';
// 创建目标文件路径
final String tempFilePath = path.join(tempDir.path, uniqueFileName);
// 复制文件到临时目录
var sourceFile = File(pickedFile.path);
final File copiedFile = await sourceFile.copy(tempFilePath);
// 通过image_size_getter获取图片尺寸
final sizeResult = ImageSizeGetter.getSizeResult(FileInput(sourceFile));
final thumbnailPath = await _genThumbnail(sourceFile, tempDir);
// 返回一个元素的数组
return {
"tempFilePath": "/temp${copiedFile.path}",
"size": copiedFile.lengthSync(),
"width": sizeResult.size.width,
"height": sizeResult.size.height,
"thumbTempFilePath": thumbnailPath,
"fileType": copiedFile.path.split('/').last.split('.').last,
};
}
Future<String?> _genThumbnail(File imageFile, Directory tempDir) async {
try {
// 缩略图路径
final tempPath = tempDir.path;
final targetPath = '$tempPath/thumbnail_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 压缩生成缩略图文件
final compressedFile = await FlutterImageCompress.compressAndGetFile(imageFile.absolute.path, targetPath);
return compressedFile!.path;
} catch (e) {
print('生成缩略图出错: $e');
return null;
}
_webCubit!.setChooseImageCmdFlag(true, _message!);
}
}
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart';
class ChooseVideoHandler extends MessageHandler {
late WebCubit? _webCubit;
late String? _message;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
void setMessage(String message) {
this._message = message;
}
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var sourceType = params['sourceType'] as String;
if (sourceType != 'album' && sourceType != 'camera') {
throw Exception('参数错误');
}
int count = 1;
if (params.containsKey('count')) {
count = params['count'] as int;
if (count < 1 || count > 9) {
throw Exception('参数错误');
}
}
int maxDuration = 60;
if (params.containsKey('maxDuration')) {
maxDuration = params['maxDuration'] as int;
if (maxDuration < 1 || maxDuration > 600) {
throw Exception('参数错误');
}
}
_webCubit!.setChooseVideoCmdFlag(true, _message!);
}
}
// class ChooseVideoHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(params) async {
// if (params is! Map<String, dynamic>) {
// throw Exception('参数错误');
// }
// var sourceType = params['sourceType'] as String;
// if (sourceType != 'album' && sourceType != 'camera') {
// throw Exception('参数错误');
// }
// // 暂时忽略对此参数的处理
// List<dynamic>? sizeType;
// if (params.containsKey('sizeType')) {
// sizeType = params['sizeType'] as List<dynamic>;
// if (sizeType.isEmpty || sizeType.length > 2) {
// throw Exception('参数错误');
// }
// }
//
// int count = 1;
// if (params.containsKey('count')) {
// count = params['count'] as int;
// if (count < 1 || count > 9) {
// throw Exception('参数错误');
// }
// }
//
// int number = 60;
// if (params.containsKey('number')) {
// number = params['number'] as int;
// if (number < 1 || number > 60) {
// throw Exception('参数错误');
// }
// }
//
// // 从相册选择
// if (sourceType == 'album') {
// if (count == 1) {
// return await _selectSingleVideo();
// } else {
// return await _selectMultiVideo(count);
// }
// }
// // 拍摄
// else {
// return await _cameraSingle();
// }
// }
//
// Future<List<dynamic>> _selectSingleVideo() async {
// final ImagePicker picker = ImagePicker();
// final XFile? pickedVideo = await picker.pickVideo(source: ImageSource.gallery);
//
// // 用户取消选择,返回空数组
// if (pickedVideo == null) {
// return [];
// }
//
// // 获取临时目录
// final Directory tempDir = await getTemporaryDirectory();
//
// return [await _handleOne(pickedVideo, tempDir)];
// }
//
// Future<List<dynamic>> _selectMultiVideo(int limit) async {
// final ImagePicker picker = ImagePicker();
// final List<XFile> pickedVideoList = await picker.pickMultiVideo(limit: limit);
//
// // 用户取消选择,返回空数组
// if (pickedVideoList.isEmpty) {
// return [];
// }
//
// // 限制最多limit张
// if (pickedVideoList.length > limit) {
// pickedVideoList.removeRange(limit, pickedVideoList.length);
// }
//
// // 获取临时目录
// final Directory tempDir = await getTemporaryDirectory();
//
// final List<Map<String, dynamic>> result = [];
// for (final XFile? file in pickedVideoList) {
// if (file != null) {
// result.add(await _handleOne(file, tempDir));
// }
// }
// return result;
// }
//
// ///
// /// 拍照
// ///
// Future<List<Map<String, dynamic>>?> _cameraSingle() async {
// final ImagePicker picker = ImagePicker();
//
// final XFile? pickedFile = await picker.pickVideo(source: ImageSource.camera);
//
// // 用户取消选择,返回空数组
// if (pickedFile == null) {
// return [];
// }
//
// // 获取临时目录
// final Directory tempDir = await getTemporaryDirectory();
//
// return [await _handleOne(pickedFile, tempDir)];
// }
//
// Future<Map<String, dynamic>> _handleOne(XFile pickedFile, Directory tempDir) async {
// var sourceFile = File(pickedFile.path);
//
// final thumbnailPath = await ThumbnailUtil.genVideoThumbnail(pickedFile.path, tempDir);
// // 暂时这样进行接口测试
// // 根据视频预览图,获取视频的宽度和高度
// final sizeResult = ImageSizeGetter.getSizeResult(FileInput(File(thumbnailPath!)));
//
// // 返回一个元素的数组
// return {
// "tempFilePath": "/temp${sourceFile.path}",
// "size": sourceFile.lengthSync(),
// "width": sizeResult.size.width,
// "height": sizeResult.size.height,
// "thumbTempFilePath": '/temp$thumbnailPath',
// "fileType": sourceFile.path.split('/').last.split('.').last,
// };
// }
// }
import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:dio/dio.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:video_compress/video_compress.dart';
class CompressImageHandler implements MessageHandler {
/// 压缩图片
///
class CompressImageHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
String url = params['url'];
int quality = params['quality'];
int compressedWidth = params['compressedWidth'];
int compressedHeight = params['compressedHeight'];
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final url = params['url'] as String?;
if (url == null || url.isEmpty) {
throw Exception('参数错误');
}
final quality = params['quality'] as int?;
if (quality == null) {
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('参数错误');
}
if (compressedWidth == null) {
compressedWidth = compressedHeight;
} else if (compressedHeight == null) {
compressedHeight = compressedWidth;
}
var originFile = File(url);
// 获取后缀名
String ext = path.extension(url);
final Directory tempDir = await getTemporaryDirectory();
String srcPath;
// 如果是远程文件,先进行下载
if (url.startsWith('http')) {
srcPath = path.join(tempDir.path, '${DateTime.now().millisecondsSinceEpoch}$ext');
// Dio 下载文件
final resp = await Dio().download(url, srcPath);
if (resp.statusCode != 200) {
throw Exception('远程文件下载失败');
}
} else {
srcPath = url;
}
var originFile = File(srcPath);
if (!originFile.existsSync()) {
throw Exception('文件不存在');
}
final String uniqueFileName = '${DateTime.now().millisecondsSinceEpoch}$ext';
final String tempPath = path.join(tempDir.path, uniqueFileName);
var result = await FlutterImageCompress.compressAndGetFile(
// 压缩处理
var croppedFile = await FlutterImageCompress.compressAndGetFile(
originFile.absolute.path,
uniqueFileName,
quality: 88,
rotate: 180,
tempPath,
quality: quality,
minWidth: compressedWidth!,
minHeight: compressedHeight!,
);
return true;
if (croppedFile == null) {
throw Exception('图片压缩失败');
}
return {"tempFilePath": "/temp${croppedFile.path}", "size": await croppedFile.length()};
}
}
class CompressVideoHandler implements MessageHandler {
/// 压缩视频
///
class CompressVideoHandler extends MessageHandler {
@override
Future handleMessage(dynamic params) {
// TODO: implement handleMessage
throw UnimplementedError();
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final url = params['url'] as String?;
if (url == null || url.isEmpty) {
throw Exception('参数错误');
}
final quality = params['quality'] as String?;
if (quality == null || quality.isEmpty) {
throw Exception('参数错误');
}
final resolution = params['resolution'] as double?;
if (resolution == null || resolution > 1 || resolution <= 0) {
throw Exception('参数错误');
}
String srcPath;
if (url.startsWith('http')) {
String ext = path.extension(url);
final Directory tempDir = await getTemporaryDirectory();
srcPath = path.join(tempDir.path, '${DateTime.now().millisecondsSinceEpoch}$ext');
// Dio 下载文件
final resp = await Dio().download(url, srcPath);
if (resp.statusCode != 200) {
throw Exception('远程文件下载失败');
}
} else {
srcPath = url;
}
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 'dart:io';
import 'dart:typed_data';
import 'package:appframe/config/constant.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:dio/dio.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
class CropImageHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var url = params['url'] as String?;
if (url == null || url.isEmpty) {
throw Exception('参数错误');
}
var cropScale = params['cropScale'] as String?;
if (cropScale == null || cropScale.isEmpty) {
throw Exception('参数错误');
}
var scaleArray = ['16:9', '9:16', '4:3', '3:4', '5:4', '4:5', '1:1'];
if (!scaleArray.contains(cropScale)) {
throw Exception('参数错误');
}
try {
String outputPath = await _cropImageByRatio(url, cropScale);
return {'tempFilePath': '${Constant.localServerTempFileUrl}$outputPath'};
} catch (e) {
throw Exception('裁剪出错');
}
}
Future<String> _cropImageByRatio(String url, String cropScale) async {
if (url.startsWith(Constant.localServerUrl)) {
url = url.replaceFirst(Constant.localServerUrl, '');
}
if (url.startsWith(Constant.localServerTemp)) {
url = url.replaceFirst(Constant.localServerTemp, '');
}
String extension = path.extension(url);
Directory cacheDirectory = await getApplicationCacheDirectory();
File imageFile;
// 对于远程图片,先下载到应用缓存目录,然后生成File
if (url.startsWith('https://') || url.startsWith('http://')) {
String tempFilePath = '${cacheDirectory.path}/${Uuid().v4()}$extension';
Dio dio = Dio();
Response resp = await dio.download(url, tempFilePath);
if (resp.statusCode != 200) {
throw Exception('下载图片失败');
}
dio.close(force: true);
imageFile = File(tempFilePath);
}
// 对于本地路径,直接生成File
else {
imageFile = File(url);
}
// 1. 读取图片文件并解码
Uint8List bytes = await imageFile.readAsBytes();
img.Image originalImage = img.decodeImage(bytes)!;
int originalWidth = originalImage.width;
int originalHeight = originalImage.height;
// 2. 计算目标裁剪区域
var targetRatio = cropScale.split(":");
double targetAspect = int.parse(targetRatio[0]) / int.parse(targetRatio[1]); // 例如 16/9
double currentAspect = originalWidth / originalHeight;
int cropWidth, cropHeight;
if (currentAspect > targetAspect) {
// 原图更宽,以高度为基准裁剪宽度
cropHeight = originalHeight;
cropWidth = (originalHeight * targetAspect).round();
} else {
// 原图更高,以宽度为基准裁剪高度
cropWidth = originalWidth;
cropHeight = (originalWidth / targetAspect).round();
}
// 3. 计算居中裁剪的起始点
int startX = (originalWidth - cropWidth) ~/ 2;
int startY = (originalHeight - cropHeight) ~/ 2;
// 4. 执行裁剪
img.Image croppedImage = img.copyCrop(originalImage, x: startX, y: startY, width: cropWidth, height: cropHeight);
// 5. 保存裁剪后的图片
String outputPath = '${cacheDirectory.path}/${Uuid().v4()}$extension';
await File(outputPath).writeAsBytes(img.encodeJpg(croppedImage));
return outputPath;
}
}
......@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:device_info_plus/device_info_plus.dart';
class DeviceInfoHandler implements MessageHandler {
class DeviceInfoHandler extends MessageHandler {
@override
Future<Map<String, dynamic>> handleMessage(dynamic params) async {
var deviceInfoPlugin = DeviceInfoPlugin();
......@@ -12,11 +12,15 @@ class DeviceInfoHandler implements MessageHandler {
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
return {
"brand": androidInfo.brand,
"model": androidInfo.model,
"system": androidInfo.version.release,
"platform": "Android",
"memorySize": (androidInfo.physicalRamSize ~/ (1024 * 1024)).toString(),
'abi': '${androidInfo.supportedAbis}',
'deviceAbi': androidInfo.supportedAbis.isNotEmpty ? androidInfo.supportedAbis[0] : '',
'benchmarkLevel': -1,
'brand': androidInfo.brand,
'model': androidInfo.model,
'system': androidInfo.version.release,
'platform': "Android",
'cpuType': androidInfo.hardware,
"memorySize": androidInfo.physicalRamSize,
};
} else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
......
import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:dio/dio.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class DownloadFileHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final url = params['url'] as String;
// 获取后缀名
String ext = path.extension(url);
// 获取应用文档目录路径
final Directory tempDir = await getApplicationDocumentsDirectory();
final targetPath = path.join(tempDir.path, '${DateTime.now().millisecondsSinceEpoch}$ext');
final resp = await Dio().download(url, targetPath);
if (resp.statusCode != 200) {
throw Exception('文件下载失败');
}
return {'tempFilePath': '/temp$targetPath'};
}
}
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart';
class GoLoginHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
Future<dynamic> handleMessage(params) async {
_webCubit!.goWechatAuth();
return;
}
}
import 'dart:io';
import 'dart:ui' as ui;
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart';
import 'package:dio/dio.dart';
import 'package:exif/exif.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class ImageInfoHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final url = params['url'] as String;
String filePath;
if (url.startsWith('http')) {
// 获取后缀名
String ext = path.extension(url);
// 获取应用文档目录路径
final Directory tempDir = await getApplicationDocumentsDirectory();
final targetPath = path.join(tempDir.path, '${DateTime.now().millisecondsSinceEpoch}$ext');
final resp = await Dio().download(url, targetPath);
if (resp.statusCode != 200) {
throw Exception('文件下载失败');
}
filePath = targetPath;
} else {
filePath = url;
}
// 读取图片信息
final file = File(filePath);
if (!file.existsSync()) {
throw Exception('图片文件不存在');
}
final bytes = await file.readAsBytes();
// 获取基本图片信息(宽高)
final codec = await ui.instantiateImageCodec(bytes);
final frameInfo = await codec.getNextFrame();
final image = frameInfo.image;
int width = image.width;
int height = image.height;
// 读取 EXIF 信息
int? orientation;
final exifData = await readExifFromBytes(bytes);
if (exifData.isNotEmpty && exifData.containsKey('Image Orientation')) {
final orientationTag = exifData['Image Orientation'];
if (orientationTag != null) {
orientation = orientationTag.values.firstAsInt();
}
}
String orientationStr;
switch (orientation) {
case 1:
orientationStr = 'up';
break;
case 2:
orientationStr = 'up-mirrored';
break;
case 3:
orientationStr = 'down';
break;
case 4:
orientationStr = 'down-mirrored';
break;
case 5:
orientationStr = 'left-mirrored';
break;
case 6:
orientationStr = 'left';
break;
case 8:
orientationStr = 'right';
break;
default:
orientationStr = 'up';
}
// 获取MIME类型并转换为文件扩展名
final mimeType = await FileTypeUtil.getMimeType(file);
final fileExtension = FileTypeUtil.getExtensionFromMime(mimeType);
return {
'tempFilePath': '/temp$filePath',
'width': width,
'height': height,
'orientation': orientationStr,
'type': fileExtension,
};
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:geolocator/geolocator.dart';
class LocationHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
// if (params is! Map<String, dynamic>) {
// throw Exception('参数错误');
// }
//
// var type = params['type'] as String?;
// type = type ?? 'gcj02';
GeolocatorPlatform geolocator = GeolocatorPlatform.instance;
// 检查定位服务是否启用
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
throw '定位服务未启用';
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
throw 'no auth';
}
}
if (permission == LocationPermission.deniedForever) {
throw 'no auth';
}
final pos = await geolocator.getCurrentPosition();
return {
'latitude': pos.latitude,
'longitude': pos.longitude,
'speed': pos.speed,
'accuracy': pos.accuracy,
'altitude': pos.altitude,
'verticalAccuracy': 0,
'horizontalAccuracy': 0,
};
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
class NetworkTypeHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
var connectivity = Connectivity();
final List<ConnectivityResult> connectivityResult = await connectivity.checkConnectivity();
String networkType;
if (connectivityResult.contains(ConnectivityResult.wifi)) {
// Wi-fi is available.
// Note for Android:
// When both mobile and Wi-Fi are turned on system will return Wi-Fi only as active network type
networkType = "wifi";
} else if (connectivityResult.contains(ConnectivityResult.mobile)) {
// Mobile network available.
networkType = "mobile";
} else if (connectivityResult.contains(ConnectivityResult.ethernet)) {
// Ethernet connection available.
networkType = "ethernet";
} /*else if (connectivityResult.contains(ConnectivityResult.vpn)) {
// Vpn connection active.
// Note for iOS and macOS:
// There is no separate network interface type for [vpn].
// It returns [other] on any device (also simulator)
} else if (connectivityResult.contains(ConnectivityResult.bluetooth)) {
// Bluetooth connection available.
} */else if (connectivityResult.contains(ConnectivityResult.other)) {
// Connected to a network which is not in the above mentioned networks.
networkType = "none";
} else if (connectivityResult.contains(ConnectivityResult.none)) {
// No available network types
networkType = "none";
} else {
networkType = "none";
}
return {
"networkType": networkType,
"signalStrength":1,
"weakNet":false
};
}
}
import 'package:appframe/config/constant.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:open_file/open_file.dart';
import 'package:url_launcher/url_launcher.dart';
class OpenDocumentHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var url = params['url'] as String;
if (url.isEmpty) {
throw Exception('参数错误');
}
if (url.startsWith(Constant.localServerUrl)) {
url = url.replaceFirst(Constant.localServerUrl, '');
}
if (url.startsWith(Constant.localServerTemp)) {
url = url.replaceFirst(Constant.localServerTemp, '');
}
if (url.startsWith('http')) {
return await _launchInBrowser(Uri.parse(url));
} else {
return await _open(url);
}
}
Future<bool> _launchInBrowser(Uri url) async {
if (await canLaunchUrl(url)) {
return await launchUrl(url, mode: LaunchMode.platformDefault);
} else {
throw Exception('Could not launch $url');
}
}
/// 暂时就简单打开一下,未做复杂调用
Future<bool> _open(String filePath) async {
var r = await OpenFile.open("filePath");
return r.type == ResultType.done;
}
}
import 'package:appframe/config/routes.dart';
import 'package:appframe/services/dispatcher.dart';
class OpenLinkHandler extends MessageHandler {
@override
Future<bool> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final String url = params['url'] as String;
if (url.isEmpty) {
throw Exception('参数错误');
}
return _openLink(url);
}
bool _openLink(String url) {
router.push('/link', extra: {'url': url});
return true;
}
}
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:fluwx/fluwx.dart';
class OpenWeappHandler extends MessageHandler {
late Fluwx _fluwx;
@override
Future<bool> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final appid = params['appid'] as String;
final path = params['path'] as String;
final envVersion = params['envVersion'] as String?;
if (appid.isEmpty || path.isEmpty) {
throw Exception('参数错误');
}
if (envVersion != null && envVersion != 'release' && envVersion != 'trial' && envVersion != 'develop') {
throw Exception('参数错误');
}
_fluwx = getIt.get<Fluwx>();
// _fluwx.addSubscriber(_responseListener);
try {
return await _fluwx.open(
target: MiniProgram(username: appid, path: path, miniProgramType: _getWXMiniProgramType(envVersion)),
);
} catch (e) {
print(e);
return false;
}
}
// void _responseListener(response) {
// print("response--------------------------------");
// if (response is WeChatLaunchMiniProgramResponse) {
// print("小程序跳转 2 --------------------------------");
// print(response);
// _fluwx.removeSubscriber(_responseListener);
// }
// }
WXMiniProgramType _getWXMiniProgramType(String? envVersion) {
switch (envVersion) {
case 'release':
return WXMiniProgramType.release;
case 'trial':
return WXMiniProgramType.preview;
case 'develop':
return WXMiniProgramType.test;
default:
return WXMiniProgramType.preview;
}
}
}
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart';
class OrientationHandler extends MessageHandler {
late WebCubit? _webCubit;
late String? _message;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
void setMessage(String message) {
this._message = message;
}
@override
Future<dynamic> handleMessage(params) async {
_webCubit!.setOrientationCmdFlag(true, _message!);
}
}
......@@ -97,42 +97,42 @@ Future<void> _resetPlayerStatus() async {
localPlayedFilePath = null;
}
class AudioPlayHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await startLocalPlayer(params['url']);
return result;
}
}
class AudioPauseHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await pauseLocalPlayer();
return result;
}
}
class AudioResumeHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await resumeLocalPlayer();
return result;
}
}
class AudioStopHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await stopLocalPlayer();
return result;
}
}
class AudioClearHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
closeLocalPlayer();
return true;
}
}
// class AudioPlayHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// bool result = await startLocalPlayer(params['url']);
// return result;
// }
// }
//
// class AudioPauseHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// bool result = await pauseLocalPlayer();
// return result;
// }
// }
//
// class AudioResumeHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// bool result = await resumeLocalPlayer();
// return result;
// }
// }
//
// class AudioStopHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// bool result = await stopLocalPlayer();
// return result;
// }
// }
//
// class AudioClearHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// closeLocalPlayer();
// return true;
// }
// }
......@@ -13,7 +13,7 @@ Future<bool> initLocalRecorder() async {
// 请求麦克风权限
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('麦克风权限未授权!');
throw RecordingPermissionException('no auth');
}
if (!(!isLocalRecording && !isLocalPauseRecording && localRecordedFilePath == null)) {
......@@ -133,41 +133,41 @@ Future<void> _resetRecorderStatus() async {
localRecordedFilePath = null;
}
class AudioRecorderStartHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await startLocalRecording();
return result;
}
}
class AudioRecorderPauseHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await pauseLocalRecording();
return result;
}
}
class AudioRecorderResumeHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
bool result = await resumeLocalRecording();
return result;
}
}
class AudioRecorderStopHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
return await stopLocalRecording();
}
}
class AudioRecorderClearHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
closeLocalRecorder();
return true;
}
}
// class AudioRecorderStartHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// bool result = await startLocalRecording();
// return result;
// }
// }
//
// class AudioRecorderPauseHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// bool result = await pauseLocalRecording();
// return result;
// }
// }
//
// class AudioRecorderResumeHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// bool result = await resumeLocalRecording();
// return result;
// }
// }
//
// class AudioRecorderStopHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// return await stopLocalRecording();
// }
// }
//
// class AudioRecorderClearHandler extends MessageHandler {
// @override
// Future<dynamic> handleMessage(dynamic params) async {
// closeLocalRecorder();
// return true;
// }
// }
import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:dio/dio.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class SaveFileToDisKHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var filePath = params['filePath'] as String;
if (filePath.isEmpty) {
throw Exception('参数错误');
}
String ext = path.extension(filePath);
final Directory tempDir = await getApplicationDocumentsDirectory();
final targetPath = path.join(tempDir.path, '${DateTime.now().millisecondsSinceEpoch}$ext');
if (filePath.startsWith('http')) {
final resp = await Dio().download(filePath, targetPath);
if (resp.statusCode != 200) {
throw Exception('文件下载失败');
}
return true;
} else {
// 将filePath路径的文件保存到targetPath
final f = await File(filePath).copy(targetPath);
if (f.existsSync()) {
return true;
} else {
return false;
}
}
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:gallery_saver_plus/gallery_saver.dart';
class SaveToAlbumHandler implements MessageHandler {
class SaveToAlbumHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
......
import 'dart:async';
import 'package:appframe/services/dispatcher.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter/material.dart';
import 'package:appframe/bloc/web_cubit.dart';
class ScanCodeHandler extends MessageHandler {
late WebCubit? _webCubit;
@override
Future<dynamic> handleMessage(params) async {
try {
final result = await _webCubit!.goScanCode();
// 返回扫码结果
return result;
} finally {
_unfollowCubit();
}
}
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
}
\ No newline at end of file
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 {
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);
}
}
......@@ -2,7 +2,7 @@ import 'package:appframe/config/locator.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SetStorageSyncHandler extends MessageHandler {
class SetStorageHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
......@@ -24,7 +24,7 @@ class SetStorageSyncHandler extends MessageHandler {
}
}
class GetStorageSyncHandler extends MessageHandler {
class GetStorageHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! String || params.isEmpty) {
......@@ -39,7 +39,22 @@ class GetStorageSyncHandler extends MessageHandler {
}
}
class ClearStorageSyncHandler extends MessageHandler {
class RemoveStorageHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! String || params.isEmpty) {
throw Exception('参数错误');
}
try {
return await getIt.get<SharedPreferences>().remove(params);
} catch (e) {
throw Exception(e.toString());
}
}
}
class ClearStorageHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
return await getIt.get<SharedPreferences>().clear();
......
import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:archive/archive.dart';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
class UpgradeHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final url = params['url'] as String;
final version = params['version'] as String;
// 1 下载
var direct = await getExternalStorageDirectory();
var saveFilePath = '${direct?.path}/dist_$version.zip';
var dio = Dio();
final resp = await dio.download(
url,
saveFilePath,
onReceiveProgress: (received, total) {
if (total != -1) {
print((received / total * 100).toStringAsFixed(0) + "%");
}
},
);
dio.close();
if (resp.statusCode != 200) {
throw Exception('文件下载失败');
}
// 2 解压
var outputDirectory = '${direct?.path}/http_dist_assets';
await _extract(saveFilePath, outputDirectory);
}
Future<void> _extract(String zipFilePath, String outputDirectory) async {
var file = File(zipFilePath);
var bytes = await file.readAsBytes();
// 解码 ZIP 文件
final archive = ZipDecoder().decodeBytes(bytes);
// 提取文件到指定目录
for (final file in archive) {
final filename = file.name;
if (file.isFile) {
final data = file.content as List<int>;
final outputFile = File('$outputDirectory/$filename');
outputFile.createSync(recursive: true);
outputFile.writeAsBytesSync(data);
} else {
// 创建目录
Directory('$outputDirectory/$filename').createSync(recursive: true);
}
}
}
}
import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as path;
import 'package:uuid/uuid.dart';
class UploadFileHandler extends MessageHandler {
@override
Future 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('参数错误');
}
final String? busi = params['busi'] as String?;
if (busi == null || busi.isEmpty) {
throw Exception('参数错误');
}
final String? subBusi = params['subBusi'] as String?;
if (subBusi == null || subBusi.isEmpty) {
throw Exception('参数错误');
}
final result = await compute(_handleUpload, {'filePath': tempFilePath, 'busi': busi, 'subBusi': subBusi});
// final result = await _handleUpload({'filePath': tempFilePath});
return result;
}
static const _bxeBaseUrl = 'https://iotapp-dev.banxiaoer.com/iotapp';
static const _signatureNewUrl = '/api/v1/obs/multipart/signaturenew';
static const _signatureNextUrl = '/api/v1/obs/multipart/signaturenext';
static const _completeUrl = '/api/v1/obs/multipart/complete';
/// 在Isolate中执行
static Future<Map<String, dynamic>> _handleUpload(Map<String, dynamic> fileParams) async {
String filePath = fileParams['filePath'] as String;
String busi = fileParams['busi'] as String;
String subBusi = fileParams['subBusi'] as String;
print('参数-------');
print('filePath:$filePath ');
print('busi:$busi ');
print('subBusi:$subBusi ');
print('参数-------');
if (filePath.startsWith(Constant.localServerUrl)) {
filePath = filePath.replaceFirst(Constant.localServerUrl, '');
}
if (filePath.startsWith(Constant.localServerTemp)) {
filePath = filePath.replaceFirst(Constant.localServerTemp, '');
}
final bxeApiService = ApiService(baseUrl: _bxeBaseUrl);
// 由于服务端签名时未设置Content-Type,这里必须设置为空,否则会报签名错误
// 由于封装有默认值,所以不能不设置
final obsApiService = ApiService(defaultHeaders: {'Content-Type': '', 'Accept': ''});
String logicPrefix = _getLoginPrefix(busi, subBusi);
print('logicPrefix: $logicPrefix');
//并行上传分段
final uploadResult = await _uploadInParallel(bxeApiService, obsApiService, logicPrefix, filePath);
String objectKey = uploadResult['objectKey'] as String;
String bucket = uploadResult['bucket'] as String;
String uploadId = uploadResult['uploadId'] as String;
Map<int, String> tagsMap = uploadResult['tagsMap'] as Map<int, String>;
//请求合并文件
String location = await _merge(bxeApiService, objectKey, bucket, uploadId, tagsMap);
print('location: $location');
//关闭Dio
bxeApiService.close();
obsApiService.close();
return {'url': _addPreUrl(location)};
}
/// 并行上传
static Future<Map<String, dynamic>> _uploadInParallel(
ApiService bxeApiService,
ApiService obsApiService,
String logicPrefix,
String filePath, {
int maxConcurrency = 5,
}) async {
//判断文件
File file = File(filePath);
if (!file.existsSync()) {
throw Exception('文件不存在');
}
//暂时仅支持200M的文件上传
final fileSize = file.lengthSync();
if (fileSize > 1024 * 1024 * 200) {
throw Exception('上传的文件过大');
}
//分段大小2M
final chunkSize = Constant.obsUploadChunkSize;
//分段总数
final totalChunks = (fileSize / chunkSize).ceil();
final randomAccessFile = file.openSync();
//bucket 存储桶名称 : bxe-files | bxe-pics | bxe-videos
String bucket;
if (await FileTypeUtil.isImage(file)) {
bucket = 'bxe-pics';
} else if (await FileTypeUtil.isVideo(file)) {
bucket = 'bxe-videos';
} else {
bucket = 'bxe-files';
}
//生成唯一文件名,
// String objectKey = 'd2/test/file.csv';
var uuid = Uuid();
String objectKey = '$logicPrefix/${uuid.v4()}${path.extension(file.path)}';
print('objectKey: $objectKey');
String uploadId = '';
Map<int, String> tagsMap = {};
final futures = <Future>[];
for (int i = 0; i < totalChunks; i++) {
// 控制并发数量
if (futures.length >= maxConcurrency) {
await Future.wait(futures);
futures.clear();
}
final start = i * chunkSize;
final actualChunkSize = (i + 1) * chunkSize > fileSize ? fileSize - start : chunkSize;
final chunk = Uint8List(actualChunkSize);
randomAccessFile.setPositionSync(start);
await randomAccessFile.readInto(chunk, 0, actualChunkSize);
String chunkSignUrl;
if (i == 0) {
final initResult = await _init(bxeApiService, objectKey, bucket);
uploadId = initResult['upload_id'] as String;
chunkSignUrl = initResult['signed_url'] as String;
} else {
final nextResult = await _next(bxeApiService, objectKey, bucket, uploadId, i + 1);
chunkSignUrl = nextResult['signed_url'] as String;
}
print('chunkSignUrl: $chunkSignUrl');
// await _uploadChunkWithRetry(obsApiService, chunkSignUrl, i, chunk, tagsMap);
final future = _uploadChunkWithRetry(obsApiService, chunkSignUrl, i, chunk, tagsMap);
futures.add(future);
}
// 等待剩余的上传完成
if (futures.isNotEmpty) {
await Future.wait(futures);
}
randomAccessFile.closeSync();
return {'objectKey': objectKey, 'bucket': bucket, 'uploadId': uploadId, 'tagsMap': tagsMap};
}
/// 初始化,请求后端获取签名信息和上传任务ID
static Future<Map<String, dynamic>> _init(ApiService bxeApiService, String objectKey, String bucket) async {
var endpoint = '$_signatureNewUrl?objectKey=$objectKey&bucket=$bucket';
final resp = await bxeApiService.get(endpoint);
return resp.data;
}
/// 每次上传前,请求后端获取签名信息
static Future<Map<String, dynamic>> _next(
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;
}
/// 上传段,按照最大重试次数进行上传重试
static Future<void> _uploadChunkWithRetry(
ApiService obsApiService,
String signUrl,
int chunkIndex,
Uint8List chunk,
Map<int, String> tagsMap, {
int maxRetries = 3,
}) async {
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
final resp = await _uploadChunk(obsApiService, signUrl, chunk);
if (resp.statusCode == 200) {
final etags = resp.headers['etag'] as List<String>;
tagsMap[chunkIndex + 1] = etags[0];
return; // 上传成功
} else {
throw Exception('Chunk $chunkIndex upload failed: ${resp.statusCode}');
}
} catch (e) {
if (attempt == maxRetries) {
throw Exception('Chunk $chunkIndex upload failed after $maxRetries attempts: $e');
}
// 等待后重试
await Future.delayed(Duration(seconds: 2 * attempt));
}
}
}
/// 上传段
static Future<Response> _uploadChunk(ApiService obsApiService, String signUrl, Uint8List chunk) async {
var url = signUrl.replaceFirst('AWSAccessKeyId=', 'AccessKeyId=').replaceFirst(':443', '');
try {
Response response = await obsApiService.put(url, chunk);
return response;
} catch (e) {
print('Chunk upload failed: $e');
throw Exception('Chunk upload failed: $e');
}
}
/// 请求合并文件
static Future<String> _merge(
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]});
}
final response = await bxeApiService.post(_completeUrl, {
'objectKey': objectKey,
'bucket': bucket,
'uploadId': uploadId,
'parts': parts,
});
if (response.statusCode != 200) {
throw Exception('合并文件失败');
}
return response.data["location"];
}
static String _getLoginPrefix(String busi, String subBusi) {
var now = DateTime.now();
var year = now.year;
var month = now.month;
var day = now.day;
return 'd2/pridel/user/$year$month$day/bxe/${busi}_$subBusi';
}
static String _addPreUrl(String location) {
// /bxe-pics/d2/pridel/user/20251017/bxe/bxe_homework/f4ea233d-9e1b-4a3f-bc8f-b64e776f42a6.jpg
if (location.startsWith('/bxe-files')) {
return 'https://files-obs.banxiaoer.com${location.substring(10)}';
} else if (location.startsWith('/bxe-pics')) {
return 'https://pics-obs.banxiaoer.com${location.substring(9)}';
} else if (location.startsWith('/bxe-videos')) {
return 'https://videos-obs.banxiaoer.com${location.substring(11)}';
} else {
return location;
}
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:vibration/vibration.dart';
class VibrateShortHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
if (params != null && params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
int? duration;
int? amplitude;
if (params != null) {
var type = params['type'] as String?;
type ??= 'light';
duration = params['duration'] as int?;
duration ??= 15;
if (type == 'light') {
amplitude = 10;
} else if (type == 'medium') {
amplitude = 125;
} else if (type == 'heavy') {
amplitude = 250;
} else {
amplitude = 10;
}
}
Vibration.vibrate(duration: duration ?? 15, amplitude: amplitude ?? 10);
return true;
}
}
import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart';
import 'package:dio/dio.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
Future handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
final url = params['url'] as String;
String filePath;
if (url.startsWith('http')) {
// 获取后缀名
String ext = path.extension(url);
// 获取应用文档目录路径
final Directory tempDir = await getApplicationDocumentsDirectory();
final targetPath = path.join(tempDir.path, '${DateTime.now().millisecondsSinceEpoch}$ext');
final resp = await Dio().download(url, targetPath);
if (resp.statusCode != 200) {
throw Exception('文件下载失败');
}
filePath = targetPath;
} else {
filePath = url;
}
final file = File(filePath);
if (!file.existsSync()) {
throw Exception('视频文件不存在');
}
// 使用video_compress获取视频信息
final mediaInfo = await VideoCompress.getMediaInfo(filePath);
// 获取文件大小
final size = await file.length();
// 获取MIME类型并转换为文件扩展名
final mimeType = await FileTypeUtil.getMimeType(file);
final fileExtension = FileTypeUtil.getExtensionFromMime(mimeType);
return {
'tempFilePath': '/temp$filePath',
'width': mediaInfo.width ?? 0,
'height': mediaInfo.height ?? 0,
'type': fileExtension,
'duration': (mediaInfo.duration ?? 0) / 1000, // 转换为秒
'size': size,
};
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:network_info_plus/network_info_plus.dart';
class WifiInfoHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
final info = NetworkInfo();
// final wifiName = await info.getWifiName(); // "FooNetwork"
// final wifiBSSID = await info.getWifiBSSID(); // 11:22:33:44:55:66
// final wifiIP = await info.getWifiIP(); // 192.168.1.43
// final wifiIPv6 = await info.getWifiIPv6(); // 2001:0db8:85a3:0000:0000:8a2e:0370:7334
// final wifiSubmask = await info.getWifiSubmask(); // 255.255.255.0
// final wifiBroadcast = await info.getWifiBroadcast(); // 192.168.1.255
// final wifiGateway = await info.getWifiGatewayIP(); // 192.168.1.1
// SSID: string, // Wi-Fi 的 SSID
// BSSID: string, // Wi-Fi 的 BSSID
// secure: boolean, // Wi-Fi 是否安全
// signalStrength: number, // Wi-Fi 信号强度, 安卓取值 0 ~ 100 ,iOS 取值 0 ~ 1 ,值越大强度越大
// frequency: number // Wi-Fi 频段单位 MHz
return {
// 'SSID': ''
'BSSID': await info.getWifiBSSID(),
// 'secure': null,
// 'signalStrength': 100,
// 'frequency':''
};
}
}
\ No newline at end of file
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/services/dispatcher.dart';
class WindowInfoHandler extends MessageHandler {
late WebCubit? _webCubit;
late String? _message;
@override
void setCubit(WebCubit cubit) {
this._webCubit = cubit;
}
void _unfollowCubit() {
this._webCubit = null;
}
@override
void setMessage(String message) {
this._message = message;
}
@override
Future<dynamic> handleMessage(dynamic params) async {
_webCubit!.setWindowInfoCmdFlag(true, _message!);
}
}
{
"@@locale": "en",
"appTitle": "WeChat Asset Picker Demo",
"appVersion": "Version: {version}",
"appVersionUnknown": "unknown",
"navMulti": "Multi",
"navSingle": "Single",
"navCustom": "Custom",
"selectedAssetsText": "Selected Assets",
"pickMethodNotice": "Pickers in this page are located at the {dist}, defined by `pickMethods`.",
"pickMethodImageName": "Image picker",
"pickMethodImageDescription": "Only pick image from device.",
"pickMethodVideoName": "Video picker",
"pickMethodVideoDescription": "Only pick video from device. (Includes Live Photos on iOS and macOS.)",
"pickMethodAudioName": "Audio picker",
"pickMethodAudioDescription": "Only pick audio from device.",
"pickMethodLivePhotoName": "Live Photo picker",
"pickMethodLivePhotoDescription": "Only pick Live Photos from device.",
"pickMethodCameraName": "Pick from camera",
"pickMethodCameraDescription": "Allow to pick an asset through camera.",
"pickMethodCameraAndStayName": "Pick from camera and stay",
"pickMethodCameraAndStayDescription": "Take a photo or video with the camera picker, select the result and stay in the entities list.",
"pickMethodCommonName": "Common picker",
"pickMethodCommonDescription": "Pick images and videos.",
"pickMethodThreeItemsGridName": "3 items grid",
"pickMethodThreeItemsGridDescription": "Picker will served as 3 items on cross axis. (pageSize must be a multiple of the gridCount)",
"pickMethodCustomFilterOptionsName": "Custom filter options",
"pickMethodCustomFilterOptionsDescription": "Add filter options for the picker.",
"pickMethodPrependItemName": "Prepend special item",
"pickMethodPrependItemDescription": "A special item will prepend to the assets grid.",
"pickMethodNoPreviewName": "No preview",
"pickMethodNoPreviewDescription": "You cannot preview assets during the picking, the behavior is like the WhatsApp/MegaTok pattern.",
"pickMethodKeepScrollOffsetName": "Keep scroll offset",
"pickMethodKeepScrollOffsetDescription": "Pick assets from same scroll position.",
"pickMethodChangeLanguagesName": "Change Languages",
"pickMethodChangeLanguagesDescription": "Pass AssetPickerTextDelegate to change between languages (e.g. EnglishAssetPickerTextDelegate).",
"pickMethodPreventGIFPickedName": "Prevent GIF being picked",
"pickMethodPreventGIFPickedDescription": "Use selectPredicate to banned GIF picking when tapped.",
"pickMethodCustomizableThemeName": "Customizable theme (ThemeData)",
"pickMethodCustomizableThemeDescription": "Picking assets with the light theme or with a different color.",
"pickMethodPathNameBuilderName": "Path name builder",
"pickMethodPathNameBuilderDescription": "Add \uD83C\uDF6D after paths name.",
"pickMethodWeChatMomentName": "WeChat Moment",
"pickMethodWeChatMomentDescription": "Pick assets with images or only 1 video.",
"pickMethodCustomImagePreviewThumbSizeName": "Custom image preview thumb size",
"pickMethodCustomImagePreviewThumbSizeDescription": "You can reduce the thumb size to get faster load speed.",
"customPickerNotice": "This page contains customized pickers with different asset types, different UI layouts, or some use case for specific apps. Contribute to add your custom picker are welcomed.\nPickers in this page are located at the lib/customs/pickers folder.",
"customPickerCallThePickerButton": "\uD83C\uDF81 Call the Picker",
"customPickerDirectoryAndFileName": "Directory+File picker",
"customPickerDirectoryAndFileDescription": "This is a custom picker built for `File`.\nBy browsing this picker, we want you to know that you can build your own picker components using the entity's type you desired.\n\nIn this page, picker will grab files from `getApplicationDocumentsDirectory`, then check whether it contains images. Put files into the path to see how this custom picker work.",
"customPickerMultiTabName": "Multi tab picker",
"customPickerMultiTabDescription": "The picker contains multiple tab with different types of assets for the picking at the same time.",
"customPickerMultiTabTab1": "All",
"customPickerMultiTabTab2": "Videos",
"customPickerMultiTabTab3": "Images",
"customPickerInstagramLayoutName": "Instagram layout picker",
"customPickerInstagramLayoutDescription": "The picker reproduces Instagram layout with preview and scroll animations. It's also published as the package insta_assets_picker."
}
\ No newline at end of file
{
"@@locale": "zh",
"appTitle": "WeChat Asset Picker 示例",
"appVersion": "版本:{version}",
"appVersionUnknown": "未知",
"navMulti": "多选",
"navSingle": "单选",
"navCustom": "自定义",
"selectedAssetsText": "已选的资源",
"pickMethodNotice": "该页面的所有选择器的代码位于 {dist},由 `pickMethods` 定义。",
"pickMethodCommonName": "常用选择",
"pickMethodCommonDescription": "选择图片和视频。",
"pickMethodImageName": "图片选择",
"pickMethodImageDescription": "仅选择图片。",
"pickMethodVideoName": "视频选择",
"pickMethodVideoDescription": "仅选择视频。",
"pickMethodAudioName": "音频选择",
"pickMethodAudioDescription": "仅选择音频。",
"pickMethodLivePhotoName": "实况图片选择",
"pickMethodLivePhotoDescription": "仅选择实况图片。",
"pickMethodCameraName": "从相机生成选择",
"pickMethodCameraDescription": "通过相机拍照生成并选择资源",
"pickMethodCameraAndStayName": "从相机生成选择并停留",
"pickMethodCameraAndStayDescription": "通过相机拍照生成选择资源,并停留在选择界面。",
"pickMethodThreeItemsGridName": "横向 3 格",
"pickMethodThreeItemsGridDescription": "选择器每行为 3 格。(pageSize 必须为 gridCount 的倍数)",
"pickMethodCustomFilterOptionsName": "自定义过滤条件",
"pickMethodCustomFilterOptionsDescription": "为选择器添加自定义过滤条件。",
"pickMethodPrependItemName": "往网格前插入 widget",
"pickMethodPrependItemDescription": "网格的靠前位置会添加一个自定义的 widget。",
"pickMethodNoPreviewName": "禁止预览",
"pickMethodNoPreviewDescription": "无法预览选择的资源,与 WhatsApp/MegaTok 的行为类似。",
"pickMethodKeepScrollOffsetName": "保持滚动位置",
"pickMethodKeepScrollOffsetDescription": "可以从上次滚动到的位置再次开始选择。",
"pickMethodChangeLanguagesName": "更改语言",
"pickMethodChangeLanguagesDescription": "传入 AssetPickerTextDelegate 手动更改选择器的语言(例如 EnglishAssetPickerTextDelegate)。",
"pickMethodPreventGIFPickedName": "禁止选择 GIF 图片",
"pickMethodPreventGIFPickedDescription": "通过 selectPredicate 来禁止 GIF 图片在点击时被选择。",
"pickMethodCustomizableThemeName": "自定义主题 (ThemeData)",
"pickMethodCustomizableThemeDescription": "可以用亮色或其他颜色及自定义的主题进行选择。",
"pickMethodPathNameBuilderName": "构建路径名称",
"pickMethodPathNameBuilderDescription": "在路径后添加 \uD83C\uDF6D 进行自定义。",
"pickMethodWeChatMomentName": "微信朋友圈模式",
"pickMethodWeChatMomentDescription": "允许选择图片或仅 1 个视频。",
"pickMethodCustomImagePreviewThumbSizeName": "自定义图片预览的缩略图大小",
"pickMethodCustomImagePreviewThumbSizeDescription": "通过降低缩略图的质量来获得更快的加载速度。",
"customPickerNotice": "本页面包含了多种方式、不同界面和特定应用的自定义选择器。欢迎贡献添加你自定义的选择器。\n该页面的所有选择器的代码位于 lib/customs/pickers 目录。",
"customPickerCallThePickerButton": "\uD83C\uDF81 开始选择资源",
"customPickerDirectoryAndFileName": "Directory+File 选择器",
"customPickerDirectoryAndFileDescription": "为 `File` 构建的自定义选择器。\n通过阅读该选择器的源码,你可以学习如何完全以你自定义的资源类型来构建并选择器的界面。\n\n该选择器会从 `getApplicationDocumentsDirectory` 目录获取资源,然后检查它是否包含图片。你需要将图片放在该目录来查看选择器的效果。",
"customPickerMultiTabName": "多 Tab 选择器",
"customPickerMultiTabDescription": "该选择器会以多 Tab 的形式同时展示多种资源类型的选择器。",
"customPickerMultiTabTab1": "全部",
"customPickerMultiTabTab2": "视频",
"customPickerMultiTabTab3": "图片",
"customPickerInstagramLayoutName": "Instagram 布局的选择器",
"customPickerInstagramLayoutDescription": "该选择器以 Instagram 的布局模式构建,在选择时可以同时预览。其已发布为单独的 package:insta_assets_picker。"
}
\ No newline at end of file
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'WeChat Asset Picker Demo';
@override
String appVersion(Object version) {
return 'Version: $version';
}
@override
String get appVersionUnknown => 'unknown';
@override
String get navMulti => 'Multi';
@override
String get navSingle => 'Single';
@override
String get navCustom => 'Custom';
@override
String get selectedAssetsText => 'Selected Assets';
@override
String pickMethodNotice(Object dist) {
return 'Pickers in this page are located at the $dist, defined by `pickMethods`.';
}
@override
String get pickMethodImageName => 'Image picker';
@override
String get pickMethodImageDescription => 'Only pick image from device.';
@override
String get pickMethodVideoName => 'Video picker';
@override
String get pickMethodVideoDescription =>
'Only pick video from device. (Includes Live Photos on iOS and macOS.)';
@override
String get pickMethodAudioName => 'Audio picker';
@override
String get pickMethodAudioDescription => 'Only pick audio from device.';
@override
String get pickMethodLivePhotoName => 'Live Photo picker';
@override
String get pickMethodLivePhotoDescription =>
'Only pick Live Photos from device.';
@override
String get pickMethodCameraName => 'Pick from camera';
@override
String get pickMethodCameraDescription =>
'Allow to pick an asset through camera.';
@override
String get pickMethodCameraAndStayName => 'Pick from camera and stay';
@override
String get pickMethodCameraAndStayDescription =>
'Take a photo or video with the camera picker, select the result and stay in the entities list.';
@override
String get pickMethodCommonName => 'Common picker';
@override
String get pickMethodCommonDescription => 'Pick images and videos.';
@override
String get pickMethodThreeItemsGridName => '3 items grid';
@override
String get pickMethodThreeItemsGridDescription =>
'Picker will served as 3 items on cross axis. (pageSize must be a multiple of the gridCount)';
@override
String get pickMethodCustomFilterOptionsName => 'Custom filter options';
@override
String get pickMethodCustomFilterOptionsDescription =>
'Add filter options for the picker.';
@override
String get pickMethodPrependItemName => 'Prepend special item';
@override
String get pickMethodPrependItemDescription =>
'A special item will prepend to the assets grid.';
@override
String get pickMethodNoPreviewName => 'No preview';
@override
String get pickMethodNoPreviewDescription =>
'You cannot preview assets during the picking, the behavior is like the WhatsApp/MegaTok pattern.';
@override
String get pickMethodKeepScrollOffsetName => 'Keep scroll offset';
@override
String get pickMethodKeepScrollOffsetDescription =>
'Pick assets from same scroll position.';
@override
String get pickMethodChangeLanguagesName => 'Change Languages';
@override
String get pickMethodChangeLanguagesDescription =>
'Pass AssetPickerTextDelegate to change between languages (e.g. EnglishAssetPickerTextDelegate).';
@override
String get pickMethodPreventGIFPickedName => 'Prevent GIF being picked';
@override
String get pickMethodPreventGIFPickedDescription =>
'Use selectPredicate to banned GIF picking when tapped.';
@override
String get pickMethodCustomizableThemeName =>
'Customizable theme (ThemeData)';
@override
String get pickMethodCustomizableThemeDescription =>
'Picking assets with the light theme or with a different color.';
@override
String get pickMethodPathNameBuilderName => 'Path name builder';
@override
String get pickMethodPathNameBuilderDescription => 'Add 🍭 after paths name.';
@override
String get pickMethodWeChatMomentName => 'WeChat Moment';
@override
String get pickMethodWeChatMomentDescription =>
'Pick assets with images or only 1 video.';
@override
String get pickMethodCustomImagePreviewThumbSizeName =>
'Custom image preview thumb size';
@override
String get pickMethodCustomImagePreviewThumbSizeDescription =>
'You can reduce the thumb size to get faster load speed.';
@override
String get customPickerNotice =>
'This page contains customized pickers with different asset types, different UI layouts, or some use case for specific apps. Contribute to add your custom picker are welcomed.\nPickers in this page are located at the lib/customs/pickers folder.';
@override
String get customPickerCallThePickerButton => '🎁 Call the Picker';
@override
String get customPickerDirectoryAndFileName => 'Directory+File picker';
@override
String get customPickerDirectoryAndFileDescription =>
'This is a custom picker built for `File`.\nBy browsing this picker, we want you to know that you can build your own picker components using the entity\'s type you desired.\n\nIn this page, picker will grab files from `getApplicationDocumentsDirectory`, then check whether it contains images. Put files into the path to see how this custom picker work.';
@override
String get customPickerMultiTabName => 'Multi tab picker';
@override
String get customPickerMultiTabDescription =>
'The picker contains multiple tab with different types of assets for the picking at the same time.';
@override
String get customPickerMultiTabTab1 => 'All';
@override
String get customPickerMultiTabTab2 => 'Videos';
@override
String get customPickerMultiTabTab3 => 'Images';
@override
String get customPickerInstagramLayoutName => 'Instagram layout picker';
@override
String get customPickerInstagramLayoutDescription =>
'The picker reproduces Instagram layout with preview and scroll animations. It\'s also published as the package insta_assets_picker.';
}
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get appTitle => 'WeChat Asset Picker 示例';
@override
String appVersion(Object version) {
return '版本:$version';
}
@override
String get appVersionUnknown => '未知';
@override
String get navMulti => '多选';
@override
String get navSingle => '单选';
@override
String get navCustom => '自定义';
@override
String get selectedAssetsText => '已选的资源';
@override
String pickMethodNotice(Object dist) {
return '该页面的所有选择器的代码位于 $dist,由 `pickMethods` 定义。';
}
@override
String get pickMethodImageName => '图片选择';
@override
String get pickMethodImageDescription => '仅选择图片。';
@override
String get pickMethodVideoName => '视频选择';
@override
String get pickMethodVideoDescription => '仅选择视频。';
@override
String get pickMethodAudioName => '音频选择';
@override
String get pickMethodAudioDescription => '仅选择音频。';
@override
String get pickMethodLivePhotoName => '实况图片选择';
@override
String get pickMethodLivePhotoDescription => '仅选择实况图片。';
@override
String get pickMethodCameraName => '从相机生成选择';
@override
String get pickMethodCameraDescription => '通过相机拍照生成并选择资源';
@override
String get pickMethodCameraAndStayName => '从相机生成选择并停留';
@override
String get pickMethodCameraAndStayDescription => '通过相机拍照生成选择资源,并停留在选择界面。';
@override
String get pickMethodCommonName => '常用选择';
@override
String get pickMethodCommonDescription => '选择图片和视频。';
@override
String get pickMethodThreeItemsGridName => '横向 3 格';
@override
String get pickMethodThreeItemsGridDescription =>
'选择器每行为 3 格。(pageSize 必须为 gridCount 的倍数)';
@override
String get pickMethodCustomFilterOptionsName => '自定义过滤条件';
@override
String get pickMethodCustomFilterOptionsDescription => '为选择器添加自定义过滤条件。';
@override
String get pickMethodPrependItemName => '往网格前插入 widget';
@override
String get pickMethodPrependItemDescription => '网格的靠前位置会添加一个自定义的 widget。';
@override
String get pickMethodNoPreviewName => '禁止预览';
@override
String get pickMethodNoPreviewDescription =>
'无法预览选择的资源,与 WhatsApp/MegaTok 的行为类似。';
@override
String get pickMethodKeepScrollOffsetName => '保持滚动位置';
@override
String get pickMethodKeepScrollOffsetDescription => '可以从上次滚动到的位置再次开始选择。';
@override
String get pickMethodChangeLanguagesName => '更改语言';
@override
String get pickMethodChangeLanguagesDescription =>
'传入 AssetPickerTextDelegate 手动更改选择器的语言(例如 EnglishAssetPickerTextDelegate)。';
@override
String get pickMethodPreventGIFPickedName => '禁止选择 GIF 图片';
@override
String get pickMethodPreventGIFPickedDescription =>
'通过 selectPredicate 来禁止 GIF 图片在点击时被选择。';
@override
String get pickMethodCustomizableThemeName => '自定义主题 (ThemeData)';
@override
String get pickMethodCustomizableThemeDescription => '可以用亮色或其他颜色及自定义的主题进行选择。';
@override
String get pickMethodPathNameBuilderName => '构建路径名称';
@override
String get pickMethodPathNameBuilderDescription => '在路径后添加 🍭 进行自定义。';
@override
String get pickMethodWeChatMomentName => '微信朋友圈模式';
@override
String get pickMethodWeChatMomentDescription => '允许选择图片或仅 1 个视频。';
@override
String get pickMethodCustomImagePreviewThumbSizeName => '自定义图片预览的缩略图大小';
@override
String get pickMethodCustomImagePreviewThumbSizeDescription =>
'通过降低缩略图的质量来获得更快的加载速度。';
@override
String get customPickerNotice =>
'本页面包含了多种方式、不同界面和特定应用的自定义选择器。欢迎贡献添加你自定义的选择器。\n该页面的所有选择器的代码位于 lib/customs/pickers 目录。';
@override
String get customPickerCallThePickerButton => '🎁 开始选择资源';
@override
String get customPickerDirectoryAndFileName => 'Directory+File 选择器';
@override
String get customPickerDirectoryAndFileDescription =>
'为 `File` 构建的自定义选择器。\n通过阅读该选择器的源码,你可以学习如何完全以你自定义的资源类型来构建并选择器的界面。\n\n该选择器会从 `getApplicationDocumentsDirectory` 目录获取资源,然后检查它是否包含图片。你需要将图片放在该目录来查看选择器的效果。';
@override
String get customPickerMultiTabName => '多 Tab 选择器';
@override
String get customPickerMultiTabDescription =>
'该选择器会以多 Tab 的形式同时展示多种资源类型的选择器。';
@override
String get customPickerMultiTabTab1 => '全部';
@override
String get customPickerMultiTabTab2 => '视频';
@override
String get customPickerMultiTabTab3 => '图片';
@override
String get customPickerInstagramLayoutName => 'Instagram 布局的选择器';
@override
String get customPickerInstagramLayoutDescription =>
'该选择器以 Instagram 的布局模式构建,在选择时可以同时预览。其已发布为单独的 package:insta_assets_picker。';
}
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/wechat.dart';
import 'package:flutter/material.dart';
import 'app.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await registerWechatApi();
await setupLocator();
runApp(const App());
......
import 'package:dio/dio.dart';
// // 使用通用的 ApiService
// final apiService = ApiService(baseUrl: 'https://bxe.cn');
//
// // 发送POST请求
// final response = await apiService.post('/users', {
// 'name': 'xiaoming',
// 'email': 'xiaoming@example.com'
// });
class ApiService {
late Dio _dio;
......@@ -59,8 +50,28 @@ class ApiService {
);
}
/// 发送PUT请求
Future<Response<T>> put<T>(
String endpoint,
dynamic data, {
Map<String, dynamic>? queryParameters,
Map<String, dynamic>? headers,
Options? options,
}) async {
return await _dio.put<T>(
endpoint,
data: data,
queryParameters: queryParameters,
options: options?.copyWith(headers: {...?headers}),
);
}
/// 添加拦截器
void addInterceptor(Interceptor interceptor) {
_dio.interceptors.add(interceptor);
}
void close() {
_dio.close();
}
}
import 'dart:convert';
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/data/models/message/h5_message.dart';
import 'package:appframe/data/models/message/h5_resp.dart';
// 消息处理器抽象类
abstract class MessageHandler {
// Future<Map<String, dynamic>> handleMessage(Map<String, dynamic> params);
// Future<dynamic> handleMessage(Map<String, dynamic> params);
Future<dynamic> handleMessage(dynamic params);
void setCubit(WebCubit cubit) {}
void setMessage(String message) {}
}
// 消息分发器
......@@ -24,7 +29,10 @@ class MessageDispatcher {
}
// 分发处理
Future<void> dispatch(H5Message h5Message, Function callback) async {
Future<void> dispatch(String message, Function callback, {WebCubit? webCubit}) async {
final Map<String, dynamic> data = json.decode(message);
H5Message h5Message = H5Message.fromJson(data);
var handler = _handlers[h5Message.cmd];
if (handler == null) {
try {
......@@ -39,7 +47,24 @@ class MessageDispatcher {
}
try {
// 设置传递的cubit,进行业务操作
if (h5Message.cmd == 'scanCode' ||
h5Message.cmd == "getOrientation" ||
h5Message.cmd == "getWindowInfo" ||
h5Message.cmd == "chooseImage" ||
h5Message.cmd == "chooseVideo" ||
h5Message.cmd == "goLogin" ||
h5Message.cmd.startsWith("audio") ||
h5Message.cmd.startsWith("setTitle")) {
handler.setCubit(webCubit!);
handler.setMessage(message);
}
final result = await handler.handleMessage(h5Message.params);
// 有些命令需要通过监听器调用Cubit,触发调用时不需要返回结果,不处理回调
if (result == null) {
return;
}
H5Resp h5Resp = H5Resp(h5Message.unique, h5Message.cmd, result, '');
callback(h5Resp.toJson());
} catch (e) {
......
import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:archive/archive.dart';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
class LocalServerService {
String? _httpDirectory;
// 启动本地HTTP服务器
Future<HttpServer> startLocalServer() async {
HttpServer server = await HttpServer.bind(InternetAddress.loopbackIPv4, 35982);
// 测试情况下, 每次启动服务,先解压dist文件
_extractDist();
HttpServer server = await HttpServer.bind(InternetAddress.loopbackIPv4, Constant.localServerPort);
print('本地服务器启动在端口: ${server.port}');
server.listen((HttpRequest request) async {
final String requestPath = request.uri.path == '/' ? '/index.html' : request.uri.path;
try {
if (requestPath.startsWith('/temp/')) {
// 临时目录文件的请求
if (requestPath.startsWith('${Constant.localServerTemp}/')) {
// 目录文件服务逻辑
await _serveTempFile(request, requestPath);
} else if (requestPath.startsWith('/test/')) {
// assets文件服务逻辑
} else if (requestPath.startsWith('${Constant.localServerTest}/')) {
// 内部assets文件服务逻辑
await _serveAssetFile(request, requestPath);
} else {
// asset/dist.zip 文件服务逻辑
await _serveZipFileContent(request, requestPath);
// 内部集成H5文件服务逻辑
await _serveHttpFile(request, requestPath);
}
} catch (e) {
print('处理请求时出错: $e');
......@@ -35,12 +43,12 @@ class LocalServerService {
return server;
}
// 临时目录的文件
// 目录下的文件
Future<void> _serveTempFile(HttpRequest request, String requestPath) async {
try {
// 临时文件已经设备路径
// 临时文件路径
// 构建文件路径(移除 /temp 前缀)
final String filePath = requestPath.substring('/temp/'.length);
final String filePath = requestPath.substring(Constant.localServerTemp.length);
// 检查文件是否存在
final File file = File(filePath);
......@@ -67,52 +75,32 @@ class LocalServerService {
}
}
Future<void> _serveZipFileContent(HttpRequest request, String requestPath) async {
String zipAssetPath = 'assets/dist.zip';
Future<void> _serveHttpFile(HttpRequest request, String requestPath) async {
try {
// 使用 rootBundle.load 加载资源文件
final ByteData data = await rootBundle.load(zipAssetPath);
final List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
// 读取并解压zip文件内容
final Archive archive = ZipDecoder().decodeBytes(bytes);
// 查找请求的内部文件
ArchiveFile? targetFile;
for (final file in archive) {
// 标准化路径分隔符(统一使用 '/')
String zipFileName = file.name.replaceAll('\\', '/');
var httpDirectory = await getHttpDirectory();
final String filePath = '$httpDirectory$requestPath';
// 移除开头的 '/'(如果存在)
if (requestPath.startsWith('/')) {
requestPath = requestPath.substring(1);
}
if (zipFileName == requestPath) {
targetFile = file;
break;
}
}
// 检查文件是否存在
final File file = File(filePath);
if (await file.exists()) {
// 读取文件内容
final List<int> bytes = await file.readAsBytes();
if (targetFile == null) {
request.response
..headers.contentType = ContentType.parse(_getContentType(filePath))
..add(bytes)
..close();
} else {
request.response
..statusCode = HttpStatus.notFound
..write('File not found in zip: $requestPath')
..write('File not found: $filePath')
..close();
return;
}
// 返回文件内容
request.response
..headers.contentType = ContentType.parse(_getContentType(requestPath))
..add(targetFile.content as List<int>)
..close();
} catch (e) {
print('读取zip文件时出错: $e');
print('读取临时文件时出错: $e');
request.response
..statusCode = HttpStatus.internalServerError
..write('Error reading zip file')
..statusCode = HttpStatus.notFound
..write('File not found')
..close();
}
}
......@@ -120,7 +108,7 @@ class LocalServerService {
// 访问assets目录下的文件
Future<void> _serveAssetFile(HttpRequest request, String requestPath) async {
// 构建文件路径(移除 /test 前缀)
final String path = requestPath.substring('/test'.length);
final String path = requestPath.substring(Constant.localServerTest.length);
final String filePath = 'assets$path';
try {
......@@ -147,4 +135,74 @@ class LocalServerService {
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg';
return 'application/octet-stream';
}
Future<String> getHttpDirectory() async {
if (_httpDirectory == null) {
await _initHttpDirectory();
}
return _httpDirectory!;
}
Future<void> _initHttpDirectory() async {
var direct = await getExternalStorageDirectory();
_httpDirectory = '${direct?.path}/http_dist_assets';
// var direct = await getApplicationSupportDirectory();
// _httpDirectory = '${direct.path}/http_dist_assets';
}
Future<void> _extractDist() async {
var outputDirectory = await getHttpDirectory();
// 判断目录存在则不需要再解压
if (Directory(outputDirectory).existsSync()) {
return;
}
var zipFilePath = "assets/dist.zip";
final ByteData data = await rootBundle.load(zipFilePath);
final bytes = data.buffer.asUint8List();
// 解码 ZIP 文件
final archive = ZipDecoder().decodeBytes(bytes);
// 提取文件到指定目录
for (final file in archive) {
final filename = file.name;
if (file.isFile) {
final data = file.content as List<int>;
final outputFile = File('$outputDirectory/$filename');
outputFile.createSync(recursive: true);
outputFile.writeAsBytesSync(data);
} else {
// 创建目录
Directory('$outputDirectory/$filename').createSync(recursive: true);
}
}
}
Future<void> _clearDist() async {
var outputDirectory = await getHttpDirectory();
Directory(outputDirectory).deleteSync(recursive: true);
}
void _downloadDist() async {
var httpDirectory = await getHttpDirectory();
var distUrl = "https://github.com/xinxin-wu/flutter_web_dist/releases/download/v1.0.0/dist.zip";
// Dio进行下载
var dio = Dio();
dio
.download(
distUrl,
'$httpDirectory/dist.zip',
onReceiveProgress: (received, total) {
if (total != -1) {
print((received / total * 100).toStringAsFixed(0) + "%");
}
},
)
.then((_) {
_extractDist();
});
dio.close();
}
}
import 'package:appframe/bloc/link_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:webview_flutter/webview_flutter.dart';
class LinkPage extends StatelessWidget {
const LinkPage({super.key});
@override
Widget build(BuildContext buildContext) {
final Map<String, dynamic>? extraData = GoRouterState.of(buildContext).extra as Map<String, dynamic>?;
final String? url = extraData?['url'];
return BlocProvider(
create: (context) => LinkCubit(LinkState(loaded: false, url: url!)),
child: BlocConsumer<LinkCubit, LinkState>(
builder: (ctx, state) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
ctx.read<LinkCubit>().handleBack(ctx);
},
child: Scaffold(
appBar: AppBar(
title: Text('adv'),
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () async {
await ctx.read<LinkCubit>().handleBack(ctx);
},
),
),
body: state.loaded
? SizedBox(
height: MediaQuery.of(ctx).size.height - 120, // 减去100像素留空
child: WebViewWidget(controller: ctx.read<LinkCubit>().controller),
)
: const Center(child: CircularProgressIndicator()),
),
);
},
listener: (context, state) {},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScanCodePage extends StatelessWidget {
const ScanCodePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('扫码'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.pop();
},
),
),
body: MobileScanner(
onDetect: (BarcodeCapture barcode) {
final String? code = barcode.barcodes.first.rawValue;
if (code != null) {
// 返回扫码结果
context.pop(code);
}
},
fit: BoxFit.contain,
),
);
}
}
\ No newline at end of file
import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebPage extends StatelessWidget {
const WebPage({super.key});
@override
Widget build(BuildContext context) {
final Map<String, dynamic>? extraData = GoRouterState.of(context).extra as Map<String, dynamic>?;
Widget build(BuildContext buildContext) {
final Map<String, dynamic>? extraData = GoRouterState.of(buildContext).extra as Map<String, dynamic>?;
print("接收到的参数: $extraData");
var ip = extraData?['ip'] ?? Constant.localServerHost;
var sessionCode = extraData?['sessionCode'];
var userCode = extraData?['userCode'];
var classCode = extraData?['classCode'];
var userType = extraData?['userType'];
var stuId = extraData?['stuId'];
print("sessionCode:$sessionCode");
if (sessionCode == null || sessionCode == '') {
var sharedPreferences = getIt.get<SharedPreferences>();
sessionCode = sharedPreferences.getString('auth_sessionCode');
userCode = sharedPreferences.getString('auth_userCode');
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(false, '界面加载中...', false, sessionCode, userCode, classCode, userType, stuId)),
create: (context) => WebCubit(
WebState(
ip: ip,
sessionCode: sessionCode,
userCode: userCode,
classCode: classCode,
userType: userType,
stuId: stuId,
),
),
child: BlocConsumer<WebCubit, WebState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: Text(state.title)),
body: state.loaded
? WebViewWidget(controller: context.read<WebCubit>().controller)
: const Center(child: CircularProgressIndicator()),
// 用于测试一下点击跳转路由
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: "btn1",
onPressed: () {
context.read<WebCubit>().goAuth();
},
child: const Icon(Icons.add),
),
const SizedBox(height: 16),
FloatingActionButton(
heroTag: "btn2",
onPressed: () {
context.read<WebCubit>().goAuth2();
},
child: const Icon(Icons.add_alarm),
),
]
)
builder: (ctx, state) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
ctx.read<WebCubit>().handleBack();
},
child: Scaffold(
appBar: AppBar(
title: Text(state.title),
centerTitle: true,
automaticallyImplyLeading: false,
leading: state.beBack
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
ctx.read<WebCubit>().handleBack();
},
)
: null,
actions: [
Builder(
builder: (BuildContext context) {
return IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return SizedBox(
height: 300,
child: Column(
children: [
ListTile(
leading: const Icon(Icons.chat_outlined),
title: const Text('微信授权'),
onTap: () {
Navigator.pop(context);
ctx.read<WebCubit>().goWechatAuth();
},
),
ListTile(
leading: const Icon(Icons.accessibility_new),
title: const Text('身份认证'),
onTap: () {
Navigator.pop(context);
ctx.read<WebCubit>().goAuth();
},
),
ListTile(
leading: const Icon(Icons.app_blocking_sharp),
title: const Text('打开小程序'),
onTap: () {
Navigator.pop(context);
ctx.read<WebCubit>().goMiniProgram();
},
),
ListTile(
leading: const Icon(Icons.refresh),
title: const Text('刷新'),
onTap: () {
Navigator.pop(context);
ctx.read<WebCubit>().refresh();
},
),
ListTile(
leading: const Icon(Icons.cleaning_services),
title: const Text('清理缓存'),
onTap: () {
Navigator.pop(context);
ctx.read<WebCubit>().clearStorage();
},
),
],
),
);
},
);
},
);
},
),
],
),
body: state.loaded
? SizedBox(
height: MediaQuery.of(ctx).size.height - 120, // 减去100像素留空
child: WebViewWidget(controller: ctx.read<WebCubit>().controller),
)
: const Center(child: CircularProgressIndicator()),
),
);
},
listener: (context, state) {
print("web page listener -------------------------");
// 跳转到微信授权页面
if (state.needAuth) {
print("跳转到微信授权页面");
context.go("/wechatAuth");
if (state.orientationCmdFlag) {
context.read<WebCubit>().getOrientation(context);
} else if (state.windowInfoCmdFlag) {
context.read<WebCubit>().getWindowInfo(context);
} else if (state.chooseImageCmdFlag) {
context.read<WebCubit>().chooseImage(context);
} else if (state.chooseVideoCmdFlag) {
context.read<WebCubit>().chooseVideo(context);
}
},
),
);
......
import 'package:appframe/bloc/wechat_auth_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class WechatAuthPage extends StatelessWidget {
const WechatAuthPage({super.key});
......@@ -9,49 +8,43 @@ class WechatAuthPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WechatAuthCubit(WechatAuthState(null, null, null, null, null, 'no msg!')),
create: (context) => WechatAuthCubit(WechatAuthState()),
child: BlocConsumer<WechatAuthCubit, WechatAuthState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: Text('微信授权')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.result ?? 'no msg!'),
ElevatedButton(
onPressed: () {
context.read<WechatAuthCubit>().auth();
},
child: const Text('授权'),
),
],
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) {
print("wechat auth page listener-------------------------");
print(state.sessionCode);
print(state.userCode);
print(state.classCode);
print(state.userType);
print(state.stuId);
print(state.result);
print('带参数跳转webview');
context.go(
'/web',
extra: {
"sessionCode": state.sessionCode,
"userCode": state.userCode,
"classCode": state.classCode,
"userType": state.userType,
"stuId": state.stuId,
},
);
},
listener: (context, state) {},
),
);
}
......
import 'package:flutter/material.dart';
import 'package:flutter_sound/public/flutter_sound_recorder.dart';
/// 利用 StatefulWidget 的状态管理,自动清理 FlutterSoundRecorder, 释放资源
class RecorderWidget extends StatefulWidget {
late final FlutterSoundRecorder _recorder;
FlutterSoundRecorder get recorder => _recorder;
RecorderWidget({super.key, required FlutterSoundRecorder recorder}) {
_recorder = recorder;
}
@override
State<StatefulWidget> createState() {
return _RecorderWidgetState();
}
}
class _RecorderWidgetState extends State<RecorderWidget> {
late BuildContext _context;
@override
Widget build(BuildContext buildContext) {
return SizedBox(
);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
print('销毁 RecorderWidget -------------------->');
final widget = _context.widget as RecorderWidget;
try {
widget.recorder.closeRecorder();
} catch (e) {
print(e);
}
super.dispose();
}
}
import 'dart:convert';
import 'package:ffmpeg_kit_flutter_new_audio/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new_audio/ffprobe_kit.dart';
import 'package:ffmpeg_kit_flutter_new_audio/return_code.dart';
class AudioUtil {
/// 转码
static Future<bool> convertAacToMp3(Map<String, dynamic> params) async {
final aacPath = params['accPath'] as String;
final mp3Path = params['mp3Path'] as String;
final session = await FFmpegKit.execute('-i "$aacPath" -vn -ar 44100 -ac 2 -ab 192k -f mp3 "$mp3Path"');
final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode);
}
/// 获取音频时长
static Future<Duration> getAudioDuration(String filePath) async {
try {
String command = '-v quiet -print_format json -show_format "$filePath"';
var session = await FFprobeKit.execute(command);
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
var output = await session.getOutput();
var json = jsonDecode(output!);
var format = json['format'];
var duration = format['duration'];
final durationInSeconds = double.tryParse(duration) ?? 0.0;
return Duration(seconds: durationInSeconds.toInt());
}
return Duration.zero;
} catch (e) {
return Duration.zero;
}
}
}
......@@ -29,4 +29,39 @@ class FileTypeUtil {
await randomAccessFile.close();
}
}
}
/// 根据MIME类型获取常见的文件扩展名
static String getExtensionFromMime(String? mimeType) {
if (mimeType == null) return '';
// 常见视频MIME类型到扩展名的映射
const videoMimeToExtension = {
'video/mp4': 'mp4',
'video/avi': 'avi',
'video/quicktime': 'mov',
'video/x-matroska': 'mkv',
'video/webm': 'webm',
'video/3gpp': '3gp',
'video/3gpp2': '3g2',
'video/x-msvideo': 'avi',
'video/x-ms-wmv': 'wmv',
'video/mpeg': 'mpg',
'video/ogg': 'ogv',
};
// 常见图片MIME类型到扩展名的映射
const imageMimeToExtension = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/webp': 'webp',
'image/bmp': 'bmp',
'image/svg+xml': 'svg',
'image/tiff': 'tiff',
};
return videoMimeToExtension[mimeType] ??
imageMimeToExtension[mimeType] ??
mimeType.split('/').last;
}
}
\ No newline at end of file
import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:video_compress/video_compress.dart';
/// 缩略图工具类
///
......@@ -28,20 +28,28 @@ class ThumbnailUtil {
/// 返回缩略图路径
static Future<String?> genVideoThumbnail(String videoPath, Directory dir) async {
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;
var fileThumbnail = await VideoCompress.getFileThumbnail(videoPath, quality: 50, position: -1);
return fileThumbnail.path;
} 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;
// }
}
}
\ No newline at end of file
}
......@@ -5,20 +5,28 @@
import FlutterMacOS
import Foundation
import connectivity_plus
import device_info_plus
import file_picker
import file_selector_macos
import flutter_image_compress_macos
import geolocator_apple
import mobile_scanner
import network_info_plus
import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
import video_compress
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
}
......@@ -41,18 +41,34 @@ dependencies:
sdk: flutter
fluwx: ^5.7.2
go_router: ^16.2.1
flutter_bloc: ^9.1.1
webview_flutter: ^4.13.0
permission_handler: ^12.0.1
path: ^1.9.1
path_provider: ^2.1.5
file_picker: ^10.3.2
image_picker: ^1.2.0
equatable: ^2.0.7
dio: ^5.9.0
archive: ^4.0.7
connectivity_plus: ^7.0.0
device_info_plus: ^11.5.0
dio: ^5.9.0
equatable: ^2.0.7
exif: ^3.3.0
ffmpeg_kit_flutter_new_audio: ^1.1.0
file_picker: ^10.3.2
flutter_bloc: ^9.1.1
flutter_localization: ^0.3.3
flutter_image_compress: ^2.4.0
flutter_sound: ^9.28.0
fluwx: ^5.7.2
gallery_saver_plus: ^3.2.9
get_it: ^8.2.0
geolocator: ^14.0.2
go_router: ^16.2.1
# image_picker: ^1.2.0
image: ^4.5.4
image_size_getter: ^2.4.1
json_annotation: ^4.9.0
mime: ^2.0.0
mobile_scanner: ^7.0.1
network_info_plus: ^7.0.0
open_file: ^3.5.10
path: ^1.9.1
path_provider: ^2.1.5
permission_handler: ^12.0.1
shared_preferences: ^2.5.3
flutter_sound: ^9.28.0
device_info_plus: ^12.1.0
......@@ -61,6 +77,14 @@ dependencies:
image_size_getter: ^2.4.1
video_thumbnail: ^0.5.6
mime: ^2.0.0
url_launcher: ^6.3.2
uuid: ^4.5.1
vibration: ^3.1.3
video_compress: ^3.1.4
video_player: ^2.10.0
webview_flutter: ^4.13.0
wechat_assets_picker: ^9.8.0
wechat_camera_picker: ^4.4.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
......
......@@ -6,12 +6,18 @@
#include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
}
......@@ -3,7 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
file_selector_windows
geolocator_windows
permission_handler_windows
)
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!