chore: 重构
This commit is contained in:
parent
56d8737bc9
commit
5abb5a6836
@ -23,6 +23,7 @@ export default function AskScreen() {
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAuthStatus(router);
|
checkAuthStatus(router);
|
||||||
|
router.replace('/login');
|
||||||
}, []);
|
}, []);
|
||||||
// 在组件内部添加 ref
|
// 在组件内部添加 ref
|
||||||
const scrollViewRef = useRef<ScrollView>(null);
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { fetchApi } from '@/lib/server-api-util';
|
import { addMaterial, confirmUpload, getUploadUrl } from '@/lib/background-uploader/api';
|
||||||
import { defaultExifData, ExifData, ImagesuploaderProps, UploadUrlResponse } from '@/types/upload';
|
import { ConfirmUpload, FileUploadItem, UploadResult, UploadTask } from '@/lib/background-uploader/types';
|
||||||
|
import { uploadFileWithProgress } from '@/lib/background-uploader/uploader';
|
||||||
|
import { compressImage } from '@/lib/image-process/imageCompress';
|
||||||
|
import { defaultExifData, ExifData, ImagesuploaderProps } from '@/types/upload';
|
||||||
import * as ImageManipulator from 'expo-image-manipulator';
|
import * as ImageManipulator from 'expo-image-manipulator';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import * as Location from 'expo-location';
|
import * as Location from 'expo-location';
|
||||||
@ -8,46 +11,6 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native';
|
import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native';
|
||||||
import UploadPreview from './preview';
|
import UploadPreview from './preview';
|
||||||
|
|
||||||
// 在文件顶部添加这些类型
|
|
||||||
type UploadTask = {
|
|
||||||
file: File;
|
|
||||||
metadata: {
|
|
||||||
isCompressed: string;
|
|
||||||
type: string;
|
|
||||||
isThumbnail?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type FileUploadItem = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
progress: number;
|
|
||||||
status: 'pending' | 'uploading' | 'done' | 'error';
|
|
||||||
error: string | null;
|
|
||||||
type: 'image' | 'video';
|
|
||||||
thumbnail: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConfirmUpload = {
|
|
||||||
file_id: string;
|
|
||||||
upload_url: string;
|
|
||||||
name: string;
|
|
||||||
size: number;
|
|
||||||
content_type: string;
|
|
||||||
file_path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UploadResult = {
|
|
||||||
originalUrl?: string;
|
|
||||||
compressedUrl: string;
|
|
||||||
file: File | null;
|
|
||||||
exif: any;
|
|
||||||
originalFile: ConfirmUpload;
|
|
||||||
compressedFile: ConfirmUpload;
|
|
||||||
thumbnail: string;
|
|
||||||
thumbnailFile: File;
|
|
||||||
};
|
|
||||||
export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
@ -81,191 +44,6 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取上传URL
|
|
||||||
const getUploadUrl = async (file: File, metadata: Record<string, any> = {}): Promise<UploadUrlResponse> => {
|
|
||||||
const body = {
|
|
||||||
filename: file.name,
|
|
||||||
content_type: file.type,
|
|
||||||
file_size: file.size,
|
|
||||||
metadata: {
|
|
||||||
...metadata,
|
|
||||||
originalName: file.name,
|
|
||||||
fileType: 'image',
|
|
||||||
isCompressed: metadata.isCompressed || 'false',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return await fetchApi<UploadUrlResponse>("/file/generate-upload-url", {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 向服务端confirm上传
|
|
||||||
const confirmUpload = async (file_id: string): Promise<ConfirmUpload> => await fetchApi<ConfirmUpload>('/file/confirm-upload', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_id
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// 新增素材
|
|
||||||
const addMaterial = async (file: string, compressFile: string) => {
|
|
||||||
await fetchApi('/material', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify([{
|
|
||||||
"file_id": file,
|
|
||||||
"preview_file_id": compressFile
|
|
||||||
}])
|
|
||||||
}).catch((error) => {
|
|
||||||
// console.log(error);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传文件到URL
|
|
||||||
const uploadFileToUrl = async (file: File, uploadUrl: string, onProgress: (progress: number) => void): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhr.open('PUT', uploadUrl);
|
|
||||||
xhr.setRequestHeader('Content-Type', file.type);
|
|
||||||
|
|
||||||
// 进度监听
|
|
||||||
xhr.upload.onprogress = (event) => {
|
|
||||||
if (event.lengthComputable) {
|
|
||||||
const progress = Math.round((event.loaded / event.total) * 100);
|
|
||||||
onProgress(progress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = () => {
|
|
||||||
reject(new Error('Network error during upload'));
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(file);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 压缩并处理图片
|
|
||||||
const processImage = async (uri: string, fileName: string, mimeType: string) => {
|
|
||||||
try {
|
|
||||||
// 压缩图片
|
|
||||||
const manipResult = await ImageManipulator.manipulateAsync(
|
|
||||||
uri,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
resize: {
|
|
||||||
width: maxWidth,
|
|
||||||
height: maxHeight,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
compress: compressQuality,
|
|
||||||
format: ImageManipulator.SaveFormat.JPEG,
|
|
||||||
base64: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取压缩后的图片数据
|
|
||||||
const response = await fetch(manipResult.uri);
|
|
||||||
const blob = await response.blob();
|
|
||||||
|
|
||||||
// 创建文件对象
|
|
||||||
const file = new File([blob], `compressed_${Date.now()}_${fileName}`, {
|
|
||||||
type: mimeType,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { file, uri: manipResult.uri };
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('图片压缩失败:', error);
|
|
||||||
throw new Error('图片处理失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const uploadWithProgress = async (file: File, metadata: any): Promise<ConfirmUpload> => {
|
|
||||||
let timeoutId: number
|
|
||||||
console.log("uploadWithProgress", metadata);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Starting upload for file:", file.name, "size:", file.size, "type:", file.type);
|
|
||||||
|
|
||||||
// 检查文件大小
|
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
|
||||||
throw new Error(`文件大小超过限制 (${(MAX_FILE_SIZE / 1024 / 1024).toFixed(1)}MB)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadUrlData = await getUploadUrl(file, { ...metadata, GPSVersionID: undefined });
|
|
||||||
console.log("Got upload URL for:", file.name);
|
|
||||||
|
|
||||||
return new Promise<ConfirmUpload>((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
// 设置超时
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
reject(new Error('上传超时,请检查网络连接'));
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
// 上传文件
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('PUT', uploadUrlData.upload_url, true);
|
|
||||||
xhr.setRequestHeader('Content-Type', file.type);
|
|
||||||
|
|
||||||
xhr.upload.onprogress = (event) => {
|
|
||||||
if (event.lengthComputable) {
|
|
||||||
const progress = Math.round((event.loaded / event.total) * 100);
|
|
||||||
console.log(`Upload progress for ${file.name}: ${progress}%`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onload = async () => {
|
|
||||||
clearTimeout(timeoutId!);
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
|
||||||
try {
|
|
||||||
const result = await confirmUpload(uploadUrlData.file_id);
|
|
||||||
resolve({
|
|
||||||
...result,
|
|
||||||
file_id: uploadUrlData.file_id,
|
|
||||||
upload_url: uploadUrlData.upload_url,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(new Error(`上传失败,状态码: ${xhr.status}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = () => {
|
|
||||||
clearTimeout(timeoutId!);
|
|
||||||
reject(new Error('网络错误,请检查网络连接'));
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(file);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
clearTimeout(timeoutId!);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in uploadWithProgress:', {
|
|
||||||
error,
|
|
||||||
fileName: file?.name,
|
|
||||||
fileSize: file?.size,
|
|
||||||
fileType: file?.type
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理单个资源
|
// 处理单个资源
|
||||||
const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise<UploadResult | null> => {
|
const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise<UploadResult | null> => {
|
||||||
console.log("asset111111", asset);
|
console.log("asset111111", asset);
|
||||||
@ -329,15 +107,11 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
{ type: 'image/jpeg' }
|
{ type: 'image/jpeg' }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 处理图片
|
// 处理图片,主图和缩略图都用 compressImage 方法
|
||||||
const [originalResponse, compressedFileResult] = await Promise.all([
|
// 主图压缩(按 maxWidth/maxHeight/compressQuality)
|
||||||
fetch(asset.uri),
|
const { file: compressedFile } = await compressImage(asset.uri, maxWidth);
|
||||||
ImageManipulator.manipulateAsync(
|
// 缩略图压缩(宽度800)
|
||||||
asset.uri,
|
const { file: thumbFile } = await compressImage(asset.uri, 800);
|
||||||
[{ resize: { width: 800 } }],
|
|
||||||
{ compress: 0.7, format: ImageManipulator.SaveFormat.JPEG }
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 如果保留 EXIF 数据,则获取
|
// 如果保留 EXIF 数据,则获取
|
||||||
if (preserveExif && asset.exif) {
|
if (preserveExif && asset.exif) {
|
||||||
@ -358,20 +132,10 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const originalBlob = await originalResponse.blob();
|
// 用压缩后主图作为上传主文件
|
||||||
const compressedBlob = await compressedFileResult.file;
|
file = compressedFile as File;
|
||||||
|
// 用缩略图文件作为预览
|
||||||
file = new File(
|
thumbnailFile = thumbFile as File;
|
||||||
[originalBlob],
|
|
||||||
`original_${Date.now()}_${asset.fileName || 'photo.jpg'}`,
|
|
||||||
{ type: asset.mimeType || 'image/jpeg' }
|
|
||||||
);
|
|
||||||
|
|
||||||
thumbnailFile = new File(
|
|
||||||
[compressedBlob],
|
|
||||||
`compressed_${Date.now()}_${asset.fileName || 'photo.jpg'}`,
|
|
||||||
{ type: 'image/jpeg' }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备上传任务
|
// 准备上传任务
|
||||||
@ -401,8 +165,11 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
const uploadResultsList = [];
|
const uploadResultsList = [];
|
||||||
for (const task of uploadTasks) {
|
for (const task of uploadTasks) {
|
||||||
try {
|
try {
|
||||||
const result = await uploadWithProgress(task.file, task.metadata);
|
// 统一通过 lib 的 uploadFileWithProgress 实现上传
|
||||||
uploadResultsList.push(result);
|
const uploadUrlData = await getUploadUrl(task.file, { ...task.metadata, GPSVersionID: undefined });
|
||||||
|
await uploadFileWithProgress(task.file, uploadUrlData.upload_url, updateProgress, 30000);
|
||||||
|
const result = await confirmUpload(uploadUrlData.file_id);
|
||||||
|
uploadResultsList.push({ ...result, file_id: uploadUrlData.file_id, upload_url: uploadUrlData.upload_url });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload failed:', error);
|
console.error('Upload failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -435,7 +202,7 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
if (uploadResults.originalFile?.file_id) {
|
if (uploadResults.originalFile?.file_id) {
|
||||||
await addMaterial(
|
await addMaterial(
|
||||||
uploadResults.originalFile.file_id,
|
uploadResults.originalFile.file_id,
|
||||||
uploadResults.thumbnail
|
uploadResults.compressedFile?.file_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,39 +1,39 @@
|
|||||||
import * as ImageManipulator from 'expo-image-manipulator';
|
// import * as ImageManipulator from 'expo-image-manipulator';
|
||||||
import * as VideoThumbnail from 'expo-video-thumbnails';
|
// import * as VideoThumbnail from 'expo-video-thumbnails';
|
||||||
|
|
||||||
export const extractVideoThumbnail = async (videoUri: string): Promise<{ uri: string; file: File }> => {
|
// export const extractVideoThumbnail = async (videoUri: string): Promise<{ uri: string; file: File }> => {
|
||||||
try {
|
// try {
|
||||||
// 获取视频的第一帧
|
// // 获取视频的第一帧
|
||||||
const { uri: thumbnailUri } = await VideoThumbnail.getThumbnailAsync(
|
// const { uri: thumbnailUri } = await VideoThumbnail.getThumbnailAsync(
|
||||||
videoUri,
|
// videoUri,
|
||||||
{
|
// {
|
||||||
time: 1000, // 1秒的位置
|
// time: 1000, // 1秒的位置
|
||||||
quality: 0.8,
|
// quality: 0.8,
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
// 转换为 WebP 格式
|
// // 转换为 WebP 格式
|
||||||
const manipResult = await ImageManipulator.manipulateAsync(
|
// const manipResult = await ImageManipulator.manipulateAsync(
|
||||||
thumbnailUri,
|
// thumbnailUri,
|
||||||
[{ resize: { width: 800 } }], // 调整大小以提高性能
|
// [{ resize: { width: 800 } }], // 调整大小以提高性能
|
||||||
{
|
// {
|
||||||
compress: 0.8,
|
// compress: 0.8,
|
||||||
format: ImageManipulator.SaveFormat.WEBP
|
// format: ImageManipulator.SaveFormat.WEBP
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
// 转换为 File 对象
|
// // 转换为 File 对象
|
||||||
const response = await fetch(manipResult.uri);
|
// const response = await fetch(manipResult.uri);
|
||||||
const blob = await response.blob();
|
// const blob = await response.blob();
|
||||||
const file = new File(
|
// const file = new File(
|
||||||
[blob],
|
// [blob],
|
||||||
`thumb_${Date.now()}.webp`,
|
// `thumb_${Date.now()}.webp`,
|
||||||
{ type: 'image/webp' }
|
// { type: 'image/webp' }
|
||||||
);
|
// );
|
||||||
|
|
||||||
return { uri: manipResult.uri, file };
|
// return { uri: manipResult.uri, file };
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('Error generating video thumbnail:', error);
|
// console.error('Error generating video thumbnail:', error);
|
||||||
throw new Error('无法生成视频缩略图: ' + (error instanceof Error ? error.message : String(error)));
|
// throw new Error('无法生成视频缩略图: ' + (error instanceof Error ? error.message : String(error)));
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
@ -1,138 +1,138 @@
|
|||||||
/**
|
// /**
|
||||||
* 从视频文件中提取第一帧并返回为File对象
|
// * 从视频文件中提取第一帧并返回为File对象
|
||||||
* @param videoFile 视频文件
|
// * @param videoFile 视频文件
|
||||||
* @returns 包含视频第一帧的File对象
|
// * @returns 包含视频第一帧的File对象
|
||||||
*/
|
// */
|
||||||
export const extractVideoFirstFrame = (videoFile: File): Promise<File> => {
|
// export const extractVideoFirstFrame = (videoFile: File): Promise<File> => {
|
||||||
return new Promise((resolve, reject) => {
|
// return new Promise((resolve, reject) => {
|
||||||
const videoUrl = URL.createObjectURL(videoFile);
|
// const videoUrl = URL.createObjectURL(videoFile);
|
||||||
const video = document.createElement('video');
|
// const video = document.createElement('video');
|
||||||
video.src = videoUrl;
|
// video.src = videoUrl;
|
||||||
video.crossOrigin = 'anonymous';
|
// video.crossOrigin = 'anonymous';
|
||||||
video.muted = true;
|
// video.muted = true;
|
||||||
video.preload = 'metadata';
|
// video.preload = 'metadata';
|
||||||
|
|
||||||
video.onloadeddata = () => {
|
// video.onloadeddata = () => {
|
||||||
try {
|
// try {
|
||||||
// 设置视频时间到第一帧
|
// // 设置视频时间到第一帧
|
||||||
video.currentTime = 0.1;
|
// video.currentTime = 0.1;
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
URL.revokeObjectURL(videoUrl);
|
// URL.revokeObjectURL(videoUrl);
|
||||||
reject(e);
|
// reject(e);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
video.onseeked = () => {
|
// video.onseeked = () => {
|
||||||
try {
|
// try {
|
||||||
const canvas = document.createElement('canvas');
|
// const canvas = document.createElement('canvas');
|
||||||
canvas.width = video.videoWidth;
|
// canvas.width = video.videoWidth;
|
||||||
canvas.height = video.videoHeight;
|
// canvas.height = video.videoHeight;
|
||||||
const ctx = canvas.getContext('2d');
|
// const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
if (!ctx) {
|
// if (!ctx) {
|
||||||
throw new Error('无法获取canvas上下文');
|
// throw new Error('无法获取canvas上下文');
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 绘制视频帧到canvas
|
// // 绘制视频帧到canvas
|
||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
// ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// 将canvas转换为DataURL
|
// // 将canvas转换为DataURL
|
||||||
const dataUrl = canvas.toDataURL('image/jpeg');
|
// const dataUrl = canvas.toDataURL('image/jpeg');
|
||||||
|
|
||||||
// 将DataURL转换为Blob
|
// // 将DataURL转换为Blob
|
||||||
const byteString = atob(dataUrl.split(',')[1]);
|
// const byteString = atob(dataUrl.split(',')[1]);
|
||||||
const mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0];
|
// const mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0];
|
||||||
const ab = new ArrayBuffer(byteString.length);
|
// const ab = new ArrayBuffer(byteString.length);
|
||||||
const ia = new Uint8Array(ab);
|
// const ia = new Uint8Array(ab);
|
||||||
for (let i = 0; i < byteString.length; i++) {
|
// for (let i = 0; i < byteString.length; i++) {
|
||||||
ia[i] = byteString.charCodeAt(i);
|
// ia[i] = byteString.charCodeAt(i);
|
||||||
}
|
// }
|
||||||
const blob = new Blob([ab], { type: mimeString });
|
// const blob = new Blob([ab], { type: mimeString });
|
||||||
|
|
||||||
// 创建File对象
|
// // 创建File对象
|
||||||
const frameFile = new File(
|
// const frameFile = new File(
|
||||||
[blob],
|
// [blob],
|
||||||
`${videoFile.name.replace(/\.[^/.]+$/, '')}_frame.jpg`,
|
// `${videoFile.name.replace(/\.[^/.]+$/, '')}_frame.jpg`,
|
||||||
{ type: 'image/jpeg' }
|
// { type: 'image/jpeg' }
|
||||||
);
|
// );
|
||||||
|
|
||||||
// 清理URL对象
|
// // 清理URL对象
|
||||||
URL.revokeObjectURL(videoUrl);
|
// URL.revokeObjectURL(videoUrl);
|
||||||
resolve(frameFile);
|
// resolve(frameFile);
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
URL.revokeObjectURL(videoUrl);
|
// URL.revokeObjectURL(videoUrl);
|
||||||
reject(e);
|
// reject(e);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
video.onerror = () => {
|
// video.onerror = () => {
|
||||||
URL.revokeObjectURL(videoUrl);
|
// URL.revokeObjectURL(videoUrl);
|
||||||
reject(new Error('视频加载失败'));
|
// reject(new Error('视频加载失败'));
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 获取视频时长
|
// // 获取视频时长
|
||||||
export const getVideoDuration = (file: File): Promise<number> => {
|
// export const getVideoDuration = (file: File): Promise<number> => {
|
||||||
return new Promise((resolve) => {
|
// return new Promise((resolve) => {
|
||||||
const video = document.createElement('video');
|
// const video = document.createElement('video');
|
||||||
video.preload = 'metadata';
|
// video.preload = 'metadata';
|
||||||
|
|
||||||
video.onloadedmetadata = () => {
|
// video.onloadedmetadata = () => {
|
||||||
URL.revokeObjectURL(video.src);
|
// URL.revokeObjectURL(video.src);
|
||||||
resolve(video.duration);
|
// resolve(video.duration);
|
||||||
};
|
// };
|
||||||
|
|
||||||
video.onerror = () => {
|
// video.onerror = () => {
|
||||||
URL.revokeObjectURL(video.src);
|
// URL.revokeObjectURL(video.src);
|
||||||
resolve(0); // Return 0 if we can't get the duration
|
// resolve(0); // Return 0 if we can't get the duration
|
||||||
};
|
// };
|
||||||
|
|
||||||
video.src = URL.createObjectURL(file);
|
// video.src = URL.createObjectURL(file);
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 根据 mp4 的url来获取视频时长
|
// // 根据 mp4 的url来获取视频时长
|
||||||
/**
|
// /**
|
||||||
* 根据视频URL获取视频时长
|
// * 根据视频URL获取视频时长
|
||||||
* @param videoUrl 视频的URL
|
// * @param videoUrl 视频的URL
|
||||||
* @returns 返回一个Promise,解析为视频时长(秒)
|
// * @returns 返回一个Promise,解析为视频时长(秒)
|
||||||
*/
|
// */
|
||||||
export const getVideoDurationFromUrl = async (videoUrl: string): Promise<number> => {
|
// export const getVideoDurationFromUrl = async (videoUrl: string): Promise<number> => {
|
||||||
return await new Promise((resolve, reject) => {
|
// return await new Promise((resolve, reject) => {
|
||||||
// 创建临时的video元素
|
// // 创建临时的video元素
|
||||||
const video = document.createElement('video');
|
// const video = document.createElement('video');
|
||||||
|
|
||||||
// 设置为只加载元数据,不加载整个视频
|
// // 设置为只加载元数据,不加载整个视频
|
||||||
video.preload = 'metadata';
|
// video.preload = 'metadata';
|
||||||
|
|
||||||
// 处理加载成功
|
// // 处理加载成功
|
||||||
video.onloadedmetadata = () => {
|
// video.onloadedmetadata = () => {
|
||||||
// 释放URL对象
|
// // 释放URL对象
|
||||||
URL.revokeObjectURL(video.src);
|
// URL.revokeObjectURL(video.src);
|
||||||
// 返回视频时长(秒)
|
// // 返回视频时长(秒)
|
||||||
resolve(video.duration);
|
// resolve(video.duration);
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 处理加载错误
|
// // 处理加载错误
|
||||||
video.onerror = () => {
|
// video.onerror = () => {
|
||||||
URL.revokeObjectURL(video.src);
|
// URL.revokeObjectURL(video.src);
|
||||||
reject(new Error('无法加载视频'));
|
// reject(new Error('无法加载视频'));
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 处理网络错误
|
// // 处理网络错误
|
||||||
video.onabort = () => {
|
// video.onabort = () => {
|
||||||
URL.revokeObjectURL(video.src);
|
// URL.revokeObjectURL(video.src);
|
||||||
reject(new Error('视频加载被中止'));
|
// reject(new Error('视频加载被中止'));
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 设置视频源
|
// // 设置视频源
|
||||||
video.src = videoUrl;
|
// video.src = videoUrl;
|
||||||
|
|
||||||
// 添加跨域属性(如果需要)
|
// // 添加跨域属性(如果需要)
|
||||||
video.setAttribute('crossOrigin', 'anonymous');
|
// video.setAttribute('crossOrigin', 'anonymous');
|
||||||
|
|
||||||
// 开始加载元数据
|
// // 开始加载元数据
|
||||||
video.load();
|
// video.load();
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|||||||
11
jest.config.ts
Normal file
11
jest.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const { createDefaultPreset } = require("ts-jest");
|
||||||
|
|
||||||
|
const tsJestTransformCfg = createDefaultPreset().transform;
|
||||||
|
|
||||||
|
/** @type {import("jest").Config} **/
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: "node",
|
||||||
|
transform: {
|
||||||
|
...tsJestTransformCfg,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -12,7 +12,7 @@ export async function identityCheck(token: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.code == 0;
|
return data.code != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,10 +28,7 @@ export async function checkAuthStatus(router: ReturnType<typeof useRouter>, onAu
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loggedIn = !!token && await identityCheck(token);
|
const loggedIn = !!token && await identityCheck(token);
|
||||||
console.log('token', token);
|
|
||||||
console.log('loggedIn', loggedIn);
|
|
||||||
if (!loggedIn) {
|
if (!loggedIn) {
|
||||||
console.log('未登录');
|
|
||||||
router.replace('/login');
|
router.replace('/login');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { fetchApi } from '@/lib/server-api-util';
|
import { fetchApi } from '@/lib/server-api-util';
|
||||||
|
import { ConfirmUpload, UploadUrlResponse } from './types';
|
||||||
|
|
||||||
// 获取上传URL
|
// 获取上传URL
|
||||||
export const getUploadUrl = async (file: File, metadata: Record<string, any> = {}): Promise<{ upload_url: string; file_id: string }> => {
|
export const getUploadUrl = async (file: File, metadata: Record<string, any> = {}): Promise<UploadUrlResponse> => {
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
content_type: file.type,
|
content_type: file.type,
|
||||||
@ -11,19 +11,19 @@ export const getUploadUrl = async (file: File, metadata: Record<string, any> = {
|
|||||||
...metadata,
|
...metadata,
|
||||||
originalName: file.name,
|
originalName: file.name,
|
||||||
fileType: file.type.startsWith('video/') ? 'video' : 'image',
|
fileType: file.type.startsWith('video/') ? 'video' : 'image',
|
||||||
isCompressed: 'true',
|
isCompressed: metadata.isCompressed || 'false',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
return await fetchApi<UploadUrlResponse>('/file/generate-upload-url', {
|
||||||
return await fetchApi<{ upload_url: string; file_id: string }>("/file/generate-upload-url", {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认上传
|
// 确认上传
|
||||||
export const confirmUpload = async (file_id: string) => {
|
// 确认上传
|
||||||
return await fetchApi('/file/confirm-upload', {
|
export const confirmUpload = async (file_id: string): Promise<ConfirmUpload> => {
|
||||||
|
return await fetchApi<ConfirmUpload>('/file/confirm-upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ file_id })
|
body: JSON.stringify({ file_id })
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import * as FileSystem from 'expo-file-system';
|
import * as FileSystem from 'expo-file-system';
|
||||||
import * as ImageManipulator from 'expo-image-manipulator';
|
import { confirmUpload, getUploadUrl } from './api';
|
||||||
import { ExtendedAsset } from './types';
|
import { ExtendedAsset } from './types';
|
||||||
import { getUploadUrl, confirmUpload } from './api';
|
|
||||||
import { uploadFile } from './uploader';
|
import { uploadFile } from './uploader';
|
||||||
|
|
||||||
// 将 HEIC 图片转化
|
// 将 HEIC 图片转化
|
||||||
@ -39,16 +38,7 @@ export const convertHeicToJpeg = async (uri: string): Promise<File> => {
|
|||||||
throw new Error('Failed to read file as base64');
|
throw new Error('Failed to read file as base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 创建 Blob
|
|
||||||
const response = await fetch(`data:image/jpeg;base64,${base64}`);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Fetch failed with status ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob();
|
|
||||||
if (!blob || blob.size === 0) {
|
|
||||||
throw new Error('Failed to create blob from base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 创建文件名
|
// 5. 创建文件名
|
||||||
const originalName = uri.split('/').pop() || 'converted';
|
const originalName = uri.split('/').pop() || 'converted';
|
||||||
@ -78,35 +68,7 @@ export const convertHeicToJpeg = async (uri: string): Promise<File> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 压缩图片
|
// 压缩图片
|
||||||
export const compressImage = async (uri: string): Promise<{ uri: string; file: File }> => {
|
import { compressImage } from '../image-process/imageCompress';
|
||||||
try {
|
|
||||||
const manipResult = await ImageManipulator.manipulateAsync(
|
|
||||||
uri,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
resize: {
|
|
||||||
width: 1200,
|
|
||||||
height: 1200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
compress: 0.7,
|
|
||||||
format: ImageManipulator.SaveFormat.JPEG,
|
|
||||||
base64: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await fetch(manipResult.uri);
|
|
||||||
const blob = await response.blob();
|
|
||||||
const filename = uri.split('/').pop() || `image_${Date.now()}.jpg`;
|
|
||||||
const file = new File([blob], filename, { type: 'image/jpeg' });
|
|
||||||
|
|
||||||
return { uri: manipResult.uri, file };
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提取视频的首帧进行压缩并上传
|
// 提取视频的首帧进行压缩并上传
|
||||||
export const uploadVideoThumbnail = async (asset: ExtendedAsset) => {
|
export const uploadVideoThumbnail = async (asset: ExtendedAsset) => {
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { transformData } from '@/components/utils/objectFlat';
|
|||||||
import { ExtendedAsset } from './types';
|
import { ExtendedAsset } from './types';
|
||||||
import { getMediaByDateRange } from './media';
|
import { getMediaByDateRange } from './media';
|
||||||
import { checkMediaLibraryPermission, getFileExtension, getMimeType } from './utils';
|
import { checkMediaLibraryPermission, getFileExtension, getMimeType } from './utils';
|
||||||
import { convertHeicToJpeg, compressImage, uploadVideoThumbnail } from './fileProcessor';
|
import { convertHeicToJpeg, uploadVideoThumbnail } from './fileProcessor';
|
||||||
|
import { compressImage } from '../image-process/imageCompress';
|
||||||
import { getUploadUrl, confirmUpload, addMaterial } from './api';
|
import { getUploadUrl, confirmUpload, addMaterial } from './api';
|
||||||
import { uploadFile } from './uploader';
|
import { uploadFile } from './uploader';
|
||||||
import * as MediaLibrary from 'expo-media-library';
|
import * as MediaLibrary from 'expo-media-library';
|
||||||
|
|||||||
@ -2,4 +2,54 @@ import * as MediaLibrary from 'expo-media-library';
|
|||||||
|
|
||||||
export type ExtendedAsset = MediaLibrary.Asset & {
|
export type ExtendedAsset = MediaLibrary.Asset & {
|
||||||
exif?: Record<string, any>;
|
exif?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传任务类型
|
||||||
|
export type UploadTask = {
|
||||||
|
file: File;
|
||||||
|
metadata: {
|
||||||
|
isCompressed: string;
|
||||||
|
type: string;
|
||||||
|
isThumbnail?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传队列项
|
||||||
|
export type FileUploadItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
status: 'pending' | 'uploading' | 'done' | 'error';
|
||||||
|
error: string | null;
|
||||||
|
type: 'image' | 'video';
|
||||||
|
thumbnail: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认上传返回
|
||||||
|
export type ConfirmUpload = {
|
||||||
|
file_id: string;
|
||||||
|
upload_url: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
content_type: string;
|
||||||
|
file_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传结果
|
||||||
|
export type UploadResult = {
|
||||||
|
originalUrl?: string;
|
||||||
|
compressedUrl: string;
|
||||||
|
file: File | null;
|
||||||
|
exif: any;
|
||||||
|
originalFile: ConfirmUpload;
|
||||||
|
compressedFile: ConfirmUpload;
|
||||||
|
thumbnail: string;
|
||||||
|
thumbnailFile: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传URL响应类型
|
||||||
|
export type UploadUrlResponse = {
|
||||||
|
upload_url: string;
|
||||||
|
file_id: string;
|
||||||
};
|
};
|
||||||
@ -1,19 +1,49 @@
|
|||||||
// 上传文件到URL
|
// 上传文件到URL(基础版,无进度回调)
|
||||||
export const uploadFile = async (file: File, uploadUrl: string): Promise<void> => {
|
export const uploadFile = async (file: File, uploadUrl: string): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('PUT', uploadUrl);
|
||||||
|
xhr.setRequestHeader('Content-Type', file.type);
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
reject(new Error('Network error during upload'));
|
||||||
|
};
|
||||||
|
xhr.send(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 支持进度回调和超时的上传实现
|
||||||
|
export const uploadFileWithProgress = async (
|
||||||
|
file: File,
|
||||||
|
uploadUrl: string,
|
||||||
|
onProgress?: (progress: number) => void,
|
||||||
|
timeout: number = 30000
|
||||||
|
): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
let timeoutId: number | undefined;
|
||||||
|
|
||||||
xhr.open('PUT', uploadUrl);
|
xhr.open('PUT', uploadUrl);
|
||||||
xhr.setRequestHeader('Content-Type', file.type);
|
xhr.setRequestHeader('Content-Type', file.type);
|
||||||
|
|
||||||
// 进度监听
|
// 进度监听
|
||||||
xhr.upload.onprogress = (event) => {
|
if (onProgress) {
|
||||||
if (event.lengthComputable) {
|
xhr.upload.onprogress = (event) => {
|
||||||
const progress = Math.round((event.loaded / event.total) * 100);
|
if (event.lengthComputable) {
|
||||||
}
|
const progress = Math.round((event.loaded / event.total) * 100);
|
||||||
};
|
onProgress(progress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
@ -22,9 +52,16 @@ export const uploadFile = async (file: File, uploadUrl: string): Promise<void> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
xhr.onerror = () => {
|
xhr.onerror = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
reject(new Error('Network error during upload'));
|
reject(new Error('Network error during upload'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 超时处理
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
xhr.abort();
|
||||||
|
reject(new Error('上传超时,请检查网络连接'));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
xhr.send(file);
|
xhr.send(file);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
47
lib/image-process/heicConvert.ts
Normal file
47
lib/image-process/heicConvert.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
|
||||||
|
// 将 HEIC/HEIF 图片转为 JPEG
|
||||||
|
export const convertHeicToJpeg = async (uri: string): Promise<File> => {
|
||||||
|
try {
|
||||||
|
// 1. 将文件复制到缓存目录
|
||||||
|
const cacheDir = FileSystem.cacheDirectory;
|
||||||
|
if (!cacheDir) {
|
||||||
|
throw new Error('Cache directory not available');
|
||||||
|
}
|
||||||
|
const tempUri = `${cacheDir}${Date.now()}.heic`;
|
||||||
|
await FileSystem.copyAsync({ from: uri, to: tempUri });
|
||||||
|
// 2. 检查文件是否存在
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(tempUri);
|
||||||
|
if (!fileInfo.exists) {
|
||||||
|
throw new Error('Temporary file was not created');
|
||||||
|
}
|
||||||
|
// 3. 读取文件为 base64
|
||||||
|
const base64 = await FileSystem.readAsStringAsync(tempUri, {
|
||||||
|
encoding: FileSystem.EncodingType.Base64,
|
||||||
|
});
|
||||||
|
if (!base64) {
|
||||||
|
throw new Error('Failed to read file as base64');
|
||||||
|
}
|
||||||
|
// 4. 创建 Blob
|
||||||
|
const response = await fetch(`data:image/jpeg;base64,${base64}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Fetch failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
if (!blob || blob.size === 0) {
|
||||||
|
throw new Error('Failed to create blob from base64');
|
||||||
|
}
|
||||||
|
// 5. 创建文件名
|
||||||
|
const originalName = uri.split('/').pop() || 'converted';
|
||||||
|
const filename = originalName.replace(/\.(heic|heif)$/i, '.jpg');
|
||||||
|
// 清理临时文件
|
||||||
|
try {
|
||||||
|
await FileSystem.deleteAsync(tempUri, { idempotent: true });
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to clean up temporary file:', cleanupError);
|
||||||
|
}
|
||||||
|
return new File([blob], filename, { type: 'image/jpeg' });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
throw new Error(`Failed to convert HEIC image: ${error instanceof Error ? error.message : 'An unknown error occurred'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
56
lib/image-process/imageCompress.ts
Normal file
56
lib/image-process/imageCompress.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import * as ImageManipulator from 'expo-image-manipulator';
|
||||||
|
import { Image } from 'react-native';
|
||||||
|
|
||||||
|
// 压缩图片,自动等比缩放,最大边不超过 800
|
||||||
|
export const compressImage = async (uri: string, maxSize = 800): Promise<{ uri: string; file: File }> => {
|
||||||
|
// 获取原图尺寸
|
||||||
|
const getImageSize = (uri: string): Promise<{ width: number; height: number }> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
Image.getSize(
|
||||||
|
uri,
|
||||||
|
(width, height) => resolve({ width, height }),
|
||||||
|
reject
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { width, height } = await getImageSize(uri);
|
||||||
|
let targetWidth = width;
|
||||||
|
let targetHeight = height;
|
||||||
|
if (width > maxSize || height > maxSize) {
|
||||||
|
if (width > height) {
|
||||||
|
targetWidth = maxSize;
|
||||||
|
targetHeight = Math.round((height / width) * maxSize);
|
||||||
|
} else {
|
||||||
|
targetHeight = maxSize;
|
||||||
|
targetWidth = Math.round((width / height) * maxSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manipResult = await ImageManipulator.manipulateAsync(
|
||||||
|
uri,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
resize: {
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
compress: 0.7,
|
||||||
|
format: ImageManipulator.SaveFormat.WEBP,
|
||||||
|
base64: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await fetch(manipResult.uri);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const filename = uri.split('/').pop() || `image_${Date.now()}.webp`;
|
||||||
|
const file = new File([blob], filename, { type: 'image/webp' });
|
||||||
|
|
||||||
|
return { uri: manipResult.uri, file };
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
31
lib/video-process/videoThumbnail.ts
Normal file
31
lib/video-process/videoThumbnail.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { getUploadUrl, confirmUpload } from '../background-uploader/api';
|
||||||
|
import { uploadFile } from '../background-uploader/uploader';
|
||||||
|
import { compressImage } from '../image-process/imageCompress';
|
||||||
|
import { ExtendedAsset } from '../background-uploader/types';
|
||||||
|
|
||||||
|
// 提取视频的首帧进行压缩并上传
|
||||||
|
export const uploadVideoThumbnail = async (asset: ExtendedAsset) => {
|
||||||
|
try {
|
||||||
|
const manipResult = await compressImage(asset.uri);
|
||||||
|
const response = await fetch(manipResult.uri);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const filename = asset.filename ?
|
||||||
|
`compressed_${asset.filename}` :
|
||||||
|
`image_${Date.now()}_compressed.jpg`;
|
||||||
|
const compressedFile = new File([blob], filename, { type: 'image/jpeg' });
|
||||||
|
|
||||||
|
const { upload_url, file_id } = await getUploadUrl(compressedFile, {
|
||||||
|
originalUri: asset.uri,
|
||||||
|
creationTime: asset.creationTime,
|
||||||
|
mediaType: 'image',
|
||||||
|
isCompressed: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await uploadFile(compressedFile, upload_url);
|
||||||
|
await confirmUpload(file_id);
|
||||||
|
|
||||||
|
return { success: true, file_id };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
};
|
||||||
5433
package-lock.json
generated
5433
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,8 @@
|
|||||||
"web": "expo start --web --port 5173",
|
"web": "expo start --web --port 5173",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"prebuild": "npm run generate:translations",
|
"prebuild": "npm run generate:translations",
|
||||||
"generate:translations": "tsx i18n/generate-imports.ts"
|
"generate:translations": "tsx i18n/generate-imports.ts",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
@ -74,13 +75,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.0.3",
|
"@types/node": "^24.0.3",
|
||||||
"@types/react": "~19.0.10",
|
"@types/react": "~19.0.10",
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~9.2.0",
|
"eslint-config-expo": "~9.2.0",
|
||||||
|
"jest": "^30.0.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||||
"react-native-svg-transformer": "^1.5.1",
|
"react-native-svg-transformer": "^1.5.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
"ts-jest": "^29.4.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "~5.8.3"
|
"typescript": "~5.8.3"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"extends": "expo/tsconfig.base",
|
"extends": "expo/tsconfig.base",
|
||||||
"typeRoots": ["./node_modules/@types", "./src/types"],
|
"typeRoots": ["./node_modules/@types", "./src/types"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"types": ["jest", "node"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user