QinGang_interested/lib/customWidget/DocumentPicker.dart

271 lines
8.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 封装文件document_picker.dart
// 说明只支持从相册选图片单张或多张和选择文件pdf/任意文件),不支持拍照。
// 同时在内部封装了一个从底部弹出的选择框(“从相册获取”、“选择文件”、“取消”),
// 并在调用相册选择前检查权限(若未授权会引导用户去设置)。
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:file_picker/file_picker.dart';
/// 选中的文件统一结构
class SelectedFile {
final String name;
final String? path; // 若无法直接拿到物理文件,则为 null
final Uint8List? bytes; // 当 path 不可用时,可能通过 bytes 提供内容
final int? size; // 字节数
final String? mimeType; // 可选 mime
final SourceType source;
SelectedFile({
required this.name,
this.path,
this.bytes,
this.size,
this.mimeType,
required this.source,
});
bool get hasFile => path != null || bytes != null;
}
enum SourceType { gallery, assetPicker, filePicker }
class DocumentPicker {
DocumentPicker._(); // 不可实例化,使用静态方法
static final ImagePicker _imagePicker = ImagePicker();
/// 检查并请求相册/媒体库权限。
/// 返回 true 表示有权限可访问false 表示拒绝或受限limited 也视为可用)。
static Future<bool> ensurePhotoPermission() async {
final PermissionState ps = await PhotoManager.requestPermissionExtend();
return ps.isAuth || ps == PermissionState.limited;
}
/// 单张从相册选择image_picker
static Future<SelectedFile?> pickSingleImageFromGallery({int? maxSizeInBytes}) async {
try {
final XFile? xfile = await _imagePicker.pickImage(source: ImageSource.gallery, imageQuality: 90);
if (xfile == null) return null;
final File f = File(xfile.path);
final int size = await f.length();
if (maxSizeInBytes != null && size > maxSizeInBytes) return null;
final bytes = await f.readAsBytes();
return SelectedFile(
name: xfile.name,
path: xfile.path,
bytes: bytes,
size: size,
mimeType: null,
source: SourceType.gallery,
);
} catch (e) {
debugPrint('pickSingleImageFromGallery error: $e');
return null;
}
}
/// 多选图片(推荐使用 wechat_assets_picker
static Future<List<SelectedFile>> pickAssets({
required BuildContext context,
int maxAssets = 9,
int? maxSizeInBytes,
}) async {
try {
final hasAuth = await ensurePhotoPermission();
if (!hasAuth) {
// 未授权,返回空,需要调用者处理引导用户
debugPrint('Photo permission denied');
return [];
}
final List<AssetEntity>? result = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: maxAssets,
requestType: RequestType.image,
specialPickerType: SpecialPickerType.noPreview,
),
);
if (result == null || result.isEmpty) return [];
final List<SelectedFile> out = [];
for (final asset in result) {
final File? file = await asset.file;
if (file != null) {
final int size = await file.length();
if (maxSizeInBytes != null && size > maxSizeInBytes) continue;
final bytes = await file.readAsBytes();
out.add(SelectedFile(
name: file.path.split('/').last,
path: file.path,
bytes: bytes,
size: size,
mimeType: null,
source: SourceType.assetPicker,
));
} else {
final Uint8List? originData = await asset.originBytes;
if (originData == null) continue;
final int size = originData.lengthInBytes;
if (maxSizeInBytes != null && size > maxSizeInBytes) continue;
out.add(SelectedFile(
name: '${asset.id}.jpg',
path: null,
bytes: originData,
size: size,
mimeType: null,
source: SourceType.assetPicker,
));
}
}
return out;
} catch (e) {
debugPrint('pickAssets error: $e');
return [];
}
}
/// 选择任意文件(如 pdf/doc 等)
static Future<List<SelectedFile>> pickFiles({
bool allowMultiple = false,
List<String>? allowedExtensions,
int? maxSizeInBytes,
}) async {
try {
final FileType ft = (allowedExtensions == null) ? FileType.any : FileType.custom;
final FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: allowMultiple,
type: ft,
allowedExtensions: allowedExtensions,
withData: true,
);
if (result == null) return [];
final List<SelectedFile> out = [];
for (final pf in result.files) {
final int size = pf.size ?? (pf.bytes?.lengthInBytes ?? 0);
if (maxSizeInBytes != null && size > maxSizeInBytes) continue;
out.add(SelectedFile(
name: pf.name,
path: pf.path,
bytes: pf.bytes,
size: size,
mimeType: pf.extension,
source: SourceType.filePicker,
));
}
return out;
} catch (e) {
debugPrint('pickFiles error: $e');
return [];
}
}
/// 小工具:把 SelectedFile 转换为上传需要的 File (如果 path 可用),否则保存 bytes 到临时文件并返回 File
static Future<File?> toFile(SelectedFile sf) async {
try {
if (sf.path != null) return File(sf.path!);
if (sf.bytes != null) {
final temp = await File('${Directory.systemTemp.path}/${sf.name}').create();
await temp.writeAsBytes(sf.bytes!);
return temp;
}
return null;
} catch (e) {
debugPrint('toFile error: $e');
return null;
}
}
/// 底部弹窗,用户从 "从相册获取" / "选择文件" / "取消" 中选择。
/// 该方法会在用户选择后直接执行对应的选择逻辑并返回选中的文件列表。
/// - maxAssets: 多选图片时的最大数量
/// - maxSizeInBytes: 单个文件最大字节数限制
/// - allowedExtensions: 选择文件时允许的扩展名(为空表示不限制)
static Future<List<SelectedFile>> showPickerModal(
BuildContext context, {
int maxAssets = 9,
int? maxSizeInBytes,
List<String>? allowedExtensions,
bool allowMultipleFiles = false,
}) async {
final choice = await showModalBottomSheet<String>(
context: context,
builder: (c) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Center(child: Text('从相册获取')),
onTap: () => Navigator.of(c).pop('gallery'),
),
const Divider(height: 1),
ListTile(
title: const Center(child: Text('选择文件')),
onTap: () => Navigator.of(c).pop('file'),
),
const Divider(height: 1),
ListTile(
title: const Center(child: Text('取消')),
onTap: () => Navigator.of(c).pop(null),
),
],
),
);
},
);
if (choice == null) return [];
if (choice == 'gallery') {
final hasAuth = await ensurePhotoPermission();
if (!hasAuth) {
// 未授权,提示并引导用户去设置
await showDialog<void>(
context: context,
builder: (d) {
return AlertDialog(
title: const Text('权限未开启'),
content: const Text('应用暂无相册权限,是否去设置开启?'),
actions: [
TextButton(onPressed: () => Navigator.of(d).pop(), child: const Text('取消')),
TextButton(
onPressed: () {
PhotoManager.openSetting();
Navigator.of(d).pop();
},
child: const Text('去设置'),
),
],
);
},
);
return [];
}
// 先提供多选wechat_assets_picker如果你只想要单选可替换为 pickSingleImageFromGallery
final images = await pickAssets(context: context, maxAssets: maxAssets, maxSizeInBytes: maxSizeInBytes);
return images;
}
if (choice == 'file') {
final files = await pickFiles(allowMultiple: allowMultipleFiles, allowedExtensions: allowedExtensions, maxSizeInBytes: maxSizeInBytes);
return files;
}
return [];
}
}