feat: 上传进度条组件

This commit is contained in:
Junhui Chen 2025-07-17 12:28:33 +08:00
parent e2c5493c8c
commit d35ee35bad
15 changed files with 429 additions and 75 deletions

View File

@ -23,7 +23,6 @@ export default function AskScreen() {
const insets = useSafeAreaInsets();
useEffect(() => {
checkAuthStatus(router);
router.replace('/login');
}, []);
// 在组件内部添加 ref
const scrollViewRef = useRef<ScrollView>(null);

View File

@ -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<UploadTask[]>([]); // 新增上传任务状态
// 历史消息
const [historyList, setHistoryList] = React.useState<Chat[]>([]);
@ -39,8 +41,37 @@ const MemoList = () => {
getHistoryList()
}, [])
useFocusEffect(
useCallback(() => {
// 设置定时器,每秒查询一次上传进度
const intervalId = setInterval(async () => {
const tasks = await getUploadTasks();
setUploadTasks(tasks);
}, 1000);
return () => clearInterval(intervalId); // 清理定时器
}, [])
);
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
{/* 上传进度展示区域 */}
<View className="w-full h-80">
{uploadTasks.length >= 0 && (
<View style={{ padding: 10, backgroundColor: '#f0f0f0', borderBottomWidth: 1, borderBottomColor: '#ccc' }}>
<Text style={{ fontWeight: 'bold', marginBottom: 5 }}></Text>
{uploadTasks.map((task) => (
<Text key={task.uri}>
{task.filename}: {task.status} ({task.progress}%)
</Text>
))}
</View>
)}
<AutoUploadScreen />
</View>
{/* 顶部标题和上传按钮 */}
<View style={styles.header}>
<Text style={styles.title}>Memo List</Text>

View File

@ -3,13 +3,12 @@ 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 { 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 { FileUploadItem } from '@/types/upload';
import { User } from '@/types/user';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import { KeyboardAvoidingView, Platform, ScrollView, StatusBar, Text, View } from 'react-native';
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,7 +19,6 @@ export default function UserMessage() {
const [fileData, setFileData] = useState<FileUploadItem[]>([])
const [isLoading, setIsLoading] = useState(false);
const [userInfo, setUserInfo] = useState<User | null>(null);
const [uploadTasks, setUploadTasks] = useState<UploadTask[]>([]); // 新增上传任务状态
const statusBarHeight = StatusBar?.currentHeight ?? 0;
// 获取路由参数
@ -29,14 +27,6 @@ export default function UserMessage() {
useEffect(() => {
checkAuthStatus(router);
// 设置定时器,每秒查询一次上传进度
const intervalId = setInterval(async () => {
const tasks = await getUploadTasks();
setUploadTasks(tasks);
}, 1000);
return () => clearInterval(intervalId); // 清理定时器
}, []);
// 获取用户信息
@ -80,17 +70,6 @@ export default function UserMessage() {
keyboardShouldPersistTaps="handled"
bounces={false}
>
{/* 上传进度展示区域 */}
{uploadTasks.length > 0 && (
<View style={{ padding: 10, backgroundColor: '#f0f0f0', borderBottomWidth: 1, borderBottomColor: '#ccc' }}>
<Text style={{ fontWeight: 'bold', marginBottom: 5 }}></Text>
{uploadTasks.map((task) => (
<Text key={task.uri}>
{task.filename}: {task.status} ({task.progress}%)
</Text>
))}
</View>
)}
<View className="h-full" key={steps}>
{(() => {
const components = {

View File

@ -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() {
</View>
{isLoading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff" />
<Text style={styles.loadingText}>...</Text>
</View>
<UploaderProgressBar
imageUrl={uploadProgress.currentFileUrl}
uploadedSize={uploadProgress.uploadedSize}
totalSize={uploadProgress.totalSize}
uploadedCount={uploadProgress.uploadedCount}
totalCount={uploadProgress.totalCount}
/>
)}
</View>
);

View File

@ -158,7 +158,19 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
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) {

View File

@ -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<UploaderProgressBarProps> = ({
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 (
<View style={styles.container}>
<View style={styles.imageContainer}>
<Image source={{ uri: imageUrl }} style={styles.thumbnail} />
</View>
<View style={styles.progressSection}>
<View style={styles.progressInfo}>
<Text style={styles.progressText}>{`${formattedUploadedSize}/${formattedTotalSize}`}</Text>
<Text style={styles.statusText}>Uploading...</Text>
</View>
<Progress.Bar
progress={progress}
width={null} // Fills the container
height={4}
color={'#4A4A4A'}
unfilledColor={'rgba(255, 255, 255, 0.5)'}
borderWidth={0}
style={styles.progressBar}
/>
</View>
<Text style={styles.countText}>{`${uploadedCount}/${totalCount}`}</Text>
</View>
);
};
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;

View File

@ -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) {
</View>
}
/>
<AutoUploadScreen />
<MediaStatsScreen />
{/* <AutoUploadScreen /> */}
{/* <MediaStatsScreen /> */}
</View>
<View className="w-full">

View File

@ -12,7 +12,7 @@ export async function identityCheck(token: string) {
},
});
const data = await res.json();
return data.code != 0;
return data.code == 0;
}
/**

View File

@ -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<string, { loaded: number; total: number }>();
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);

View File

@ -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
};

View File

@ -1,7 +1,8 @@
import * as MediaLibrary from 'expo-media-library';
export type ExtendedAsset = MediaLibrary.Asset & {
exif?: Record<string, any>;
size?: number;
exif?: Record<string, any> | null;
};
// 上传任务类型

View File

@ -30,7 +30,10 @@ export const uploadFile = async (file: File, uploadUrl: string): Promise<void> =
// 处理单个媒体文件上传的核心逻辑
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<void> => {
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 });
}
};
}

View File

@ -78,7 +78,7 @@ export async function updateUploadTaskProgress(uri: string, progress: number): P
// 获取所有上传任务
export async function getUploadTasks(): Promise<UploadTask[]> {
console.log('Fetching all upload tasks...');
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
const results = db.getAllSync<UploadTask>(
'SELECT uri, filename, status, progress, file_id FROM upload_tasks;'
);

173
package-lock.json generated
View File

@ -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",
@ -49,6 +50,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",
@ -2521,6 +2523,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",
@ -2533,6 +2550,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",
@ -5924,6 +5953,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",
@ -12182,6 +12221,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",
@ -12195,6 +12250,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",
@ -12393,6 +12461,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",
@ -12428,6 +12512,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",
@ -14246,6 +14343,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",
@ -14345,6 +14458,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",
@ -17046,15 +17172,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"
@ -17075,6 +17201,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",
@ -18088,7 +18241,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": {
@ -21317,12 +21470,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"

View File