278 lines
7.6 KiB
Dart
278 lines
7.6 KiB
Dart
import 'dart:io';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_general_repository_imp.dart';
|
||
import 'package:yumi/modules/user/crop/crop_image_page.dart';
|
||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||
|
||
class SCPickUtils {
|
||
static final ImagePicker _pkr = ImagePicker();
|
||
static const int _defaultMaxImageBytes = 4000000;
|
||
static const int _gifOriginalMaxImageBytes = 10 * 1024 * 1024;
|
||
|
||
static void pickImage(
|
||
BuildContext context,
|
||
OnUpLoadCallBack onUpLoadCallBack, {
|
||
double? aspectRatio,
|
||
bool? backOriginalFile = true,
|
||
bool? neeCrop = true,
|
||
bool allowGif = false,
|
||
bool skipCropForGif = true,
|
||
}) async {
|
||
try {
|
||
final XFile? pickedFile =
|
||
allowGif
|
||
? await _pkr.pickImage(source: ImageSource.gallery)
|
||
: await _pkr.pickImage(
|
||
source: ImageSource.gallery,
|
||
imageQuality: 90,
|
||
maxWidth: 1920,
|
||
maxHeight: 1080,
|
||
);
|
||
|
||
if (pickedFile == null) return;
|
||
final isGif = await _igif(pickedFile);
|
||
if (!context.mounted) {
|
||
onUpLoadCallBack(false, "");
|
||
return;
|
||
}
|
||
if (isGif) {
|
||
if (!allowGif) {
|
||
SCTts.show("GIF sc_images are not supported");
|
||
return;
|
||
}
|
||
} else if (!SCPathUtils.fileTypeIsPic(pickedFile.path)) {
|
||
SCTts.show(
|
||
allowGif
|
||
? "Please select sc_images in .jpg, .jpeg, .png, .gif format."
|
||
: "Please select sc_images in .jpg, .jpeg, .png format.",
|
||
);
|
||
return;
|
||
}
|
||
|
||
final File imageFile = File(pickedFile.path);
|
||
if (isGif && skipCropForGif) {
|
||
await _uploadOriginalImage(
|
||
context,
|
||
imageFile,
|
||
onUpLoadCallBack,
|
||
maxBytes: _gifOriginalMaxImageBytes,
|
||
oversizeMessage: "The GIF image size cannot exceed 10M",
|
||
);
|
||
return;
|
||
}
|
||
if (neeCrop ?? false) {
|
||
cropImage(
|
||
imageFile,
|
||
context,
|
||
aspectRatio,
|
||
backOriginalFile,
|
||
onUpLoadCallBack,
|
||
);
|
||
} else {
|
||
await _uploadOriginalImage(
|
||
context,
|
||
imageFile,
|
||
onUpLoadCallBack,
|
||
maxBytes: _defaultMaxImageBytes,
|
||
oversizeMessage:
|
||
SCAppLocalizations.of(context)!.theImageSizeCannotExceed,
|
||
);
|
||
}
|
||
} catch (e) {
|
||
onUpLoadCallBack(false, "");
|
||
SCTts.show("Failed to select image");
|
||
}
|
||
}
|
||
|
||
static Future<void> _uploadOriginalImage(
|
||
BuildContext context,
|
||
File imageFile,
|
||
OnUpLoadCallBack onUpLoadCallBack, {
|
||
required int maxBytes,
|
||
required String oversizeMessage,
|
||
}) async {
|
||
if (imageFile.lengthSync() > maxBytes) {
|
||
SCTts.show(oversizeMessage);
|
||
onUpLoadCallBack(false, "");
|
||
return;
|
||
}
|
||
SCLoadingManager.show(context: context);
|
||
try {
|
||
String fileUrl = await SCGeneralRepositoryImp().upload(imageFile);
|
||
SCLoadingManager.hide();
|
||
onUpLoadCallBack(true, fileUrl);
|
||
} catch (e) {
|
||
SCTts.show("upload fail $e");
|
||
onUpLoadCallBack(false, "");
|
||
SCLoadingManager.hide();
|
||
}
|
||
}
|
||
|
||
/// 检查图像是否为 GIF 格式
|
||
static Future<bool> _igif(XFile file) async {
|
||
try {
|
||
// 方法1: 通过文件扩展名检查
|
||
if (file.path.toLowerCase().endsWith('.gif')) {
|
||
return true;
|
||
}
|
||
|
||
// 方法2: 通过读取文件头检查 (更可靠)
|
||
final bytes = await file.readAsBytes();
|
||
if (bytes.length >= 6) {
|
||
// GIF 文件头特征: "GIF87a" 或 "GIF89a"
|
||
final header = String.fromCharCodes(bytes.sublist(0, 6));
|
||
return header == 'GIF87a' || header == 'GIF89a';
|
||
}
|
||
|
||
return false;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static Future<bool> _iwp(XFile file) async {
|
||
try {
|
||
// 方法1: 通过文件扩展名检查
|
||
if (file.path.toLowerCase().endsWith('.webp')) {
|
||
return true;
|
||
}
|
||
|
||
// 方法2: 通过读取文件头检查
|
||
final bytes = await file.readAsBytes();
|
||
|
||
if (bytes.length < 12) {
|
||
return false;
|
||
}
|
||
|
||
// 检查基本的 WebP 文件头
|
||
final riffHeader = String.fromCharCodes(bytes.sublist(0, 4));
|
||
final webpHeader = String.fromCharCodes(bytes.sublist(8, 12));
|
||
|
||
if (riffHeader == 'RIFF' && webpHeader == 'WEBP') {
|
||
return true;
|
||
}
|
||
|
||
// 检查扩展的 WebP 格式(如果有更多字节可用)
|
||
if (bytes.length >= 16) {
|
||
// 检查 VP8 (有损) WebP
|
||
if (bytes[12] == 0x56 &&
|
||
bytes[13] == 0x50 &&
|
||
bytes[14] == 0x38 &&
|
||
bytes[15] == 0x20) {
|
||
return true;
|
||
}
|
||
|
||
// 检查 VP8L (无损) WebP
|
||
if (bytes[12] == 0x56 &&
|
||
bytes[13] == 0x50 &&
|
||
bytes[14] == 0x38 &&
|
||
bytes[15] == 0x4C) {
|
||
return true;
|
||
}
|
||
|
||
// 检查 VP8X (扩展) WebP
|
||
if (bytes[12] == 0x56 &&
|
||
bytes[13] == 0x50 &&
|
||
bytes[14] == 0x38 &&
|
||
bytes[15] == 0x58) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 拍照并过滤 GIF
|
||
static void takePhoto(
|
||
BuildContext context,
|
||
OnUpLoadCallBack onUpLoadCallBack, {
|
||
double? aspectRatio,
|
||
bool? backOriginalFile = true,
|
||
}) async {
|
||
try {
|
||
final XFile? pickedFile = await _pkr.pickImage(
|
||
source: ImageSource.camera,
|
||
imageQuality: 90,
|
||
maxWidth: 1920,
|
||
maxHeight: 1080,
|
||
);
|
||
|
||
if (pickedFile == null) return;
|
||
|
||
// 相机拍摄的照片不会是 GIF,但为了安全也可以检查
|
||
final isGif = await _igif(pickedFile);
|
||
if (!context.mounted) {
|
||
onUpLoadCallBack(false, "");
|
||
return;
|
||
}
|
||
if (isGif) {
|
||
SCTts.show("Unsupported image format");
|
||
return;
|
||
}
|
||
|
||
final File imageFile = File(pickedFile.path);
|
||
cropImage(
|
||
imageFile,
|
||
context,
|
||
aspectRatio,
|
||
backOriginalFile,
|
||
onUpLoadCallBack,
|
||
);
|
||
} catch (e) {
|
||
SCTts.show("Camera failed");
|
||
}
|
||
}
|
||
|
||
/// 选择视频 (不涉及 GIF 过滤)
|
||
static Future<File?> pickVideo(BuildContext context) async {
|
||
try {
|
||
final XFile? videoFile = await _pkr.pickVideo(
|
||
source: ImageSource.gallery,
|
||
);
|
||
|
||
if (videoFile != null) {
|
||
return File(videoFile.path);
|
||
}
|
||
return null;
|
||
} catch (e) {
|
||
SCTts.show("Failed to select video_pic");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 原有的 cropImage 方法保持不变
|
||
static void cropImage(
|
||
File originalImage,
|
||
BuildContext context,
|
||
double? aspectRatio,
|
||
bool? backOriginalFile,
|
||
OnUpLoadCallBack onUpLoadCallBack, {
|
||
bool needUpload = true,
|
||
}) async {
|
||
await Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder:
|
||
(context) => CropImagePage(
|
||
originalImage,
|
||
aspectRatio: aspectRatio,
|
||
backOriginalFile: backOriginalFile,
|
||
needUpload: needUpload,
|
||
onUpLoadCallBack: (success, url) {
|
||
if (url.isNotEmpty) {
|
||
onUpLoadCallBack(success, url);
|
||
}
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|