diff --git a/app.json b/app.json index 0b3bf6b..e8efb01 100644 --- a/app.json +++ b/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", { diff --git a/components/file-upload/autoUploadScreen.tsx b/components/file-upload/autoUploadScreen.tsx new file mode 100644 index 0000000..87b1a6e --- /dev/null +++ b/components/file-upload/autoUploadScreen.tsx @@ -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 ( + + 自动上传设置 + + + 选择时间范围: + + setTimeRange('day')} + > + 一天 + + setTimeRange('week')} + > + 一周 + + setTimeRange('month')} + > + 一个月 + + setTimeRange('all')} + > + 全部 + + + + {getDateRangeText(timeRange)} + + + + + + + {isLoading ? '上传中...' : '开始上传'} + + + + + + + 后台自动上传状态: {isRegistered ? '已启用' : '未启用'} + + + 系统会自动在后台上传过去24小时内的新照片和视频 + + + + {isLoading && ( + + + 正在上传,请稍候... + + )} + + ); +} + +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', + }, +}); \ No newline at end of file diff --git a/components/file-upload/backgroundUploader.ts b/components/file-upload/backgroundUploader.ts new file mode 100644 index 0000000..368596b --- /dev/null +++ b/components/file-upload/backgroundUploader.ts @@ -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 = {}): 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 => { + 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; + } +}; diff --git a/components/file-upload/mediaLibraryUtils.ts b/components/file-upload/mediaLibraryUtils.ts new file mode 100644 index 0000000..3c4e53a --- /dev/null +++ b/components/file-upload/mediaLibraryUtils.ts @@ -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 => { + 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; +}; \ No newline at end of file diff --git a/components/file-upload/uploadQueueManager.ts b/components/file-upload/uploadQueueManager.ts new file mode 100644 index 0000000..38e722d --- /dev/null +++ b/components/file-upload/uploadQueueManager.ts @@ -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 => { + 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 = {}): Promise => { + 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("/file/generate-upload-url", { + method: 'POST', + body: JSON.stringify(body) + }); +}; + +// 向服务端confirm上传 +const confirmUpload = async (file_id: string): Promise => 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); + }) +} \ No newline at end of file diff --git a/components/user-message.tsx/look.tsx b/components/user-message.tsx/look.tsx index d593d92..ffb4c46 100644 --- a/components/user-message.tsx/look.tsx +++ b/components/user-message.tsx/look.tsx @@ -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' })} - } /> + } + /> + diff --git a/contexts/auth-context.tsx b/contexts/auth-context.tsx index 60ca9bb..6b9d9eb 100644 --- a/contexts/auth-context.tsx +++ b/contexts/auth-context.tsx @@ -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); diff --git a/package-lock.json b/package-lock.json index 32c4a54..068ae76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -15830,4 +15896,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 6236ffb..bc50a88 100644 --- a/package.json +++ b/package.json @@ -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", @@ -90,4 +92,4 @@ } } } -} +} \ No newline at end of file