|
import picker from '@ohos.file.picker';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import camera from '@ohos.multimedia.cameraPicker';
import common from '@ohos.app.ability.common';
import fileIo from '@ohos.file.fs';
import fileUri from '@ohos.file.fileuri';
/**
* 处理 `<input type=file>` 触发的文件选择请求。
*
* 根据 H5 `accept` / `capture` / `multiple` 属性,自动选择系统相机、图库、
* 或文档选择器,并返回选中文件的 URI 列表交还 ArkWeb。
*/
export class FileChooserHandler {
/**
* ArkWeb 把 H5 `accept="image/*"` 在原生层展开后的扩展名清单。
* 用于在仅拿到扩展名(如 `.jpg`)时反查媒体大类。
*/
private static readonly IMAGE_EXTS: ReadonlySet<string> = new Set<string>([
'.tif', '.xbm', '.tiff', '.pjp', '.jfif', '.bmp', '.avif', '.apng', '.ico',
'.webp', '.svg', '.gif', '.svgz', '.jpg', '.jpeg', '.png', '.pjpeg'
]);
/**
* ArkWeb 把 H5 `accept="video/*"` 在原生层展开后的扩展名清单。
*/
private static readonly VIDEO_EXTS: ReadonlySet<string> = new Set<string>([
'.mp4', '.mpg', '.mpeg', '.m4v', '.ogm', '.ogv', '.webm'
]);
/**
* 入口:根据 ArkWeb 传入的文件选择参数选择合适的系统选择器。
*
* @param context UIAbilityContext,用于拉起系统应用
* @param acceptTypes `getAcceptType()` 返回的扩展名/MIME 数组
* @param isCapture 是否要求即时拍摄(对应 H5 `capture` 属性)
* @param mode 单选 / 多选(对应 H5 `multiple` 属性)
* @returns 选中文件的 URI 列表;用户取消或失败均返回 `[]`
*/
public static async pick(
context: common.UIAbilityContext,
acceptTypes: string[],
isCapture: boolean,
mode: 'single' | 'multiple'
): Promise<string[]> {
const hasImage: boolean = FileChooserHandler.hasCategory(acceptTypes, 'image');
const hasVideo: boolean = FileChooserHandler.hasCategory(acceptTypes, 'video');
if (isCapture) {
return await FileChooserHandler.captureFromCamera(context, hasImage, hasVideo);
}
if (hasImage || hasVideo) {
return await FileChooserHandler.pickPhoto(mode, hasImage, hasVideo);
}
return await FileChooserHandler.pickDocument(mode, acceptTypes);
}
/**
* 拉起系统相机进行拍照或录像。
*
* `accept` 仅含 image → 拍照;仅含 video → 录像;两者皆有/皆无 → 由系统 picker 让用户选择。
* 录像需预先在沙箱内创建 saveUri 文件,否则 5.0.x 上 cameraPicker 会回退到拍照模式。
*/
private static async captureFromCamera(
context: common.UIAbilityContext,
hasImage: boolean,
hasVideo: boolean
): Promise<string[]> {
try {
const isVideoOnly: boolean = hasVideo && !hasImage;
const isPhotoOnly: boolean = hasImage && !hasVideo;
const mediaTypes: camera.PickerMediaType[] = isVideoOnly
? [camera.PickerMediaType.VIDEO]
: isPhotoOnly
? [camera.PickerMediaType.PHOTO]
: [camera.PickerMediaType.PHOTO, camera.PickerMediaType.VIDEO];
const ext: string = isVideoOnly ? '.mp4' : '.jpg';
const filePath: string = context.filesDir + '/qian_' + Date.now() + ext;
fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);
const saveUri: string = fileUri.getUriFromPath(filePath);
const profile: ESObject = {
cameraPosition: 1,
saveUri: saveUri
};
if (isVideoOnly) {
// videoDuration 单位秒,0 表示不限时长
profile.videoDuration = 0;
}
const result: camera.PickerResult = await camera.pick(
context,
mediaTypes,
profile as camera.PickerProfile
);
if (result === null || result === undefined || result.resultCode !== 0) {
return [];
}
const uri: string = result.resultUri;
return uri !== null && uri !== undefined && uri.length > 0 ? [uri] : [];
} catch (_e) {
return [];
}
}
/** 拉起系统图库选择器(图片 / 视频 / 混合)。 */
private static async pickPhoto(
mode: 'single' | 'multiple',
hasImage: boolean,
hasVideo: boolean
): Promise<string[]> {
try {
const photoPicker = new photoAccessHelper.PhotoViewPicker();
const options: photoAccessHelper.PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = FileChooserHandler.resolvePhotoMimeType(hasImage, hasVideo);
options.maxSelectNumber = mode === 'multiple' ? 9 : 1;
const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options);
const uris: string[] = result.photoUris;
return uris !== null && uris !== undefined ? uris : [];
} catch (_e) {
return [];
}
}
/** 根据 hasImage / hasVideo 选择图库 MIMEType。 */
private static resolvePhotoMimeType(
hasImage: boolean,
hasVideo: boolean
): photoAccessHelper.PhotoViewMIMETypes {
if (hasImage && hasVideo) {
return photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
}
return hasVideo
? photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE
: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
}
/** 拉起文档选择器,按 accept 限定可选后缀。 */
private static async pickDocument(
mode: 'single' | 'multiple',
acceptTypes: string[]
): Promise<string[]> {
try {
const documentPicker = new picker.DocumentViewPicker();
const options: picker.DocumentSelectOptions = new picker.DocumentSelectOptions();
options.maxSelectNumber = mode === 'multiple' ? 9 : 1;
const filters: string[] = FileChooserHandler.acceptTypesToSuffixFilters(acceptTypes);
if (filters.length > 0) {
(options as ESObject).fileSuffixFilters = filters;
}
const uris: string[] = await documentPicker.select(options);
return uris !== null && uris !== undefined ? uris : [];
} catch (_e) {
return [];
}
}
/**
* 判断 accept 是否覆盖某个媒体大类。
*
* `getAcceptType()` 返回的元素既可能是 MIME(`image/png`、`video/*`),也可能是
* 扩展名(`.jpg`、`.mp4`)——ArkWeb 会把通配 MIME 展开成扩展名清单。两种格式同时识别。
*/
private static hasCategory(
acceptTypes: string[],
category: 'image' | 'video'
): boolean {
const whitelist: ReadonlySet<string> = category === 'image'
? FileChooserHandler.IMAGE_EXTS
: FileChooserHandler.VIDEO_EXTS;
for (const raw of FileChooserHandler.flatten(acceptTypes)) {
if (raw === category || raw.indexOf(category + '/') === 0) {
return true;
}
if (raw.charAt(0) === '.' && whitelist.has(raw)) {
return true;
}
}
return false;
}
/** 规范化 accept 数组:拆逗号、trim、转小写、去空,得到扁平的 token 列表。 */
private static flatten(values: string[]): string[] {
if (values === null || values === undefined || values.length === 0) {
return [];
}
const out: string[] = [];
for (const raw of values) {
if (raw === null || raw === undefined) {
continue;
}
const parts: string[] = raw.split(',');
for (const p of parts) {
const t: string = p.trim().toLowerCase();
if (t.length > 0) {
out.push(t);
}
}
}
return out;
}
/**
* 把 accept 列表转换为文档选择器的后缀过滤数组。
* 含通配(`* / *`、`image/*`)或未识别 MIME 时返回空数组,表示不过滤。
*/
private static acceptTypesToSuffixFilters(acceptTypes: string[]): string[] {
if (acceptTypes === null || acceptTypes === undefined || acceptTypes.length === 0) {
return [];
}
const filters: string[] = [];
const seen: Set<string> = new Set<string>();
for (const raw of acceptTypes) {
const t: string = raw.trim().toLowerCase();
if (t.length === 0) {
continue;
}
if (t.indexOf('/*') >= 0 || t === '*' || t === '*/*') {
return [];
}
if (t.charAt(0) === '.') {
const filterStr: string = FileChooserHandler.suffixToFilter(t);
if (!seen.has(filterStr)) {
seen.add(filterStr);
filters.push(filterStr);
}
continue;
}
const ext: string = FileChooserHandler.mimeToSuffix(t);
if (ext.length === 0) {
return [];
}
const filterStr: string = FileChooserHandler.suffixToFilter(ext);
if (!seen.has(filterStr)) {
seen.add(filterStr);
filters.push(filterStr);
}
}
return filters;
}
/** 后缀转 picker filter 字符串。 */
private static suffixToFilter(suffix: string): string {
const ext: string = suffix.charAt(0) === '.' ? suffix : ('.' + suffix);
const upper: string = ext.substring(1).toUpperCase();
return upper + '文件|' + ext;
}
/** 常见 MIME 到文件后缀的映射。 */
private static mimeToSuffix(mime: string): string {
switch (mime) {
case 'application/pdf':
return '.pdf';
case 'image/png':
return '.png';
case 'image/jpeg':
case 'image/jpg':
return '.jpg';
case 'image/gif':
return '.gif';
case 'image/bmp':
return '.bmp';
case 'image/webp':
return '.webp';
case 'application/msword':
return '.doc';
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return '.docx';
case 'application/vnd.ms-excel':
return '.xls';
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return '.xlsx';
case 'application/vnd.ms-powerpoint':
return '.ppt';
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
return '.pptx';
case 'text/plain':
return '.txt';
case 'application/zip':
return '.zip';
default:
return '';
}
}
private constructor() {}
}
|