Remove DataService and OfflineDataService, refactor book and character operations to use streamlined handlers in LocalSystem
- Delete `data.service.ts` and `offline-data.service.ts`, consolidating functionality into `LocalSystem`. - Refactor book, character, and conversation operations to adopt unified, multilingual, and session-enabled IPC handlers in `LocalSystem`. - Simplify redundant legacy methods, enhancing maintainability and consistency.
This commit is contained in:
@@ -27,7 +27,7 @@ import GuideTour, {GuideStep} from "@/components/GuideTour";
|
|||||||
import {UserProps} from "@/lib/models/User";
|
import {UserProps} from "@/lib/models/User";
|
||||||
import {useTranslations} from "next-intl";
|
import {useTranslations} from "next-intl";
|
||||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||||
import {getOfflineDataService} from "@/lib/services/offline-data.service";
|
// TODO: Refactor to use window.electron.invoke() instead of OfflineDataService
|
||||||
|
|
||||||
interface MinMax {
|
interface MinMax {
|
||||||
min: number;
|
min: number;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import GuideTour, {GuideStep} from "@/components/GuideTour";
|
|||||||
import User from "@/lib/models/User";
|
import User from "@/lib/models/User";
|
||||||
import {useTranslations} from "next-intl";
|
import {useTranslations} from "next-intl";
|
||||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||||
import {getOfflineDataService} from "@/lib/services/offline-data.service";
|
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||||
|
|
||||||
export default function BookList() {
|
export default function BookList() {
|
||||||
const {session, setSession} = useContext(SessionContext);
|
const {session, setSession} = useContext(SessionContext);
|
||||||
@@ -22,6 +22,7 @@ export default function BookList() {
|
|||||||
const {setBook} = useContext(BookContext);
|
const {setBook} = useContext(BookContext);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const {lang} = useContext<LangContextProps>(LangContext)
|
const {lang} = useContext<LangContextProps>(LangContext)
|
||||||
|
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext)
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
const [groupedBooks, setGroupedBooks] = useState<Record<string, BookProps[]>>({});
|
const [groupedBooks, setGroupedBooks] = useState<Record<string, BookProps[]>>({});
|
||||||
@@ -114,10 +115,12 @@ export default function BookList() {
|
|||||||
async function getBooks(): Promise<void> {
|
async function getBooks(): Promise<void> {
|
||||||
setIsLoadingBooks(true);
|
setIsLoadingBooks(true);
|
||||||
try {
|
try {
|
||||||
const offlineDataService = getOfflineDataService();
|
let bookResponse: BookListProps[] = [];
|
||||||
const bookResponse: BookListProps[] = await offlineDataService.getBooks(
|
if (!isCurrentlyOffline()) {
|
||||||
() => System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang)
|
bookResponse = await System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang);
|
||||||
);
|
} else {
|
||||||
|
bookResponse = await window.electron.invoke<BookListProps[]>('db:book:books');
|
||||||
|
}
|
||||||
if (bookResponse) {
|
if (bookResponse) {
|
||||||
const booksByType: Record<string, BookProps[]> = bookResponse.reduce((groups: Record<string, BookProps[]>, book: BookListProps): Record<string, BookProps[]> => {
|
const booksByType: Record<string, BookProps[]> = bookResponse.reduce((groups: Record<string, BookProps[]>, book: BookListProps): Record<string, BookProps[]> => {
|
||||||
const imageDataUrl: string = book.coverImage ? 'data:image/jpeg;base64,' + book.coverImage : '';
|
const imageDataUrl: string = book.coverImage ? 'data:image/jpeg;base64,' + book.coverImage : '';
|
||||||
|
|||||||
47
electron.d.ts
vendored
47
electron.d.ts
vendored
@@ -1,27 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* TypeScript declarations for window.electron API
|
||||||
|
* Must match exactly with electron/preload.ts
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Use invoke<T>(channel, ...args) for all IPC calls
|
||||||
|
* - Shortcuts are provided for common operations (tokens, lang, encryption)
|
||||||
|
*/
|
||||||
export interface IElectronAPI {
|
export interface IElectronAPI {
|
||||||
|
// Platform info
|
||||||
platform: NodeJS.Platform;
|
platform: NodeJS.Platform;
|
||||||
|
|
||||||
|
// Generic invoke method - use this for all IPC calls
|
||||||
|
invoke: <T = any>(channel: string, ...args: any[]) => Promise<T>;
|
||||||
|
|
||||||
|
// Token management (shortcuts for convenience)
|
||||||
getToken: () => Promise<string | null>;
|
getToken: () => Promise<string | null>;
|
||||||
setToken: (token: string) => Promise<boolean>;
|
setToken: (token: string) => Promise<void>;
|
||||||
removeToken: () => Promise<boolean>;
|
removeToken: () => Promise<void>;
|
||||||
loginSuccess: (token: string) => void;
|
|
||||||
|
// Language management (shortcuts for convenience)
|
||||||
|
getLang: () => Promise<'fr' | 'en'>;
|
||||||
|
setLang: (lang: 'fr' | 'en') => Promise<void>;
|
||||||
|
|
||||||
|
// Auth events (one-way communication)
|
||||||
|
loginSuccess: (token: string, userId: string) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
|
||||||
// Database operations
|
// Encryption key management (shortcuts for convenience)
|
||||||
generateEncryptionKey: (userId: string) => Promise<{ success: boolean; key?: string; error?: string }>;
|
generateEncryptionKey: (userId: string) => Promise<string>;
|
||||||
getUserEncryptionKey: (userId: string) => Promise<string | null>;
|
getUserEncryptionKey: (userId: string) => Promise<string | null>;
|
||||||
setUserEncryptionKey: (userId: string, encryptionKey: string) => Promise<boolean>;
|
setUserEncryptionKey: (userId: string, encryptionKey: string) => Promise<void>;
|
||||||
dbInitialize: (userId: string, encryptionKey: string) => Promise<{ success: boolean; error?: string }>;
|
|
||||||
dbGetBooks: () => Promise<{ success: boolean; data?: any[]; error?: string }>;
|
// Database initialization (shortcut for convenience)
|
||||||
dbGetBook: (bookId: string) => Promise<{ success: boolean; data?: any; error?: string }>;
|
dbInitialize: (userId: string, encryptionKey: string) => Promise<boolean>;
|
||||||
dbSaveBook: (book: any, authorId?: string) => Promise<{ success: boolean; error?: string }>;
|
|
||||||
dbDeleteBook: (bookId: string) => Promise<{ success: boolean; error?: string }>;
|
|
||||||
dbSaveChapter: (chapter: any, bookId: string, contentId?: string) => Promise<{ success: boolean; error?: string }>;
|
|
||||||
dbGetCharacters: (bookId: string) => Promise<{ success: boolean; data?: any[]; error?: string }>;
|
|
||||||
dbSaveCharacter: (character: any, bookId: string) => Promise<{ success: boolean; error?: string }>;
|
|
||||||
dbGetConversations: (bookId: string) => Promise<{ success: boolean; data?: any[]; error?: string }>;
|
|
||||||
dbSaveConversation: (conversation: any, bookId: string) => Promise<{ success: boolean; error?: string }>;
|
|
||||||
dbGetSyncStatus: () => Promise<{ success: boolean; data?: any[]; error?: string }>;
|
|
||||||
dbGetPendingChanges: (limit?: number) => Promise<{ success: boolean; data?: any[]; error?: string }>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import type { IpcMainInvokeEvent } from 'electron';
|
import type { IpcMainInvokeEvent } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
// Electron store instance for session management
|
// ============================================================
|
||||||
|
// SESSION MANAGEMENT - Auto-inject userId and lang
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron store instance for session management
|
||||||
|
* - userId: Set during login via 'login-success' event
|
||||||
|
* - userLang: Set via 'set-lang' handler
|
||||||
|
*/
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
encryptionKey: 'eritors-scribe-secure-key'
|
encryptionKey: 'eritors-scribe-secure-key'
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// SESSION MANAGEMENT - Retrieve userId and lang from store
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get userId from electron-store
|
* Get userId from electron-store
|
||||||
* Set during login via 'login-success' event
|
* Set during login via 'login-success' event
|
||||||
@@ -27,122 +31,67 @@ function getLangFromSession(): 'fr' | 'en' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// LEGACY HANDLERS - Manual userId injection, lang must be passed
|
// UNIVERSAL HANDLER - Like a Fastify route
|
||||||
// Keep these for backward compatibility
|
// Automatically injects: userId, lang
|
||||||
// Updated to support Promises
|
// Optional body parameter (for GET, POST, PUT, DELETE)
|
||||||
// ============================================================
|
// Generic return type (void, object, etc.)
|
||||||
|
|
||||||
export function createDbHandler<TReturn>(
|
|
||||||
handler: (userId: string) => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent): Promise<TReturn> {
|
|
||||||
const userId = getUserIdFromSession();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error('User not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(userId);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[DB] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error('An unknown error occurred.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDbHandler1<T1, TReturn>(
|
|
||||||
handler: (userId: string, arg1: T1) => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent, arg1: T1) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent, arg1: T1): Promise<TReturn> {
|
|
||||||
const userId = getUserIdFromSession();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error('User not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(userId, arg1);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[DB] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error('An unknown error occurred.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDbHandler2<T1, T2, TReturn>(
|
|
||||||
handler: (userId: string, arg1: T1, arg2: T2) => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2): Promise<TReturn> {
|
|
||||||
const userId = getUserIdFromSession();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error('User not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(userId, arg1, arg2);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[DB] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error('An unknown error occurred.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDbHandler3<T1, T2, T3, TReturn>(
|
|
||||||
handler: (userId: string, arg1: T1, arg2: T2, arg3: T3) => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3): Promise<TReturn> {
|
|
||||||
const userId = getUserIdFromSession();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error('User not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(userId, arg1, arg2, arg3);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[DB] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error('An unknown error occurred.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// AUTO HANDLERS - Automatically inject userId AND lang
|
|
||||||
// Use these for new handlers - no need to pass lang from frontend
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-handler with 0 parameters
|
* Universal IPC handler - works like a Fastify route
|
||||||
* Automatically injects: userId, lang
|
* Automatically injects: userId, lang from session
|
||||||
|
*
|
||||||
|
* @template TBody - Request body type (use void for no params)
|
||||||
|
* @template TReturn - Response type (use void for no return)
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ipcMain.handle('db:user:get', createAutoHandler<UserProps>(
|
* // GET with no params
|
||||||
* function(userId: string, lang: 'fr' | 'en') {
|
* ipcMain.handle('db:books:getAll',
|
||||||
* return User.getUser(userId, lang);
|
* createHandler<void, BookProps[]>(
|
||||||
|
* async (userId, body, lang) => {
|
||||||
|
* return await Book.getBooks(userId, lang);
|
||||||
* }
|
* }
|
||||||
* ));
|
* )
|
||||||
|
* );
|
||||||
|
* // Frontend: invoke('db:books:getAll')
|
||||||
*
|
*
|
||||||
* // Frontend call (no params needed):
|
* @example
|
||||||
* const user = await window.electron.invoke('db:user:get');
|
* // GET with 1 param
|
||||||
|
* ipcMain.handle('db:book:get',
|
||||||
|
* createHandler<string, BookProps>(
|
||||||
|
* async (userId, bookId, lang) => {
|
||||||
|
* return await Book.getBook(bookId, userId, lang);
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* );
|
||||||
|
* // Frontend: invoke('db:book:get', bookId)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // POST with object body
|
||||||
|
* ipcMain.handle('db:book:create',
|
||||||
|
* createHandler<CreateBookData, string>(
|
||||||
|
* async (userId, data, lang) => {
|
||||||
|
* return await Book.addBook(userId, data, lang);
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* );
|
||||||
|
* // Frontend: invoke('db:book:create', { title: '...', ... })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // DELETE with void return
|
||||||
|
* ipcMain.handle('db:book:delete',
|
||||||
|
* createHandler<string, void>(
|
||||||
|
* async (userId, bookId, lang) => {
|
||||||
|
* await Book.deleteBook(bookId, userId, lang);
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* );
|
||||||
|
* // Frontend: invoke('db:book:delete', bookId)
|
||||||
*/
|
*/
|
||||||
export function createAutoHandler<TReturn>(
|
export function createHandler<TBody = void, TReturn = void>(
|
||||||
handler: (userId: string, lang: 'fr' | 'en') => TReturn | Promise<TReturn>
|
handler: (userId: string, body: TBody, lang: 'fr' | 'en') => TReturn | Promise<TReturn>
|
||||||
): (event: IpcMainInvokeEvent) => Promise<TReturn> {
|
): (event: IpcMainInvokeEvent, body?: TBody) => Promise<TReturn> {
|
||||||
return async function(event: IpcMainInvokeEvent): Promise<TReturn> {
|
return async function(event: IpcMainInvokeEvent, body?: TBody): Promise<TReturn> {
|
||||||
const userId = getUserIdFromSession();
|
const userId = getUserIdFromSession();
|
||||||
const lang = getLangFromSession();
|
const lang = getLangFromSession();
|
||||||
|
|
||||||
@@ -151,7 +100,7 @@ export function createAutoHandler<TReturn>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await handler(userId, lang);
|
return await handler(userId, body as TBody, lang);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error(`[DB] ${error.message}`);
|
console.error(`[DB] ${error.message}`);
|
||||||
@@ -161,122 +110,3 @@ export function createAutoHandler<TReturn>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-handler with 1 parameter
|
|
||||||
* Automatically injects: userId, lang
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ipcMain.handle('db:book:get', createAutoHandler1<string, BookProps>(
|
|
||||||
* function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
|
||||||
* return Book.getBook(bookId, userId, lang);
|
|
||||||
* }
|
|
||||||
* ));
|
|
||||||
*
|
|
||||||
* // Frontend call (only bookId needed):
|
|
||||||
* const book = await window.electron.invoke('db:book:get', bookId);
|
|
||||||
*/
|
|
||||||
export function createAutoHandler1<T1, TReturn>(
|
|
||||||
handler: (userId: string, arg1: T1, lang: 'fr' | 'en') => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent, arg1: T1) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent, arg1: T1): Promise<TReturn> {
|
|
||||||
const userId = getUserIdFromSession();
|
|
||||||
const lang = getLangFromSession();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error('User not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(userId, arg1, lang);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[DB] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error('An unknown error occurred.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-handler with 2 parameters
|
|
||||||
* Automatically injects: userId, lang
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ipcMain.handle('db:book:create', createAutoHandler1<CreateBookData, string>(
|
|
||||||
* function(userId: string, data: CreateBookData, lang: 'fr' | 'en') {
|
|
||||||
* return Book.addBook(null, userId, data.title, ..., lang);
|
|
||||||
* }
|
|
||||||
* ));
|
|
||||||
*
|
|
||||||
* // Frontend call (only data needed):
|
|
||||||
* const bookId = await window.electron.invoke('db:book:create', bookData);
|
|
||||||
*/
|
|
||||||
export function createAutoHandler2<T1, T2, TReturn>(
|
|
||||||
handler: (userId: string, arg1: T1, arg2: T2, lang: 'fr' | 'en') => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2): Promise<TReturn> {
|
|
||||||
const userId = getUserIdFromSession();
|
|
||||||
const lang = getLangFromSession();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error('User not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(userId, arg1, arg2, lang);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[DB] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error('An unknown error occurred.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-handler with 3 parameters
|
|
||||||
* Automatically injects: userId, lang
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ipcMain.handle('db:book:cover:update', createAutoHandler2<string, string, boolean>(
|
|
||||||
* function(userId: string, bookId: string, coverImageName: string, lang: 'fr' | 'en') {
|
|
||||||
* return Book.updateBookCover(userId, bookId, coverImageName, lang);
|
|
||||||
* }
|
|
||||||
* ));
|
|
||||||
*
|
|
||||||
* // Frontend call (bookId and coverImageName needed):
|
|
||||||
* const success = await window.electron.invoke('db:book:cover:update', bookId, coverImageName);
|
|
||||||
*/
|
|
||||||
export function createAutoHandler3<T1, T2, T3, TReturn>(
|
|
||||||
handler: (userId: string, arg1: T1, arg2: T2, arg3: T3, lang: 'fr' | 'en') => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3): Promise<TReturn> {
|
|
||||||
const userId = getUserIdFromSession();
|
|
||||||
const lang = getLangFromSession();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error('User not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await handler(userId, arg1, arg2, arg3, lang);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[DB] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error('An unknown error occurred.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createUniqueId(): string {
|
|
||||||
return crypto.randomUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentDate(): string {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export default class Book {
|
|||||||
return BookRepo.insertBook(id,userId,encryptedTitle,hashedTitle,encryptedSubTitle,hashedSubTitle,encryptedSummary,type,serie,publicationDate,desiredWordCount,lang);
|
return BookRepo.insertBook(id,userId,encryptedTitle,hashedTitle,encryptedSubTitle,hashedSubTitle,encryptedSummary,type,serie,publicationDate,desiredWordCount,lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getBook(userId:string,bookId: string, lang: 'fr' | 'en' = 'fr'): Promise<BookProps> {
|
public static async getBook(userId:string,bookId: string): Promise<BookProps> {
|
||||||
const book:Book = new Book(bookId);
|
const book:Book = new Book(bookId);
|
||||||
await book.getBookInfos(userId);
|
await book.getBookInfos(userId);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,34 +1,30 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import {
|
import { createHandler } from '../database/LocalSystem.js';
|
||||||
createDbHandler2,
|
|
||||||
// Auto-handlers: automatically inject userId AND lang
|
|
||||||
createAutoHandler,
|
|
||||||
createAutoHandler1
|
|
||||||
} from '../database/LocalSystem.js';
|
|
||||||
import Book from '../database/models/Book.js';
|
import Book from '../database/models/Book.js';
|
||||||
import type { BookProps, GuideLine, GuideLineAI, Act, Issue, WorldProps } from '../database/models/Book.js';
|
import type { BookProps, GuideLine, GuideLineAI, Act, Issue, WorldProps } from '../database/models/Book.js';
|
||||||
|
|
||||||
ipcMain.handle(
|
// ============================================================
|
||||||
'db:book:getAll',
|
// 1. GET /books - Get all books
|
||||||
createAutoHandler<BookProps[]>(
|
// ============================================================
|
||||||
async function(userId: string, lang: 'fr' | 'en') {
|
ipcMain.handle('db:book:books', createHandler<void, BookProps[]>(
|
||||||
|
async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<BookProps[]> {
|
||||||
return await Book.getBooks(userId, lang);
|
return await Book.getBooks(userId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(
|
// ============================================================
|
||||||
'db:book:get',
|
// 2. GET /book/:id - Get single book
|
||||||
createAutoHandler1<string, BookProps>(
|
// ============================================================
|
||||||
async function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
|
||||||
return await Book.getBook(bookId, userId, lang);
|
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> {
|
||||||
|
return await Book.getBook(bookId, userId);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// Frontend call: await window.electron.invoke('db:book:get', bookId);
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 3. POST /book/basic-information
|
// 3. POST /book/basic-information - Update book basic info
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface UpdateBookBasicData {
|
interface UpdateBookBasicData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -41,7 +37,7 @@ interface UpdateBookBasicData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:updateBasicInformation',
|
'db:book:updateBasicInformation',
|
||||||
createAutoHandler1<UpdateBookBasicData, boolean>(
|
createHandler<UpdateBookBasicData, boolean>(
|
||||||
function(userId: string, data: UpdateBookBasicData, lang: 'fr' | 'en') {
|
function(userId: string, data: UpdateBookBasicData, lang: 'fr' | 'en') {
|
||||||
return Book.updateBookBasicInformation(
|
return Book.updateBookBasicInformation(
|
||||||
userId,
|
userId,
|
||||||
@@ -56,22 +52,23 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// Frontend call: await window.electron.invoke('db:book:updateBasicInformation', data);
|
// Frontend: invoke('db:book:updateBasicInformation', data)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 4. GET /book/guide-line
|
// 4. GET /book/guide-line - Get guideline
|
||||||
// ============================================================
|
// ============================================================
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:guideline:get',
|
'db:book:guideline:get',
|
||||||
createAutoHandler1<string, GuideLine | null>(
|
createHandler<string, GuideLine | null>(
|
||||||
async function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
async function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
return await Book.getGuideLine(userId, bookId, lang);
|
return await Book.getGuideLine(userId, bookId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:guideline:get', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 5. POST /book/guide-line
|
// 5. POST /book/guide-line - Update guideline
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface UpdateGuideLineData {
|
interface UpdateGuideLineData {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -89,7 +86,7 @@ interface UpdateGuideLineData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:guideline:update',
|
'db:book:guideline:update',
|
||||||
createAutoHandler1<UpdateGuideLineData, boolean>(
|
createHandler<UpdateGuideLineData, boolean>(
|
||||||
async function(userId: string, data: UpdateGuideLineData, lang: 'fr' | 'en') {
|
async function(userId: string, data: UpdateGuideLineData, lang: 'fr' | 'en') {
|
||||||
return await Book.updateGuideLine(
|
return await Book.updateGuideLine(
|
||||||
userId,
|
userId,
|
||||||
@@ -109,9 +106,10 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:guideline:update', data)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 6. GET /book/story
|
// 6. GET /book/story - Get story data (acts + issues)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface StoryData {
|
interface StoryData {
|
||||||
acts: Act[];
|
acts: Act[];
|
||||||
@@ -120,7 +118,7 @@ interface StoryData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:story:get',
|
'db:book:story:get',
|
||||||
createDbHandler2<string, 'fr' | 'en', StoryData>(
|
createHandler<string, StoryData>(
|
||||||
async function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
async function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
const acts = await Book.getActsData(userId, bookId, lang);
|
const acts = await Book.getActsData(userId, bookId, lang);
|
||||||
const issues = await Book.getIssuesFromBook(userId, bookId, lang);
|
const issues = await Book.getIssuesFromBook(userId, bookId, lang);
|
||||||
@@ -131,9 +129,10 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:story:get', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 7. POST /book/story
|
// 7. POST /book/story - Update story
|
||||||
// TODO: Implement updateStory in Book.ts
|
// TODO: Implement updateStory in Book.ts
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// interface StoryUpdateData {
|
// interface StoryUpdateData {
|
||||||
@@ -144,15 +143,16 @@ ipcMain.handle(
|
|||||||
//
|
//
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:story:update',
|
// 'db:book:story:update',
|
||||||
// createDbHandler2<StoryUpdateData, 'fr' | 'en', boolean>(
|
// createHandler<StoryUpdateData, boolean>(
|
||||||
// function(userId: string, data: StoryUpdateData, lang: 'fr' | 'en') {
|
// function(userId: string, data: StoryUpdateData, lang: 'fr' | 'en') {
|
||||||
// return Book.updateStory(userId, data.bookId, data.acts, data.mainChapters, lang);
|
// return Book.updateStory(userId, data.bookId, data.acts, data.mainChapters, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:story:update', data)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 8. POST /book/add
|
// 8. POST /book/add - Create new book
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface CreateBookData {
|
interface CreateBookData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -166,7 +166,7 @@ interface CreateBookData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:create',
|
'db:book:create',
|
||||||
createDbHandler2<CreateBookData, 'fr' | 'en', string>(
|
createHandler<CreateBookData, string>(
|
||||||
function(userId: string, data: CreateBookData, lang: 'fr' | 'en') {
|
function(userId: string, data: CreateBookData, lang: 'fr' | 'en') {
|
||||||
return Book.addBook(
|
return Book.addBook(
|
||||||
null,
|
null,
|
||||||
@@ -183,22 +183,29 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:create', data)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 9. POST /book/cover
|
// 9. POST /book/cover - Update book cover
|
||||||
// ============================================================
|
|
||||||
// TODO: Implement updateBookCover in Book.ts
|
// TODO: Implement updateBookCover in Book.ts
|
||||||
|
// ============================================================
|
||||||
|
// interface UpdateCoverData {
|
||||||
|
// bookId: string;
|
||||||
|
// coverImageName: string;
|
||||||
|
// }
|
||||||
|
//
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:cover:update',
|
// 'db:book:cover:update',
|
||||||
// createDbHandler3<string, string, 'fr' | 'en', boolean>(
|
// createHandler<UpdateCoverData, boolean>(
|
||||||
// function(userId: string, bookId: string, coverImageName: string, lang: 'fr' | 'en') {
|
// function(userId: string, data: UpdateCoverData, lang: 'fr' | 'en') {
|
||||||
// return Book.updateBookCover(userId, bookId, coverImageName, lang);
|
// return Book.updateBookCover(userId, data.bookId, data.coverImageName, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:cover:update', { bookId, coverImageName })
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 10. POST /book/incident/new
|
// 10. POST /book/incident/new - Add incident
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface AddIncidentData {
|
interface AddIncidentData {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -207,15 +214,16 @@ interface AddIncidentData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:incident:add',
|
'db:book:incident:add',
|
||||||
createDbHandler2<AddIncidentData, 'fr' | 'en', string>(
|
createHandler<AddIncidentData, string>(
|
||||||
function(userId: string, data: AddIncidentData, lang: 'fr' | 'en') {
|
function(userId: string, data: AddIncidentData, lang: 'fr' | 'en') {
|
||||||
return Book.addNewIncident(userId, data.bookId, data.name, lang);
|
return Book.addNewIncident(userId, data.bookId, data.name, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:incident:add', { bookId, name })
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 11. DELETE /book/incident/remove
|
// 11. DELETE /book/incident/remove - Remove incident
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface RemoveIncidentData {
|
interface RemoveIncidentData {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -224,15 +232,16 @@ interface RemoveIncidentData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:incident:remove',
|
'db:book:incident:remove',
|
||||||
createDbHandler2<RemoveIncidentData, 'fr' | 'en', boolean>(
|
createHandler<RemoveIncidentData, boolean>(
|
||||||
function(userId: string, data: RemoveIncidentData, lang: 'fr' | 'en') {
|
function(userId: string, data: RemoveIncidentData, lang: 'fr' | 'en') {
|
||||||
return Book.removeIncident(userId, data.bookId, data.incidentId, lang);
|
return Book.removeIncident(userId, data.bookId, data.incidentId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:incident:remove', { bookId, incidentId })
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 12. POST /book/plot/new
|
// 12. POST /book/plot/new - Add plot point
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface AddPlotPointData {
|
interface AddPlotPointData {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -242,7 +251,7 @@ interface AddPlotPointData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:plot:add',
|
'db:book:plot:add',
|
||||||
createDbHandler2<AddPlotPointData, 'fr' | 'en', string>(
|
createHandler<AddPlotPointData, string>(
|
||||||
function(userId: string, data: AddPlotPointData, lang: 'fr' | 'en') {
|
function(userId: string, data: AddPlotPointData, lang: 'fr' | 'en') {
|
||||||
return Book.addNewPlotPoint(
|
return Book.addNewPlotPoint(
|
||||||
userId,
|
userId,
|
||||||
@@ -254,21 +263,23 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:plot:add', { bookId, name, incidentId })
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 13. DELETE /book/plot/remove
|
// 13. DELETE /book/plot/remove - Remove plot point
|
||||||
// ============================================================
|
// ============================================================
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:plot:remove',
|
'db:book:plot:remove',
|
||||||
createDbHandler2<string, 'fr' | 'en', boolean>(
|
createHandler<string, boolean>(
|
||||||
function(userId: string, plotPointId: string, lang: 'fr' | 'en') {
|
function(userId: string, plotPointId: string, lang: 'fr' | 'en') {
|
||||||
return Book.removePlotPoint(userId, plotPointId, lang);
|
return Book.removePlotPoint(userId, plotPointId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:plot:remove', plotPointId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 14. POST /book/issue/add
|
// 14. POST /book/issue/add - Add issue
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface AddIssueData {
|
interface AddIssueData {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -277,39 +288,42 @@ interface AddIssueData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:issue:add',
|
'db:book:issue:add',
|
||||||
createDbHandler2<AddIssueData, 'fr' | 'en', string>(
|
createHandler<AddIssueData, string>(
|
||||||
function(userId: string, data: AddIssueData, lang: 'fr' | 'en') {
|
function(userId: string, data: AddIssueData, lang: 'fr' | 'en') {
|
||||||
return Book.addNewIssue(userId, data.bookId, data.name, lang);
|
return Book.addNewIssue(userId, data.bookId, data.name, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:issue:add', { bookId, name })
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 15. DELETE /book/issue/remove
|
// 15. DELETE /book/issue/remove - Remove issue
|
||||||
// ============================================================
|
// ============================================================
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:issue:remove',
|
'db:book:issue:remove',
|
||||||
createDbHandler2<string, 'fr' | 'en', boolean>(
|
createHandler<string, boolean>(
|
||||||
function(userId: string, issueId: string, lang: 'fr' | 'en') {
|
function(userId: string, issueId: string, lang: 'fr' | 'en') {
|
||||||
return Book.removeIssue(userId, issueId, lang);
|
return Book.removeIssue(userId, issueId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:issue:remove', issueId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 16. GET /book/worlds
|
// 16. GET /book/worlds - Get worlds for book
|
||||||
// ============================================================
|
// ============================================================
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:worlds:get',
|
'db:book:worlds:get',
|
||||||
createDbHandler2<string, 'fr' | 'en', WorldProps[]>(
|
createHandler<string, WorldProps[]>(
|
||||||
function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
return Book.getWorlds(userId, bookId, lang);
|
return Book.getWorlds(userId, bookId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:worlds:get', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 17. POST /book/world/add
|
// 17. POST /book/world/add - Add world
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface AddWorldData {
|
interface AddWorldData {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -318,15 +332,16 @@ interface AddWorldData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:world:add',
|
'db:book:world:add',
|
||||||
createDbHandler2<AddWorldData, 'fr' | 'en', string>(
|
createHandler<AddWorldData, string>(
|
||||||
function(userId: string, data: AddWorldData, lang: 'fr' | 'en') {
|
function(userId: string, data: AddWorldData, lang: 'fr' | 'en') {
|
||||||
return Book.addNewWorld(userId, data.bookId, data.worldName, lang);
|
return Book.addNewWorld(userId, data.bookId, data.worldName, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:world:add', { bookId, worldName })
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 18. POST /book/world/element/add
|
// 18. POST /book/world/element/add - Add element to world
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface AddWorldElementData {
|
interface AddWorldElementData {
|
||||||
worldId: string;
|
worldId: string;
|
||||||
@@ -336,7 +351,7 @@ interface AddWorldElementData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:world:element:add',
|
'db:book:world:element:add',
|
||||||
createDbHandler2<AddWorldElementData, 'fr' | 'en', string>(
|
createHandler<AddWorldElementData, string>(
|
||||||
function(userId: string, data: AddWorldElementData, lang: 'fr' | 'en') {
|
function(userId: string, data: AddWorldElementData, lang: 'fr' | 'en') {
|
||||||
return Book.addNewElementToWorld(
|
return Book.addNewElementToWorld(
|
||||||
userId,
|
userId,
|
||||||
@@ -348,71 +363,77 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:world:element:add', { worldId, elementName, elementType })
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 19. DELETE /book/world/element/delete
|
// 19. DELETE /book/world/element/delete - Remove element from world
|
||||||
// ============================================================
|
// ============================================================
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:world:element:remove',
|
'db:book:world:element:remove',
|
||||||
createDbHandler2<string, 'fr' | 'en', boolean>(
|
createHandler<string, boolean>(
|
||||||
function(userId: string, elementId: string, lang: 'fr' | 'en') {
|
function(userId: string, elementId: string, lang: 'fr' | 'en') {
|
||||||
return Book.removeElementFromWorld(userId, elementId, lang);
|
return Book.removeElementFromWorld(userId, elementId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:world:element:remove', elementId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 20. PUT /book/world/update
|
// 20. PUT /book/world/update - Update world
|
||||||
// TODO: Implement updateWorld in Book.ts
|
// TODO: Implement updateWorld in Book.ts
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:world:update',
|
// 'db:book:world:update',
|
||||||
// createDbHandler2<WorldProps, 'fr' | 'en', boolean>(
|
// createHandler<WorldProps, boolean>(
|
||||||
// function(userId: string, world: WorldProps, lang: 'fr' | 'en') {
|
// function(userId: string, world: WorldProps, lang: 'fr' | 'en') {
|
||||||
// return Book.updateWorld(userId, world, lang);
|
// return Book.updateWorld(userId, world, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:world:update', worldData)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 21. DELETE /book/cover/delete
|
// 21. DELETE /book/cover/delete - Delete book cover
|
||||||
// TODO: Implement deleteCoverPicture in Book.ts
|
// TODO: Implement deleteCoverPicture in Book.ts
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:cover:delete',
|
// 'db:book:cover:delete',
|
||||||
// createDbHandler2<string, 'fr' | 'en', boolean>(
|
// createHandler<string, boolean>(
|
||||||
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
// return Book.deleteCoverPicture(userId, bookId, lang);
|
// return Book.deleteCoverPicture(userId, bookId, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:cover:delete', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 22. DELETE /book/delete
|
// 22. DELETE /book/delete - Delete book
|
||||||
// ============================================================
|
// ============================================================
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:delete',
|
'db:book:delete',
|
||||||
createDbHandler2<string, 'fr' | 'en', boolean>(
|
createHandler<string, boolean>(
|
||||||
function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
return Book.removeBook(userId, bookId, lang);
|
return Book.removeBook(userId, bookId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:delete', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 23. GET /book/ai/guideline
|
// 23. GET /book/ai/guideline - Get AI guideline
|
||||||
// ============================================================
|
// ============================================================
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:guideline:ai:get',
|
'db:book:guideline:ai:get',
|
||||||
createDbHandler2<string, 'fr' | 'en', GuideLineAI>(
|
createHandler<string, GuideLineAI>(
|
||||||
function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
return Book.getGuideLineAI(bookId, userId, lang);
|
return Book.getGuideLineAI(bookId, userId, lang);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:guideline:ai:get', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 24. POST /book/ai/guideline (set)
|
// 24. POST /book/ai/guideline (set) - Set AI guideline
|
||||||
// ============================================================
|
// ============================================================
|
||||||
interface SetAIGuideLineData {
|
interface SetAIGuideLineData {
|
||||||
bookId: string;
|
bookId: string;
|
||||||
@@ -427,7 +448,7 @@ interface SetAIGuideLineData {
|
|||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'db:book:guideline:ai:set',
|
'db:book:guideline:ai:set',
|
||||||
createDbHandler2<SetAIGuideLineData, 'fr' | 'en', boolean>(
|
createHandler<SetAIGuideLineData, boolean>(
|
||||||
function(userId: string, data: SetAIGuideLineData, lang: 'fr' | 'en') {
|
function(userId: string, data: SetAIGuideLineData, lang: 'fr' | 'en') {
|
||||||
return Book.setAIGuideLine(
|
return Book.setAIGuideLine(
|
||||||
data.bookId,
|
data.bookId,
|
||||||
@@ -444,48 +465,52 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Frontend: invoke('db:book:guideline:ai:set', data)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 25. GET /book/transform/epub
|
// 25. GET /book/transform/epub - Export to EPUB
|
||||||
// ============================================================
|
|
||||||
// TODO: Implement transformToEpub in Book.ts
|
// TODO: Implement transformToEpub in Book.ts
|
||||||
|
// ============================================================
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:export:epub',
|
// 'db:book:export:epub',
|
||||||
// createDbHandler2<string, 'fr' | 'en', ExportData>(
|
// createHandler<string, ExportData>(
|
||||||
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
// return Book.transformToEpub(userId, bookId, lang);
|
// return Book.transformToEpub(userId, bookId, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:export:epub', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 26. GET /book/transform/pdf
|
// 26. GET /book/transform/pdf - Export to PDF
|
||||||
// ============================================================
|
|
||||||
// TODO: Implement transformToPDF in Book.ts
|
// TODO: Implement transformToPDF in Book.ts
|
||||||
|
// ============================================================
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:export:pdf',
|
// 'db:book:export:pdf',
|
||||||
// createDbHandler2<string, 'fr' | 'en', ExportData>(
|
// createHandler<string, ExportData>(
|
||||||
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
// return Book.transformToPDF(userId, bookId, lang);
|
// return Book.transformToPDF(userId, bookId, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:export:pdf', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 27. GET /book/transform/docx
|
// 27. GET /book/transform/docx - Export to DOCX
|
||||||
// ============================================================
|
|
||||||
// TODO: Implement transformToDOCX in Book.ts
|
// TODO: Implement transformToDOCX in Book.ts
|
||||||
|
// ============================================================
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:export:docx',
|
// 'db:book:export:docx',
|
||||||
// createDbHandler2<string, 'fr' | 'en', ExportData>(
|
// createHandler<string, ExportData>(
|
||||||
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
// return Book.transformToDOCX(userId, bookId, lang);
|
// return Book.transformToDOCX(userId, bookId, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:export:docx', bookId)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 28. GET /book/tags
|
// 28. GET /book/tags - Get tags from book
|
||||||
// TODO: Implement getTagsFromBook in Book.ts
|
// TODO: Implement getTagsFromBook in Book.ts
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// interface BookTags {
|
// interface BookTags {
|
||||||
@@ -497,11 +522,12 @@ ipcMain.handle(
|
|||||||
//
|
//
|
||||||
// ipcMain.handle(
|
// ipcMain.handle(
|
||||||
// 'db:book:tags:get',
|
// 'db:book:tags:get',
|
||||||
// createDbHandler2<string, 'fr' | 'en', BookTags>(
|
// createHandler<string, BookTags>(
|
||||||
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
// function(userId: string, bookId: string, lang: 'fr' | 'en') {
|
||||||
// return Book.getTagsFromBook(userId, bookId, lang);
|
// return Book.getTagsFromBook(userId, bookId, lang);
|
||||||
// }
|
// }
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
|
// // Frontend: invoke('db:book:tags:get', bookId)
|
||||||
|
|
||||||
console.log('[IPC] Book handlers registered');
|
console.log('[IPC] Book handlers registered');
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import Store from 'electron-store';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { getDatabaseService } from './database/database.service.js';
|
import { getDatabaseService } from './database/database.service.js';
|
||||||
|
|
||||||
|
// Import IPC handlers
|
||||||
|
import './ipc/book.ipc.js';
|
||||||
|
|
||||||
// Fix pour __dirname en ES modules
|
// Fix pour __dirname en ES modules
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|||||||
@@ -1,36 +1,34 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
// Exposer des APIs sécurisées au renderer process
|
/**
|
||||||
|
* Exposer des APIs sécurisées au renderer process
|
||||||
|
* Utilise invoke() générique pour tous les appels IPC
|
||||||
|
*/
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
|
// Platform info
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
|
||||||
// Token management
|
// Generic invoke method - use this for all IPC calls
|
||||||
|
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
|
||||||
|
|
||||||
|
// Token management (shortcuts for convenience)
|
||||||
getToken: () => ipcRenderer.invoke('get-token'),
|
getToken: () => ipcRenderer.invoke('get-token'),
|
||||||
setToken: (token: string) => ipcRenderer.invoke('set-token', token),
|
setToken: (token: string) => ipcRenderer.invoke('set-token', token),
|
||||||
removeToken: () => ipcRenderer.invoke('remove-token'),
|
removeToken: () => ipcRenderer.invoke('remove-token'),
|
||||||
|
|
||||||
// Language management
|
// Language management (shortcuts for convenience)
|
||||||
getLang: () => ipcRenderer.invoke('get-lang'),
|
getLang: () => ipcRenderer.invoke('get-lang'),
|
||||||
setLang: (lang: 'fr' | 'en') => ipcRenderer.invoke('set-lang', lang),
|
setLang: (lang: 'fr' | 'en') => ipcRenderer.invoke('set-lang', lang),
|
||||||
|
|
||||||
// Auth events
|
// Auth events (use send for one-way communication)
|
||||||
loginSuccess: (token: string, userId: string) => ipcRenderer.send('login-success', token, userId),
|
loginSuccess: (token: string, userId: string) => ipcRenderer.send('login-success', token, userId),
|
||||||
logout: () => ipcRenderer.send('logout'),
|
logout: () => ipcRenderer.send('logout'),
|
||||||
|
|
||||||
// Database operations
|
// Encryption key management (shortcuts for convenience)
|
||||||
generateEncryptionKey: (userId: string) => ipcRenderer.invoke('generate-encryption-key', userId),
|
generateEncryptionKey: (userId: string) => ipcRenderer.invoke('generate-encryption-key', userId),
|
||||||
getUserEncryptionKey: (userId: string) => ipcRenderer.invoke('get-user-encryption-key', userId),
|
getUserEncryptionKey: (userId: string) => ipcRenderer.invoke('get-user-encryption-key', userId),
|
||||||
setUserEncryptionKey: (userId: string, encryptionKey: string) => ipcRenderer.invoke('set-user-encryption-key', userId, encryptionKey),
|
setUserEncryptionKey: (userId: string, encryptionKey: string) => ipcRenderer.invoke('set-user-encryption-key', userId, encryptionKey),
|
||||||
|
|
||||||
|
// Database initialization (shortcut for convenience)
|
||||||
dbInitialize: (userId: string, encryptionKey: string) => ipcRenderer.invoke('db-initialize', userId, encryptionKey),
|
dbInitialize: (userId: string, encryptionKey: string) => ipcRenderer.invoke('db-initialize', userId, encryptionKey),
|
||||||
dbGetBooks: () => ipcRenderer.invoke('db-get-books'),
|
|
||||||
dbGetBook: (bookId: string) => ipcRenderer.invoke('db-get-book', bookId),
|
|
||||||
dbSaveBook: (book: any, authorId?: string) => ipcRenderer.invoke('db-save-book', book, authorId),
|
|
||||||
dbDeleteBook: (bookId: string) => ipcRenderer.invoke('db-delete-book', bookId),
|
|
||||||
dbSaveChapter: (chapter: any, bookId: string, contentId?: string) => ipcRenderer.invoke('db-save-chapter', chapter, bookId, contentId),
|
|
||||||
dbGetCharacters: (bookId: string) => ipcRenderer.invoke('db-get-characters', bookId),
|
|
||||||
dbSaveCharacter: (character: any, bookId: string) => ipcRenderer.invoke('db-save-character', character, bookId),
|
|
||||||
dbGetConversations: (bookId: string) => ipcRenderer.invoke('db-get-conversations', bookId),
|
|
||||||
dbSaveConversation: (conversation: any, bookId: string) => ipcRenderer.invoke('db-save-conversation', conversation, bookId),
|
|
||||||
dbGetSyncStatus: () => ipcRenderer.invoke('db-get-sync-status'),
|
|
||||||
dbGetPendingChanges: (limit?: number) => ipcRenderer.invoke('db-get-pending-changes', limit),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,366 +0,0 @@
|
|||||||
import System from '@/lib/models/System';
|
|
||||||
import { BookProps, BookListProps } from '@/lib/models/Book';
|
|
||||||
import { ChapterProps } from '@/lib/models/Chapter';
|
|
||||||
import { CharacterProps } from '@/lib/models/Character';
|
|
||||||
import { Conversation } from '@/lib/models/QuillSense';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DataService - Smart routing layer between server API and local database
|
|
||||||
* Automatically routes requests based on offline/online status
|
|
||||||
*/
|
|
||||||
export class DataService {
|
|
||||||
private static isOffline: boolean = false;
|
|
||||||
private static accessToken: string | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set offline mode status
|
|
||||||
*/
|
|
||||||
static setOfflineMode(offline: boolean): void {
|
|
||||||
this.isOffline = offline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set access token for API requests
|
|
||||||
*/
|
|
||||||
static setAccessToken(token: string | null): void {
|
|
||||||
this.accessToken = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if currently offline
|
|
||||||
*/
|
|
||||||
static isCurrentlyOffline(): boolean {
|
|
||||||
return this.isOffline;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== BOOK OPERATIONS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all books
|
|
||||||
*/
|
|
||||||
static async getBooks(): Promise<BookListProps[]> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Use local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbGetBooks();
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to get books from local DB');
|
|
||||||
}
|
|
||||||
return result.data || [];
|
|
||||||
} else {
|
|
||||||
// Use server API
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await System.authGetQueryToServer<BookListProps[]>(
|
|
||||||
'books',
|
|
||||||
this.accessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single book with all data
|
|
||||||
*/
|
|
||||||
static async getBook(bookId: string): Promise<BookProps | null> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Use local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbGetBook(bookId);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to get book from local DB');
|
|
||||||
}
|
|
||||||
return result.data || null;
|
|
||||||
} else {
|
|
||||||
// Use server API
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await System.authGetQueryToServer<BookProps>(
|
|
||||||
`books/${bookId}`,
|
|
||||||
this.accessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data || null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save or update a book
|
|
||||||
*/
|
|
||||||
static async saveBook(book: BookProps | BookListProps, authorId?: string): Promise<void> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Save to local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbSaveBook(book, authorId);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to save book to local DB');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Save to server
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdate = 'bookId' in book && book.bookId;
|
|
||||||
if (isUpdate) {
|
|
||||||
await System.authPutToServer(`books/${book.bookId || (book as any).id}`, book, this.accessToken);
|
|
||||||
} else {
|
|
||||||
await System.authPostToServer('books', book, this.accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also save to local DB for caching
|
|
||||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
|
||||||
await (window as any).electron.dbSaveBook(book, authorId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a book
|
|
||||||
*/
|
|
||||||
static async deleteBook(bookId: string): Promise<void> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Delete from local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbDeleteBook(bookId);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to delete book from local DB');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Delete from server
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
await System.authDeleteToServer(`books/${bookId}`, {}, this.accessToken);
|
|
||||||
|
|
||||||
// Also delete from local DB
|
|
||||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
|
||||||
await (window as any).electron.dbDeleteBook(bookId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== CHAPTER OPERATIONS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save or update a chapter
|
|
||||||
*/
|
|
||||||
static async saveChapter(chapter: ChapterProps, bookId: string, contentId?: string): Promise<void> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Save to local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbSaveChapter(chapter, bookId, contentId);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to save chapter to local DB');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Save to server
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdate = !!chapter.chapterId;
|
|
||||||
if (isUpdate) {
|
|
||||||
await System.authPutToServer(`chapters/${chapter.chapterId}`, chapter, this.accessToken);
|
|
||||||
} else {
|
|
||||||
await System.authPostToServer('chapters', { ...chapter, bookId }, this.accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also save to local DB for caching
|
|
||||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
|
||||||
await (window as any).electron.dbSaveChapter(chapter, bookId, contentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== CHARACTER OPERATIONS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all characters for a book
|
|
||||||
*/
|
|
||||||
static async getCharacters(bookId: string): Promise<CharacterProps[]> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Use local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbGetCharacters(bookId);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to get characters from local DB');
|
|
||||||
}
|
|
||||||
return result.data || [];
|
|
||||||
} else {
|
|
||||||
// Use server API
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await System.authGetQueryToServer<CharacterProps[]>(
|
|
||||||
`characters?bookId=${bookId}`,
|
|
||||||
this.accessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save or update a character
|
|
||||||
*/
|
|
||||||
static async saveCharacter(character: CharacterProps, bookId: string): Promise<void> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Save to local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbSaveCharacter(character, bookId);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to save character to local DB');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Save to server
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdate = !!character.id;
|
|
||||||
if (isUpdate) {
|
|
||||||
await System.authPutToServer(`characters/${character.id}`, character, this.accessToken);
|
|
||||||
} else {
|
|
||||||
await System.authPostToServer('characters', { ...character, bookId }, this.accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also save to local DB for caching
|
|
||||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
|
||||||
await (window as any).electron.dbSaveCharacter(character, bookId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== AI CONVERSATION OPERATIONS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all AI conversations for a book
|
|
||||||
*/
|
|
||||||
static async getConversations(bookId: string): Promise<Conversation[]> {
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Use local database
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
throw new Error('Electron API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbGetConversations(bookId);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || 'Failed to get conversations from local DB');
|
|
||||||
}
|
|
||||||
return result.data || [];
|
|
||||||
} else {
|
|
||||||
// Use server API
|
|
||||||
if (!this.accessToken) {
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await System.authGetQueryToServer<Conversation[]>(
|
|
||||||
`ai/conversations?bookId=${bookId}`,
|
|
||||||
this.accessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save an AI conversation (always saves locally when using AI)
|
|
||||||
*/
|
|
||||||
static async saveConversation(conversation: Conversation, bookId: string): Promise<void> {
|
|
||||||
// Always save AI conversations to local DB first
|
|
||||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
|
||||||
const result = await (window as any).electron.dbSaveConversation(conversation, bookId);
|
|
||||||
if (!result.success) {
|
|
||||||
console.error('Failed to save conversation to local DB:', result.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If online, also sync to server
|
|
||||||
if (!this.isOffline && this.accessToken) {
|
|
||||||
try {
|
|
||||||
const isUpdate = !!conversation.id;
|
|
||||||
if (isUpdate) {
|
|
||||||
await System.authPutToServer(
|
|
||||||
`ai/conversations/${conversation.id}`,
|
|
||||||
conversation,
|
|
||||||
this.accessToken
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await System.authPostToServer(
|
|
||||||
'ai/conversations',
|
|
||||||
{ ...conversation, bookId },
|
|
||||||
this.accessToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to sync conversation to server:', error);
|
|
||||||
// Don't throw - local save succeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== SYNC STATUS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get sync status from local database
|
|
||||||
*/
|
|
||||||
static async getSyncStatus(): Promise<any[]> {
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbGetSyncStatus();
|
|
||||||
if (!result.success) {
|
|
||||||
console.error('Failed to get sync status:', result.error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return result.data || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get pending changes awaiting sync
|
|
||||||
*/
|
|
||||||
static async getPendingChanges(limit: number = 100): Promise<any[]> {
|
|
||||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await (window as any).electron.dbGetPendingChanges(limit);
|
|
||||||
if (!result.success) {
|
|
||||||
console.error('Failed to get pending changes:', result.error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return result.data || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DataService;
|
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
import { BookProps, BookListProps } from '@/lib/models/Book';
|
|
||||||
import { CharacterProps } from '@/lib/models/Character';
|
|
||||||
import { Conversation } from '@/lib/models/QuillSense';
|
|
||||||
import { WorldProps } from '@/lib/models/World';
|
|
||||||
import { ChapterProps } from '@/lib/models/Chapter';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OfflineDataService - Manages data retrieval with offline/online mode
|
|
||||||
* Automatically caches to local DB when online, serves from local DB when offline
|
|
||||||
*/
|
|
||||||
export class OfflineDataService {
|
|
||||||
private isOffline: boolean = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set offline status (called by OfflineProvider)
|
|
||||||
*/
|
|
||||||
setOfflineStatus(offline: boolean): void {
|
|
||||||
this.isOffline = offline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current offline status
|
|
||||||
*/
|
|
||||||
getOfflineStatus(): boolean {
|
|
||||||
return this.isOffline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all books - from local DB if offline, from server otherwise
|
|
||||||
*/
|
|
||||||
async getBooks(fetchFromServer: () => Promise<BookListProps[]>): Promise<BookListProps[]> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await fetchFromServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Fetch from local DB
|
|
||||||
const result = await window.electron.dbGetBooks();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const titles = result.data.map(b => b.title).join(', ');
|
|
||||||
console.log(`📚 Books from LOCAL DB: ${titles}`);
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
throw new Error(result.error || 'Failed to get books from local DB');
|
|
||||||
} else {
|
|
||||||
// Fetch from server and save to local DB
|
|
||||||
const books = await fetchFromServer();
|
|
||||||
const titles = books.map(b => b.title).join(', ');
|
|
||||||
console.log(`📚 Books from SERVER: ${titles}`);
|
|
||||||
|
|
||||||
// Save to local DB in background
|
|
||||||
for (const book of books) {
|
|
||||||
try {
|
|
||||||
await window.electron.dbSaveBook(book);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save book to local DB:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return books;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get single book - from local DB if offline, from server otherwise
|
|
||||||
*/
|
|
||||||
async getBook(
|
|
||||||
bookId: string,
|
|
||||||
fetchFromServer: () => Promise<BookProps>
|
|
||||||
): Promise<BookProps> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await fetchFromServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
const result = await window.electron.dbGetBook(bookId);
|
|
||||||
if (result.success && result.data) {
|
|
||||||
console.log(`📖 "${result.data.title}" from LOCAL DB`);
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
throw new Error(result.error || 'Book not found in local DB');
|
|
||||||
} else {
|
|
||||||
const book = await fetchFromServer();
|
|
||||||
console.log(`📖 "${book.title}" from SERVER`);
|
|
||||||
|
|
||||||
// Save to local DB
|
|
||||||
try {
|
|
||||||
await window.electron.dbSaveBook(book);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save book to local DB:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return book;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new book - creates on server if online, local UUID if offline
|
|
||||||
*/
|
|
||||||
async createBook(
|
|
||||||
bookData: Omit<BookProps, 'bookId'>,
|
|
||||||
authorId: string,
|
|
||||||
createOnServer: () => Promise<string>
|
|
||||||
): Promise<string> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await createOnServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
// Generate local UUID and save to local DB
|
|
||||||
const localBookId = crypto.randomUUID();
|
|
||||||
const book: BookProps = { ...bookData, bookId: localBookId };
|
|
||||||
await window.electron.dbSaveBook(book, authorId);
|
|
||||||
console.log(`💾 Book "${book.title}" created locally (offline mode)`);
|
|
||||||
return localBookId;
|
|
||||||
} else {
|
|
||||||
// Create on server and save to local DB
|
|
||||||
const serverBookId = await createOnServer();
|
|
||||||
const book: BookProps = { ...bookData, bookId: serverBookId };
|
|
||||||
await window.electron.dbSaveBook(book, authorId);
|
|
||||||
return serverBookId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save book - save to local DB and sync to server later if offline
|
|
||||||
*/
|
|
||||||
async saveBook(
|
|
||||||
book: BookProps,
|
|
||||||
authorId: string | undefined,
|
|
||||||
saveToServer: () => Promise<void>
|
|
||||||
): Promise<void> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await saveToServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always save to local DB first
|
|
||||||
await window.electron.dbSaveBook(book, authorId);
|
|
||||||
|
|
||||||
if (!this.isOffline) {
|
|
||||||
// Also save to server
|
|
||||||
try {
|
|
||||||
await saveToServer();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save to server, will sync later:', error);
|
|
||||||
// Data is already in local DB, will be synced later
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`💾 Book queued for sync (offline mode)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get characters for a book
|
|
||||||
*/
|
|
||||||
async getCharacters(
|
|
||||||
bookId: string,
|
|
||||||
fetchFromServer: () => Promise<CharacterProps[]>
|
|
||||||
): Promise<CharacterProps[]> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await fetchFromServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
const result = await window.electron.dbGetCharacters(bookId);
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const names = result.data.map(c => c.name).join(', ');
|
|
||||||
console.log(`👤 Characters from LOCAL DB: ${names}`);
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
throw new Error(result.error || 'Failed to get characters from local DB');
|
|
||||||
} else {
|
|
||||||
const characters = await fetchFromServer();
|
|
||||||
const names = characters.map(c => c.name).join(', ');
|
|
||||||
console.log(`👤 Characters from SERVER: ${names}`);
|
|
||||||
|
|
||||||
// Save to local DB
|
|
||||||
for (const character of characters) {
|
|
||||||
try {
|
|
||||||
await window.electron.dbSaveCharacter(character, bookId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save character to local DB:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return characters;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create character
|
|
||||||
*/
|
|
||||||
async createCharacter(
|
|
||||||
characterData: Omit<CharacterProps, 'id'>,
|
|
||||||
bookId: string,
|
|
||||||
createOnServer: () => Promise<string>
|
|
||||||
): Promise<string> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await createOnServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
const localId = crypto.randomUUID();
|
|
||||||
const character = { ...characterData, id: localId };
|
|
||||||
await window.electron.dbSaveCharacter(character, bookId);
|
|
||||||
console.log(`💾 Character "${character.name}" created locally (offline mode)`);
|
|
||||||
return localId;
|
|
||||||
} else {
|
|
||||||
const serverId = await createOnServer();
|
|
||||||
const character = { ...characterData, id: serverId };
|
|
||||||
await window.electron.dbSaveCharacter(character, bookId);
|
|
||||||
return serverId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save character
|
|
||||||
*/
|
|
||||||
async saveCharacter(
|
|
||||||
character: CharacterProps,
|
|
||||||
bookId: string,
|
|
||||||
saveToServer: () => Promise<void>
|
|
||||||
): Promise<void> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await saveToServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always save to local DB first
|
|
||||||
await window.electron.dbSaveCharacter(character, bookId);
|
|
||||||
|
|
||||||
if (!this.isOffline) {
|
|
||||||
try {
|
|
||||||
await saveToServer();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save to server, will sync later:', error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`💾 Character queued for sync (offline mode)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get conversations for a book
|
|
||||||
*/
|
|
||||||
async getConversations(
|
|
||||||
bookId: string,
|
|
||||||
fetchFromServer: () => Promise<Conversation[]>
|
|
||||||
): Promise<Conversation[]> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await fetchFromServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
const result = await window.electron.dbGetConversations(bookId);
|
|
||||||
if (result.success) {
|
|
||||||
const titles = result.data?.map((c: any) => c.title).join(', ') || 'none';
|
|
||||||
console.log(`💬 Conversations from LOCAL DB: ${titles}`);
|
|
||||||
return result.data || [];
|
|
||||||
}
|
|
||||||
throw new Error(result.error || 'Failed to get conversations from local DB');
|
|
||||||
} else {
|
|
||||||
const conversations = await fetchFromServer();
|
|
||||||
const titles = conversations.map(c => c.title).join(', ');
|
|
||||||
console.log(`💬 Conversations from SERVER: ${titles}`);
|
|
||||||
|
|
||||||
// Save to local DB
|
|
||||||
for (const conversation of conversations) {
|
|
||||||
try {
|
|
||||||
await window.electron.dbSaveConversation(conversation, bookId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save conversation to local DB:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create conversation
|
|
||||||
*/
|
|
||||||
async createConversation(
|
|
||||||
conversationData: Omit<Conversation, 'id'>,
|
|
||||||
bookId: string,
|
|
||||||
createOnServer: () => Promise<string>
|
|
||||||
): Promise<string> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await createOnServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
const localId = crypto.randomUUID();
|
|
||||||
const conversation = { ...conversationData, id: localId };
|
|
||||||
await window.electron.dbSaveConversation(conversation, bookId);
|
|
||||||
console.log(`💾 Conversation "${conversation.title}" created locally (offline mode)`);
|
|
||||||
return localId;
|
|
||||||
} else {
|
|
||||||
const serverId = await createOnServer();
|
|
||||||
const conversation = { ...conversationData, id: serverId };
|
|
||||||
await window.electron.dbSaveConversation(conversation, bookId);
|
|
||||||
return serverId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save conversation
|
|
||||||
*/
|
|
||||||
async saveConversation(
|
|
||||||
conversation: Conversation,
|
|
||||||
bookId: string,
|
|
||||||
saveToServer: () => Promise<void>
|
|
||||||
): Promise<void> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await saveToServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always save to local DB first
|
|
||||||
await window.electron.dbSaveConversation(conversation, bookId);
|
|
||||||
|
|
||||||
if (!this.isOffline) {
|
|
||||||
try {
|
|
||||||
await saveToServer();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save to server, will sync later:', error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`💾 Conversation queued for sync (offline mode)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create chapter
|
|
||||||
*/
|
|
||||||
async createChapter(
|
|
||||||
chapterData: Omit<ChapterProps, 'chapterId'>,
|
|
||||||
bookId: string,
|
|
||||||
createOnServer: () => Promise<string>
|
|
||||||
): Promise<string> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await createOnServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOffline) {
|
|
||||||
const localId = crypto.randomUUID();
|
|
||||||
const chapter = { ...chapterData, chapterId: localId };
|
|
||||||
await window.electron.dbSaveChapter(chapter, bookId);
|
|
||||||
console.log(`💾 Chapter "${chapter.title}" created locally (offline mode)`);
|
|
||||||
return localId;
|
|
||||||
} else {
|
|
||||||
const serverId = await createOnServer();
|
|
||||||
const chapter = { ...chapterData, chapterId: serverId };
|
|
||||||
await window.electron.dbSaveChapter(chapter, bookId);
|
|
||||||
return serverId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save chapter content
|
|
||||||
*/
|
|
||||||
async saveChapter(
|
|
||||||
chapter: ChapterProps,
|
|
||||||
bookId: string,
|
|
||||||
saveToServer: () => Promise<void>
|
|
||||||
): Promise<void> {
|
|
||||||
if (!window.electron) {
|
|
||||||
return await saveToServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always save to local DB first
|
|
||||||
await window.electron.dbSaveChapter(chapter, bookId);
|
|
||||||
|
|
||||||
if (!this.isOffline) {
|
|
||||||
try {
|
|
||||||
await saveToServer();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save to server, will sync later:', error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`💾 Chapter "${chapter.title}" queued for sync (offline mode)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Singleton instance
|
|
||||||
let offlineDataServiceInstance: OfflineDataService | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get OfflineDataService singleton instance
|
|
||||||
*/
|
|
||||||
export function getOfflineDataService(): OfflineDataService {
|
|
||||||
if (!offlineDataServiceInstance) {
|
|
||||||
offlineDataServiceInstance = new OfflineDataService();
|
|
||||||
}
|
|
||||||
return offlineDataServiceInstance;
|
|
||||||
}
|
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"public/**/*",
|
"public/**/*",
|
||||||
"fonts/**/*",
|
"fonts/**/*",
|
||||||
"electron/**/*",
|
"electron/**/*",
|
||||||
|
"electron.d.ts",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts"
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user