From 26c0baf975bda08b8fa52021f1d53c8369d4a16a Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Thu, 17 Jul 2025 12:28:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BC=A0=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E6=9D=A1=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/ask.tsx | 1 - app/(tabs)/memo-list.tsx | 37 +++- app/(tabs)/user-message.tsx | 33 +--- components/file-upload/autoUploadScreen.tsx | 35 +++- components/file-upload/files-uploader.tsx | 14 +- .../file-upload/uploader-progress-bar.tsx | 119 ++++++++++++ components/user-message.tsx/look.tsx | 6 +- lib/auth.ts | 2 +- lib/background-uploader/manual.ts | 59 ++++-- lib/background-uploader/media.ts | 7 +- lib/background-uploader/types.ts | 3 +- lib/background-uploader/uploader.ts | 19 +- lib/db.ts | 2 +- package-lock.json | 173 +++++++++++++++++- scripts/android_local_build.sh | 0 15 files changed, 432 insertions(+), 78 deletions(-) create mode 100644 components/file-upload/uploader-progress-bar.tsx create mode 100644 scripts/android_local_build.sh diff --git a/app/(tabs)/ask.tsx b/app/(tabs)/ask.tsx index e528525..5b6a83e 100644 --- a/app/(tabs)/ask.tsx +++ b/app/(tabs)/ask.tsx @@ -15,7 +15,6 @@ export default function AskScreen() { const insets = useSafeAreaInsets(); useEffect(() => { checkAuthStatus(router); - router.replace('/login'); }, []); // 在组件内部添加 ref const scrollViewRef = useRef(null); diff --git a/app/(tabs)/memo-list.tsx b/app/(tabs)/memo-list.tsx index 602f2f1..42090c0 100644 --- a/app/(tabs)/memo-list.tsx +++ b/app/(tabs)/memo-list.tsx @@ -1,14 +1,16 @@ import ChatSvg from "@/assets/icons/svg/chat.svg"; +import AutoUploadScreen from "@/components/file-upload/autoUploadScreen"; import AskNavbar from "@/components/layout/ask"; +import { getUploadTasks, UploadTask } from "@/lib/db"; import { fetchApi } from "@/lib/server-api-util"; import { Chat } from "@/types/ask"; -import { router } from "expo-router"; -import React, { useEffect } from 'react'; +import { router, useFocusEffect } from "expo-router"; +import React, { useCallback, useEffect, useState } from 'react'; import { FlatList, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const MemoList = () => { const insets = useSafeAreaInsets(); - + const [uploadTasks, setUploadTasks] = useState([]); // 新增上传任务状态 // 历史消息 const [historyList, setHistoryList] = React.useState([]); @@ -39,8 +41,37 @@ const MemoList = () => { getHistoryList() }, []) + + useFocusEffect( + useCallback(() => { + // 设置定时器,每秒查询一次上传进度 + const intervalId = setInterval(async () => { + const tasks = await getUploadTasks(); + setUploadTasks(tasks); + }, 1000); + + return () => clearInterval(intervalId); // 清理定时器 + }, []) + ); + return ( + {/* 上传进度展示区域 */} + + + {uploadTasks.length >= 0 && ( + + 上传任务: + {uploadTasks.map((task) => ( + + {task.filename}: {task.status} ({task.progress}%) + + ))} + + )} + + + {/* 顶部标题和上传按钮 */} Memo List diff --git a/app/(tabs)/user-message.tsx b/app/(tabs)/user-message.tsx index a5f137a..c276c10 100644 --- a/app/(tabs)/user-message.tsx +++ b/app/(tabs)/user-message.tsx @@ -2,14 +2,13 @@ import Choice from '@/components/user-message.tsx/choice'; import Done from '@/components/user-message.tsx/done'; import Look from '@/components/user-message.tsx/look'; import UserName from '@/components/user-message.tsx/userName'; -import { fetchApi } from '@/lib/server-api-util'; -import { FileUploadItem } from '@/types/upload'; -import { User } from '@/types/user'; -import { useEffect, useState } from 'react'; -import { KeyboardAvoidingView, Platform, ScrollView, StatusBar, View, Text } from 'react-native'; -import { useRouter } from 'expo-router'; import { checkAuthStatus } from '@/lib/auth'; -import { getUploadTasks, UploadTask } from '@/lib/db'; +import { FileUploadItem } from '@/lib/background-uploader/types'; +import { fetchApi } from '@/lib/server-api-util'; +import { User } from '@/types/user'; +import { useRouter } from 'expo-router'; +import { useEffect, useState } from 'react'; +import { KeyboardAvoidingView, Platform, ScrollView, StatusBar, View } from 'react-native'; export type Steps = "userName" | "look" | "choice" | "done"; export default function UserMessage() { const router = useRouter(); @@ -20,19 +19,10 @@ export default function UserMessage() { const [fileData, setFileData] = useState([]) const [isLoading, setIsLoading] = useState(false); const [userInfo, setUserInfo] = useState(null); - const [uploadTasks, setUploadTasks] = useState([]); // 新增上传任务状态 const statusBarHeight = StatusBar?.currentHeight ?? 0; useEffect(() => { checkAuthStatus(router); - - // 设置定时器,每秒查询一次上传进度 - const intervalId = setInterval(async () => { - const tasks = await getUploadTasks(); - setUploadTasks(tasks); - }, 1000); - - return () => clearInterval(intervalId); // 清理定时器 }, []); // 获取用户信息 @@ -76,17 +66,6 @@ export default function UserMessage() { keyboardShouldPersistTaps="handled" bounces={false} > - {/* 上传进度展示区域 */} - {uploadTasks.length > 0 && ( - - 上传任务: - {uploadTasks.map((task) => ( - - {task.filename}: {task.status} ({task.progress}%) - - ))} - - )} {(() => { const components = { diff --git a/components/file-upload/autoUploadScreen.tsx b/components/file-upload/autoUploadScreen.tsx index 57774d3..3f45ece 100644 --- a/components/file-upload/autoUploadScreen.tsx +++ b/components/file-upload/autoUploadScreen.tsx @@ -1,12 +1,20 @@ import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic'; import { triggerManualUpload } from '@/lib/background-uploader/manual'; import React, { useEffect, useState } from 'react'; -import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import UploaderProgressBar from './uploader-progress-bar'; export default function AutoUploadScreen() { const [timeRange, setTimeRange] = useState('day'); const [isLoading, setIsLoading] = useState(false); const [isRegistered, setIsRegistered] = useState(false); + const [uploadProgress, setUploadProgress] = useState({ + totalCount: 0, + uploadedCount: 0, + currentFileUrl: '', + uploadedSize: 0, + totalSize: 0, + }); // 注册后台任务 useEffect(() => { @@ -22,7 +30,19 @@ export default function AutoUploadScreen() { const handleManualUpload = async () => { try { setIsLoading(true); - await triggerManualUpload(getDateRange(timeRange)[0], getDateRange(timeRange)[1]); + await triggerManualUpload( + getDateRange(timeRange)[0], + getDateRange(timeRange)[1], + (progress) => { + setUploadProgress({ + totalCount: progress.totalCount, + uploadedCount: progress.uploadedCount, + currentFileUrl: progress.currentAsset.uri, + uploadedSize: progress.uploadedBytes, + totalSize: progress.totalBytes, + }); + } + ); } catch (error) { console.error('Upload error:', error); } finally { @@ -127,10 +147,13 @@ export default function AutoUploadScreen() { {isLoading && ( - - - 正在上传,请稍候... - + )} ); diff --git a/components/file-upload/files-uploader.tsx b/components/file-upload/files-uploader.tsx index 9ca2871..75aa81c 100644 --- a/components/file-upload/files-uploader.tsx +++ b/components/file-upload/files-uploader.tsx @@ -158,7 +158,19 @@ export const ImagesUploader: React.FC = ({ try { // 统一通过 lib 的 uploadFileWithProgress 实现上传 const uploadUrlData = await getUploadUrl(task.file, { ...task.metadata, GPSVersionID: undefined }); - await uploadFileWithProgress(task.file, uploadUrlData.upload_url, updateProgress, 30000); + const taskIndex = uploadTasks.indexOf(task); + const totalTasks = uploadTasks.length; + const baseProgress = (taskIndex / totalTasks) * 100; + + await uploadFileWithProgress( + task.file, + uploadUrlData.upload_url, + (progress) => { + const taskProgress = progress.total > 0 ? (progress.loaded / progress.total) * (100 / totalTasks) : 0; + updateProgress(baseProgress + taskProgress); + }, + 30000 + ); const result = await confirmUpload(uploadUrlData.file_id); uploadResultsList.push({ ...result, file_id: uploadUrlData.file_id, upload_url: uploadUrlData.upload_url }); } catch (error) { diff --git a/components/file-upload/uploader-progress-bar.tsx b/components/file-upload/uploader-progress-bar.tsx new file mode 100644 index 0000000..a5b842a --- /dev/null +++ b/components/file-upload/uploader-progress-bar.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { Image, StyleSheet, Text, View } from 'react-native'; +import * as Progress from 'react-native-progress'; + +// Helper to format bytes into a readable string (e.g., KB, MB) +const formatBytes = (bytes: number, decimals = 1) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; +}; + +interface UploaderProgressBarProps { + imageUrl: string; + uploadedSize: number; // in bytes + totalSize: number; // in bytes + uploadedCount: number; + totalCount: number; +} + +const UploaderProgressBar: React.FC = ({ + imageUrl, + uploadedSize, + totalSize, + uploadedCount, + totalCount, +}) => { + const progress = totalSize > 0 ? uploadedSize / totalSize : 0; + // The image shows 1.1M/6.3M, so we format the bytes + const formattedUploadedSize = formatBytes(uploadedSize, 1).replace(' ', ''); + const formattedTotalSize = formatBytes(totalSize, 1).replace(' ', ''); + + return ( + + + + + + + {`${formattedUploadedSize}/${formattedTotalSize}`} + Uploading... + + + + {`${uploadedCount}/${totalCount}`} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F5B941', // From image + borderRadius: 25, + paddingHorizontal: 8, + paddingVertical: 8, + marginHorizontal: 15, + height: 50, + }, + imageContainer: { + width: 40, + height: 40, + justifyContent: 'center', + alignItems: 'center', + // This container helps with the skewed frame effect + transform: [{ rotate: '-5deg' }], + marginRight: 8, + }, + thumbnail: { + width: 36, + height: 36, + borderRadius: 8, + borderWidth: 2, + borderColor: 'white', + transform: [{ rotate: '5deg' }], // Counter-rotate to keep image straight + }, + progressSection: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + marginRight: 15, + }, + progressInfo: { + flexDirection: 'row', + alignItems: 'baseline', + marginBottom: 4, + }, + progressText: { + color: '#4A4A4A', + fontWeight: 'bold', + fontSize: 12, + marginRight: 8, + }, + statusText: { + color: '#4A4A4A', + fontSize: 12, + }, + progressBar: { + width: '100%', + }, + countText: { + color: '#4A4A4A', + fontWeight: 'bold', + fontSize: 16, + }, +}); + +export default UploaderProgressBar; diff --git a/components/user-message.tsx/look.tsx b/components/user-message.tsx/look.tsx index 7498d00..bd11a46 100644 --- a/components/user-message.tsx/look.tsx +++ b/components/user-message.tsx/look.tsx @@ -5,9 +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'; -import MediaStatsScreen from '../file-upload/getTotal'; interface Props { setSteps?: (steps: Steps) => void; @@ -64,8 +62,8 @@ export default function Look(props: Props) { } /> - - + {/* */} + {/* */} diff --git a/lib/auth.ts b/lib/auth.ts index 8dd73d6..e4d4913 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -12,7 +12,7 @@ export async function identityCheck(token: string) { }, }); const data = await res.json(); - return data.code != 0; + return data.code == 0; } /** diff --git a/lib/background-uploader/manual.ts b/lib/background-uploader/manual.ts index 688b1cd..23b52ea 100644 --- a/lib/background-uploader/manual.ts +++ b/lib/background-uploader/manual.ts @@ -10,7 +10,17 @@ const CONCURRENCY_LIMIT = 10; // 同时最多上传10个文件 const limit = pLimit(CONCURRENCY_LIMIT); // 手动触发上传 -export const triggerManualUpload = async (startDate: Date, endDate: Date) => { +export const triggerManualUpload = async ( + startDate: Date, + endDate: Date, + onProgress?: (progress: { + totalCount: number; + uploadedCount: number; + totalBytes: number; // Overall total size + uploadedBytes: number; // Overall uploaded size + currentAsset: ExtendedAsset; // To show a thumbnail + }) => void +) => { try { const media = await getMediaByDateRange(startDate, endDate); if (media.length === 0) { @@ -18,20 +28,41 @@ export const triggerManualUpload = async (startDate: Date, endDate: Date) => { return []; } - const uploadPromises = media.map((asset: ExtendedAsset) => - limit(async () => { - const existingTask = await getUploadTaskStatus(asset.uri); - if (!existingTask) { - await insertUploadTask(asset.uri, asset.filename); - } else if (existingTask.status === 'success' || existingTask.status === 'skipped') { - console.log(`File ${asset.uri} already ${existingTask.status}, skipping processing.`); - return null; // Skip processing if already successful or skipped - } - return processAndUploadMedia(asset); - }) - ); + const progressMap = new Map(); - const results = await Promise.all(uploadPromises); + const results = []; + let uploadedCount = 0; + + for (const asset of media) { + const existingTask = await getUploadTaskStatus(asset.uri); + if (!existingTask) { + await insertUploadTask(asset.uri, asset.filename); + } else if (existingTask.status === 'success' || existingTask.status === 'skipped') { + console.log(`File ${asset.uri} already ${existingTask.status}, skipping processing.`); + uploadedCount++; // Also count skipped files as 'processed' + continue; + } + + const result = await limit(() => processAndUploadMedia(asset, (fileProgress) => { + progressMap.set(asset.uri, fileProgress); + const uploadedBytes = Array.from(progressMap.values()).reduce((sum, p) => sum + p.loaded, 0); + const totalBytes = Array.from(progressMap.values()).reduce((sum, p) => sum + p.total, 0); + onProgress?.({ + totalCount: media.length, + uploadedCount, + totalBytes, + uploadedBytes, + currentAsset: asset, + }); + })); + + if (result) { + results.push(result); + if(result.originalSuccess) { + uploadedCount++; + } + } + } // 过滤掉因为已上传而返回 null 的结果 const finalResults = results.filter(result => result !== null); diff --git a/lib/background-uploader/media.ts b/lib/background-uploader/media.ts index 953d7c1..95b89be 100644 --- a/lib/background-uploader/media.ts +++ b/lib/background-uploader/media.ts @@ -1,9 +1,10 @@ import * as MediaLibrary from 'expo-media-library'; +import { MediaTypeValue } from 'expo-media-library'; import { ExtendedAsset } from './types'; // Assuming ExtendedAsset is defined in types.ts // Helper to fetch assets with pagination and EXIF info const fetchAssetsWithExif = async ( - mediaType: MediaLibrary.MediaType[], + mediaType: MediaTypeValue[], createdAfter?: number, createdBefore?: number, descending: boolean = true // Default to descending @@ -16,11 +17,10 @@ const fetchAssetsWithExif = async ( const media = await MediaLibrary.getAssetsAsync({ mediaType, first: 500, // Fetch in batches of 500 - sortBy: [MediaLibrary.SortBy.creationTime], + sortBy: [MediaLibrary.SortBy.creationTime, descending], createdAfter, createdBefore, after, - descending, }); allAssets = allAssets.concat(media.assets); @@ -35,6 +35,7 @@ const fetchAssetsWithExif = async ( const assetInfo = await MediaLibrary.getAssetInfoAsync(asset.id); return { ...asset, + exif: assetInfo.exif || null, location: assetInfo.location || null }; diff --git a/lib/background-uploader/types.ts b/lib/background-uploader/types.ts index 08a763c..e7ace0c 100644 --- a/lib/background-uploader/types.ts +++ b/lib/background-uploader/types.ts @@ -1,7 +1,8 @@ import * as MediaLibrary from 'expo-media-library'; export type ExtendedAsset = MediaLibrary.Asset & { - exif?: Record; + size?: number; + exif?: Record | null; }; // 上传任务类型 diff --git a/lib/background-uploader/uploader.ts b/lib/background-uploader/uploader.ts index e242449..14793be 100644 --- a/lib/background-uploader/uploader.ts +++ b/lib/background-uploader/uploader.ts @@ -30,7 +30,10 @@ export const uploadFile = async (file: File, uploadUrl: string): Promise = // 处理单个媒体文件上传的核心逻辑 -export const processAndUploadMedia = async (asset: ExtendedAsset) => { +export const processAndUploadMedia = async ( + asset: ExtendedAsset, + onProgress?: (progress: { loaded: number; total: number }) => void +) => { try { // 1. 文件去重检查 (从数据库获取状态) const existingTask = await getUploadTaskStatus(asset.uri); @@ -101,7 +104,9 @@ export const processAndUploadMedia = async (asset: ExtendedAsset) => { }); await uploadFileWithProgress(fileToUpload, upload_url, (progress) => { - updateUploadTaskProgress(asset.uri, Math.round(progress * 0.5)); // 原始文件占总进度的50% + if (onProgress) onProgress(progress); + const percentage = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0; + updateUploadTaskProgress(asset.uri, Math.round(percentage * 0.5)); // 原始文件占总进度的50% }); await confirmUpload(file_id); @@ -127,7 +132,10 @@ export const processAndUploadMedia = async (asset: ExtendedAsset) => { }); await uploadFileWithProgress(compressedFile, upload_url, (progress) => { - updateUploadTaskProgress(asset.uri, 50 + Math.round(progress * 0.5)); // 压缩文件占总进度的后50% + // For compressed files, we can't easily report byte progress relative to the whole process, + // as we don't know the compressed size in advance. We'll just update the DB progress. + const percentage = progress.total > 0 ? (progress.loaded / progress.total) * 100 : 0; + updateUploadTaskProgress(asset.uri, 50 + Math.round(percentage * 0.5)); // 压缩文件占总进度的后50% }); await confirmUpload(file_id); return { success: true, file_id }; @@ -187,7 +195,7 @@ export const processAndUploadMedia = async (asset: ExtendedAsset) => { export const uploadFileWithProgress = async ( file: File, uploadUrl: string, - onProgress?: (progress: number) => void, + onProgress?: (progress: { loaded: number; total: number }) => void, timeout: number = 30000 ): Promise => { return new Promise((resolve, reject) => { @@ -201,8 +209,7 @@ export const uploadFileWithProgress = async ( if (onProgress) { xhr.upload.onprogress = (event) => { if (event.lengthComputable) { - const progress = Math.round((event.loaded / event.total) * 100); - onProgress(progress); + onProgress({ loaded: event.loaded, total: event.total }); } }; } diff --git a/lib/db.ts b/lib/db.ts index 1c17b06..61ce5c8 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -78,7 +78,7 @@ export async function updateUploadTaskProgress(uri: string, progress: number): P // 获取所有上传任务 export async function getUploadTasks(): Promise { - console.log('Fetching all upload tasks...'); + console.log('Fetching all upload tasks... time:', new Date().toLocaleString()); const results = db.getAllSync( 'SELECT uri, filename, status, progress, file_id FROM upload_tasks;' ); diff --git a/package-lock.json b/package-lock.json index fff03db..4baaa82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", "@reduxjs/toolkit": "^2.8.2", + "@types/p-limit": "^2.2.0", "@types/react-redux": "^7.1.34", "expo": "~53.0.12", "expo-audio": "~0.4.7", @@ -48,6 +49,7 @@ "i18next-http-backend": "^3.0.2", "lottie-react-native": "7.2.2", "nativewind": "^4.1.23", + "p-limit": "^6.2.0", "react": "19.0.0", "react-dom": "19.0.0", "react-i18next": "^15.5.3", @@ -2520,6 +2522,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@expo/fingerprint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@expo/fingerprint/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -2532,6 +2549,18 @@ "node": ">=10" } }, + "node_modules/@expo/fingerprint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@expo/image-utils": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.7.6.tgz", @@ -5923,6 +5952,16 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/p-limit": { + "version": "2.2.0", + "resolved": "http://192.168.31.115:8081/repository/npm/@types/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-fGFbybl1r0oE9mqgfc2EHHUin9ZL5rbQIexWI6jYRU1ADVn4I3LHzT+g/kpPpZsfp8PB94CQ655pfAjNF8LP6A==", + "deprecated": "This is a stub types definition. p-limit provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "p-limit": "*" + } + }, "node_modules/@types/ramda": { "version": "0.27.66", "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.66.tgz", @@ -12170,6 +12209,22 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-changed-files/node_modules/picomatch": { "version": "4.0.3", "resolved": "http://192.168.31.115:8081/repository/npm/picomatch/-/picomatch-4.0.3.tgz", @@ -12183,6 +12238,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/jest-changed-files/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-circus": { "version": "30.0.4", "resolved": "http://192.168.31.115:8081/repository/npm/jest-circus/-/jest-circus-30.0.4.tgz", @@ -12381,6 +12449,22 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-circus/node_modules/picomatch": { "version": "4.0.3", "resolved": "http://192.168.31.115:8081/repository/npm/picomatch/-/picomatch-4.0.3.tgz", @@ -12416,6 +12500,19 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-circus/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-cli": { "version": "30.0.4", "resolved": "http://192.168.31.115:8081/repository/npm/jest-cli/-/jest-cli-30.0.4.tgz", @@ -14234,6 +14331,22 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-runner/node_modules/picomatch": { "version": "4.0.3", "resolved": "http://192.168.31.115:8081/repository/npm/picomatch/-/picomatch-4.0.3.tgz", @@ -14333,6 +14446,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/jest-runner/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-runtime": { "version": "30.0.4", "resolved": "http://192.168.31.115:8081/repository/npm/jest-runtime/-/jest-runtime-30.0.4.tgz", @@ -17034,15 +17160,15 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "6.2.0", + "resolved": "http://192.168.31.115:8081/repository/npm/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -17063,6 +17189,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "http://192.168.31.115:8081/repository/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -18076,7 +18229,7 @@ }, "node_modules/react-native-progress": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.1.tgz", + "resolved": "http://192.168.31.115:8081/repository/npm/react-native-progress/-/react-native-progress-5.0.1.tgz", "integrity": "sha512-TYfJ4auAe5vubDma2yfFvt7ktSI+UCfysqJnkdHEcLXqAitRFOozgF/cLgN5VNi/iLdaf3ga1ETi2RF4jVZ/+g==", "license": "MIT", "dependencies": { @@ -21305,12 +21458,12 @@ } }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "version": "1.2.1", + "resolved": "http://192.168.31.115:8081/repository/npm/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/scripts/android_local_build.sh b/scripts/android_local_build.sh new file mode 100644 index 0000000..e69de29