Commit 73f17c33 by tanghuan

dev

1 parent 975c9217
......@@ -10,15 +10,32 @@
<div id="resp"></div>
<button onclick="clearResp()">清除响应数据</button>
<br>
<br>
<button onclick="getDeviceInfoSync()">获取当前设备信息</button>
<br>
<br>
<button onclick="setStorageSync()">设置本地缓存</button>
<button onclick="getStorageSync()">获取本地缓存</button>
<button onclick="chooseImage()">选择图片</button>
<button onclick="clearStorageSync()">清空本地缓存</button>
<br>
<br>
<button onclick="chooseImage()">选择单个图片</button>
<button onclick="chooseMultipleImage()">选择多个图片</button>
<br>
<br>
<a href="/test/test2.html">跳转测试2</a>
<br>
<br>
<a href="/index.html">iOS跳转</a>
<br>
<img src="" alt="test" id=testImg>
<br>
<script src="/test/test.js"></script>
......
// 显示来自Flutter的警告
function showAlert(message) {
// 显示来自Flutter的警告
function showAlert(message) {
alert(message);
}
}
// 清空响应数据
function clearResp() {
// 清空响应数据
function clearResp() {
document.getElementById('resp').innerHTML = '';
}
}
// 接收Flutter响应数据
function xeJsBridgeCallback(message) {
let jsonData = JSON.parse(message);
document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + jsonData.data + '</p>';
// 接收Flutter响应数据
function xeJsBridgeCallback(message) {
document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + message + '</p>';
}
// 显示图片测试
document.getElementById('testImg').src=jsonData.data;
}
// 测试获取设备信息
function getDeviceInfoSync() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "getDeviceInfoSync", "params": {} }';
xeJsBridge.postMessage(message);
}
// 测试获取设备信息
function getDeviceInfoSync() {
let message='{ "timestamp": 1, "unique": "123", "cmd": "getDeviceInfoSync", "params": {} }';
function setStorageSync() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "setStorageSync", "params": {"key":"test1","value":"hello world!Hey!"} }';
xeJsBridge.postMessage(message);
}
}
function setStorageSync() {
let message='{ "timestamp": 1, "unique": "123", "cmd": "setStorageSync", "params": {"key":"test1","value":"hello world!Hey!"} }';
function getStorageSync() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "getStorageSync", "params": "test1" }';
xeJsBridge.postMessage(message);
}
}
function getStorageSync() {
let message='{ "timestamp": 1, "unique": "123", "cmd": "getStorageSync", "params": {"key":"test1"} }';
function clearStorageSync() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "clearStorageSync", "params": {} }';
xeJsBridge.postMessage(message);
}
function chooseImage(sourceType) {
let params = {
"timestamp": 1, "unique": "123", "cmd": "chooseImage", "params": {
"sourceType": sourceType == 1 ? "album" : "camera",
"count": 1,
"sizeType": ["original", "compressed"],
}
};
xeJsBridge.postMessage(JSON.stringify(params));
function chooseImage() {
let message='{ "timestamp": 1, "unique": "123", "cmd": "chooseImage", "params": {} }';
xeJsBridge.postMessage(message);
}
function chooseMultipleImage() {
let params = {
"timestamp": 1, "unique": "123", "cmd": "chooseImage", "params": {
"sourceType": "album",
"count": 9,
"sizeType": ["original", "compressed"],
}
};
xeJsBridge.postMessage(JSON.stringify(params));
}
......@@ -10,14 +10,22 @@
<div id="resp"></div>
<button onclick="getStorageSync()">获取本地缓存</button>
<a href="/test/test.html">跳转测试1</a>
<button onclick="startRecord()">开始录音</button>
<button onclick="">暂停录音</button>
<button onclick="">唤醒录音</button>
<button onclick="stopRecord()">停止录音</button>
<br>
<br>
<input type="hidden" id="url" value="">
<button onclick="startPlay()">开始播放</button>
<button onclick="">暂停播放</button>
<button onclick="">唤醒播放</button>
<button onclick="stopPlay()">停止播放</button>
<br>
<img src="" alt="test" id=testImg>
<br>
<a href="/test/test.html">跳转测试1</a>
</body>
......@@ -26,12 +34,34 @@
function xeJsBridgeCallback(message) {
let jsonData = JSON.parse(message);
document.getElementById('resp').innerHTML = '<p><strong>响应:</strong> ' + jsonData.data + '</p>';
// 设置id为url的input的值
document.getElementById('url').value = jsonData.data;
}
function startRecord() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioRecorderStart", "params": {} }';
xeJsBridge.postMessage(message);
}
function stopRecord() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioRecorderStop", "params": {} }';
xeJsBridge.postMessage(message);
}
function getStorageSync() {
let message='{ "timestamp": 1, "unique": "123", "cmd": "getStorageSync", "params": {"key":"test1"} }';
function startPlay() {
let url = document.getElementById('url').value;
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioPlay", "params": {"url":"' + url + '"} }';
xeJsBridge.postMessage(message);
}
function stopPlay() {
let message = '{ "timestamp": 1, "unique": "123", "cmd": "audioStop", "params": {} }';
xeJsBridge.postMessage(message);
}
</script>
</html>
\ No newline at end of file
......@@ -2,6 +2,8 @@ import 'dart:convert';
import 'dart:io';
import 'package:appframe/data/models/message/h5_message.dart';
import 'package:appframe/data/repositories/message/player_handler.dart';
import 'package:appframe/data/repositories/message/recorder_handler.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/local_server_service.dart';
import 'package:equatable/equatable.dart';
......@@ -159,6 +161,8 @@ class WebCubit extends Cubit<WebState> {
@override
Future<void> close() async {
_server.close();
closeLocalRecorder();
closeLocalPlayer();
return super.close();
}
}
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/services.dart';
late HttpServer localServer;
Future<void> startLocalServer() async {
HttpServer localServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
print('本地服务器启动在端口: ${localServer.port}');
localServer.listen((HttpRequest request) async {
final String requestPath = request.uri.path == '/' ? '/index.html' : request.uri.path;
try {
if (requestPath.startsWith('/temp/')) {
// 临时目录文件的请求
await _serveTempFile(request, requestPath);
} else if (requestPath.startsWith('/test/')) {
// assets文件服务逻辑
await _serveAssetFile(request, requestPath);
} else {
// asset/dist.zip 文件服务逻辑
await _serveZipFileContent(request, requestPath);
}
} catch (e) {
print('处理请求时出错: $e');
request.response
..statusCode = HttpStatus.internalServerError
..write('Internal server error')
..close();
}
});
}
// 临时目录的文件
Future<void> _serveTempFile(HttpRequest request, String requestPath) async {
try {
// 临时文件已经设备路径
// 构建文件路径(移除 /temp 前缀)
final String filePath = requestPath.substring('/temp/'.length);
// 检查文件是否存在
final File file = File(filePath);
if (await file.exists()) {
// 读取文件内容
final List<int> bytes = await file.readAsBytes();
request.response
..headers.contentType = ContentType.parse(_getContentType(filePath))
..add(bytes)
..close();
} else {
request.response
..statusCode = HttpStatus.notFound
..write('File not found: $filePath')
..close();
}
} catch (e) {
print('读取临时文件时出错: $e');
request.response
..statusCode = HttpStatus.notFound
..write('File not found')
..close();
}
}
Future<void> _serveZipFileContent(HttpRequest request, String requestPath) async {
String zipAssetPath = 'assets/dist.zip';
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('\\', '/');
// 移除开头的 '/'(如果存在)
if (requestPath.startsWith('/')) {
requestPath = requestPath.substring(1);
}
if (zipFileName == requestPath) {
targetFile = file;
break;
}
}
if (targetFile == null) {
request.response
..statusCode = HttpStatus.notFound
..write('File not found in zip: $requestPath')
..close();
return;
}
// 返回文件内容
request.response
..headers.contentType = ContentType.parse(_getContentType(requestPath))
..add(targetFile.content as List<int>)
..close();
} catch (e) {
print('读取zip文件时出错: $e');
request.response
..statusCode = HttpStatus.internalServerError
..write('Error reading zip file')
..close();
}
}
// 访问assets目录下的文件
Future<void> _serveAssetFile(HttpRequest request, String requestPath) async {
// 构建文件路径(移除 /test 前缀)
final String path = requestPath.substring('/test'.length);
final String filePath = 'assets$path';
try {
final ByteData data = await rootBundle.load(filePath);
final List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
request.response
..headers.contentType = ContentType.parse(_getContentType(filePath))
..add(bytes)
..close();
} catch (e) {
request.response
..statusCode = HttpStatus.notFound
..write('File not found: $filePath')
..close();
}
}
String _getContentType(String filePath) {
if (filePath.endsWith('.html')) return 'text/html';
if (filePath.endsWith('.js')) return 'application/javascript';
if (filePath.endsWith('.css')) return 'text/css';
if (filePath.endsWith('.png')) return 'image/png';
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg';
return 'application/octet-stream';
}
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/clipboard_data_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/save_to_album_handler.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/data/repositories/message/storage_handler.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
......@@ -12,18 +17,56 @@ final getIt = GetIt.instance;
Future<void> setupLocator() async {
getIt.registerSingleton<SharedPreferences>(await SharedPreferences.getInstance());
// 按指令名称注册 message handler
// 视情况,对大概率会执行的指令直接加载,对概率较低的指令使用懒加载
// 使用懒加载,用户实际用到了才会进行加载
/// 按指令名称注册 message handler
/// 视情况,对大概率会执行的指令直接加载,对概率较低的指令使用懒加载
/// 使用懒加载,用户实际用到了才会进行加载
/// 设备信息
getIt.registerLazySingleton<MessageHandler>(() => DeviceInfoHandler(), instanceName: 'getDeviceInfoSync');
/// 本地缓存
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => GetStorageSyncHandler(), instanceName: 'getStorageSync');
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => SetStorageSyncHandler(), instanceName: 'setStorageSync');
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => ClearStorageSyncHandler(), instanceName: 'clearStorageSync');
/// 剪贴板
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => SetClipboardDataHandler(), instanceName: 'setClipboardData');
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => GetClipboardDataHandler(), instanceName: 'getClipboardData');
/// 选择图片
/// 暂未处理参数:sizeType: string[], // 尺寸类型,可多选,compressed=压缩图,original=原图, 默认['original', 'compressed']
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => ChooseImageHandler(), instanceName: 'chooseImage');
// service
/// 选择文件
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => ChooseFileHandler(), instanceName: 'chooseFile');
/// 保存文件/视频到相册
// Android已测试通过
getIt.registerLazySingleton<MessageHandler>(() => SaveToAlbumHandler(), instanceName: 'saveToAlbum');
/// 录音
getIt.registerLazySingleton<MessageHandler>(() => AudioRecorderStartHandler(), instanceName: 'audioRecorderStart');
getIt.registerLazySingleton<MessageHandler>(() => AudioRecorderPauseHandler(), instanceName: 'audioRecorderPause');
getIt.registerLazySingleton<MessageHandler>(() => AudioRecorderResumeHandler(), instanceName: 'audioRecorderResume');
getIt.registerLazySingleton<MessageHandler>(() => AudioRecorderStopHandler(), instanceName: 'audioRecorderStop');
getIt.registerLazySingleton<MessageHandler>(() => AudioRecorderClearHandler(), instanceName: 'audioRecorderClear');
/// 播放
getIt.registerLazySingleton<MessageHandler>(() => AudioPlayHandler(), instanceName: 'audioPlay');
getIt.registerLazySingleton<MessageHandler>(() => AudioPauseHandler(), instanceName: 'audioPause');
getIt.registerLazySingleton<MessageHandler>(() => AudioResumeHandler(), instanceName: 'audioResume');
getIt.registerLazySingleton<MessageHandler>(() => AudioStopHandler(), instanceName: 'audioStop');
getIt.registerLazySingleton<MessageHandler>(() => AudioClearHandler(), instanceName: 'audioClear');
/// service
getIt.registerLazySingleton<ApiService>(() => ApiService(baseUrl: 'https://dev.banxiaoer.net'));
// repository
/// repository
getIt.registerLazySingleton<WechatAuthRepository>(() => WechatAuthRepository());
}
......@@ -7,7 +7,7 @@ class H5Message {
int timestamp;
String unique;
String cmd;
Map<String, dynamic> params;
dynamic params;
H5Message(this.timestamp, this.unique, this.cmd, this.params);
......
......@@ -10,7 +10,7 @@ H5Message _$H5MessageFromJson(Map<String, dynamic> json) => H5Message(
(json['timestamp'] as num).toInt(),
json['unique'] as String,
json['cmd'] as String,
json['params'] as Map<String, dynamic>,
json['params'],
);
Map<String, dynamic> _$H5MessageToJson(H5Message instance) => <String, dynamic>{
......
......@@ -9,7 +9,7 @@ part of 'h5_resp.dart';
H5Resp _$H5RespFromJson(Map<String, dynamic> json) => H5Resp(
json['unique'] as String,
json['cmd'] as String,
json['data'] as Map<String, dynamic>,
json['data'],
json['errMsg'] as String,
);
......
import 'dart:io';
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';
class ChooseFileHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var count = params['count'] as int;
var fileTypes = params['fileTypes'] as List<dynamic>;
FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: fileTypes.map((e) => e.toString()).toList(),
allowMultiple: count > 1,
);
// 用户取消选择,返回空数组
if (filePickerResult == null) {
return [];
}
// 限制最多count个文件
if (filePickerResult.files.length > count) {
filePickerResult.files.removeRange(count, filePickerResult.files.length);
}
final List<Map<String, dynamic>> result = [];
for (var file in filePickerResult.files) {
result.add(await _handleFile(file));
}
return 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 isVideo = false;
if (!isImage) {
isVideo = await FileTypeUtil.isVideo(copiedFile);
}
// 通过image_size_getter获取图片尺寸
SizeResult? sizeResult;
String? thumbTempFilePath;
if (isImage) {
sizeResult = ImageSizeGetter.getSizeResult(FileInput(copiedFile));
thumbTempFilePath = await ThumbnailUtil.genTempThumbnail(copiedFile, tempDir);
}
if (isVideo) {
thumbTempFilePath = await ThumbnailUtil.genVideoThumbnail(copiedFile.path, tempDir);
}
// 返回临时文件信息
return {
'tempFilePath': tempFilePath,
'size': file.size,
'width': sizeResult != null ? sizeResult.size.width : '',
'height': sizeResult != null ? sizeResult.size.height : '',
'thumbTempFilePath': thumbTempFilePath ?? '',
'fileType': copiedFile.path.split('/').last.split('.').last,
};
}
}
import 'dart:io';
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 {
@override
Future<dynamic> handleMessage(Map<String, dynamic> params) async {
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var sourceType = params['sourceType'] as String;
if (sourceType != 'album' && sourceType != 'camera') {
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('参数错误');
}
try {
// 初始化图片选择器
// 从相册选择
if (sourceType == 'album') {
if (count == 1) {
return await _selectSingle();
} else {
return await _selectMulti(count);
}
}
// 拍照
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, // 可以改为 ImageSource.camera 用于拍照
);
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
// 用户取消选择,返回空数组
if (pickedFile == null) {
return [];
}
if (pickedFile != null) {
// 获取临时目录
final Directory tempDir = await getTemporaryDirectory();
// 生成唯一文件名
final String fileName = path.basename(pickedFile.path);
final String uniqueFileName = '${DateTime.now().millisecondsSinceEpoch}_$fileName';
return [await _handleOne(pickedFile, tempDir)];
}
// 创建目标文件路径
final String tempFilePath = path.join(tempDir.path, uniqueFileName);
///
/// 选择多张图片
///
/// 将选择的图片放到应用的临时目录中,并在每个文件路径前面添加“/temp”,返回给调用方后,调用方可通过http方式访问图片
///
Future<List<Map<String, dynamic>>?> _selectMulti(int limit) async {
final ImagePicker picker = ImagePicker();
// 复制文件到临时目录
final File copiedFile = await File(pickedFile.path).copy(tempFilePath);
final List<XFile> pickedFileList = await picker.pickMultiImage(limit: limit);
// 返回临时文件路径
// 前面加上特殊路径,用于后续处理
return "/temp${copiedFile.path}";
// 用户取消选择,返回空数组
if (pickedFileList.isEmpty) {
return [];
}
return null; // 用户取消选择
} catch (e) {
print(e);
return null;
// 限制最多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));
}
}
Future<List<Map<String, dynamic>>?> _selectOne(String sourceType) async {
return result;
}
///
/// 拍照
///
Future<List<Map<String, dynamic>>?> _cameraSingle() async {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(
source: sourceType == 'album' ? ImageSource.gallery : ImageSource.camera,
);
final XFile? pickedFile = await picker.pickImage(source: ImageSource.camera);
// 用户取消选择,返回空数组
if (pickedFile == null) {
return [];
}
if (pickedFile != null) {
// 获取临时目录
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';
......@@ -63,25 +124,37 @@ class ChooseImageHandler implements MessageHandler {
final String tempFilePath = path.join(tempDir.path, uniqueFileName);
// 复制文件到临时目录
final File copiedFile = await File(pickedFile.path).copy(tempFilePath);
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 [
{
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,
},
];
} else {
return null;
}
};
}
/*Future<List<Map<String, dynamic>>?> _selectMulti(int count, String sourceType) async {
final ImagePicker picker = ImagePicker();
List<XFile> fileList = await picker.pickMultiImage();
if (fileList.isNotEmpty) {
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;
}
}
}*/
}
import 'package:appframe/services/dispatcher.dart' as dispatcher;
import 'package:flutter/services.dart';
class SetClipboardDataHandler extends dispatcher.MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! String) {
throw Exception('参数错误');
}
try {
await Clipboard.setData(ClipboardData(text: params));
return true;
} catch (e) {
throw Exception(e.toString());
}
}
}
class GetClipboardDataHandler extends dispatcher.MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
try {
ClipboardData? data = await Clipboard.getData('text/plain');
return data?.text ?? '';
} catch (e) {
throw Exception(e.toString());
}
}
}
import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class CompressImageHandler implements MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
String url = params['url'];
int quality = params['quality'];
int compressedWidth = params['compressedWidth'];
int compressedHeight = params['compressedHeight'];
var originFile = File(url);
// 获取后缀名
String ext = path.extension(url);
final Directory tempDir = await getTemporaryDirectory();
final String uniqueFileName = '${DateTime.now().millisecondsSinceEpoch}$ext';
var result = await FlutterImageCompress.compressAndGetFile(
originFile.absolute.path,
uniqueFileName,
quality: 88,
rotate: 180,
);
return true;
}
}
class CompressVideoHandler implements MessageHandler {
@override
Future handleMessage(dynamic params) {
// TODO: implement handleMessage
throw UnimplementedError();
}
}
......@@ -5,7 +5,7 @@ import 'package:device_info_plus/device_info_plus.dart';
class DeviceInfoHandler implements MessageHandler {
@override
Future<Map<String, dynamic>> handleMessage(Map<String, dynamic> params) async {
Future<Map<String, dynamic>> handleMessage(dynamic params) async {
var deviceInfoPlugin = DeviceInfoPlugin();
// 测试效果,先随便响应一些数据
......
import 'dart:ffi';
import 'package:appframe/services/dispatcher.dart';
import 'package:flutter_sound/flutter_sound.dart';
FlutterSoundPlayer? localPlayer;
bool isLocalPlaying = false;
bool isLocalPausePlaying = false;
String? localPlayedFilePath;
Future<bool> initLocalPlayer() async {
localPlayer = FlutterSoundPlayer();
try {
await localPlayer!.openPlayer();
} catch (e) {
print(e);
await _resetPlayerStatus();
throw Exception('打开播放器失败!');
}
return true;
}
Future<bool> startLocalPlayer(String url) async {
if (localPlayer == null) {
await initLocalPlayer();
}
try {
localPlayedFilePath = url;
localPlayer!.startPlayer(fromURI: url);
return true;
} catch (e) {
print(e);
await _resetPlayerStatus();
return false;
}
}
Future<bool> pauseLocalPlayer() async {
if(!(isLocalPlaying==true && isLocalPausePlaying==false)) {
return false;
}
try {
await localPlayer!.pausePlayer();
return true;
} catch (e) {
print(e);
await _resetPlayerStatus();
return false;
}
}
Future<bool> resumeLocalPlayer() async {
if(!(isLocalPlaying==true && isLocalPausePlaying==true)) {
return false;
}
try {
await localPlayer!.resumePlayer();
return true;
} catch (e) {
print(e);
await _resetPlayerStatus();
return false;
}
}
Future<bool> stopLocalPlayer() async {
try {
localPlayer!.stopPlayer();
return true;
} catch (e) {
print(e);
return false;
} finally {
await _resetPlayerStatus();
}
}
void closeLocalPlayer() async {
await _resetPlayerStatus();
}
Future<void> _resetPlayerStatus() async {
try {
await localPlayer?.closePlayer();
} catch (e) {
print(e);
}
localPlayer = null;
isLocalPlaying = false;
isLocalPausePlaying = false;
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;
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
/*录音相关全局参数*/
FlutterSoundRecorder? localRecorder;
bool isLocalRecording = false;
bool isLocalPauseRecording = false;
String? localRecordedFilePath;
Future<bool> initLocalRecorder() async {
// 请求麦克风权限
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('麦克风权限未授权!');
}
if (!(!isLocalRecording && !isLocalPauseRecording && localRecordedFilePath == null)) {
return false;
}
// 打开录音器
FlutterSoundRecorder recorder = FlutterSoundRecorder();
try {
localRecorder = await recorder.openRecorder();
return true;
} catch (e) {
print(e);
await _resetRecorderStatus();
throw Exception('打开录音器失败!');
}
}
Future<bool> startLocalRecording() async {
if (isLocalRecording) {
return false;
}
if (!await initLocalRecorder()) {
return false;
}
try {
// 获取应用文档目录路径
final directory = await getApplicationDocumentsDirectory();
String filePath = '${directory.path}/bxe_recording.aac';
await localRecorder!.startRecorder(toFile: filePath, codec: Codec.aacMP4);
isLocalRecording = true;
isLocalPauseRecording = false;
localRecordedFilePath = filePath;
return true;
} catch (e) {
print(e);
await _resetRecorderStatus();
return false;
}
}
Future<bool> pauseLocalRecording() async {
if (!(isLocalRecording && !isLocalPauseRecording)) {
return false;
}
try {
await localRecorder!.pauseRecorder();
isLocalPauseRecording = true;
return true;
} catch (e) {
print(e);
await _resetRecorderStatus();
return false;
}
}
Future<bool> resumeLocalRecording() async {
if (!(isLocalRecording && isLocalPauseRecording)) {
return false;
}
try {
await localRecorder!.resumeRecorder();
isLocalPauseRecording = false;
return true;
} catch (e) {
print(e);
await _resetRecorderStatus();
return false;
}
}
Future<String?> stopLocalRecording() async {
if (!(localRecorder != null && isLocalRecording)) {
throw Exception("录音器未初始化");
}
try {
String? url = await localRecorder!.stopRecorder();
isLocalRecording = false;
isLocalPauseRecording = false;
if (url == null || url == "") {
throw Exception("录音失败");
}
await _resetRecorderStatus();
return url;
} catch (e) {
print(e);
await _resetRecorderStatus();
throw Exception("录音失败");
}
}
/// 关闭录音器,释放资源
void closeLocalRecorder() async {
await _resetRecorderStatus();
}
Future<void> _resetRecorderStatus() async {
try {
await localRecorder?.closeRecorder();
} catch (e) {
print(e);
}
localRecorder == null;
isLocalRecording = false;
isLocalPauseRecording = false;
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;
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:gallery_saver_plus/gallery_saver.dart';
class SaveToAlbumHandler implements MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var filePath = params['filePath'] as String?;
if (filePath == null || filePath.isEmpty) {
return false;
}
bool isVideo =
filePath.endsWith('.mp4') ||
filePath.endsWith('.avi') ||
filePath.endsWith('.mov') ||
filePath.endsWith('.mkv') ||
filePath.endsWith('.wmv') ||
filePath.endsWith('.flv') ||
filePath.endsWith('.webm') ||
filePath.endsWith('.m4v') ||
filePath.endsWith('.3gp');
try {
if (isVideo) {
final bool? success = await GallerySaver.saveVideo(filePath);
return success ?? false;
} else {
final bool? success = await GallerySaver.saveImage(filePath);
return success ?? false;
}
} catch (e) {
print(e);
return false;
}
}
}
......@@ -4,35 +4,44 @@ import 'package:shared_preferences/shared_preferences.dart';
class SetStorageSyncHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(Map<String, dynamic> params) async {
final key = params['key'];
final value = params['value'];
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
var key = params['key'];
var value = params['value'];
if (key is! String || value is! String) {
if (key is! String || value is! String || key.isEmpty) {
throw Exception('参数错误');
}
bool result = await getIt.get<SharedPreferences>().setString(key, value);
return result;
try {
return await getIt.get<SharedPreferences>().setString(key, value);
} catch (e) {
throw Exception(e.toString());
}
}
}
class GetStorageSyncHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(Map<String, dynamic> params) async {
final key = params['key'];
if (key is! String) {
Future<dynamic> handleMessage(dynamic params) async {
if (params is! String || params.isEmpty) {
throw Exception('参数错误');
}
String? value = getIt.get<SharedPreferences>().getString(key);
return value ?? "";
try {
return getIt.get<SharedPreferences>().getString(params) ?? "";
} catch (e) {
throw Exception(e.toString());
}
}
}
class ClearStorageSyncHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(Map<String, dynamic> params) async {
Future<dynamic> handleMessage(dynamic params) async {
return await getIt.get<SharedPreferences>().clear();
}
}
......@@ -5,7 +5,8 @@ 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(Map<String, dynamic> params);
Future<dynamic> handleMessage(dynamic params);
}
// 消息分发器
......
import 'dart:io';
import 'package:mime/mime.dart';
class FileTypeUtil {
static Future<bool> isImage(File file) async {
var mimeType = await getMimeType(file);
return mimeType?.startsWith('image/') ?? false;
}
static Future<bool> isVideo(File file) async {
var mimeType = await getMimeType(file);
return mimeType?.startsWith('video/') ?? false;
}
static Future<String?> getMimeType(File file) async {
var headerBytes = await readHeaderBytes(file.path, 12);
return lookupMimeType(file.path, headerBytes: headerBytes);
}
static Future<List<int>> readHeaderBytes(String filePath, int count) async {
final file = File(filePath);
final randomAccessFile = await file.open();
try {
// 读取前count个字节
final bytes = await randomAccessFile.read(count);
return bytes;
} finally {
await randomAccessFile.close();
}
}
}
import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
/// 缩略图工具类
///
class ThumbnailUtil {
/// 在指定目录下生成缩略图
/// 返回缩略图路径
static Future<String?> genTempThumbnail(File imageFile, Directory dir) async {
try {
// 缩略图路径
final tempPath = dir.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;
}
}
/// 为视频文件生成缩略图
/// 返回缩略图路径
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;
} catch (e) {
print('生成视频缩略图出错: $e');
return null;
}
}
}
\ No newline at end of file
......@@ -185,6 +185,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.2"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
device_info_plus:
dependency: "direct main"
description:
......@@ -249,6 +257,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.3.2"
file_selector_linux:
dependency: transitive
description:
......@@ -302,6 +318,54 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.1.1"
flutter_image_compress:
dependency: "direct main"
description:
name: flutter_image_compress
sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
flutter_image_compress_common:
dependency: transitive
description:
name: flutter_image_compress_common
sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.6"
flutter_image_compress_macos:
dependency: transitive
description:
name: flutter_image_compress_macos
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.3"
flutter_image_compress_ohos:
dependency: transitive
description:
name: flutter_image_compress_ohos
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.3"
flutter_image_compress_platform_interface:
dependency: transitive
description:
name: flutter_image_compress_platform_interface
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
flutter_image_compress_web:
dependency: transitive
description:
name: flutter_image_compress_web
sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.5"
flutter_lints:
dependency: "direct dev"
description:
......@@ -368,6 +432,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
gallery_saver_plus:
dependency: "direct main"
description:
name: gallery_saver_plus
sha256: fd4a50eb7b90b1dd8d926ab34dc62abd428feb72f780d4aaf17dcaae12f6743f
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.9"
get_it:
dependency: "direct main"
description:
......@@ -400,6 +472,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
hashcodes:
dependency: transitive
description:
name: hashcodes
sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
http:
dependency: transitive
description:
......@@ -488,6 +568,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
image_size_getter:
dependency: "direct main"
description:
name: image_size_getter
sha256: "7c26937e0ae341ca558b7556591fd0cc456fcc454583b7cb665d2f03e93e590f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
io:
dependency: transitive
description:
......@@ -585,7 +673,7 @@ packages:
source: hosted
version: "1.16.0"
mime:
dependency: transitive
dependency: "direct main"
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
......@@ -712,6 +800,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
platform:
dependency: transitive
description:
......@@ -941,6 +1037,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
video_thumbnail:
dependency: "direct main"
description:
name: video_thumbnail
sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.6"
vm_service:
dependency: transitive
description:
......@@ -1037,6 +1141,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:
......
......@@ -37,6 +37,7 @@ dependencies:
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
......@@ -46,6 +47,11 @@ dependencies:
shared_preferences: ^2.5.3
flutter_sound: ^9.28.0
device_info_plus: ^11.5.0
gallery_saver_plus: ^3.2.9
flutter_image_compress: ^2.4.0
image_size_getter: ^2.4.1
video_thumbnail: ^0.5.6
mime: ^2.0.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!