upload-jh #7
@ -1,4 +1,5 @@
|
||||
import { HapticTab } from '@/components/HapticTab';
|
||||
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
||||
import TabBarBackground from '@/components/ui/TabBarBackground';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
@ -279,6 +280,19 @@ export default function TabLayout() {
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
{/* Debug Screen - only in development */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Tabs.Screen
|
||||
name="debug"
|
||||
options={{
|
||||
title: 'Debug',
|
||||
tabBarIcon: ({ color, focused }) => (
|
||||
<TabBarIcon name={focused ? 'bug' : 'bug-outline'} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tabs >
|
||||
);
|
||||
}
|
||||
|
||||
132
app/(tabs)/debug.tsx
Normal file
132
app/(tabs)/debug.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, TextInput, Button, Text, StyleSheet, ScrollView, SafeAreaView, ActivityIndicator, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
import { executeSql } from '@/lib/db';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
|
||||
const DebugScreen = () => {
|
||||
const [sql, setSql] = useState('SELECT * FROM upload_tasks;');
|
||||
const [results, setResults] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleExecuteSql = async (query: string) => {
|
||||
if (!query) return;
|
||||
setLoading(true);
|
||||
setResults(null);
|
||||
try {
|
||||
const result = await executeSql(query);
|
||||
setResults(result);
|
||||
} catch (error) {
|
||||
setResults({ error: (error as Error).message });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const presetQueries = [
|
||||
{ title: 'All Uploads', query: 'SELECT * FROM upload_tasks;' },
|
||||
{ title: 'Delete All Uploads', query: 'DELETE FROM upload_tasks;' },
|
||||
{ title: 'Show Tables', query: "SELECT name FROM sqlite_master WHERE type='table';" },
|
||||
];
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardAvoidingView}
|
||||
>
|
||||
<ThemedText type="title" style={styles.title}>SQL Debugger</ThemedText>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
onChangeText={setSql}
|
||||
value={sql}
|
||||
placeholder="Enter SQL query"
|
||||
multiline
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<Button title="Execute" onPress={() => handleExecuteSql(sql)} disabled={loading} />
|
||||
</View>
|
||||
<View style={styles.presetsContainer}>
|
||||
{presetQueries.map((item, index) => (
|
||||
<View key={index} style={styles.presetButton}>
|
||||
<Button title={item.title} onPress={() => {
|
||||
setSql(item.query);
|
||||
handleExecuteSql(item.query);
|
||||
}} disabled={loading} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<ThemedText type="subtitle" style={styles.resultTitle}>Results:</ThemedText>
|
||||
<ScrollView style={styles.resultsContainer}>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="large" />
|
||||
) : (
|
||||
<Text selectable style={styles.resultsText}>
|
||||
{results ? JSON.stringify(results, null, 2) : 'No results yet.'}
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 15,
|
||||
textAlign: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
padding: 10,
|
||||
marginBottom: 10,
|
||||
minHeight: 100,
|
||||
textAlignVertical: 'top',
|
||||
backgroundColor: 'white',
|
||||
fontSize: 16,
|
||||
},
|
||||
presetsContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start',
|
||||
marginBottom: 15,
|
||||
},
|
||||
presetButton: {
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
resultTitle: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
resultsContainer: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
padding: 10,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
resultsText: {
|
||||
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||||
color: '#333',
|
||||
},
|
||||
});
|
||||
|
||||
export default DebugScreen;
|
||||
@ -1,16 +1,14 @@
|
||||
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, useFocusEffect } from "expo-router";
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { router } from "expo-router";
|
||||
import React, { useEffect } 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[]>([]);
|
||||
|
||||
@ -41,34 +39,11 @@ 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>
|
||||
)}
|
||||
<View className="w-full h-full">
|
||||
<AutoUploadScreen />
|
||||
</View>
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic';
|
||||
import { triggerManualUpload } from '@/lib/background-uploader/manual';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import UploaderProgressBar from './uploader-progress-bar';
|
||||
@ -135,6 +136,15 @@ export default function AutoUploadScreen() {
|
||||
{isLoading ? '上传中...' : '开始上传'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className='mt-2'
|
||||
style={[styles.uploadButton]}
|
||||
onPress={() => router.push('/debug')}
|
||||
>
|
||||
<Text style={styles.uploadButtonText}>
|
||||
进入db调试页面
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.statusContainer}>
|
||||
@ -146,11 +156,11 @@ export default function AutoUploadScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{isLoading && (
|
||||
{
|
||||
// isLoading &&
|
||||
(
|
||||
<UploaderProgressBar
|
||||
imageUrl={uploadProgress.currentFileUrl}
|
||||
uploadedSize={uploadProgress.uploadedSize}
|
||||
totalSize={uploadProgress.totalSize}
|
||||
uploadedCount={uploadProgress.uploadedCount}
|
||||
totalCount={uploadProgress.totalCount}
|
||||
/>
|
||||
|
||||
@ -1,36 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Image, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActivityIndicator, 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(' ', '');
|
||||
const progress = totalCount > 0 ? (uploadedCount / totalCount) * 100 : 0;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@ -39,11 +24,11 @@ const UploaderProgressBar: React.FC<UploaderProgressBarProps> = ({
|
||||
</View>
|
||||
<View style={styles.progressSection}>
|
||||
<View style={styles.progressInfo}>
|
||||
<Text style={styles.progressText}>{`${formattedUploadedSize}/${formattedTotalSize}`}</Text>
|
||||
<Text style={styles.statusText}>Uploading...</Text>
|
||||
<Text style={styles.statusText}>{t('upload.uploading', { ns: 'upload' })}</Text>
|
||||
<ActivityIndicator size={12} color="#4A4A4A" className='ml-2' />
|
||||
</View>
|
||||
<Progress.Bar
|
||||
progress={progress}
|
||||
progress={progress / 100}
|
||||
width={null} // Fills the container
|
||||
height={4}
|
||||
color={'#4A4A4A'}
|
||||
@ -62,11 +47,11 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F5B941', // From image
|
||||
borderRadius: 25,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
marginHorizontal: 15,
|
||||
height: 50,
|
||||
marginHorizontal: 0,
|
||||
height: 60,
|
||||
},
|
||||
imageContainer: {
|
||||
width: 40,
|
||||
@ -93,9 +78,12 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
progressInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
activityIndicator: {
|
||||
marginLeft: 5,
|
||||
},
|
||||
progressText: {
|
||||
color: '#4A4A4A',
|
||||
fontWeight: 'bold',
|
||||
|
||||
8
components/navigation/TabBarIcon.tsx
Normal file
8
components/navigation/TabBarIcon.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
// This file is based on the default template provided by Expo.
|
||||
import { type IconProps } from '@expo/vector-icons/build/createIconSet';
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import { type ComponentProps } from 'react';
|
||||
|
||||
export function TabBarIcon({ style, ...rest }: IconProps<ComponentProps<typeof Ionicons>['name']>) {
|
||||
return <Ionicons size={28} style={[{ marginBottom: -3 }, style]} {...rest} />;
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { Steps } from '@/app/(tabs)/user-message';
|
||||
import ChoicePhoto from '@/assets/icons/svg/choicePhoto.svg';
|
||||
import LookSvg from '@/assets/icons/svg/look.svg';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { FileUploadItem } from '@/types/upload';
|
||||
import { FileUploadItem } from '@/lib/background-uploader/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActivityIndicator, Image, TouchableOpacity, View } from 'react-native';
|
||||
import FilesUploader from '../file-upload/files-uploader';
|
||||
@ -32,11 +32,11 @@ export default function Look(props: Props) {
|
||||
{t('auth.userMessage.avatorText2', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
{
|
||||
fileData[0]?.preview
|
||||
fileData[0]?.previewUrl
|
||||
?
|
||||
<Image
|
||||
className='rounded-full w-[10rem] h-[10rem]'
|
||||
source={{ uri: fileData[0].preview }}
|
||||
source={{ uri: fileData[0].previewUrl }}
|
||||
/>
|
||||
:
|
||||
avatar
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"task": "Task",
|
||||
"taskName": "dynamic family portrait",
|
||||
"taskStatus": "processing",
|
||||
"noName": "No Name"
|
||||
"noName": "No Name",
|
||||
"uploading": "Uploading"
|
||||
},
|
||||
"library": {
|
||||
"title": "My Memory",
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"task": "任务",
|
||||
"taskName": "动态全家福",
|
||||
"taskStatus": "正在处理中",
|
||||
"noName": "未命名作品"
|
||||
"noName": "未命名作品",
|
||||
"uploading": "上传中"
|
||||
},
|
||||
"library": {
|
||||
"title": "My Memory",
|
||||
|
||||
@ -7,7 +7,7 @@ import { processAndUploadMedia } from './uploader';
|
||||
|
||||
const BACKGROUND_UPLOAD_TASK = 'background-upload-task';
|
||||
|
||||
const CONCURRENCY_LIMIT = 3; // 后台上传并发数,例如同时上传3个文件
|
||||
const CONCURRENCY_LIMIT = 1; // 后台上传并发数,例如同时上传3个文件
|
||||
const limit = pLimit(CONCURRENCY_LIMIT);
|
||||
|
||||
// 注册后台任务
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { Alert } from 'react-native';
|
||||
import pLimit from 'p-limit';
|
||||
import { Alert } from 'react-native';
|
||||
import { getUploadTaskStatus, insertUploadTask } from '../db';
|
||||
import { getMediaByDateRange } from './media';
|
||||
import { processAndUploadMedia } from './uploader';
|
||||
import { ExtendedAsset } from './types';
|
||||
import { insertUploadTask, getUploadTaskStatus } from '../db';
|
||||
import { processAndUploadMedia } from './uploader';
|
||||
|
||||
// 设置最大并发数
|
||||
const CONCURRENCY_LIMIT = 10; // 同时最多上传10个文件
|
||||
const CONCURRENCY_LIMIT = 1; // 同时最多上传10个文件
|
||||
const limit = pLimit(CONCURRENCY_LIMIT);
|
||||
|
||||
// 手动触发上传
|
||||
@ -58,7 +58,7 @@ export const triggerManualUpload = async (
|
||||
|
||||
if (result) {
|
||||
results.push(result);
|
||||
if(result.originalSuccess) {
|
||||
if (result.originalSuccess) {
|
||||
uploadedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ const fetchAssetsWithExif = async (
|
||||
const media = await MediaLibrary.getAssetsAsync({
|
||||
mediaType,
|
||||
first: 500, // Fetch in batches of 500
|
||||
sortBy: [MediaLibrary.SortBy.creationTime, descending],
|
||||
sortBy: [MediaLibrary.SortBy.creationTime],
|
||||
createdAfter,
|
||||
createdBefore,
|
||||
after,
|
||||
|
||||
21
lib/db.ts
21
lib/db.ts
@ -93,3 +93,24 @@ export async function cleanUpUploadTasks(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// for debug page
|
||||
export async function executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||
try {
|
||||
// Trim and check if it's a SELECT query
|
||||
const isSelect = sql.trim().toLowerCase().startsWith('select');
|
||||
if (isSelect) {
|
||||
const results = db.getAllSync(sql, params);
|
||||
return results;
|
||||
} else {
|
||||
const result = db.runSync(sql, params);
|
||||
return {
|
||||
changes: result.changes,
|
||||
lastInsertRowId: result.lastInsertRowId,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error executing SQL:", error);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
eas build --platform android --profile development --local
|
||||
Loading…
x
Reference in New Issue
Block a user