2025-11-14 11:35:45 +08:00
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:dio/dio.dart';
|
2025-10-20 10:44:59 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
2025-11-14 11:35:45 +08:00
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
|
import 'package:flutter_common/upload_image/ossUtil.dart';
|
2025-10-20 10:44:59 +08:00
|
|
|
|
import 'package:get/get.dart';
|
2025-11-14 11:35:45 +08:00
|
|
|
|
import 'package:image_editor_plus/image_editor_plus.dart';
|
|
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
2025-10-20 10:44:59 +08:00
|
|
|
|
import 'package:photo_view/photo_view_gallery.dart';
|
|
|
|
|
|
|
|
|
|
|
|
class LookImagesTool {
|
|
|
|
|
|
static lookImages({
|
|
|
|
|
|
required List<String> listData,
|
|
|
|
|
|
int? currentPage,
|
2025-11-14 13:55:55 +08:00
|
|
|
|
void Function(String)? onCallBack,
|
2025-11-14 11:35:45 +08:00
|
|
|
|
String? oSSAccessKeyId,
|
2025-11-14 13:55:55 +08:00
|
|
|
|
Function? callBack,
|
2025-11-14 11:35:45 +08:00
|
|
|
|
String? policy,
|
|
|
|
|
|
String? callback,
|
|
|
|
|
|
String? signature,
|
|
|
|
|
|
String? ossDirectory,
|
|
|
|
|
|
String? ossHost,
|
2025-10-20 10:44:59 +08:00
|
|
|
|
}) async {
|
|
|
|
|
|
showDialog(
|
|
|
|
|
|
context: Get.context!,
|
2025-11-03 15:03:54 +08:00
|
|
|
|
useSafeArea: false,
|
2025-10-20 10:44:59 +08:00
|
|
|
|
builder: (_) {
|
|
|
|
|
|
return LookImagesWidget(
|
2025-11-14 11:35:45 +08:00
|
|
|
|
listData: listData,
|
|
|
|
|
|
currentPage: currentPage,
|
|
|
|
|
|
oSSAccessKeyId: oSSAccessKeyId,
|
|
|
|
|
|
policy: policy,
|
|
|
|
|
|
callback: callback,
|
|
|
|
|
|
signature: signature,
|
|
|
|
|
|
ossDirectory: ossDirectory,
|
|
|
|
|
|
ossHost: ossHost);
|
2025-10-20 10:44:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class LookImagesWidget extends StatefulWidget {
|
|
|
|
|
|
final List<String> listData;
|
|
|
|
|
|
final int? currentPage;
|
2025-11-14 11:35:45 +08:00
|
|
|
|
final String? oSSAccessKeyId;
|
|
|
|
|
|
final String? policy;
|
|
|
|
|
|
final String? callback;
|
|
|
|
|
|
final String? signature;
|
|
|
|
|
|
final String? ossDirectory;
|
|
|
|
|
|
final String? ossHost;
|
2025-11-14 13:55:55 +08:00
|
|
|
|
final void Function(String)? onCallBack;
|
2025-10-20 10:44:59 +08:00
|
|
|
|
|
|
|
|
|
|
const LookImagesWidget({
|
|
|
|
|
|
super.key,
|
|
|
|
|
|
required this.listData,
|
|
|
|
|
|
this.currentPage,
|
2025-11-14 11:35:45 +08:00
|
|
|
|
this.oSSAccessKeyId,
|
|
|
|
|
|
this.policy,
|
|
|
|
|
|
this.callback,
|
|
|
|
|
|
this.signature,
|
|
|
|
|
|
this.ossDirectory,
|
|
|
|
|
|
this.ossHost,
|
2025-11-14 13:55:55 +08:00
|
|
|
|
this.onCallBack,
|
2025-10-20 10:44:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<LookImagesWidget> createState() => _LookImagesWidgetState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _LookImagesWidgetState extends State<LookImagesWidget> {
|
|
|
|
|
|
List listData = [];
|
|
|
|
|
|
late int currentPage;
|
|
|
|
|
|
late int initialPage = 0;
|
|
|
|
|
|
|
2025-11-14 11:35:45 +08:00
|
|
|
|
/// Dio 最简版:网络图片转 Uint8List
|
|
|
|
|
|
Future<Uint8List?> networkImageToUint8ListWithDio(String imageUrl) async {
|
|
|
|
|
|
final dio = Dio(); // 初始化 Dio 实例
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 发起 GET 请求,响应类型设为字节数组(关键)
|
|
|
|
|
|
final response = await dio.get<List<int>>(
|
|
|
|
|
|
imageUrl,
|
|
|
|
|
|
options: Options(responseType: ResponseType.bytes),
|
|
|
|
|
|
);
|
|
|
|
|
|
// 响应成功且数据非空时,直接转为 Uint8List
|
|
|
|
|
|
return response.statusCode == 200 && response.data != null ? Uint8List.fromList(response.data!) : null;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
print('图片转换失败:$e'); // 捕获网络错误、URL 非法等异常
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<Uint8List?> editImage({required String url}) async {
|
|
|
|
|
|
Uint8List? imageBytes = await networkImageToUint8ListWithDio(url);
|
|
|
|
|
|
ImageEditor.setI18n({
|
|
|
|
|
|
'crop': '裁剪',
|
|
|
|
|
|
'rotate left': '左旋转',
|
|
|
|
|
|
'rotate right': '右旋转',
|
|
|
|
|
|
'flip': '水平翻转',
|
|
|
|
|
|
'brush': '涂抹',
|
|
|
|
|
|
'link': '链接',
|
|
|
|
|
|
'save': '保存',
|
|
|
|
|
|
'text': '文本',
|
|
|
|
|
|
'blur': '模糊',
|
|
|
|
|
|
'filter': '滤镜',
|
|
|
|
|
|
'size': '大小',
|
|
|
|
|
|
'color': '颜色',
|
|
|
|
|
|
'background color': '背景颜色',
|
|
|
|
|
|
'background opacity': '背景透明度',
|
|
|
|
|
|
'reset': '重置',
|
|
|
|
|
|
'freeform': '自由裁剪',
|
|
|
|
|
|
'remove': '移除',
|
|
|
|
|
|
'emoji': '表情',
|
|
|
|
|
|
'slider color': '滑块颜色',
|
|
|
|
|
|
'color opacity': '透明度',
|
|
|
|
|
|
'blur radius': '模糊半径',
|
|
|
|
|
|
});
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
Uint8List? editedImage = await Navigator.push(
|
|
|
|
|
|
context,
|
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
|
builder: (context) => ImageEditor(
|
|
|
|
|
|
image: imageBytes,
|
|
|
|
|
|
blurOption: null,
|
|
|
|
|
|
filtersOption: null,
|
|
|
|
|
|
brushOption: null,
|
|
|
|
|
|
textOption: null,
|
|
|
|
|
|
emojiOption: null,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
return editedImage;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-14 13:43:24 +08:00
|
|
|
|
/// 可选:根据字节头自动识别文件后缀(如图片、视频)
|
|
|
|
|
|
String getExtension(Uint8List uint8List) {
|
|
|
|
|
|
if (uint8List.length < 4) return "bin"; // 无法识别时返回二进制后缀
|
|
|
|
|
|
|
|
|
|
|
|
// PNG 头:89 50 4E 47
|
2025-11-14 13:55:55 +08:00
|
|
|
|
if (uint8List[0] == 0x89 && uint8List[1] == 0x50 && uint8List[2] == 0x4E && uint8List[3] == 0x47) {
|
2025-11-14 13:43:24 +08:00
|
|
|
|
return "png";
|
|
|
|
|
|
}
|
|
|
|
|
|
// JPG 头:FF D8 FF
|
|
|
|
|
|
else if (uint8List[0] == 0xFF && uint8List[1] == 0xD8 && uint8List[2] == 0xFF) {
|
|
|
|
|
|
return "jpg";
|
|
|
|
|
|
}
|
|
|
|
|
|
// MP4 头:00 00 00 18 66 74 79 70
|
|
|
|
|
|
else if (uint8List.length >= 8 &&
|
|
|
|
|
|
uint8List[0] == 0x00 &&
|
|
|
|
|
|
uint8List[1] == 0x00 &&
|
|
|
|
|
|
uint8List[2] == 0x00 &&
|
|
|
|
|
|
uint8List[3] == 0x18 &&
|
|
|
|
|
|
uint8List[4] == 0x66 &&
|
|
|
|
|
|
uint8List[5] == 0x74 &&
|
|
|
|
|
|
uint8List[6] == 0x79 &&
|
|
|
|
|
|
uint8List[7] == 0x70) {
|
|
|
|
|
|
return "mp4";
|
|
|
|
|
|
}
|
|
|
|
|
|
// 其他格式可自行扩展(如 GIF、PDF 等)
|
|
|
|
|
|
else {
|
|
|
|
|
|
return "bin";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-14 13:55:55 +08:00
|
|
|
|
/// Uint8List 转临时 File 并且上传到oss并返回访问路径
|
|
|
|
|
|
Future<String?> uint8ListToTempFile(Uint8List uint8List, {String fileName = "temp_file"}) async {
|
2025-11-14 13:43:24 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 1. 获取临时存储目录(跨平台兼容)
|
|
|
|
|
|
Directory tempDir = await getTemporaryDirectory();
|
2025-11-14 13:55:55 +08:00
|
|
|
|
|
2025-11-14 13:43:24 +08:00
|
|
|
|
String tempPath = tempDir.path;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 拼接文件路径(可自定义后缀,如 .png、.mp4 等)
|
|
|
|
|
|
File tempFile = File("$tempPath/$fileName.${getExtension(uint8List)}"); // 自动识别后缀(可选)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 将 Uint8List 写入文件
|
|
|
|
|
|
await tempFile.writeAsBytes(uint8List);
|
2025-11-14 13:55:55 +08:00
|
|
|
|
|
|
|
|
|
|
// print("临时文件路径:${tempFile.path}");
|
|
|
|
|
|
|
2025-11-14 13:43:24 +08:00
|
|
|
|
String imageUrl = await UploadOss.upload(
|
|
|
|
|
|
tempFile.path,
|
|
|
|
|
|
fileType: getExtension(uint8List),
|
|
|
|
|
|
oSSAccessKeyId: widget.oSSAccessKeyId ?? '',
|
|
|
|
|
|
ossHost: widget.ossHost ?? '',
|
|
|
|
|
|
ossDirectory: widget.ossDirectory ?? '',
|
|
|
|
|
|
policy: widget.policy ?? '',
|
|
|
|
|
|
callback: widget.callback ?? '',
|
|
|
|
|
|
signature: widget.signature ?? '',
|
|
|
|
|
|
);
|
2025-11-14 13:55:55 +08:00
|
|
|
|
// print("上传后的访问路径:$imageUrl");
|
|
|
|
|
|
return imageUrl;
|
2025-11-14 13:43:24 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
print("转换临时文件失败:$e");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 10:44:59 +08:00
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
listData = widget.listData;
|
|
|
|
|
|
if (widget.currentPage == null) {
|
|
|
|
|
|
initialPage = 0;
|
|
|
|
|
|
currentPage = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentPage = widget.currentPage ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
2025-11-03 15:01:55 +08:00
|
|
|
|
return Scaffold(
|
|
|
|
|
|
body: Stack(
|
2025-10-20 10:44:59 +08:00
|
|
|
|
children: [
|
2025-11-03 15:01:55 +08:00
|
|
|
|
PhotoViewGallery.builder(
|
|
|
|
|
|
itemCount: listData.length,
|
|
|
|
|
|
pageController: PageController(initialPage: currentPage),
|
|
|
|
|
|
onPageChanged: (index) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
currentPage = index;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
builder: (_, index) {
|
|
|
|
|
|
return PhotoViewGalleryPageOptions(
|
|
|
|
|
|
imageProvider: NetworkImage(
|
|
|
|
|
|
listData[index],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
2025-10-20 10:44:59 +08:00
|
|
|
|
),
|
2025-11-03 15:01:55 +08:00
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 15,
|
|
|
|
|
|
top: 50,
|
|
|
|
|
|
child: GestureDetector(onTap: () => Get.back(), child: Icon(Icons.arrow_back_ios, color: Colors.white))),
|
2025-11-14 09:51:16 +08:00
|
|
|
|
Positioned(
|
|
|
|
|
|
right: 15,
|
|
|
|
|
|
top: 50,
|
2025-11-14 11:35:45 +08:00
|
|
|
|
child: GestureDetector(
|
|
|
|
|
|
onTap: () async {
|
|
|
|
|
|
Uint8List? imageFile = await editImage(url: listData[currentPage]);
|
2025-11-14 13:55:55 +08:00
|
|
|
|
String? url = await uint8ListToTempFile(imageFile ?? Uint8List(0));
|
|
|
|
|
|
widget.onCallBack!(url??'');
|
2025-11-14 11:35:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
child: Icon(Icons.edit, color: Colors.white))),
|
2025-10-20 10:44:59 +08:00
|
|
|
|
//图片张数指示器
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
bottom: 20,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
"${currentPage + 1}/${listData.length}",
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
decoration: TextDecoration.none,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|