Compare commits
2 Commits
549a7d9e93
...
2f8a3d2948
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f8a3d2948 | |||
| 4864e8b9c7 |
7
app.json
7
app.json
@ -4,7 +4,7 @@
|
||||
"slug": "memowake",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "",
|
||||
"icon": "./assets/icons/png/app.png",
|
||||
"scheme": "memowake",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
@ -48,7 +48,7 @@
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": ""
|
||||
"favicon": "./assets/icons/png/app.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
@ -96,8 +96,7 @@
|
||||
"router": {},
|
||||
"eas": {
|
||||
"projectId": "04721dd4-6b15-495a-b9ec-98187c613172"
|
||||
},
|
||||
"API_ENDPOINT": "http://192.168.31.115:18080/api"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
assets/icons/png/app.png
Normal file
BIN
assets/icons/png/app.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
61
assets/icons/svg/app.svg
Normal file
61
assets/icons/svg/app.svg
Normal file
@ -0,0 +1,61 @@
|
||||
<svg width="578" height="577" viewBox="0 0 578 577" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_215_188)">
|
||||
<g clip-path="url(#clip0_215_188)">
|
||||
<rect x="3" width="572.333" height="572.333" rx="111.784" fill="white"/>
|
||||
<rect x="3" width="572.333" height="572.333" fill="#AC7E35"/>
|
||||
<path d="M34.4206 192.01C32.8885 178.266 65.8095 177.448 82.4616 178.758L56.5555 209.322C48.4291 212.492 35.9527 205.754 34.4206 192.01Z" fill="#FFDBA3"/>
|
||||
<path d="M41.5631 191.094C39.1999 179.937 62.1246 182.378 73.8823 184.994L60.656 199.713C55.2763 201.489 43.9263 202.252 41.5631 191.094Z" fill="#AC7E35"/>
|
||||
<path d="M198.913 27.5173C185.168 25.9852 184.351 58.9062 185.661 75.5583L216.225 49.6522C219.395 41.5258 212.657 29.0493 198.913 27.5173Z" fill="#FFDBA3"/>
|
||||
<path d="M197.997 34.6598C186.84 32.2966 189.281 55.2212 191.897 66.979L206.616 53.7527C208.392 48.373 209.154 37.0229 197.997 34.6598Z" fill="#AC7E35"/>
|
||||
<path d="M30.7421 448.573C-38.1574 191.436 197.139 -43.8603 454.275 25.0392L629.664 72.0346C886.801 140.934 972.926 462.355 784.689 650.592L656.295 778.986C468.058 967.223 146.637 881.099 77.7375 623.962L30.7421 448.573Z" fill="#FFD18A"/>
|
||||
<rect x="217.479" y="240.655" width="13.6147" height="19.0606" rx="6.80735" transform="rotate(135 217.479 240.655)" fill="#4C320C"/>
|
||||
<rect x="252.138" y="205.996" width="13.6147" height="19.0606" rx="6.80735" transform="rotate(135 252.138 205.996)" fill="#4C320C"/>
|
||||
<path d="M192.499 462.813C162.296 299.481 305.191 156.586 468.523 186.789L654.544 221.189C842.135 255.878 913.874 486.75 778.979 621.646L627.356 773.269C492.46 908.164 261.588 836.425 226.899 648.835L192.499 462.813Z" fill="#FFF8DE"/>
|
||||
<g filter="url(#filter1_i_215_188)">
|
||||
<ellipse cx="447.564" cy="174.232" rx="223.281" ry="159.292" transform="rotate(-45 447.564 174.232)" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_i_215_188)">
|
||||
<ellipse cx="178.97" cy="442.827" rx="221.92" ry="159.292" transform="rotate(-45 178.97 442.827)" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<ellipse cx="256.948" cy="253.172" rx="16.3376" ry="12.2532" transform="rotate(135 256.948 253.172)" fill="#FFB8B9"/>
|
||||
<ellipse cx="38.9009" cy="15.5185" rx="38.9009" ry="15.5185" transform="matrix(0.934357 -0.356338 -0.356338 -0.934357 493.079 394.049)" fill="#FFD38D"/>
|
||||
<ellipse cx="358.82" cy="530.763" rx="38.9009" ry="15.5185" transform="rotate(110.875 358.82 530.763)" fill="#FFD38D"/>
|
||||
<path d="M264.909 264.467C264.366 262.443 266.219 260.59 268.243 261.132L272.799 262.353C274.824 262.896 275.502 265.426 274.02 266.909L270.685 270.244C269.203 271.726 266.672 271.048 266.129 269.023L264.909 264.467Z" fill="#4C320C"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_215_188" x="0.764322" y="0" width="576.805" height="576.805" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2.23568"/>
|
||||
<feGaussianBlur stdDeviation="1.11784"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_215_188"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_215_188" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_i_215_188" x="248.485" y="-19.7266" width="393.038" height="415.794" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-5.1201" dy="27.8761"/>
|
||||
<feGaussianBlur stdDeviation="22.4643"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_215_188"/>
|
||||
</filter>
|
||||
<filter id="filter2_i_215_188" x="-14.2056" y="232.015" width="411.952" height="403.987" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="25.6005" dy="-17.6359"/>
|
||||
<feGaussianBlur stdDeviation="14.8767"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713974 0 0 0 0 0.272498 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_215_188"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_215_188">
|
||||
<rect x="3" width="572.333" height="572.333" rx="111.784" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
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');
|
||||
|
||||
// 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 type { UploadTask };
|
||||
|
||||
// 重新导出所有数据库函数,使用统一接口
|
||||
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,
|
||||
'@/': 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' });
|
||||
Loading…
x
Reference in New Issue
Block a user