Compare commits
2 Commits
549a7d9e93
...
80eaad039e
| Author | SHA1 | Date | |
|---|---|---|---|
| 80eaad039e | |||
| 20c1b2b767 |
@ -5,21 +5,19 @@ import StoriesSvg from '@/assets/icons/svg/stories.svg';
|
|||||||
import UsedStorageSvg from '@/assets/icons/svg/usedStorage.svg';
|
import UsedStorageSvg from '@/assets/icons/svg/usedStorage.svg';
|
||||||
import AskNavbar from '@/components/layout/ask';
|
import AskNavbar from '@/components/layout/ask';
|
||||||
import AlbumComponent from '@/components/owner/album';
|
import AlbumComponent from '@/components/owner/album';
|
||||||
import CategoryComponent from '@/components/owner/category';
|
import CarouselComponent from '@/components/owner/carousel';
|
||||||
import CountComponent from '@/components/owner/count';
|
|
||||||
import CreateCountComponent from '@/components/owner/createCount';
|
import CreateCountComponent from '@/components/owner/createCount';
|
||||||
import Ranking from '@/components/owner/ranking';
|
import Ranking from '@/components/owner/ranking';
|
||||||
import ResourceComponent from '@/components/owner/resource';
|
import ResourceComponent from '@/components/owner/resource';
|
||||||
import SettingModal from '@/components/owner/setting';
|
import SettingModal from '@/components/owner/setting';
|
||||||
import UserInfo from '@/components/owner/userName';
|
import UserInfo from '@/components/owner/userName';
|
||||||
import { formatDuration } from '@/components/utils/time';
|
|
||||||
import { checkAuthStatus } from '@/lib/auth';
|
import { checkAuthStatus } from '@/lib/auth';
|
||||||
import { fetchApi } from '@/lib/server-api-util';
|
import { fetchApi } from '@/lib/server-api-util';
|
||||||
import { CountData, UserInfoDetails } from '@/types/user';
|
import { CountData, UserInfoDetails } from '@/types/user';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FlatList, ScrollView, StyleSheet, View } from 'react-native';
|
import { FlatList, StyleSheet, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function OwnerPage() {
|
export default function OwnerPage() {
|
||||||
@ -97,27 +95,8 @@ export default function OwnerPage() {
|
|||||||
<MoreArrowSvg />
|
<MoreArrowSvg />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{/* 数据统计 */}
|
|
||||||
<CountComponent
|
|
||||||
data={[{ title: t("generalSetting.totalVideo", { ns: "personal" }), number: countData?.counter?.total_count?.video_count || 0 }, { title: t("generalSetting.totalPhoto", { ns: "personal" }), number: countData?.counter?.total_count?.photo_count || 0 }, { title: t("generalSetting.live", { ns: "personal" }), number: countData?.counter?.total_count?.live_count || 0 }, { title: t("generalSetting.videoLength", { ns: "personal" }), number: formatDuration(countData?.counter?.total_count?.video_length || 0) }]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 分类 */}
|
{/* 分类 */}
|
||||||
<View style={{ height: 145 }}>
|
<CarouselComponent data={userInfoDetails?.material_counter} />
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 16 }} >
|
|
||||||
{countData?.counter?.category_count && Object.entries(countData?.counter?.category_count).map(([key, value], index) => {
|
|
||||||
return (
|
|
||||||
<CategoryComponent
|
|
||||||
key={index}
|
|
||||||
title={key}
|
|
||||||
data={[{ title: 'Video', number: value.video_count }, { title: 'Photo', number: value.photo_count }, { title: 'Length', number: formatDuration(value.video_length || 0) }]}
|
|
||||||
bgSvg={value.cover_url}
|
|
||||||
style={{ aspectRatio: 1, flex: 1 }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 作品数据 */}
|
{/* 作品数据 */}
|
||||||
<View className='flex flex-row justify-between gap-[1rem]'>
|
<View className='flex flex-row justify-between gap-[1rem]'>
|
||||||
|
|||||||
127
components/owner/carousel.tsx
Normal file
127
components/owner/carousel.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { Counter, UserCountData } from "@/types/user";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Dimensions, StyleSheet, View } from "react-native";
|
||||||
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
|
import Carousel, {
|
||||||
|
ICarouselInstance
|
||||||
|
} from "react-native-reanimated-carousel";
|
||||||
|
import { ThemedText } from "../ThemedText";
|
||||||
|
import { formatDuration } from "../utils/time";
|
||||||
|
import CategoryComponent from "./category";
|
||||||
|
interface Props {
|
||||||
|
data: Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CarouselData {
|
||||||
|
key: string,
|
||||||
|
value: UserCountData
|
||||||
|
|
||||||
|
}[]
|
||||||
|
const width = Dimensions.get("window").width;
|
||||||
|
|
||||||
|
function CarouselComponent(props: Props) {
|
||||||
|
const { data } = props;
|
||||||
|
|
||||||
|
const ref = React.useRef<ICarouselInstance>(null);
|
||||||
|
const progress = useSharedValue<number>(0);
|
||||||
|
const [carouselDataValue, setCarouselDataValue] = React.useState<CarouselData[]>([]);
|
||||||
|
const dataHandle = () => {
|
||||||
|
const carouselData = { ...data?.category_count, total_count: data?.total_count }
|
||||||
|
// 1. 转换为数组并过滤掉 'total'
|
||||||
|
const entries = Object?.entries(carouselData)
|
||||||
|
?.filter(([key]) => key !== 'total_count')
|
||||||
|
?.map(([key, value]) => ({ key, value }));
|
||||||
|
|
||||||
|
// 2. 找到 total 数据
|
||||||
|
const totalEntry = {
|
||||||
|
key: 'total_count',
|
||||||
|
value: carouselData?.total_count
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 插入到中间位置
|
||||||
|
const middleIndex = Math.floor((entries || [])?.length / 2);
|
||||||
|
entries?.splice(middleIndex, 0, totalEntry);
|
||||||
|
setCarouselDataValue(entries)
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totleItem = (data: UserCountData) => {
|
||||||
|
return <View style={[styles.container]}>
|
||||||
|
{Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => (
|
||||||
|
<View style={styles.item} key={index}>
|
||||||
|
<ThemedText style={styles.title}>{item[0]}</ThemedText>
|
||||||
|
<ThemedText style={styles.number}>{item[1]}</ThemedText>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
dataHandle()
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Carousel
|
||||||
|
ref={ref}
|
||||||
|
width={width * 0.8}
|
||||||
|
height={width * 0.8}
|
||||||
|
data={carouselDataValue || []}
|
||||||
|
mode="parallax"
|
||||||
|
onProgressChange={progress}
|
||||||
|
defaultIndex={carouselDataValue?.findIndex((item) => item?.key === 'total_count') - 1 || 0}
|
||||||
|
renderItem={({ item }) => {
|
||||||
|
if (item?.key === 'total_count') {
|
||||||
|
return totleItem(item.value)
|
||||||
|
}
|
||||||
|
return CategoryComponent(
|
||||||
|
{
|
||||||
|
title: item?.key,
|
||||||
|
data:
|
||||||
|
[
|
||||||
|
{ title: 'Video', number: item?.value?.video_count },
|
||||||
|
{ title: 'Photo', number: item?.value?.photo_count },
|
||||||
|
{ title: 'Length', number: formatDuration(item?.value?.video_length || 0) }
|
||||||
|
],
|
||||||
|
bgSvg: item?.value?.cover_url,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: "#FFB645",
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 20,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingVertical: 8
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: "#4C320C",
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 32,
|
||||||
|
textAlign: 'right',
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: 8
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CarouselComponent;
|
||||||
@ -14,8 +14,11 @@ const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => {
|
|||||||
<View style={styles.backgroundContainer}>
|
<View style={styles.backgroundContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/animals.png')}
|
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/animals.png')}
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{
|
||||||
resizeMode="cover"
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
resizeMode: "cover"
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={styles.overlay} />
|
<View style={styles.overlay} />
|
||||||
</View>
|
</View>
|
||||||
@ -37,11 +40,12 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 32,
|
borderRadius: 32,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
aspectRatio: 1,
|
||||||
},
|
},
|
||||||
backgroundContainer: {
|
backgroundContainer: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
|
|||||||
23
lib/database/database-factory.ts
Normal file
23
lib/database/database-factory.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { DatabaseInterface } from './types';
|
||||||
|
import { SQLiteDatabase } from './sqlite-database';
|
||||||
|
|
||||||
|
class DatabaseFactory {
|
||||||
|
private static instance: DatabaseInterface | null = null;
|
||||||
|
|
||||||
|
static getInstance(): DatabaseInterface {
|
||||||
|
if (!this.instance) {
|
||||||
|
// Metro 会根据平台自动选择正确的文件
|
||||||
|
// Web: sqlite-database.web.ts
|
||||||
|
// Native: sqlite-database.ts
|
||||||
|
this.instance = new SQLiteDatabase();
|
||||||
|
}
|
||||||
|
return this.instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于测试或重置实例
|
||||||
|
static resetInstance(): void {
|
||||||
|
this.instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const database = DatabaseFactory.getInstance();
|
||||||
60
lib/database/database-test.ts
Normal file
60
lib/database/database-test.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// 数据库架构测试文件
|
||||||
|
import { database } from './database-factory';
|
||||||
|
import { UploadTask } from './types';
|
||||||
|
|
||||||
|
// 测试数据库基本功能
|
||||||
|
export async function testDatabase() {
|
||||||
|
console.log('开始测试数据库功能...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 初始化数据库
|
||||||
|
await database.initUploadTable();
|
||||||
|
console.log('✓ 数据库初始化成功');
|
||||||
|
|
||||||
|
// 测试插入任务
|
||||||
|
const testTask: Omit<UploadTask, 'created_at'> = {
|
||||||
|
uri: 'test://example.jpg',
|
||||||
|
filename: 'example.jpg',
|
||||||
|
status: 'pending',
|
||||||
|
progress: 0,
|
||||||
|
file_id: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await database.insertUploadTask(testTask);
|
||||||
|
console.log('✓ 任务插入成功');
|
||||||
|
|
||||||
|
// 测试查询任务
|
||||||
|
const retrievedTask = await database.getUploadTaskStatus(testTask.uri);
|
||||||
|
if (retrievedTask) {
|
||||||
|
console.log('✓ 任务查询成功:', retrievedTask);
|
||||||
|
} else {
|
||||||
|
console.log('✗ 任务查询失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试更新任务状态
|
||||||
|
await database.updateUploadTaskStatus(testTask.uri, 'success', 'file123');
|
||||||
|
console.log('✓ 任务状态更新成功');
|
||||||
|
|
||||||
|
// 测试获取所有任务
|
||||||
|
const allTasks = await database.getUploadTasks();
|
||||||
|
console.log('✓ 获取所有任务成功,数量:', allTasks.length);
|
||||||
|
|
||||||
|
// 测试应用状态
|
||||||
|
await database.setAppState('test_key', 'test_value');
|
||||||
|
const stateValue = await database.getAppState('test_key');
|
||||||
|
console.log('✓ 应用状态测试成功:', stateValue);
|
||||||
|
|
||||||
|
// 清理测试数据
|
||||||
|
await database.cleanUpUploadTasks();
|
||||||
|
console.log('✓ 数据清理成功');
|
||||||
|
|
||||||
|
console.log('🎉 所有数据库测试通过!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 数据库测试失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出测试函数供调用
|
||||||
|
export default testDatabase;
|
||||||
9
lib/database/empty-sqlite.js
Normal file
9
lib/database/empty-sqlite.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// 空的 SQLite 模块,用于 Web 环境
|
||||||
|
console.warn('SQLite is not available in web environment');
|
||||||
|
|
||||||
|
// 导出空的对象,避免导入错误
|
||||||
|
module.exports = {
|
||||||
|
openDatabaseSync: () => {
|
||||||
|
throw new Error('SQLite is not available in web environment');
|
||||||
|
}
|
||||||
|
};
|
||||||
6
lib/database/index.ts
Normal file
6
lib/database/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// 数据库模块统一导出
|
||||||
|
export { DatabaseInterface, UploadTask } from './types';
|
||||||
|
export { WebDatabase } from './web-database';
|
||||||
|
export { SQLiteDatabase } from './sqlite-database';
|
||||||
|
export { database } from './database-factory';
|
||||||
|
export { testDatabase } from './database-test';
|
||||||
156
lib/database/sqlite-database.ts
Normal file
156
lib/database/sqlite-database.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { DatabaseInterface, UploadTask } from './types';
|
||||||
|
|
||||||
|
export class SQLiteDatabase implements DatabaseInterface {
|
||||||
|
private db: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// 动态导入,避免在Web环境下加载
|
||||||
|
try {
|
||||||
|
const SQLite = require('expo-sqlite');
|
||||||
|
this.db = SQLite.openDatabaseSync('upload_status.db');
|
||||||
|
this.db.execSync('PRAGMA busy_timeout = 5000;');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize SQLite:', error);
|
||||||
|
throw new Error('SQLite is not available in this environment');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initUploadTable(): Promise<void> {
|
||||||
|
console.log('Initializing upload tasks table...');
|
||||||
|
await this.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,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Add created_at column to existing table if it doesn't exist
|
||||||
|
const columns = await this.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...');
|
||||||
|
await this.db.execAsync(`ALTER TABLE upload_tasks ADD COLUMN created_at INTEGER;`);
|
||||||
|
await this.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 this.db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS app_state (
|
||||||
|
key TEXT PRIMARY KEY NOT NULL,
|
||||||
|
value TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('App state table initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void> {
|
||||||
|
console.log('Inserting upload task:', task.uri);
|
||||||
|
await this.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)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
|
||||||
|
console.log('Checking upload task status for:', uri);
|
||||||
|
const result = await this.db.getFirstAsync(
|
||||||
|
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks WHERE uri = ?;',
|
||||||
|
uri
|
||||||
|
);
|
||||||
|
return result || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void> {
|
||||||
|
if (file_id) {
|
||||||
|
await this.db.runAsync('UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?', [status, file_id, uri]);
|
||||||
|
} else {
|
||||||
|
await this.db.runAsync('UPDATE upload_tasks SET status = ? WHERE uri = ?', [status, uri]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUploadTaskProgress(uri: string, progress: number): Promise<void> {
|
||||||
|
await this.db.runAsync('UPDATE upload_tasks SET progress = ? WHERE uri = ?', [progress, uri]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTasks(): Promise<UploadTask[]> {
|
||||||
|
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
|
||||||
|
const results = await this.db.getAllAsync(
|
||||||
|
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks ORDER BY created_at DESC;'
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanUpUploadTasks(): Promise<void> {
|
||||||
|
console.log('Cleaning up completed/failed upload tasks...');
|
||||||
|
await this.db.runAsync(
|
||||||
|
"DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
|
||||||
|
const rows = await this.db.getAllAsync(
|
||||||
|
'SELECT * FROM upload_tasks WHERE created_at >= ? ORDER BY created_at DESC',
|
||||||
|
[timestamp]
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exist_pending_tasks(): Promise<boolean> {
|
||||||
|
const rows = await this.db.getAllAsync(
|
||||||
|
'SELECT * FROM upload_tasks WHERE status = "pending" OR status = "uploading"'
|
||||||
|
);
|
||||||
|
return rows.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterExistingFiles(fileUris: string[]): Promise<string[]> {
|
||||||
|
if (fileUris.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholders = fileUris.map(() => '?').join(',');
|
||||||
|
const query = `SELECT uri FROM upload_tasks WHERE uri IN (${placeholders}) AND status = 'success'`;
|
||||||
|
|
||||||
|
const existingFiles = await this.db.getAllAsync(query, fileUris);
|
||||||
|
const existingUris = new Set(existingFiles.map((f: { uri: string }) => 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAppState(key: string, value: string | null): Promise<void> {
|
||||||
|
console.log(`Setting app state: ${key} = ${value}`);
|
||||||
|
await this.db.runAsync('INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)', [key, value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAppState(key: string): Promise<string | null> {
|
||||||
|
const result = await this.db.getFirstAsync('SELECT value FROM app_state WHERE key = ?;', key);
|
||||||
|
return result?.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||||
|
try {
|
||||||
|
const isSelect = sql.trim().toLowerCase().startsWith('select');
|
||||||
|
if (isSelect) {
|
||||||
|
const results = this.db.getAllSync(sql, params);
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
const result = this.db.runSync(sql, params);
|
||||||
|
return {
|
||||||
|
changes: result.changes,
|
||||||
|
lastInsertRowId: result.lastInsertRowId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error executing SQL:", error);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
140
lib/database/sqlite-database.web.ts
Normal file
140
lib/database/sqlite-database.web.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { DatabaseInterface, UploadTask } from './types';
|
||||||
|
|
||||||
|
// Web 环境下的 SQLite 数据库实现(实际使用 localStorage)
|
||||||
|
export class SQLiteDatabase implements DatabaseInterface {
|
||||||
|
private getStorageKey(table: string): string {
|
||||||
|
return `memowake_${table}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUploadTasksFromStorage(): UploadTask[] {
|
||||||
|
const data = localStorage.getItem(this.getStorageKey('upload_tasks'));
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveUploadTasks(tasks: UploadTask[]): void {
|
||||||
|
localStorage.setItem(this.getStorageKey('upload_tasks'), JSON.stringify(tasks));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAppStateData(): Record<string, string> {
|
||||||
|
const data = localStorage.getItem(this.getStorageKey('app_state'));
|
||||||
|
return data ? JSON.parse(data) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveAppStateData(state: Record<string, string>): void {
|
||||||
|
localStorage.setItem(this.getStorageKey('app_state'), JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
async initUploadTable(): Promise<void> {
|
||||||
|
console.log('Initializing web storage tables (SQLite fallback)...');
|
||||||
|
// Web端不需要初始化表结构,localStorage会自动处理
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void> {
|
||||||
|
console.log('Inserting upload task:', task.uri);
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const existingIndex = tasks.findIndex(t => t.uri === task.uri);
|
||||||
|
const newTask: UploadTask = {
|
||||||
|
...task,
|
||||||
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
tasks[existingIndex] = newTask;
|
||||||
|
} else {
|
||||||
|
tasks.push(newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveUploadTasks(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
|
||||||
|
console.log('Checking upload task status for:', uri);
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
return tasks.find(t => t.uri === uri) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||||
|
if (taskIndex >= 0) {
|
||||||
|
tasks[taskIndex].status = status;
|
||||||
|
if (file_id) {
|
||||||
|
tasks[taskIndex].file_id = file_id;
|
||||||
|
}
|
||||||
|
this.saveUploadTasks(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUploadTaskProgress(uri: string, progress: number): Promise<void> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||||
|
if (taskIndex >= 0) {
|
||||||
|
tasks[taskIndex].progress = progress;
|
||||||
|
this.saveUploadTasks(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTasks(): Promise<UploadTask[]> {
|
||||||
|
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
return tasks.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanUpUploadTasks(): Promise<void> {
|
||||||
|
console.log('Cleaning up completed/failed upload tasks...');
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const filteredTasks = tasks.filter(t =>
|
||||||
|
t.status !== 'success' && t.status !== 'failed' && t.status !== 'skipped'
|
||||||
|
);
|
||||||
|
this.saveUploadTasks(filteredTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const filteredTasks = tasks.filter(t => t.created_at >= timestamp);
|
||||||
|
return filteredTasks.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exist_pending_tasks(): Promise<boolean> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
return tasks.some(t => t.status === 'pending' || t.status === 'uploading');
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterExistingFiles(fileUris: string[]): Promise<string[]> {
|
||||||
|
if (fileUris.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const successfulUris = new Set(
|
||||||
|
tasks.filter(t => t.status === 'success').map(t => t.uri)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFileUris = fileUris.filter(uri => !successfulUris.has(uri));
|
||||||
|
|
||||||
|
console.log(`[WebDB] Total files: ${fileUris.length}, Existing successful files: ${successfulUris.size}, New files to upload: ${newFileUris.length}`);
|
||||||
|
|
||||||
|
return newFileUris;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAppState(key: string, value: string | null): Promise<void> {
|
||||||
|
console.log(`Setting app state: ${key} = ${value}`);
|
||||||
|
const state = this.getAppStateData();
|
||||||
|
if (value === null) {
|
||||||
|
delete state[key];
|
||||||
|
} else {
|
||||||
|
state[key] = value;
|
||||||
|
}
|
||||||
|
this.saveAppStateData(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAppState(key: string): Promise<string | null> {
|
||||||
|
const state = this.getAppStateData();
|
||||||
|
return state[key] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||||
|
console.warn('SQL execution not supported in web environment:', sql);
|
||||||
|
return { error: 'SQL execution not supported in web environment' };
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/database/types.ts
Normal file
24
lib/database/types.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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 interface DatabaseInterface {
|
||||||
|
initUploadTable(): Promise<void>;
|
||||||
|
insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void>;
|
||||||
|
getUploadTaskStatus(uri: string): Promise<UploadTask | null>;
|
||||||
|
updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void>;
|
||||||
|
updateUploadTaskProgress(uri: string, progress: number): Promise<void>;
|
||||||
|
getUploadTasks(): Promise<UploadTask[]>;
|
||||||
|
cleanUpUploadTasks(): Promise<void>;
|
||||||
|
getUploadTasksSince(timestamp: number): Promise<UploadTask[]>;
|
||||||
|
exist_pending_tasks(): Promise<boolean>;
|
||||||
|
filterExistingFiles(fileUris: string[]): Promise<string[]>;
|
||||||
|
setAppState(key: string, value: string | null): Promise<void>;
|
||||||
|
getAppState(key: string): Promise<string | null>;
|
||||||
|
executeSql(sql: string, params?: any[]): Promise<any>;
|
||||||
|
}
|
||||||
139
lib/database/web-database.ts
Normal file
139
lib/database/web-database.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { DatabaseInterface, UploadTask } from './types';
|
||||||
|
|
||||||
|
export class WebDatabase implements DatabaseInterface {
|
||||||
|
private getStorageKey(table: string): string {
|
||||||
|
return `memowake_${table}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUploadTasksFromStorage(): UploadTask[] {
|
||||||
|
const data = localStorage.getItem(this.getStorageKey('upload_tasks'));
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveUploadTasks(tasks: UploadTask[]): void {
|
||||||
|
localStorage.setItem(this.getStorageKey('upload_tasks'), JSON.stringify(tasks));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAppStateData(): Record<string, string> {
|
||||||
|
const data = localStorage.getItem(this.getStorageKey('app_state'));
|
||||||
|
return data ? JSON.parse(data) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveAppStateData(state: Record<string, string>): void {
|
||||||
|
localStorage.setItem(this.getStorageKey('app_state'), JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
async initUploadTable(): Promise<void> {
|
||||||
|
console.log('Initializing web storage tables...');
|
||||||
|
// Web端不需要初始化表结构,localStorage会自动处理
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void> {
|
||||||
|
console.log('Inserting upload task:', task.uri);
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const existingIndex = tasks.findIndex(t => t.uri === task.uri);
|
||||||
|
const newTask: UploadTask = {
|
||||||
|
...task,
|
||||||
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
tasks[existingIndex] = newTask;
|
||||||
|
} else {
|
||||||
|
tasks.push(newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveUploadTasks(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
|
||||||
|
console.log('Checking upload task status for:', uri);
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
return tasks.find(t => t.uri === uri) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||||
|
if (taskIndex >= 0) {
|
||||||
|
tasks[taskIndex].status = status;
|
||||||
|
if (file_id) {
|
||||||
|
tasks[taskIndex].file_id = file_id;
|
||||||
|
}
|
||||||
|
this.saveUploadTasks(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUploadTaskProgress(uri: string, progress: number): Promise<void> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||||
|
if (taskIndex >= 0) {
|
||||||
|
tasks[taskIndex].progress = progress;
|
||||||
|
this.saveUploadTasks(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTasks(): Promise<UploadTask[]> {
|
||||||
|
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
return tasks.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanUpUploadTasks(): Promise<void> {
|
||||||
|
console.log('Cleaning up completed/failed upload tasks...');
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const filteredTasks = tasks.filter(t =>
|
||||||
|
t.status !== 'success' && t.status !== 'failed' && t.status !== 'skipped'
|
||||||
|
);
|
||||||
|
this.saveUploadTasks(filteredTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const filteredTasks = tasks.filter(t => t.created_at >= timestamp);
|
||||||
|
return filteredTasks.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exist_pending_tasks(): Promise<boolean> {
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
return tasks.some(t => t.status === 'pending' || t.status === 'uploading');
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterExistingFiles(fileUris: string[]): Promise<string[]> {
|
||||||
|
if (fileUris.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = this.getUploadTasksFromStorage();
|
||||||
|
const successfulUris = new Set(
|
||||||
|
tasks.filter(t => t.status === 'success').map(t => t.uri)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFileUris = fileUris.filter(uri => !successfulUris.has(uri));
|
||||||
|
|
||||||
|
console.log(`[WebDB] Total files: ${fileUris.length}, Existing successful files: ${successfulUris.size}, New files to upload: ${newFileUris.length}`);
|
||||||
|
|
||||||
|
return newFileUris;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAppState(key: string, value: string | null): Promise<void> {
|
||||||
|
console.log(`Setting app state: ${key} = ${value}`);
|
||||||
|
const state = this.getAppStateData();
|
||||||
|
if (value === null) {
|
||||||
|
delete state[key];
|
||||||
|
} else {
|
||||||
|
state[key] = value;
|
||||||
|
}
|
||||||
|
this.saveAppStateData(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAppState(key: string): Promise<string | null> {
|
||||||
|
const state = this.getAppStateData();
|
||||||
|
return state[key] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||||
|
console.warn('SQL execution not supported in web environment:', sql);
|
||||||
|
return { error: 'SQL execution not supported in web environment' };
|
||||||
|
}
|
||||||
|
}
|
||||||
195
lib/db.ts
195
lib/db.ts
@ -1,176 +1,23 @@
|
|||||||
import * as SQLite from 'expo-sqlite';
|
// 使用数据库接口架构,支持 Web 和移动端
|
||||||
|
import { database } from './database/database-factory';
|
||||||
|
import { UploadTask } from './database/types';
|
||||||
|
|
||||||
const db = SQLite.openDatabaseSync('upload_status.db');
|
// 重新导出类型
|
||||||
|
export type { UploadTask };
|
||||||
// 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 async function initUploadTable() {
|
|
||||||
console.log('Initializing upload tasks table...');
|
|
||||||
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,
|
|
||||||
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(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 = 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) {
|
|
||||||
if (file_id) {
|
|
||||||
await db.runAsync('UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?', [status, file_id, uri]);
|
|
||||||
} else {
|
|
||||||
await db.runAsync('UPDATE upload_tasks SET status = ? WHERE uri = ?', [status, 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 = await db.getAllAsync<UploadTask>(
|
|
||||||
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks ORDER BY created_at DESC;'
|
|
||||||
);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理已完成或失败的任务 (可选,根据需求添加)
|
|
||||||
export async function cleanUpUploadTasks(): Promise<void> {
|
|
||||||
console.log('Cleaning up completed/failed upload tasks...');
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exist_pending_tasks(): Promise<boolean> {
|
|
||||||
const rows = await db.getAllAsync<UploadTask>(
|
|
||||||
'SELECT * FROM upload_tasks WHERE status = "pending" OR status = "uploading"'
|
|
||||||
);
|
|
||||||
return rows.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查一组文件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 {
|
|
||||||
// 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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 重新导出所有数据库函数,使用统一接口
|
||||||
|
export const initUploadTable = () => database.initUploadTable();
|
||||||
|
export const insertUploadTask = (task: Omit<UploadTask, 'created_at'>) => database.insertUploadTask(task);
|
||||||
|
export const getUploadTaskStatus = (uri: string) => database.getUploadTaskStatus(uri);
|
||||||
|
export const updateUploadTaskStatus = (uri: string, status: UploadTask['status'], file_id?: string) =>
|
||||||
|
database.updateUploadTaskStatus(uri, status, file_id);
|
||||||
|
export const updateUploadTaskProgress = (uri: string, progress: number) =>
|
||||||
|
database.updateUploadTaskProgress(uri, progress);
|
||||||
|
export const getUploadTasks = () => database.getUploadTasks();
|
||||||
|
export const cleanUpUploadTasks = () => database.cleanUpUploadTasks();
|
||||||
|
export const getUploadTasksSince = (timestamp: number) => database.getUploadTasksSince(timestamp);
|
||||||
|
export const exist_pending_tasks = () => database.exist_pending_tasks();
|
||||||
|
export const filterExistingFiles = (fileUris: string[]) => database.filterExistingFiles(fileUris);
|
||||||
|
export const setAppState = (key: string, value: string | null) => database.setAppState(key, value);
|
||||||
|
export const getAppState = (key: string) => database.getAppState(key);
|
||||||
|
export const executeSql = (sql: string, params: any[] = []) => database.executeSql(sql, params);
|
||||||
|
|||||||
@ -19,6 +19,15 @@ config.resolver = {
|
|||||||
...config.resolver?.alias,
|
...config.resolver?.alias,
|
||||||
'@/': path.resolve(__dirname, './'),
|
'@/': path.resolve(__dirname, './'),
|
||||||
},
|
},
|
||||||
|
platforms: ['ios', 'android', 'native', 'web'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Web 环境下的模块别名
|
||||||
|
if (process.env.EXPO_PLATFORM === 'web') {
|
||||||
|
config.resolver.alias = {
|
||||||
|
...config.resolver.alias,
|
||||||
|
'expo-sqlite': path.resolve(__dirname, './lib/database/empty-sqlite.js'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = withNativeWind(config, { input: './global.css' });
|
module.exports = withNativeWind(config, { input: './global.css' });
|
||||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -60,6 +60,7 @@
|
|||||||
"react-native-picker-select": "^9.3.1",
|
"react-native-picker-select": "^9.3.1",
|
||||||
"react-native-progress": "^5.0.1",
|
"react-native-progress": "^5.0.1",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
|
"react-native-reanimated-carousel": "^4.0.2",
|
||||||
"react-native-render-html": "^6.3.4",
|
"react-native-render-html": "^6.3.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
@ -14826,6 +14827,18 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-reanimated-carousel": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-reanimated-carousel/-/react-native-reanimated-carousel-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"react-native": ">=0.70.3",
|
||||||
|
"react-native-gesture-handler": ">=2.9.0",
|
||||||
|
"react-native-reanimated": ">=3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": {
|
"node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz",
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"expo-audio": "~0.4.8",
|
"expo-audio": "~0.4.8",
|
||||||
"expo-background-task": "^0.2.8",
|
"expo-background-task": "^0.2.8",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
|
"expo-clipboard": "~7.1.5",
|
||||||
"expo-constants": "~17.1.6",
|
"expo-constants": "~17.1.6",
|
||||||
"expo-dev-client": "~5.2.4",
|
"expo-dev-client": "~5.2.4",
|
||||||
"expo-device": "~7.1.4",
|
"expo-device": "~7.1.4",
|
||||||
@ -33,6 +34,7 @@
|
|||||||
"expo-haptics": "~14.1.4",
|
"expo-haptics": "~14.1.4",
|
||||||
"expo-image-manipulator": "~13.1.7",
|
"expo-image-manipulator": "~13.1.7",
|
||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
|
"expo-linear-gradient": "~14.1.5",
|
||||||
"expo-linking": "~7.1.7",
|
"expo-linking": "~7.1.7",
|
||||||
"expo-localization": "^16.1.5",
|
"expo-localization": "^16.1.5",
|
||||||
"expo-location": "~18.1.5",
|
"expo-location": "~18.1.5",
|
||||||
@ -64,6 +66,7 @@
|
|||||||
"react-native-picker-select": "^9.3.1",
|
"react-native-picker-select": "^9.3.1",
|
||||||
"react-native-progress": "^5.0.1",
|
"react-native-progress": "^5.0.1",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
|
"react-native-reanimated-carousel": "^4.0.2",
|
||||||
"react-native-render-html": "^6.3.4",
|
"react-native-render-html": "^6.3.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
@ -72,9 +75,7 @@
|
|||||||
"react-native-uuid": "^2.0.3",
|
"react-native-uuid": "^2.0.3",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "13.13.5",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0"
|
||||||
"expo-clipboard": "~7.1.5",
|
|
||||||
"expo-linear-gradient": "~14.1.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export interface User {
|
|||||||
avatar_file_url?: string
|
avatar_file_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserCountData {
|
export interface UserCountData {
|
||||||
video_count: number,
|
video_count: number,
|
||||||
photo_count: number,
|
photo_count: number,
|
||||||
live_count: number,
|
live_count: number,
|
||||||
@ -31,7 +31,7 @@ export interface CountData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Counter {
|
export interface Counter {
|
||||||
user_id: number,
|
user_id: number,
|
||||||
total_count: UserCountData,
|
total_count: UserCountData,
|
||||||
category_count: {
|
category_count: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user