feat: 上传自动展示进度条

This commit is contained in:
Junhui Chen 2025-07-17 15:08:03 +08:00
parent c7df8a66d0
commit d1d7fbbe30
12 changed files with 394 additions and 133 deletions

View File

@ -1,4 +1,3 @@
import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic';
import * as MediaLibrary from 'expo-media-library';
import { useRouter } from 'expo-router';
import * as SecureStore from 'expo-secure-store';
@ -39,7 +38,7 @@ export default function HomeScreen() {
// 已登录,请求必要的权限
const { status } = await MediaLibrary.requestPermissionsAsync();
if (status === 'granted') {
await registerBackgroundUploadTask();
console.log('Media library permission granted in HomeScreen.');
}
router.replace('/ask');
}

View File

@ -1,14 +1,22 @@
import ChatSvg from "@/assets/icons/svg/chat.svg";
import AutoUploadScreen from "@/components/file-upload/autoUploadScreen";
import UploaderProgress from "@/components/file-upload/upload-progress/uploader-progress";
import AskNavbar from "@/components/layout/ask";
import { endUploadSessionInDb, syncUploadSessionState } from "@/features/appState/appStateSlice";
import { triggerManualUpload } from "@/lib/background-uploader/automatic";
import { getUploadTasksSince, UploadTask } from "@/lib/db";
import { fetchApi } from "@/lib/server-api-util";
import { useAppDispatch, useAppSelector } from "@/store";
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 dispatch = useAppDispatch();
const uploadSessionStartTime = useAppSelector((state) => state.appState.uploadSessionStartTime);
// 历史消息
const [historyList, setHistoryList] = React.useState<Chat[]>([]);
@ -39,13 +47,99 @@ const MemoList = () => {
getHistoryList()
}, [])
const [progressInfo, setProgressInfo] = useState({ total: 0, completed: 0, image: '' });
useFocusEffect(
useCallback(() => {
let isActive = true;
let interval: any = null;
const manageUploadState = async () => {
if (!isActive) return;
// 首先同步Redux中的会话开始时间
const action = await dispatch(syncUploadSessionState());
const sessionStartTime = action.payload as number | null;
if (sessionStartTime) {
// 如果会话存在,则获取任务进度
const allTasks = await getUploadTasksSince(sessionStartTime);
const total = allTasks.length;
const completed = allTasks.filter((t: UploadTask) => t.status === 'success' || t.status === 'failed' || t.status === 'skipped').length;
const pending = allTasks.filter((t: UploadTask) => t.status === 'pending' || t.status === 'uploading');
if (isActive) {
setProgressInfo({ total, completed, image: allTasks[0]?.uri || '' });
}
// 如果任务完成,则结束会话并清除定时器
if (total > 0 && pending.length === 0) {
console.log('MemoList detects all tasks are complete. Ending session.');
if (interval) clearInterval(interval);
dispatch(endUploadSessionInDb());
}
} else {
// 如果没有会话,确保本地状态被重置
if (isActive) {
setProgressInfo({ total: 0, completed: 0, image: '' });
}
}
};
const triggerAndMonitor = async () => {
console.log('MemoList focused, triggering foreground media upload check.');
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// This now returns the start time string or null
const sessionStartTimeStr = await triggerManualUpload(oneDayAgo, now);
// Only start monitoring if a session was actually started.
if (sessionStartTimeStr) {
console.log(`Upload session started with time: ${sessionStartTimeStr}, beginning to monitor...`);
// Immediately check the state once, then the interval will take over.
// This ensures the progress bar appears instantly.
manageUploadState();
interval = setInterval(manageUploadState, 2000);
}
};
triggerAndMonitor();
return () => {
isActive = false;
if (interval) {
clearInterval(interval);
}
};
}, [dispatch])
);
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
{/* 上传进度展示区域 */}
{process.env.NODE_ENV === 'development' && uploadSessionStartTime && (
<View className="w-full h-20">
<UploaderProgress
imageUrl={progressInfo.image}
uploadedCount={progressInfo.completed}
totalCount={progressInfo.total}
/>
</View>
)}
<View className="w-full h-full">
{/* <View className="w-full h-full">
<AutoUploadScreen />
</View>
</View> */}
<TouchableOpacity
className='mt-2 bg-red-500 items-center h-10 justify-center'
onPress={() => router.push('/debug')}
>
<Text className="text-white">
db调试页面
</Text>
</TouchableOpacity>
{/* 顶部标题和上传按钮 */}
<View style={styles.header}>

