upload-jh #7

Merged
txcjh merged 13 commits from upload-jh into upload 2025-07-17 15:55:28 +08:00
14 changed files with 233 additions and 82 deletions
Showing only changes of commit c7df8a66d0 - Show all commits

View File

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

View File

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

View File

@ -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,15 +156,15 @@ export default function AutoUploadScreen() {
</Text>
</View>
{isLoading && (
<UploaderProgressBar
imageUrl={uploadProgress.currentFileUrl}
uploadedSize={uploadProgress.uploadedSize}
totalSize={uploadProgress.totalSize}
uploadedCount={uploadProgress.uploadedCount}
totalCount={uploadProgress.totalCount}
/>
)}
{
// isLoading &&
(
<UploaderProgressBar
imageUrl={uploadProgress.currentFileUrl}
uploadedCount={uploadProgress.uploadedCount}
totalCount={uploadProgress.totalCount}
/>
)}
</View>
);
}

View File

@ -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',

View 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} />;
}

View File

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

View File

@ -15,7 +15,8 @@
"task": "Task",
"taskName": "dynamic family portrait",
"taskStatus": "processing",
"noName": "No Name"
"noName": "No Name",
"uploading": "Uploading"
},
"library": {
"title": "My Memory",

View File

@ -15,7 +15,8 @@
"task": "任务",
"taskName": "动态全家福",
"taskStatus": "正在处理中",
"noName": "未命名作品"
"noName": "未命名作品",
"uploading": "上传中"
},
"library": {
"title": "My Memory",

View File

@ -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);
// 注册后台任务

View File

@ -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++;
}
}
@ -67,10 +67,10 @@ export const triggerManualUpload = async (
// 过滤掉因为已上传而返回 null 的结果
const finalResults = results.filter(result => result !== null);
console.log('Manual upload completed.', {
total: media.length,
uploaded: finalResults.length,
skipped: media.length - finalResults.length
console.log('Manual upload completed.', {
total: media.length,
uploaded: finalResults.length,
skipped: media.length - finalResults.length
});
return finalResults;

View File

@ -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,

View File

@ -89,7 +89,28 @@ export async function getUploadTasks(): Promise<UploadTask[]> {
export async function cleanUpUploadTasks(): Promise<void> {
console.log('Cleaning up completed/failed upload tasks...');
db.runSync(
"DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';"
"DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';"
);
}
// 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 };
}
}

View File

@ -0,0 +1 @@
eas build --platform android --profile development --local