feat: 自动上传
This commit is contained in:
parent
828e84710f
commit
3f32bb26bc
20
app.json
20
app.json
@ -13,7 +13,8 @@
|
||||
"infoPlist": {
|
||||
"NSPhotoLibraryUsageDescription": "Allow $(PRODUCT_NAME) to access your photos.",
|
||||
"NSLocationWhenInUseUsageDescription": "Allow $(PRODUCT_NAME) to access your location to get photo location data.",
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
"ITSAppUsesNonExemptEncryption": false,
|
||||
"UIBackgroundModes": ["fetch", "location", "audio"]
|
||||
},
|
||||
"bundleIdentifier": "com.memowake.app"
|
||||
},
|
||||
@ -30,7 +31,9 @@
|
||||
"android.permission.MODIFY_AUDIO_SETTINGS",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.ACCESS_MEDIA_LOCATION"
|
||||
"android.permission.ACCESS_MEDIA_LOCATION",
|
||||
"FOREGROUND_SERVICE",
|
||||
"WAKE_LOCK"
|
||||
],
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "com.memowake.app"
|
||||
@ -42,7 +45,18 @@
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
"expo-secure-store", [
|
||||
"expo-background-fetch",
|
||||
{
|
||||
"minimumInterval": 1
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-task-manager",
|
||||
{
|
||||
"transparency": "opaque"
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-location",
|
||||
{
|
||||
|
||||
227
components/file-upload/autoUploadScreen.tsx
Normal file
227
components/file-upload/autoUploadScreen.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { registerBackgroundUploadTask, triggerManualUpload } from './backgroundUploader';
|
||||
|
||||
export default function AutoUploadScreen() {
|
||||
const [timeRange, setTimeRange] = useState('day');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isRegistered, setIsRegistered] = useState(false);
|
||||
|
||||
// 注册后台任务
|
||||
useEffect(() => {
|
||||
const registerTask = async () => {
|
||||
const registered = await registerBackgroundUploadTask();
|
||||
setIsRegistered(registered);
|
||||
};
|
||||
|
||||
registerTask();
|
||||
}, []);
|
||||
|
||||
// 处理手动上传
|
||||
const handleManualUpload = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await triggerManualUpload(getDateRange(timeRange)[0], getDateRange(timeRange)[1]);
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取时间范围文本
|
||||
const getDateRangeText = (timeRange: string) => {
|
||||
switch (timeRange) {
|
||||
case 'day':
|
||||
return '最近一天';
|
||||
case 'week':
|
||||
return '最近一周';
|
||||
case 'month':
|
||||
return '最近一个月';
|
||||
case 'all':
|
||||
return '全部';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取时间范围
|
||||
const getDateRange = (timeRange: string) => {
|
||||
const date = new Date();
|
||||
switch (timeRange) {
|
||||
case 'day':
|
||||
date.setDate(date.getDate() - 1);
|
||||
break;
|
||||
case 'week':
|
||||
date.setDate(date.getDate() - 7);
|
||||
break;
|
||||
case 'month':
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
break;
|
||||
case 'all':
|
||||
date.setFullYear(date.getFullYear() - 1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return [date, new Date()];
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>自动上传设置</Text>
|
||||
|
||||
<View style={styles.buttonGroup}>
|
||||
<Text style={styles.sectionTitle}>选择时间范围:</Text>
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.timeButton, timeRange === 'day' && styles.activeButton]}
|
||||
onPress={() => setTimeRange('day')}
|
||||
>
|
||||
<Text style={styles.buttonText}>一天</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.timeButton, timeRange === 'week' && styles.activeButton]}
|
||||
onPress={() => setTimeRange('week')}
|
||||
>
|
||||
<Text style={styles.buttonText}>一周</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.timeButton, timeRange === 'month' && styles.activeButton]}
|
||||
onPress={() => setTimeRange('month')}
|
||||
>
|
||||
<Text style={styles.buttonText}>一个月</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.timeButton, timeRange === 'all' && styles.activeButton]}
|
||||
onPress={() => setTimeRange('all')}
|
||||
>
|
||||
<Text style={styles.buttonText}>全部</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.dateRangeText}>
|
||||
{getDateRangeText(timeRange)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.uploadButtonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.uploadButton, isLoading && styles.uploadButtonDisabled]}
|
||||
onPress={handleManualUpload}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={styles.uploadButtonText}>
|
||||
{isLoading ? '上传中...' : '开始上传'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.statusContainer}>
|
||||
<Text style={styles.statusText}>
|
||||
后台自动上传状态: {isRegistered ? '已启用' : '未启用'}
|
||||
</Text>
|
||||
<Text style={styles.hintText}>
|
||||
系统会自动在后台上传过去24小时内的新照片和视频
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{isLoading && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
<Text style={styles.loadingText}>正在上传,请稍候...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
buttonGroup: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
marginBottom: 10,
|
||||
color: '#333',
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 10,
|
||||
gap: 10,
|
||||
},
|
||||
timeButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 15,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
},
|
||||
activeButton: {
|
||||
backgroundColor: '#007AFF',
|
||||
borderColor: '#007AFF',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#333',
|
||||
textAlign: 'center',
|
||||
},
|
||||
dateRangeText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
marginTop: 8,
|
||||
},
|
||||
uploadButtonContainer: {
|
||||
marginTop: 20,
|
||||
},
|
||||
uploadButton: {
|
||||
backgroundColor: '#007AFF',
|
||||
padding: 15,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
uploadButtonDisabled: {
|
||||
backgroundColor: '#84c1ff',
|
||||
},
|
||||
uploadButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
statusContainer: {
|
||||
marginTop: 30,
|
||||
padding: 15,
|
||||
backgroundColor: '#f8f8f8',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#eee',
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 15,
|
||||
marginBottom: 5,
|
||||
color: '#333',
|
||||
},
|
||||
hintText: {
|
||||
fontSize: 13,
|
||||
color: '#666',
|
||||
},
|
||||
loadingContainer: {
|
||||
marginTop: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 10,
|
||||
color: '#666',
|
||||
},
|
||||
});
|
||||
383
components/file-upload/backgroundUploader.ts
Normal file
383
components/file-upload/backgroundUploader.ts
Normal file
@ -0,0 +1,383 @@
|
||||
import { fetchApi } from '@/lib/server-api-util';
|
||||
import * as BackgroundFetch from 'expo-background-fetch';
|
||||
import * as ImageManipulator from 'expo-image-manipulator';
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
import * as TaskManager from 'expo-task-manager';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
const BACKGROUND_UPLOAD_TASK = 'background-upload-task';
|
||||
|
||||
// 获取指定时间范围内的媒体文件
|
||||
export const getMediaByDateRange = async (startDate: Date, endDate: Date) => {
|
||||
try {
|
||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
console.log('Media library permission not granted');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取媒体资源
|
||||
const media = await MediaLibrary.getAssetsAsync({
|
||||
mediaType: ['photo', 'video'],
|
||||
first: 100, // 每次最多获取100个
|
||||
sortBy: [MediaLibrary.SortBy.creationTime],
|
||||
createdAfter: startDate.getTime(),
|
||||
createdBefore: endDate.getTime(),
|
||||
});
|
||||
|
||||
return media.assets;
|
||||
} catch (error) {
|
||||
console.error('Error getting media by date range:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 压缩图片
|
||||
const compressImage = async (uri: string): Promise<{ uri: string; file: File }> => {
|
||||
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) {
|
||||
console.error('Error compressing image:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取上传URL
|
||||
const getUploadUrl = async (file: File, metadata: Record<string, any> = {}): Promise<{ upload_url: string; file_id: string }> => {
|
||||
const body = {
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
file_size: file.size,
|
||||
metadata: {
|
||||
...metadata,
|
||||
originalName: file.name,
|
||||
fileType: file.type.startsWith('video/') ? 'video' : 'image',
|
||||
isCompressed: 'true',
|
||||
},
|
||||
};
|
||||
|
||||
return await fetchApi<{ upload_url: string; file_id: string }>("/file/generate-upload-url", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
};
|
||||
|
||||
// 确认上传
|
||||
const confirmUpload = async (file_id: string) => {
|
||||
return await fetchApi('/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 uploadFile = async (file: File, uploadUrl: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
// 检查并请求媒体库权限
|
||||
const checkMediaLibraryPermission = async (): Promise<{ hasPermission: boolean, status?: string }> => {
|
||||
try {
|
||||
const { status, accessPrivileges } = await MediaLibrary.getPermissionsAsync();
|
||||
|
||||
// 如果已经授权,直接返回
|
||||
if (status === 'granted' && accessPrivileges === 'all') {
|
||||
return { hasPermission: true, status };
|
||||
}
|
||||
|
||||
// 如果没有授权,请求权限
|
||||
const { status: newStatus, accessPrivileges: newPrivileges } = await MediaLibrary.requestPermissionsAsync();
|
||||
const isGranted = newStatus === 'granted' && newPrivileges === 'all';
|
||||
|
||||
if (!isGranted) {
|
||||
console.log('Media library permission not granted or limited access');
|
||||
}
|
||||
|
||||
return {
|
||||
hasPermission: isGranted,
|
||||
status: newStatus
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking media library permission:', error);
|
||||
return { hasPermission: false };
|
||||
}
|
||||
};
|
||||
|
||||
// 处理单个媒体文件上传
|
||||
const processMediaUpload = async (asset: MediaLibrary.Asset) => {
|
||||
try {
|
||||
// 检查权限
|
||||
const { hasPermission } = await checkMediaLibraryPermission();
|
||||
if (!hasPermission) {
|
||||
throw new Error('No media library permission');
|
||||
}
|
||||
|
||||
const isVideo = asset.mediaType === 'video';
|
||||
|
||||
// 上传原始文件
|
||||
const uploadOriginalFile = async () => {
|
||||
const response = await fetch(asset.uri);
|
||||
const blob = await response.blob();
|
||||
const filename = asset.filename ||
|
||||
`${isVideo ? 'video' : 'image'}_${Date.now()}_original.${isVideo ? 'mp4' : 'jpg'}`;
|
||||
|
||||
const mimeType = isVideo ? 'video/mp4' : 'image/jpeg';
|
||||
const file = new File([blob], filename, { type: mimeType });
|
||||
|
||||
console.log("Original file prepared for upload:", {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
});
|
||||
|
||||
// 获取上传URL并上传原始文件
|
||||
const { upload_url, file_id } = await getUploadUrl(file, {
|
||||
originalUri: asset.uri,
|
||||
creationTime: asset.creationTime,
|
||||
mediaType: isVideo ? 'video' : 'image',
|
||||
isCompressed: false
|
||||
});
|
||||
|
||||
await uploadFile(file, upload_url);
|
||||
await confirmUpload(file_id);
|
||||
console.log(`Successfully uploaded original: ${filename}`);
|
||||
return { success: true, file_id };
|
||||
};
|
||||
|
||||
// 上传压缩文件(仅图片)
|
||||
const uploadCompressedFile = async () => {
|
||||
if (isVideo) return { success: true, file_id: null }; // 视频不压缩
|
||||
|
||||
try {
|
||||
const { file: compressedFile } = await compressImage(asset.uri);
|
||||
const filename = asset.filename ?
|
||||
`compressed_${asset.filename}` :
|
||||
`image_${Date.now()}_compressed.jpg`;
|
||||
|
||||
console.log("Compressed file prepared for upload:", {
|
||||
name: filename,
|
||||
size: compressedFile.size,
|
||||
type: compressedFile.type
|
||||
});
|
||||
|
||||
// 获取上传URL并上传压缩文件
|
||||
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);
|
||||
console.log(`Successfully uploaded compressed: ${filename}`);
|
||||
return { success: true, file_id };
|
||||
} catch (error) {
|
||||
console.error('Error uploading compressed file:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
// 同时上传原始文件和压缩文件
|
||||
console.log("Asset info:", asset);
|
||||
const [originalResult, compressedResult] = await Promise.all([
|
||||
uploadOriginalFile(),
|
||||
uploadCompressedFile()
|
||||
]);
|
||||
console.log("originalResult", originalResult);
|
||||
console.log("compressedResult", compressedResult);
|
||||
addMaterial(originalResult.file_id, compressedResult.file_id)
|
||||
return {
|
||||
originalSuccess: originalResult.success,
|
||||
compressedSuccess: compressedResult.success,
|
||||
fileIds: {
|
||||
original: originalResult.file_id,
|
||||
compressed: compressedResult.file_id
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in processMediaUpload:', error);
|
||||
if (error.message === 'No media library permission') {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
originalSuccess: false,
|
||||
compressedSuccess: false,
|
||||
error
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 注册后台任务
|
||||
export const registerBackgroundUploadTask = async () => {
|
||||
try {
|
||||
// 检查是否已注册
|
||||
const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_UPLOAD_TASK);
|
||||
if (isRegistered) {
|
||||
await BackgroundFetch.unregisterTaskAsync(BACKGROUND_UPLOAD_TASK);
|
||||
}
|
||||
|
||||
// 注册后台任务
|
||||
await BackgroundFetch.registerTaskAsync(BACKGROUND_UPLOAD_TASK, {
|
||||
minimumInterval: 15 * 60, // 15分钟
|
||||
stopOnTerminate: false, // 应用关闭后继续运行
|
||||
startOnBoot: true, // 系统启动后自动运行
|
||||
});
|
||||
|
||||
console.log('Background upload task registered');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error registering background task:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义后台任务
|
||||
TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
|
||||
try {
|
||||
console.log('Background upload task started');
|
||||
|
||||
// 检查并请求媒体库权限
|
||||
const { hasPermission } = await checkMediaLibraryPermission();
|
||||
if (!hasPermission) {
|
||||
console.log('Media library permission not granted');
|
||||
// 返回 NoData 而不是 Failed,这样系统可能会在稍后重试
|
||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
}
|
||||
|
||||
// 获取过去24小时内的媒体
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const media = await getMediaByDateRange(yesterday, now);
|
||||
console.log("media", media);
|
||||
|
||||
if (media.length === 0) {
|
||||
console.log('No media found in the specified time range');
|
||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
}
|
||||
|
||||
console.log(`Found ${media.length} media items to process`);
|
||||
|
||||
// 处理每个媒体文件
|
||||
for (const item of media) {
|
||||
try {
|
||||
await processMediaUpload(item);
|
||||
} catch (error) {
|
||||
console.error(`Error processing media ${item.id}:`, error);
|
||||
// 如果是权限错误,直接返回失败
|
||||
if (error.message === 'No media library permission') {
|
||||
return BackgroundFetch.BackgroundFetchResult.Failed;
|
||||
}
|
||||
// 其他错误继续处理下一个文件
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Background upload task completed');
|
||||
return BackgroundFetch.BackgroundFetchResult.NewData;
|
||||
} catch (error) {
|
||||
console.error('Error in background upload task:', error);
|
||||
return BackgroundFetch.BackgroundFetchResult.Failed;
|
||||
}
|
||||
});
|
||||
|
||||
// 手动触发上传
|
||||
export const triggerManualUpload = async (startDate: Date, endDate: Date) => {
|
||||
try {
|
||||
console.log('Starting manual upload...');
|
||||
console.log("startDate", startDate);
|
||||
console.log("endDate", endDate);
|
||||
|
||||
const media = await getMediaByDateRange(startDate, endDate);
|
||||
console.log("media", media);
|
||||
if (media.length === 0) {
|
||||
Alert.alert('提示', '在指定时间范围内未找到媒体文件');
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of media) {
|
||||
try {
|
||||
const result = await processMediaUpload(item);
|
||||
results.push({
|
||||
id: item.id,
|
||||
originalSuccess: result.originalSuccess,
|
||||
compressedSuccess: result.compressedSuccess,
|
||||
fileIds: result.fileIds,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error uploading ${item.filename}:`, error);
|
||||
results.push({
|
||||
id: item.id,
|
||||
originalSuccess: false,
|
||||
compressedSuccess: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const originalSuccessCount = results.filter(r => r.originalSuccess).length;
|
||||
const compressedSuccessCount = results.filter(r => r.compressedSuccess).length;
|
||||
Alert.alert('上传完成', `成功上传 ${originalSuccessCount}/${media.length} 个原始文件,${compressedSuccessCount}/${media.length} 个压缩文件`);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error in manual upload:', error);
|
||||
Alert.alert('错误', '上传过程中出现错误');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
62
components/file-upload/mediaLibraryUtils.ts
Normal file
62
components/file-upload/mediaLibraryUtils.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
|
||||
export type MediaItem = {
|
||||
id: string;
|
||||
uri: string;
|
||||
creationTime: number;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
// 获取指定时间范围内的图片
|
||||
export const getFilteredMedia = async (range: 'today' | 'week' | 'month' | 'all'): Promise<MediaItem[]> => {
|
||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||
if (status !== 'granted') throw new Error('Permission not granted');
|
||||
console.log("statusq111111111111111111", status);
|
||||
|
||||
let cutoffDate: Date;
|
||||
|
||||
switch (range) {
|
||||
case 'today':
|
||||
cutoffDate = new Date();
|
||||
cutoffDate.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case 'week':
|
||||
cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 7);
|
||||
break;
|
||||
case 'month':
|
||||
cutoffDate = new Date();
|
||||
cutoffDate.setMonth(cutoffDate.getMonth() - 1);
|
||||
break;
|
||||
default:
|
||||
cutoffDate = new Date(0); // 所有图片
|
||||
}
|
||||
console.log("cutoffDateq111111111111111111", cutoffDate);
|
||||
|
||||
const albums = await MediaLibrary.getAlbumsAsync({ includeSmartAlbums: true });
|
||||
console.log("albumsq111111111111111111", albums);
|
||||
let allAssets: MediaItem[] = [];
|
||||
|
||||
for (const album of albums) {
|
||||
const result = await MediaLibrary.getAssetsAsync({
|
||||
album: album.id,
|
||||
mediaType: ['photo'],
|
||||
first: 1000,
|
||||
});
|
||||
console.log("result111111111111", result);
|
||||
|
||||
const filtered = result.assets
|
||||
.filter(asset => asset.creationTime > cutoffDate.getTime())
|
||||
.map(asset => ({
|
||||
id: asset.id,
|
||||
uri: asset.uri,
|
||||
creationTime: asset.creationTime,
|
||||
filename: asset.filename,
|
||||
}));
|
||||
console.log("filtered111111111111", filtered);
|
||||
|
||||
}
|
||||
|
||||
console.log("allAssetsq111111111111111111", allAssets);
|
||||
return allAssets;
|
||||
};
|
||||
93
components/file-upload/uploadQueueManager.ts
Normal file
93
components/file-upload/uploadQueueManager.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
const QUEUE_KEY = 'uploadQueue';
|
||||
|
||||
export type UploadItem = {
|
||||
uri: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
// 存储队列
|
||||
export const saveUploadQueue = async (queue: UploadItem[]) => {
|
||||
await SecureStore.setItemAsync(QUEUE_KEY, JSON.stringify(queue));
|
||||
};
|
||||
|
||||
// 获取队列
|
||||
export const getUploadQueue = async (): Promise<UploadItem[]> => {
|
||||
const data = await SecureStore.getItemAsync(QUEUE_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
};
|
||||
|
||||
// 移除已上传项
|
||||
export const removeFromQueue = async (uri: string) => {
|
||||
const queue = await getUploadQueue();
|
||||
const newQueue = queue.filter(item => item.uri !== uri);
|
||||
await saveUploadQueue(newQueue);
|
||||
};
|
||||
|
||||
export const uploadMediaFile = async (asset: any) => {
|
||||
const uri = asset.uri;
|
||||
const filename = uri.split('/').pop() || 'file.jpg';
|
||||
const type =
|
||||
asset.mediaType === 'photo'
|
||||
? `image/${filename.split('.').pop()}`
|
||||
: `video/${filename.split('.').pop()}`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', { uri, name: filename, type } as any);
|
||||
|
||||
await getUploadUrl({
|
||||
...formData,
|
||||
name: filename,
|
||||
type,
|
||||
size: asset.fileSize
|
||||
}, {}).then((res) => {
|
||||
confirmUpload(res.file_id).then((confirmRes) => {
|
||||
addMaterial(res.file_id, confirmRes.file_id)
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
})
|
||||
}).catch((error) => {
|
||||
console.log(error);
|
||||
})
|
||||
};
|
||||
|
||||
// 获取上传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);
|
||||
})
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { ThemedText } from '@/components/ThemedText';
|
||||
import { FileUploadItem } from '@/types/upload';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActivityIndicator, Image, TouchableOpacity, View } from 'react-native';
|
||||
import AutoUploadScreen from '../file-upload/autoUploadScreen';
|
||||
import FilesUploader from '../file-upload/files-uploader';
|
||||
|
||||
interface Props {
|
||||
@ -60,7 +61,9 @@ export default function Look(props: Props) {
|
||||
{t('auth.userMessage.choosePhoto', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
} />
|
||||
}
|
||||
/>
|
||||
<AutoUploadScreen />
|
||||
</View>
|
||||
|
||||
<View className="w-full">
|
||||
|
||||
@ -92,7 +92,13 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
const logout = () => {
|
||||
// 清除 Redux store 中的认证信息
|
||||
dispatch(clearCredentials());
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
localStorage.setItem('user', "");
|
||||
localStorage.setItem('token', "");
|
||||
} else {
|
||||
SecureStore.setItemAsync('user', "");
|
||||
SecureStore.setItemAsync('token', "");
|
||||
}
|
||||
// 触发事件通知
|
||||
eventEmitter.emit(EVENT_TYPES.USER_INFO_UPDATED, null);
|
||||
|
||||
|
||||
68
package-lock.json
generated
68
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"expo": "~53.0.12",
|
||||
"expo-audio": "~0.4.7",
|
||||
"expo-background-fetch": "^13.1.6",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-constants": "~17.1.6",
|
||||
"expo-dev-client": "~5.2.1",
|
||||
@ -37,6 +38,7 @@
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "~0.4.5",
|
||||
"expo-system-ui": "~5.0.9",
|
||||
"expo-task-manager": "^13.1.6",
|
||||
"expo-video": "~2.2.2",
|
||||
"expo-video-thumbnails": "~9.1.3",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
@ -48,7 +50,7 @@
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-native": "0.79.4",
|
||||
"react-native": "^0.79.4",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-modal": "^14.0.0-rc.1",
|
||||
"react-native-picker-select": "^9.3.1",
|
||||
@ -3358,6 +3360,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-async-storage/async-storage": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
|
||||
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"merge-options": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-picker/picker": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz",
|
||||
@ -7798,6 +7812,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-background-fetch": {
|
||||
"version": "13.1.6",
|
||||
"resolved": "https://registry.npmjs.org/expo-background-fetch/-/expo-background-fetch-13.1.6.tgz",
|
||||
"integrity": "sha512-hl4kR32DaxoHFYqNsILLZG2mWssCkUb4wnEAHtDGmpxUP4SCnJILcAn99J6AGDFUw5lF6FXNZZCXNfcrFioO4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expo-task-manager": "~13.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-blur": {
|
||||
"version": "14.1.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.1.5.tgz",
|
||||
@ -8261,6 +8287,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-task-manager": {
|
||||
"version": "13.1.6",
|
||||
"resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-13.1.6.tgz",
|
||||
"integrity": "sha512-sYNAftpIeZ+j6ur17Jo0OpSTk9ks/MDvTbrNCimXMyjIt69XXYL/kAPYf76bWuxOuN8bcJ8Ef8YvihkwFG9hDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unimodules-app-loader": "~5.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-updates-interface": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz",
|
||||
@ -9704,6 +9743,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@ -10771,6 +10819,18 @@
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-options": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-plain-obj": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@ -15139,6 +15199,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/unimodules-app-loader": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-5.1.3.tgz",
|
||||
"integrity": "sha512-nPUkwfkpJWvdOQrVvyQSUol93/UdmsCVd9Hkx9RgAevmKSVYdZI+S87W73NGKl6QbwK9L1BDSY5OrQuo8Oq15g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unique-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"expo": "~53.0.12",
|
||||
"expo-audio": "~0.4.7",
|
||||
"expo-background-fetch": "^13.1.6",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-constants": "~17.1.6",
|
||||
"expo-dev-client": "~5.2.1",
|
||||
@ -42,6 +43,7 @@
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "~0.4.5",
|
||||
"expo-system-ui": "~5.0.9",
|
||||
"expo-task-manager": "^13.1.6",
|
||||
"expo-video": "~2.2.2",
|
||||
"expo-video-thumbnails": "~9.1.3",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
@ -53,7 +55,7 @@
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-native": "0.79.4",
|
||||
"react-native": "^0.79.4",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-modal": "^14.0.0-rc.1",
|
||||
"react-native-picker-select": "^9.3.1",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user