initial commit

This commit is contained in:
Duc Thai
2026-01-12 12:08:04 +07:00
commit ef690fed0b
41 changed files with 13071 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
import initSqlJs from 'sql.js';
import localforage from 'localforage';
import { v4 as uuidv4 } from 'uuid';
const DB_NAME = 'ai_translate_db_v1';
const STORE_KEY = 'sqlite_binary';
let db = null;
let SQL = null;
export const dbService = {
init: async () => {
if (db) return db;
try {
// Load WASM
SQL = await initSqlJs({
// Locate the WASM file in public folder
locateFile: file => `/${file}`
});
// Try to load saved binary from IndexedDB
const savedData = await localforage.getItem(STORE_KEY);
if (savedData) {
// Load existing DB
db = new SQL.Database(new Uint8Array(savedData));
console.log("[DB] Loaded existing database.");
} else {
// Create new DB
db = new SQL.Database();
console.log("[DB] Created new database.");
dbService._createTables();
await dbService.save(); // Initial save
}
return db;
} catch (e) {
console.error("[DB] Initialization Failed:", e);
throw e;
}
},
_createTables: () => {
// Stories Table
db.run(`
CREATE TABLE IF NOT EXISTS stories (
id TEXT PRIMARY KEY,
title TEXT,
author TEXT,
summary TEXT,
cover_image TEXT, -- Base64 or URL
created_at INTEGER,
last_accessed INTEGER,
metadata TEXT -- JSON: { genres, languages, personality, world... }
);
`);
// Files Table
db.run(`
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY, -- Use existing file IDs if possible, or new UUIDs
story_id TEXT,
name TEXT,
content TEXT,
translated_content TEXT,
status TEXT,
size INTEGER,
last_modified INTEGER,
FOREIGN KEY(story_id) REFERENCES stories(id) ON DELETE CASCADE
);
`);
// Dictionary Table (Global or per story? Let's make it simple first: Per Story stored in metadata or separate?)
// For now, Dictionary is just text in App.jsx. We can store it in metadata if needed.
},
save: async () => {
if (!db) return;
try {
const data = db.export();
await localforage.setItem(STORE_KEY, data);
// console.log("[DB] Saved to IndexedDB");
} catch (e) {
console.error("[DB] Save Failed:", e);
}
},
// --- Story Operations ---
createStory: async (title = "Truyện Mới") => {
if (!db) {
console.error("[DB] Database is null!");
throw new Error("Database not initialized");
}
console.log("[DB] Executing createStory for:", title);
const id = uuidv4();
const now = Date.now();
const metadata = JSON.stringify({
genres: [], languages: ['Tiếng Trung'], mcPersonality: [], worldSetting: [], contextNotes: '', additionalDictionary: '', customPrompt: ''
});
try {
db.run(`INSERT INTO stories (id, title, created_at, last_accessed, metadata) VALUES (?, ?, ?, ?, ?)`,
[id, title, now, now, metadata]);
await dbService.save();
console.log("[DB] Insert Successful, ID:", id);
return id;
} catch (e) {
console.error("[DB] Insert Failed:", e);
throw e;
}
},
getStories: () => {
if (!db) return [];
const result = db.exec("SELECT id, title, author, cover_image, last_accessed FROM stories ORDER BY created_at DESC");
if (result.length === 0) return [];
// Transform result [columns, values] to objects
const columns = result[0].columns;
return result[0].values.map(row => {
const obj = {};
columns.forEach((col, i) => obj[col] = row[i]);
return obj;
});
},
getStory: (id) => {
if (!db) return null;
const result = db.exec("SELECT * FROM stories WHERE id = ?", [id]);
if (result.length === 0) return null;
const columns = result[0].columns;
const row = result[0].values[0];
const story = {};
columns.forEach((col, i) => story[col] = row[i]);
// Parse metadata
try {
if (story.metadata) Object.assign(story, JSON.parse(story.metadata));
} catch (e) {
console.error("Failed to parse metadata", e);
}
return story;
},
updateStory: async (id, data) => {
if (!db) return;
const now = Date.now();
// Separate columns vs metadata
const directCols = ['title', 'author', 'summary', 'cover_image'];
const metadataKeys = ['genres', 'languages', 'mcPersonality', 'worldSetting', 'contextNotes', 'image_prompt', 'additionalDictionary', 'customPrompt'];
// 1. Get current metadata
const current = dbService.getStory(id);
if (!current) return;
// 2. Prepare Updates
const updates = [];
const values = [];
directCols.forEach(col => {
if (data[col] !== undefined) {
updates.push(`${col} = ?`);
values.push(data[col]);
}
});
// 3. Update Metadata
const newMetadata = {};
metadataKeys.forEach(k => {
if (data[k] !== undefined) newMetadata[k] = data[k];
else if (current[k] !== undefined) newMetadata[k] = current[k];
});
updates.push(`metadata = ?`);
values.push(JSON.stringify(newMetadata));
updates.push(`last_accessed = ?`);
values.push(now);
// Where ID
values.push(id);
const query = `UPDATE stories SET ${updates.join(', ')} WHERE id = ?`;
db.run(query, values);
await dbService.save();
},
deleteStory: async (id) => {
db.run("DELETE FROM stories WHERE id = ?", [id]);
// Files cascade delete handled by schema? sql.js usually doesn't enforce FK by default unless enabled
// Manually delete files to be safe
db.run("DELETE FROM files WHERE story_id = ?", [id]);
await dbService.save();
},
// --- File Operations ---
getFiles: (storyId) => {
const res = db.exec("SELECT * FROM files WHERE story_id = ?", [storyId]);
if (res.length === 0) return [];
const columns = res[0].columns;
return res[0].values.map(row => {
const f = {};
columns.forEach((col, i) => {
// Map snake_case to camelCase
if (col === 'translated_content') f['translatedContent'] = row[i];
else if (col === 'last_modified') f['lastModified'] = row[i];
else f[col] = row[i];
});
return f;
});
},
saveFilesBatch: async (storyId, files) => {
// Use transaction
db.run("BEGIN TRANSACTION");
const stmt = db.prepare("INSERT OR REPLACE INTO files (id, story_id, name, content, translated_content, status, size, last_modified) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
files.forEach(f => {
stmt.run([f.id, storyId, f.name, f.content, f.translatedContent || null, f.status || 'IDLE', f.content.length, Date.now()]);
});
stmt.free();
db.run("COMMIT");
await dbService.save();
},
deleteFile: async (id) => {
db.run("DELETE FROM files WHERE id = ?", [id]);
await dbService.save();
},
deleteAllFiles: async (storyId) => {
db.run("DELETE FROM files WHERE story_id = ?", [storyId]);
await dbService.save();
}
};