View File

@ -1,7 +1,10 @@
import { useColorScheme } from '@/hooks/useColorScheme';
import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import * as MediaLibrary from 'expo-media-library';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import 'react-native-reanimated';
import '../global.css';
import { Provider } from "../provider";
@ -9,6 +12,22 @@ import { Provider } from "../provider";
export default function RootLayout() {
const colorScheme = useColorScheme();
useEffect(() => {
const setupBackgroundUpload = async () => {
const { status } = await MediaLibrary.getPermissionsAsync();
if (status !== 'granted') {
console.log('Media library permission not granted. Background task registered but will wait for permission.');
}
const registered = await registerBackgroundUploadTask();
if (registered) {
console.log('Background upload task setup finished in RootLayout.');
} else {
console.error('Failed to register background upload task in RootLayout.');
}
};
setupBackgroundUpload();
}, []);
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Provider>

View File

@ -1,14 +1,12 @@
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 React, { useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import UploaderProgressBar from './uploader-progress-bar';
import UploaderProgressBar from './upload-progress/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,
@ -17,15 +15,6 @@ export default function AutoUploadScreen() {
totalSize: 0,
});
// 注册后台任务
useEffect(() => {
const registerTask = async () => {
const registered = await registerBackgroundUploadTask();
setIsRegistered(registered);
};
console.log("register background upload task");
registerTask();
}, []);
// 处理手动上传
const handleManualUpload = async () => {
@ -147,14 +136,7 @@ export default function AutoUploadScreen() {
</TouchableOpacity>
</View>
<View style={styles.statusContainer}>
<Text style={styles.statusText}>
: {isRegistered ? '已启用' : '未启用'}
</Text>
<Text style={styles.hintText}>
24
</Text>
</View>
{
// isLoading &&

View File

@ -0,0 +1,33 @@
import React from 'react';
import { View } from 'react-native';
import UploaderProgressBar from './progress-bar';
interface UploaderProgressProps {
imageUrl: string;
uploadedCount: number;
totalCount: number;
}
const UploaderProgress: React.FC<UploaderProgressProps> = ({ imageUrl, uploadedCount, totalCount }) => {
// This is now a 'dumb' component. It only displays the progress based on props.
// All logic for fetching and state management is handled by the parent component (MemoList).
if (totalCount === 0) {
// Don't render anything if there's no session or tasks.
// The parent component's logic (`uploadSessionStartTime && ...`) already handles this,
// but this is an extra safeguard.
return null;
}
return (
<View style={{ position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'transparent' }}>
<UploaderProgressBar
imageUrl={imageUrl}
uploadedCount={uploadedCount}
totalCount={totalCount}
/>
</View>
);
};
export default React.memo(UploaderProgress);

View File

@ -0,0 +1,54 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getAppState, setAppState } from '../../lib/db';
interface AppState {
uploadSessionStartTime: number | null;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
}
const initialState: AppState = {
uploadSessionStartTime: null,
status: 'idle',
};
// Thunk to fetch the session start time from the database
export const syncUploadSessionState = createAsyncThunk(
'appState/syncUploadSessionState',
async () => {
const startTimeStr = await getAppState('uploadSessionStartTime');
return startTimeStr ? parseInt(startTimeStr, 10) : null;
}
);
// Thunk to clear the session state in the database, which will then be reflected in the store
export const endUploadSessionInDb = createAsyncThunk(
'appState/endUploadSessionInDb',
async () => {
await setAppState('uploadSessionStartTime', null);
return null;
}
);
const appStateSlice = createSlice({
name: 'appState',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(syncUploadSessionState.pending, (state) => {
state.status = 'loading';
})
.addCase(syncUploadSessionState.fulfilled, (state, action: PayloadAction<number | null>) => {
state.status = 'succeeded';
state.uploadSessionStartTime = action.payload;
})
.addCase(syncUploadSessionState.rejected, (state) => {
state.status = 'failed';
})
.addCase(endUploadSessionInDb.fulfilled, (state, action: PayloadAction<null>) => {
state.uploadSessionStartTime = action.payload;
});
},
});
export default appStateSlice.reducer;

View File

@ -1,7 +1,7 @@
import * as BackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager';
import pLimit from 'p-limit';
import { getUploadTaskStatus, initUploadTable, insertUploadTask } from '../db';
import { filterExistingFiles, initUploadTable, insertUploadTask, setAppState, updateUploadTaskStatus } from '../db';
import { getMediaByDateRange } from './media';
import { processAndUploadMedia } from './uploader';
@ -11,7 +11,7 @@ const CONCURRENCY_LIMIT = 1; // 后台上传并发数例如同时上传3个
const limit = pLimit(CONCURRENCY_LIMIT);
// 注册后台任务
export const registerBackgroundUploadTask = async () => {
export async function registerBackgroundUploadTask() {
try {
// 初始化数据库表
await initUploadTable();
@ -32,68 +32,89 @@ export const registerBackgroundUploadTask = async () => {
}
};
// 触发手动或后台上传的通用函数
export async function triggerManualUpload(startDate: Date, endDate: Date): Promise<string | null> {
try {
console.log(`Triggering upload for range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
const allMedia = await getMediaByDateRange(startDate, endDate);
if (allMedia.length === 0) {
console.log('No media files found in the specified range.');
return null;
}
// 1. Batch filter out files that have already been successfully uploaded.
const allFileUris = allMedia.map(a => a.uri);
const newFileUris = await filterExistingFiles(allFileUris);
if (newFileUris.length === 0) {
console.log('No new files to upload. All are already synced.');
return null;
}
const filesToUpload = allMedia.filter(a => newFileUris.includes(a.uri));
// 2. Batch pre-register all new files in the database as 'pending'.
console.log(`Registering ${filesToUpload.length} new files as 'pending' in the database.`);
for (const file of filesToUpload) {
await insertUploadTask({ uri: file.uri, filename: file.filename, status: 'pending', progress: 0 });
}
// 3. Start the upload session.
const startTime = Math.floor(Date.now() / 1000).toString();
await setAppState('uploadSessionStartTime', startTime);
// 4. Create upload promises for the new files.
const uploadPromises = filesToUpload.map((file) =>
limit(async () => {
try {
// 5. Mark the task as 'uploading' right before the upload starts.
await updateUploadTaskStatus(file.uri, 'uploading');
const result = await processAndUploadMedia(file);
if (result === null) { // Skipped case
await updateUploadTaskStatus(file.uri, 'skipped');
return { status: 'skipped' };
}
if (result.originalSuccess) {
await updateUploadTaskStatus(file.uri, 'success', result.fileIds?.original);
return { status: 'success' };
} else {
await updateUploadTaskStatus(file.uri, 'failed');
return { status: 'failed' };
}
} catch (e) {
console.error('Upload failed for', file.uri, e);
await updateUploadTaskStatus(file.uri, 'failed');
return { status: 'failed' };
}
})
);
// We don't wait for all uploads to complete. The function returns after starting them.
// The UI will then poll for progress.
Promise.allSettled(uploadPromises).then((results) => {
console.log('All upload tasks have been settled.');
const successfulUploads = results.filter(
(result) => result.status === 'fulfilled' && result.value.status === 'success'
).length;
console.log(`${successfulUploads} files uploaded successfully.`);
});
return startTime;
} catch (error) {
console.error('Error during upload trigger:', error);
return null;
}
}
// 定义后台任务
TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
try {
console.log('Running background upload task...');
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// 获取最近24小时的媒体文件
const media = await getMediaByDateRange(oneDayAgo, now);
if (media.length === 0) {
console.log('No new media files to upload in the last 24 hours.');
return BackgroundTask.BackgroundTaskResult.Success;
}
console.log(`Found ${media.length} media files to potentially upload.`);
// 并发上传文件
let successCount = 0;
let skippedCount = 0;
let failedCount = 0;
const uploadPromises = media.map((file) =>
limit(async () => {
try {
const existingTask = await getUploadTaskStatus(file.uri);
if (!existingTask) {
await insertUploadTask(file.uri, file.filename);
} else if (existingTask.status === 'success' || existingTask.status === 'skipped') {
console.log(`File ${file.uri} already ${existingTask.status}, skipping processing.`);
return { status: 'skipped' }; // 返回状态以便统计
}
const result = await processAndUploadMedia(file);
if (result === null) {
return { status: 'skipped' };
} else if (result.originalSuccess) {
return { status: 'success' };
} else {
return { status: 'failed' };
}
} catch (e) {
console.error('Upload failed for', file.uri, e);
return { status: 'failed' };
}
})
);
const results = await Promise.all(uploadPromises);
results.forEach(result => {
if (result.status === 'success') {
successCount++;
} else if (result.status === 'skipped') {
skippedCount++;
} else if (result.status === 'failed') {
failedCount++;
}
});
console.log(`Background upload task finished. Successful: ${successCount}, Skipped: ${skippedCount}, Failed: ${failedCount}, Total: ${media.length}`);
await triggerManualUpload(oneDayAgo, now);
return BackgroundTask.BackgroundTaskResult.Success;
} catch (error) {
console.error('Background task error:', error);

View File

@ -36,7 +36,7 @@ export const triggerManualUpload = async (
for (const asset of media) {
const existingTask = await getUploadTaskStatus(asset.uri);
if (!existingTask) {
await insertUploadTask(asset.uri, asset.filename);
await insertUploadTask({ uri: asset.uri, filename: asset.filename, status: 'pending', progress: 0 });
} 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'

View File

@ -103,10 +103,10 @@ export const processAndUploadMedia = async (
GPSVersionID: undefined
});
await uploadFileWithProgress(fileToUpload, upload_url, (progress) => {
await uploadFileWithProgress(fileToUpload, upload_url, async (progress) => {
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 updateUploadTaskProgress(asset.uri, Math.round(percentage * 0.5)); // 原始文件占总进度的50%
});
await confirmUpload(file_id);
@ -131,11 +131,11 @@ export const processAndUploadMedia = async (
isCompressed: true
});
await uploadFileWithProgress(compressedFile, upload_url, (progress) => {
await uploadFileWithProgress(compressedFile, upload_url, async (progress) => {
// 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 updateUploadTaskProgress(asset.uri, 50 + Math.round(percentage * 0.5)); // 压缩文件占总进度的后50%
});
await confirmUpload(file_id);
return { success: true, file_id };
@ -156,12 +156,12 @@ export const processAndUploadMedia = async (
if (!isVideo) {
compressedResult = await uploadCompressedFile();
if (originalResult.file_id && compressedResult.file_id) {
addMaterial(originalResult.file_id, compressedResult.file_id);
await addMaterial(originalResult.file_id, compressedResult.file_id);
}
} else {
const thumbnailResult = await uploadVideoThumbnail(asset);
if (thumbnailResult.success && originalResult.file_id && thumbnailResult.file_id) {
addMaterial(originalResult.file_id, thumbnailResult.file_id);
await addMaterial(originalResult.file_id, thumbnailResult.file_id);
}
}

127
lib/db.ts
View File

@ -2,85 +2,94 @@ import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabaseSync('upload_status.db');
// Set a busy timeout to handle concurrent writes and avoid "database is locked" errors.
// This will make SQLite wait for 5 seconds if the database is locked by another process.
db.execSync('PRAGMA busy_timeout = 5000;');
export type UploadTask = {
uri: string;
filename: string;
status: 'pending' | 'uploading' | 'success' | 'failed' | 'skipped';
progress: number; // 0-100
file_id?: string; // 后端返回的文件ID
created_at: number; // unix timestamp
};
// 初始化表
export function initUploadTable() {
export async function initUploadTable() {
console.log('Initializing upload tasks table...');
db.execSync(`
await db.execAsync(`
CREATE TABLE IF NOT EXISTS upload_tasks (
uri TEXT PRIMARY KEY NOT NULL,
filename TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
progress INTEGER NOT NULL DEFAULT 0,
file_id TEXT
file_id TEXT,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`);
// Add created_at column to existing table if it doesn't exist
const columns = await db.getAllAsync('PRAGMA table_info(upload_tasks);');
const columnExists = columns.some((column: any) => column.name === 'created_at');
if (!columnExists) {
console.log('Adding created_at column to upload_tasks table...');
// SQLite doesn't support non-constant DEFAULT values on ALTER TABLE.
// So we add the column, then update existing rows.
await db.execAsync(`ALTER TABLE upload_tasks ADD COLUMN created_at INTEGER;`);
await db.execAsync(`UPDATE upload_tasks SET created_at = (strftime('%s', 'now')) WHERE created_at IS NULL;`);
console.log('created_at column added and populated.');
}
console.log('Upload tasks table initialized');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS app_state (
key TEXT PRIMARY KEY NOT NULL,
value TEXT
);
`);
console.log('App state table initialized');
}
// 插入新的上传任务
export async function insertUploadTask(uri: string, filename: string): Promise<void> {
console.log('Inserting upload task:', uri, filename);
db.runSync(
'INSERT OR IGNORE INTO upload_tasks (uri, filename, status, progress) VALUES (?, ?, ?, ?);',
uri,
filename,
'pending',
0
export async function insertUploadTask(task: Omit<UploadTask, 'created_at'>) {
console.log('Inserting upload task:', task.uri);
await db.runAsync(
'INSERT OR REPLACE INTO upload_tasks (uri, filename, status, progress, file_id, created_at) VALUES (?, ?, ?, ?, ?, ?)',
[task.uri, task.filename, task.status, task.progress, task.file_id ?? null, Math.floor(Date.now() / 1000)]
);
}
// 检查文件是否已上传或正在上传
export async function getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
console.log('Checking upload task status for:', uri);
const result = db.getFirstSync<UploadTask>(
'SELECT uri, filename, status, progress, file_id FROM upload_tasks WHERE uri = ?;',
const result = await db.getFirstAsync<UploadTask>(
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks WHERE uri = ?;',
uri
);
return result || null;
}
// 更新上传任务的状态
export async function updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void> {
console.log('Updating upload task status:', uri, status, file_id);
export async function updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string) {
if (file_id) {
db.runSync(
'UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?;',
status,
file_id,
uri
);
await db.runAsync('UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?', [status, file_id, uri]);
} else {
db.runSync(
'UPDATE upload_tasks SET status = ? WHERE uri = ?;',
status,
uri
);
await db.runAsync('UPDATE upload_tasks SET status = ? WHERE uri = ?', [status, uri]);
}
}
// 更新上传任务的进度
export async function updateUploadTaskProgress(uri: string, progress: number): Promise<void> {
console.log('Updating upload task progress:', uri, progress);
db.runSync(
'UPDATE upload_tasks SET progress = ? WHERE uri = ?;',
progress,
uri
);
export async function updateUploadTaskProgress(uri: string, progress: number) {
await db.runAsync('UPDATE upload_tasks SET progress = ? WHERE uri = ?', [progress, uri]);
}
// 获取所有上传任务
export async function getUploadTasks(): Promise<UploadTask[]> {
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;'
const results = await db.getAllAsync<UploadTask>(
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks ORDER BY created_at DESC;'
);
return results;
}
@ -88,11 +97,55 @@ 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';"
await db.runAsync(
"DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';"
);
}
// 获取某个时间点之后的所有上传任务
export async function getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
const rows = await db.getAllAsync<UploadTask>(
'SELECT * FROM upload_tasks WHERE created_at >= ? ORDER BY created_at DESC',
[timestamp]
);
return rows;
}
// 检查一组文件URI返回那些在数据库中不存在或是未成功上传的文件的URI
export async function filterExistingFiles(fileUris: string[]): Promise<string[]> {
if (fileUris.length === 0) {
return [];
}
// 创建占位符字符串 '?,?,?'
const placeholders = fileUris.map(() => '?').join(',');
// 查询已经存在且状态为 'success' 的任务
const query = `SELECT uri FROM upload_tasks WHERE uri IN (${placeholders}) AND status = 'success'`;
const existingFiles = await db.getAllAsync<{ uri: string }>(query, fileUris);
const existingUris = new Set(existingFiles.map(f => f.uri));
// 过滤出新文件
const newFileUris = fileUris.filter(uri => !existingUris.has(uri));
console.log(`[DB] Total files: ${fileUris.length}, Existing successful files: ${existingUris.size}, New files to upload: ${newFileUris.length}`);
return newFileUris;
}
// 设置全局状态值
export async function setAppState(key: string, value: string | null): Promise<void> {
console.log(`Setting app state: ${key} = ${value}`);
await db.runAsync('INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)', [key, value]);
}
// 获取全局状态值
export async function getAppState(key: string): Promise<string | null> {
const result = await db.getFirstAsync<{ value: string }>('SELECT value FROM app_state WHERE key = ?;', key);
return result?.value ?? null;
}
// for debug page
export async function executeSql(sql: string, params: any[] = []): Promise<any> {
try {

View File

@ -1,15 +1,21 @@
import { configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { counterSlice } from './components/steps';
import authReducer from './features/auth/authSlice';
import appStateReducer from './features/appState/appStateSlice';
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
auth: authReducer
auth: authReducer,
appState: appStateReducer
},
});
// 从 store 本身推断 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>;
// 推断类型:{posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
export type AppDispatch = typeof store.dispatch;
// 在整个应用中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;