feat: 照片自动上传
This commit is contained in:
parent
5b23951643
commit
5072157a01
17
app.json
17
app.json
@ -65,14 +65,15 @@
|
|||||||
"locationWhenInUsePermission": "允许 $(PRODUCT_NAME) 访问您的位置"
|
"locationWhenInUsePermission": "允许 $(PRODUCT_NAME) 访问您的位置"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
// [
|
||||||
"expo-notifications",
|
// "expo-notifications",
|
||||||
{
|
// {
|
||||||
"color": "#ffffff",
|
// "color": "#ffffff",
|
||||||
"defaultChannel": "default",
|
// "defaultChannel": "default",
|
||||||
"enableBackgroundRemoteNotifications": false
|
// "enableBackgroundRemoteNotifications": false,
|
||||||
}
|
// "mode": "client"
|
||||||
],
|
// }
|
||||||
|
// ],
|
||||||
[
|
[
|
||||||
"expo-audio",
|
"expo-audio",
|
||||||
{
|
{
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
console.log("token111111111", token);
|
console.log("token111111111", token);
|
||||||
if (token) {
|
if (token) {
|
||||||
router.push('/ask')
|
router.push('/user-message')
|
||||||
} else {
|
} else {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ module.exports = function (api) {
|
|||||||
"nativewind/babel",
|
"nativewind/babel",
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
'expo-router/babel',
|
|
||||||
'react-native-reanimated/plugin',
|
'react-native-reanimated/plugin',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,28 +8,50 @@ import { Alert } from 'react-native';
|
|||||||
|
|
||||||
const BACKGROUND_UPLOAD_TASK = 'background-upload-task';
|
const BACKGROUND_UPLOAD_TASK = 'background-upload-task';
|
||||||
// 设置最大并发数
|
// 设置最大并发数
|
||||||
const CONCURRENCY_LIMIT = 3; // 同时最多上传3个文件
|
const CONCURRENCY_LIMIT = 10; // 同时最多上传10个文件
|
||||||
// 获取指定时间范围内的媒体文件
|
|
||||||
|
// 获取指定时间范围内的媒体文件(包含 EXIF 信息)
|
||||||
export const getMediaByDateRange = async (startDate: Date, endDate: Date) => {
|
export const getMediaByDateRange = async (startDate: Date, endDate: Date) => {
|
||||||
|
console.log("getMediaByDateRange", startDate.getTime(), endDate.getTime());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||||
if (status !== 'granted') {
|
if (status !== 'granted') {
|
||||||
console.log('Media library permission not granted');
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取媒体资源
|
// 获取媒体资源
|
||||||
const media = await MediaLibrary.getAssetsAsync({
|
const media = await MediaLibrary.getAssetsAsync({
|
||||||
mediaType: ['photo', 'video'],
|
mediaType: ['photo', 'video'],
|
||||||
first: 100, // 每次最多获取100个
|
first: 10, // 每次最多获取100个
|
||||||
sortBy: [MediaLibrary.SortBy.creationTime],
|
sortBy: [MediaLibrary.SortBy.creationTime],
|
||||||
createdAfter: startDate.getTime(),
|
createdAfter: startDate.getTime(),
|
||||||
createdBefore: endDate.getTime(),
|
createdBefore: endDate.getTime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return media.assets;
|
console.log("media1111111111", media);
|
||||||
|
|
||||||
|
// 为每个资源获取完整的 EXIF 信息
|
||||||
|
const assetsWithExif = await Promise.all(
|
||||||
|
media.assets.map(async (asset) => {
|
||||||
|
try {
|
||||||
|
if (asset.mediaType === 'photo') {
|
||||||
|
const assetInfo = await MediaLibrary.getAssetInfoAsync(asset.id);
|
||||||
|
return {
|
||||||
|
...asset,
|
||||||
|
exif: assetInfo.exif || null,
|
||||||
|
location: assetInfo.location || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return asset; // 视频不处理 EXIF
|
||||||
|
} catch (error) {
|
||||||
|
return asset; // 如果获取 EXIF 失败,返回原始资源
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return assetsWithExif;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting media by date range:', error);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -61,13 +83,13 @@ const compressImage = async (uri: string): Promise<{ uri: string; file: File }>
|
|||||||
|
|
||||||
return { uri: manipResult.uri, file };
|
return { uri: manipResult.uri, file };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error compressing image:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取上传URL
|
// 获取上传URL
|
||||||
const getUploadUrl = async (file: File, metadata: Record<string, any> = {}): Promise<{ upload_url: string; file_id: string }> => {
|
const getUploadUrl = async (file: File, metadata: Record<string, any> = {}): Promise<{ upload_url: string; file_id: string }> => {
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
content_type: file.type,
|
content_type: file.type,
|
||||||
@ -103,67 +125,40 @@ const addMaterial = async (file: string, compressFile: string) => {
|
|||||||
"preview_file_id": compressFile
|
"preview_file_id": compressFile
|
||||||
}])
|
}])
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
// console.log(error);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传文件到URL
|
// 上传文件到URL
|
||||||
const uploadFile = async (file: File, uploadUrl: string): Promise<void> => {
|
const uploadFile = async (file: File, uploadUrl: string): Promise<void> => {
|
||||||
console.log('=== Starting file upload ===');
|
return new Promise((resolve, reject) => {
|
||||||
console.log('File info:', {
|
const xhr = new XMLHttpRequest();
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
lastModified: file.lastModified
|
|
||||||
});
|
|
||||||
console.log('Upload URL:', uploadUrl);
|
|
||||||
|
|
||||||
try {
|
xhr.open('PUT', uploadUrl);
|
||||||
const controller = new AbortController();
|
xhr.setRequestHeader('Content-Type', file.type);
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
console.log('Upload timeout triggered');
|
|
||||||
controller.abort();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
console.log('Sending upload request...');
|
// 进度监听
|
||||||
const startTime = Date.now();
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
const response = await fetch(uploadUrl, {
|
const progress = Math.round((event.loaded / event.total) * 100);
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': file.type,
|
|
||||||
'x-oss-forbid-overwrite': 'true'
|
|
||||||
},
|
|
||||||
body: file,
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
const endTime = Date.now();
|
|
||||||
|
|
||||||
console.log(`Upload completed in ${endTime - startTime}ms`, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: Object.fromEntries(response.headers.entries())
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text().catch(() => '');
|
|
||||||
console.error('Upload failed with response:', errorText);
|
|
||||||
throw new Error(`Upload failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
console.log('Upload successful');
|
xhr.onload = () => {
|
||||||
} catch (error: any) {
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
console.error('Upload error details:', {
|
resolve();
|
||||||
name: error.name,
|
} else {
|
||||||
message: error.message,
|
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||||
stack: error.stack
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
reject(new Error('Network error during upload'));
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 检查并请求媒体库权限
|
// 检查并请求媒体库权限
|
||||||
const checkMediaLibraryPermission = async (): Promise<{ hasPermission: boolean, status?: string }> => {
|
const checkMediaLibraryPermission = async (): Promise<{ hasPermission: boolean, status?: string }> => {
|
||||||
try {
|
try {
|
||||||
@ -187,7 +182,6 @@ const checkMediaLibraryPermission = async (): Promise<{ hasPermission: boolean,
|
|||||||
status: newStatus
|
status: newStatus
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking media library permission:', error);
|
|
||||||
return { hasPermission: false };
|
return { hasPermission: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -213,19 +207,19 @@ const processMediaUpload = async (asset: MediaLibrary.Asset) => {
|
|||||||
const mimeType = isVideo ? 'video/mp4' : 'image/jpeg';
|
const mimeType = isVideo ? 'video/mp4' : 'image/jpeg';
|
||||||
const file = new File([blob], filename, { type: mimeType });
|
const file = new File([blob], filename, { type: mimeType });
|
||||||
|
|
||||||
console.log("Original file prepared for upload:", file);
|
|
||||||
|
|
||||||
// 获取上传URL并上传原始文件
|
// 获取上传URL并上传原始文件
|
||||||
const { upload_url, file_id } = await getUploadUrl(file, {
|
const { upload_url, file_id } = await getUploadUrl(file, {
|
||||||
originalUri: asset.uri,
|
originalUri: asset.uri,
|
||||||
creationTime: asset.creationTime,
|
creationTime: asset.creationTime,
|
||||||
mediaType: isVideo ? 'video' : 'image',
|
mediaType: isVideo ? 'video' : 'image',
|
||||||
isCompressed: false
|
isCompressed: false,
|
||||||
|
...asset?.exif,
|
||||||
|
GPSVersionID: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
await uploadFile(file, upload_url);
|
await uploadFile(file, upload_url);
|
||||||
await confirmUpload(file_id);
|
await confirmUpload(file_id);
|
||||||
console.log(`Successfully uploaded original: ${filename}`);
|
|
||||||
return { success: true, file_id };
|
return { success: true, file_id };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -239,13 +233,6 @@ const processMediaUpload = async (asset: MediaLibrary.Asset) => {
|
|||||||
`compressed_${asset.filename}` :
|
`compressed_${asset.filename}` :
|
||||||
`image_${Date.now()}_compressed.jpg`;
|
`image_${Date.now()}_compressed.jpg`;
|
||||||
|
|
||||||
console.log("Compressed file prepared for upload:", {
|
|
||||||
name: filename,
|
|
||||||
size: compressedFile.size,
|
|
||||||
type: compressedFile.type
|
|
||||||
});
|
|
||||||
console.log("compressedFile", compressedFile);
|
|
||||||
|
|
||||||
// 获取上传URL并上传压缩文件
|
// 获取上传URL并上传压缩文件
|
||||||
const { upload_url, file_id } = await getUploadUrl(compressedFile, {
|
const { upload_url, file_id } = await getUploadUrl(compressedFile, {
|
||||||
originalUri: asset.uri,
|
originalUri: asset.uri,
|
||||||
@ -256,23 +243,20 @@ const processMediaUpload = async (asset: MediaLibrary.Asset) => {
|
|||||||
|
|
||||||
await uploadFile(compressedFile, upload_url);
|
await uploadFile(compressedFile, upload_url);
|
||||||
await confirmUpload(file_id);
|
await confirmUpload(file_id);
|
||||||
console.log(`Successfully uploaded compressed: ${filename}`);
|
|
||||||
return { success: true, file_id };
|
return { success: true, file_id };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading compressed file:', error);
|
|
||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同时上传原始文件和压缩文件
|
// 同时上传原始文件和压缩文件
|
||||||
console.log("Asset info:", asset);
|
|
||||||
const [originalResult, compressedResult] = await Promise.all([
|
const [originalResult, compressedResult] = await Promise.all([
|
||||||
uploadOriginalFile(),
|
uploadOriginalFile(),
|
||||||
uploadCompressedFile()
|
uploadCompressedFile()
|
||||||
]);
|
]);
|
||||||
console.log("originalResult", originalResult);
|
|
||||||
console.log("compressedResult", compressedResult);
|
// 添加素材
|
||||||
addMaterial(originalResult.file_id, compressedResult.file_id)
|
addMaterial(originalResult.file_id, compressedResult?.file_id || '')
|
||||||
return {
|
return {
|
||||||
originalSuccess: originalResult.success,
|
originalSuccess: originalResult.success,
|
||||||
compressedSuccess: compressedResult.success,
|
compressedSuccess: compressedResult.success,
|
||||||
@ -282,7 +266,6 @@ const processMediaUpload = async (asset: MediaLibrary.Asset) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error in processMediaUpload:', error);
|
|
||||||
if (error.message === 'No media library permission') {
|
if (error.message === 'No media library permission') {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -310,10 +293,8 @@ export const registerBackgroundUploadTask = async () => {
|
|||||||
startOnBoot: true, // 系统启动后自动运行
|
startOnBoot: true, // 系统启动后自动运行
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Background upload task registered');
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error registering background task:', error);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -321,12 +302,9 @@ export const registerBackgroundUploadTask = async () => {
|
|||||||
// 定义后台任务
|
// 定义后台任务
|
||||||
TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Background upload task started');
|
|
||||||
|
|
||||||
// 检查并请求媒体库权限
|
// 检查并请求媒体库权限
|
||||||
const { hasPermission } = await checkMediaLibraryPermission();
|
const { hasPermission } = await checkMediaLibraryPermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
console.log('Media library permission not granted');
|
|
||||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,15 +313,11 @@ TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
|||||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const media = await getMediaByDateRange(yesterday, now);
|
const media = await getMediaByDateRange(yesterday, now);
|
||||||
console.log("media", media);
|
|
||||||
|
|
||||||
if (media.length === 0) {
|
if (media.length === 0) {
|
||||||
console.log('No media found in the specified time range');
|
|
||||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${media.length} media items to process`);
|
|
||||||
|
|
||||||
// 创建并发限制器
|
// 创建并发限制器
|
||||||
const limit = pLimit(CONCURRENCY_LIMIT);
|
const limit = pLimit(CONCURRENCY_LIMIT);
|
||||||
|
|
||||||
@ -354,7 +328,6 @@ TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
|||||||
await processMediaUpload(item);
|
await processMediaUpload(item);
|
||||||
return { success: true, id: item.id };
|
return { success: true, id: item.id };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`Error processing media ${item.id}:`, error);
|
|
||||||
if (error.message === 'No media library permission') {
|
if (error.message === 'No media library permission') {
|
||||||
throw error; // 权限错误直接抛出
|
throw error; // 权限错误直接抛出
|
||||||
}
|
}
|
||||||
@ -372,8 +345,6 @@ TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
|||||||
).length;
|
).length;
|
||||||
const failed = results.length - succeeded;
|
const failed = results.length - succeeded;
|
||||||
|
|
||||||
console.log(`Background upload task completed. Success: ${succeeded}, Failed: ${failed}`);
|
|
||||||
|
|
||||||
// 如果有权限错误,返回失败
|
// 如果有权限错误,返回失败
|
||||||
const hasPermissionError = results.some(r =>
|
const hasPermissionError = results.some(r =>
|
||||||
r.status === 'rejected' ||
|
r.status === 'rejected' ||
|
||||||
@ -388,7 +359,6 @@ TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
|||||||
BackgroundFetch.BackgroundFetchResult.NewData :
|
BackgroundFetch.BackgroundFetchResult.NewData :
|
||||||
BackgroundFetch.BackgroundFetchResult.NoData;
|
BackgroundFetch.BackgroundFetchResult.NoData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in background upload task:', error);
|
|
||||||
return BackgroundFetch.BackgroundFetchResult.Failed;
|
return BackgroundFetch.BackgroundFetchResult.Failed;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -396,12 +366,7 @@ TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
|||||||
// 手动触发上传
|
// 手动触发上传
|
||||||
export const triggerManualUpload = async (startDate: Date, endDate: Date) => {
|
export const triggerManualUpload = async (startDate: Date, endDate: Date) => {
|
||||||
try {
|
try {
|
||||||
console.log('Starting manual upload...');
|
|
||||||
console.log("startDate", startDate);
|
|
||||||
console.log("endDate", endDate);
|
|
||||||
|
|
||||||
const media = await getMediaByDateRange(startDate, endDate);
|
const media = await getMediaByDateRange(startDate, endDate);
|
||||||
console.log("media", media);
|
|
||||||
if (media.length === 0) {
|
if (media.length === 0) {
|
||||||
Alert.alert('提示', '在指定时间范围内未找到媒体文件');
|
Alert.alert('提示', '在指定时间范围内未找到媒体文件');
|
||||||
return [];
|
return [];
|
||||||
@ -419,7 +384,6 @@ export const triggerManualUpload = async (startDate: Date, endDate: Date) => {
|
|||||||
fileIds: result.fileIds,
|
fileIds: result.fileIds,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error uploading ${item.filename}:`, error);
|
|
||||||
results.push({
|
results.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
originalSuccess: false,
|
originalSuccess: false,
|
||||||
@ -435,7 +399,6 @@ export const triggerManualUpload = async (startDate: Date, endDate: Date) => {
|
|||||||
|
|
||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in manual upload:', error);
|
|
||||||
Alert.alert('错误', '上传过程中出现错误');
|
Alert.alert('错误', '上传过程中出现错误');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -191,6 +191,7 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
};
|
};
|
||||||
const uploadWithProgress = async (file: File, metadata: any): Promise<ConfirmUpload> => {
|
const uploadWithProgress = async (file: File, metadata: any): Promise<ConfirmUpload> => {
|
||||||
let timeoutId: number
|
let timeoutId: number
|
||||||
|
console.log("uploadWithProgress", metadata);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Starting upload for file:", file.name, "size:", file.size, "type:", file.type);
|
console.log("Starting upload for file:", file.name, "size:", file.size, "type:", file.type);
|
||||||
@ -201,7 +202,7 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
throw new Error(`文件大小超过限制 (${(MAX_FILE_SIZE / 1024 / 1024).toFixed(1)}MB)`);
|
throw new Error(`文件大小超过限制 (${(MAX_FILE_SIZE / 1024 / 1024).toFixed(1)}MB)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadUrlData = await getUploadUrl(file, {});
|
const uploadUrlData = await getUploadUrl(file, { ...metadata, GPSVersionID: undefined });
|
||||||
console.log("Got upload URL for:", file.name);
|
console.log("Got upload URL for:", file.name);
|
||||||
|
|
||||||
return new Promise<ConfirmUpload>((resolve, reject) => {
|
return new Promise<ConfirmUpload>((resolve, reject) => {
|
||||||
@ -267,6 +268,8 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
|||||||
|
|
||||||
// 处理单个资源
|
// 处理单个资源
|
||||||
const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise<UploadResult | null> => {
|
const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise<UploadResult | null> => {
|
||||||
|
console.log("asset111111", asset);
|
||||||
|
|
||||||
const fileId = `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
const fileId = `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
const isVideo = asset.type === 'video';
|
const isVideo = asset.type === 'video';
|
||||||
const uploadResults: UploadResult = {
|
const uploadResults: UploadResult = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user