From d018e75be4898be6b5a57bc96406d9087af6e10b Mon Sep 17 00:00:00 2001 From: natreex Date: Tue, 18 Nov 2025 21:02:38 -0500 Subject: [PATCH] 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. --- components/book/AddNewBookForm.tsx | 2 +- components/book/BookList.tsx | 13 +- electron.d.ts | 47 ++-- electron/database/LocalSystem.ts | 298 +++++--------------- electron/database/models/Book.ts | 2 +- electron/ipc/book.ipc.ts | 176 +++++++----- electron/main.ts | 3 + electron/preload.ts | 30 +- lib/services/data.service.ts | 366 ------------------------- lib/services/offline-data.service.ts | 395 --------------------------- tsconfig.json | 1 + 11 files changed, 222 insertions(+), 1111 deletions(-) delete mode 100644 lib/services/data.service.ts delete mode 100644 lib/services/offline-data.service.ts diff --git a/components/book/AddNewBookForm.tsx b/components/book/AddNewBookForm.tsx index 961d10b..2e4df44 100644 --- a/components/book/AddNewBookForm.tsx +++ b/components/book/AddNewBookForm.tsx @@ -27,7 +27,7 @@ import GuideTour, {GuideStep} from "@/components/GuideTour"; import {UserProps} from "@/lib/models/User"; import {useTranslations} from "next-intl"; 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 { min: number; diff --git a/components/book/BookList.tsx b/components/book/BookList.tsx index a2e1972..9c918c9 100644 --- a/components/book/BookList.tsx +++ b/components/book/BookList.tsx @@ -13,7 +13,7 @@ import GuideTour, {GuideStep} from "@/components/GuideTour"; import User from "@/lib/models/User"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; -import {getOfflineDataService} from "@/lib/services/offline-data.service"; +import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; export default function BookList() { const {session, setSession} = useContext(SessionContext); @@ -22,6 +22,7 @@ export default function BookList() { const {setBook} = useContext(BookContext); const t = useTranslations(); const {lang} = useContext(LangContext) + const {isCurrentlyOffline} = useContext(OfflineContext) const [searchQuery, setSearchQuery] = useState(''); const [groupedBooks, setGroupedBooks] = useState>({}); @@ -114,10 +115,12 @@ export default function BookList() { async function getBooks(): Promise { setIsLoadingBooks(true); try { - const offlineDataService = getOfflineDataService(); - const bookResponse: BookListProps[] = await offlineDataService.getBooks( - () => System.authGetQueryToServer('books', accessToken, lang) - ); + let bookResponse: BookListProps[] = []; + if (!isCurrentlyOffline()) { + bookResponse = await System.authGetQueryToServer('books', accessToken, lang); + } else { + bookResponse = await window.electron.invoke('db:book:books'); + } if (bookResponse) { const booksByType: Record = bookResponse.reduce((groups: Record, book: BookListProps): Record => { const imageDataUrl: string = book.coverImage ? 'data:image/jpeg;base64,' + book.coverImage : ''; diff --git a/electron.d.ts b/electron.d.ts index fe7c946..1a7f276 100644 --- a/electron.d.ts +++ b/electron.d.ts @@ -1,27 +1,38 @@ +/** + * TypeScript declarations for window.electron API + * Must match exactly with electron/preload.ts + * + * Usage: + * - Use invoke(channel, ...args) for all IPC calls + * - Shortcuts are provided for common operations (tokens, lang, encryption) + */ export interface IElectronAPI { + // Platform info platform: NodeJS.Platform; + + // Generic invoke method - use this for all IPC calls + invoke: (channel: string, ...args: any[]) => Promise; + + // Token management (shortcuts for convenience) getToken: () => Promise; - setToken: (token: string) => Promise; - removeToken: () => Promise; - loginSuccess: (token: string) => void; + setToken: (token: string) => Promise; + removeToken: () => Promise; + + // Language management (shortcuts for convenience) + getLang: () => Promise<'fr' | 'en'>; + setLang: (lang: 'fr' | 'en') => Promise; + + // Auth events (one-way communication) + loginSuccess: (token: string, userId: string) => void; logout: () => void; - // Database operations - generateEncryptionKey: (userId: string) => Promise<{ success: boolean; key?: string; error?: string }>; + // Encryption key management (shortcuts for convenience) + generateEncryptionKey: (userId: string) => Promise; getUserEncryptionKey: (userId: string) => Promise; - setUserEncryptionKey: (userId: string, encryptionKey: string) => Promise; - dbInitialize: (userId: string, encryptionKey: string) => Promise<{ success: boolean; error?: string }>; - dbGetBooks: () => Promise<{ success: boolean; data?: any[]; error?: string }>; - dbGetBook: (bookId: string) => Promise<{ success: boolean; data?: any; error?: string }>; - 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 }>; + setUserEncryptionKey: (userId: string, encryptionKey: string) => Promise; + + // Database initialization (shortcut for convenience) + dbInitialize: (userId: string, encryptionKey: string) => Promise; } declare global { diff --git a/electron/database/LocalSystem.ts b/electron/database/LocalSystem.ts index 24e226a..a0dccbd 100644 --- a/electron/database/LocalSystem.ts +++ b/electron/database/LocalSystem.ts @@ -1,15 +1,19 @@ import type { IpcMainInvokeEvent } from 'electron'; 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({ encryptionKey: 'eritors-scribe-secure-key' }); -// ============================================================ -// SESSION MANAGEMENT - Retrieve userId and lang from store -// ============================================================ - /** * Get userId from electron-store * Set during login via 'login-success' event @@ -27,122 +31,67 @@ function getLangFromSession(): 'fr' | 'en' { } // ============================================================ -// LEGACY HANDLERS - Manual userId injection, lang must be passed -// Keep these for backward compatibility -// Updated to support Promises -// ============================================================ - -export function createDbHandler( - handler: (userId: string) => TReturn | Promise -): (event: IpcMainInvokeEvent) => Promise { - return async function(event: IpcMainInvokeEvent): Promise { - 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( - handler: (userId: string, arg1: T1) => TReturn | Promise -): (event: IpcMainInvokeEvent, arg1: T1) => Promise { - return async function(event: IpcMainInvokeEvent, arg1: T1): Promise { - 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( - handler: (userId: string, arg1: T1, arg2: T2) => TReturn | Promise -): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2) => Promise { - return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2): Promise { - 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( - handler: (userId: string, arg1: T1, arg2: T2, arg3: T3) => TReturn | Promise -): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3) => Promise { - return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3): Promise { - 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 +// UNIVERSAL HANDLER - Like a Fastify route +// Automatically injects: userId, lang +// Optional body parameter (for GET, POST, PUT, DELETE) +// Generic return type (void, object, etc.) // ============================================================ /** - * Auto-handler with 0 parameters - * Automatically injects: userId, lang + * Universal IPC handler - works like a Fastify route + * 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 - * ipcMain.handle('db:user:get', createAutoHandler( - * function(userId: string, lang: 'fr' | 'en') { - * return User.getUser(userId, lang); - * } - * )); + * // GET with no params + * ipcMain.handle('db:books:getAll', + * createHandler( + * async (userId, body, lang) => { + * return await Book.getBooks(userId, lang); + * } + * ) + * ); + * // Frontend: invoke('db:books:getAll') * - * // Frontend call (no params needed): - * const user = await window.electron.invoke('db:user:get'); + * @example + * // GET with 1 param + * ipcMain.handle('db:book:get', + * createHandler( + * 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( + * 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( + * async (userId, bookId, lang) => { + * await Book.deleteBook(bookId, userId, lang); + * } + * ) + * ); + * // Frontend: invoke('db:book:delete', bookId) */ -export function createAutoHandler( - handler: (userId: string, lang: 'fr' | 'en') => TReturn | Promise -): (event: IpcMainInvokeEvent) => Promise { - return async function(event: IpcMainInvokeEvent): Promise { +export function createHandler( + handler: (userId: string, body: TBody, lang: 'fr' | 'en') => TReturn | Promise +): (event: IpcMainInvokeEvent, body?: TBody) => Promise { + return async function(event: IpcMainInvokeEvent, body?: TBody): Promise { const userId = getUserIdFromSession(); const lang = getLangFromSession(); @@ -151,7 +100,7 @@ export function createAutoHandler( } try { - return await handler(userId, lang); + return await handler(userId, body as TBody, lang); } catch (error: unknown) { if (error instanceof Error) { console.error(`[DB] ${error.message}`); @@ -161,122 +110,3 @@ export function createAutoHandler( } }; } - -/** - * Auto-handler with 1 parameter - * Automatically injects: userId, lang - * - * @example - * ipcMain.handle('db:book:get', createAutoHandler1( - * 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( - handler: (userId: string, arg1: T1, lang: 'fr' | 'en') => TReturn | Promise -): (event: IpcMainInvokeEvent, arg1: T1) => Promise { - return async function(event: IpcMainInvokeEvent, arg1: T1): Promise { - 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( - * 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( - handler: (userId: string, arg1: T1, arg2: T2, lang: 'fr' | 'en') => TReturn | Promise -): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2) => Promise { - return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2): Promise { - 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( - * 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( - handler: (userId: string, arg1: T1, arg2: T2, arg3: T3, lang: 'fr' | 'en') => TReturn | Promise -): (event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3) => Promise { - return async function(event: IpcMainInvokeEvent, arg1: T1, arg2: T2, arg3: T3): Promise { - 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(); -} diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index 41e8882..82a46bf 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -254,7 +254,7 @@ export default class Book { 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 { + public static async getBook(userId:string,bookId: string): Promise { const book:Book = new Book(bookId); await book.getBookInfos(userId); return { diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts index 3be3ff2..882e145 100644 --- a/electron/ipc/book.ipc.ts +++ b/electron/ipc/book.ipc.ts @@ -1,34 +1,30 @@ import { ipcMain } from 'electron'; -import { - createDbHandler2, - // Auto-handlers: automatically inject userId AND lang - createAutoHandler, - createAutoHandler1 -} from '../database/LocalSystem.js'; +import { createHandler } from '../database/LocalSystem.js'; import Book from '../database/models/Book.js'; import type { BookProps, GuideLine, GuideLineAI, Act, Issue, WorldProps } from '../database/models/Book.js'; -ipcMain.handle( - 'db:book:getAll', - createAutoHandler( - async function(userId: string, lang: 'fr' | 'en') { +// ============================================================ +// 1. GET /books - Get all books +// ============================================================ +ipcMain.handle('db:book:books', createHandler( + async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise { return await Book.getBooks(userId, lang); } ) ); -ipcMain.handle( - 'db:book:get', - createAutoHandler1( - async function(userId: string, bookId: string, lang: 'fr' | 'en') { - return await Book.getBook(bookId, userId, lang); +// ============================================================ +// 2. GET /book/:id - Get single book +// ============================================================ +ipcMain.handle('db:book:bookBasicInformation', createHandler( + async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise { + 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 { title: string; @@ -41,7 +37,7 @@ interface UpdateBookBasicData { ipcMain.handle( 'db:book:updateBasicInformation', - createAutoHandler1( + createHandler( function(userId: string, data: UpdateBookBasicData, lang: 'fr' | 'en') { return Book.updateBookBasicInformation( 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( 'db:book:guideline:get', - createAutoHandler1( + createHandler( async function(userId: string, bookId: string, lang: 'fr' | 'en') { 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 { bookId: string; @@ -89,7 +86,7 @@ interface UpdateGuideLineData { ipcMain.handle( 'db:book:guideline:update', - createAutoHandler1( + createHandler( async function(userId: string, data: UpdateGuideLineData, lang: 'fr' | 'en') { return await Book.updateGuideLine( 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 { acts: Act[]; @@ -120,7 +118,7 @@ interface StoryData { ipcMain.handle( 'db:book:story:get', - createDbHandler2( + createHandler( async function(userId: string, bookId: string, lang: 'fr' | 'en') { const acts = await Book.getActsData(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 // ============================================================ // interface StoryUpdateData { @@ -144,15 +143,16 @@ ipcMain.handle( // // ipcMain.handle( // 'db:book:story:update', -// createDbHandler2( +// createHandler( // function(userId: string, data: StoryUpdateData, lang: 'fr' | 'en') { // 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 { title: string; @@ -166,7 +166,7 @@ interface CreateBookData { ipcMain.handle( 'db:book:create', - createDbHandler2( + createHandler( function(userId: string, data: CreateBookData, lang: 'fr' | 'en') { return Book.addBook( 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 +// ============================================================ +// interface UpdateCoverData { +// bookId: string; +// coverImageName: string; +// } +// // ipcMain.handle( // 'db:book:cover:update', -// createDbHandler3( -// function(userId: string, bookId: string, coverImageName: string, lang: 'fr' | 'en') { -// return Book.updateBookCover(userId, bookId, coverImageName, lang); +// createHandler( +// function(userId: string, data: UpdateCoverData, lang: 'fr' | 'en') { +// 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 { bookId: string; @@ -207,15 +214,16 @@ interface AddIncidentData { ipcMain.handle( 'db:book:incident:add', - createDbHandler2( + createHandler( function(userId: string, data: AddIncidentData, lang: 'fr' | 'en') { 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 { bookId: string; @@ -224,15 +232,16 @@ interface RemoveIncidentData { ipcMain.handle( 'db:book:incident:remove', - createDbHandler2( + createHandler( function(userId: string, data: RemoveIncidentData, lang: 'fr' | 'en') { 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 { bookId: string; @@ -242,7 +251,7 @@ interface AddPlotPointData { ipcMain.handle( 'db:book:plot:add', - createDbHandler2( + createHandler( function(userId: string, data: AddPlotPointData, lang: 'fr' | 'en') { return Book.addNewPlotPoint( 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( 'db:book:plot:remove', - createDbHandler2( + createHandler( function(userId: string, plotPointId: string, lang: 'fr' | 'en') { 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 { bookId: string; @@ -277,39 +288,42 @@ interface AddIssueData { ipcMain.handle( 'db:book:issue:add', - createDbHandler2( + createHandler( function(userId: string, data: AddIssueData, lang: 'fr' | 'en') { 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( 'db:book:issue:remove', - createDbHandler2( + createHandler( function(userId: string, issueId: string, lang: 'fr' | 'en') { 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( 'db:book:worlds:get', - createDbHandler2( + createHandler( function(userId: string, bookId: string, lang: 'fr' | 'en') { 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 { bookId: string; @@ -318,15 +332,16 @@ interface AddWorldData { ipcMain.handle( 'db:book:world:add', - createDbHandler2( + createHandler( function(userId: string, data: AddWorldData, lang: 'fr' | 'en') { 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 { worldId: string; @@ -336,7 +351,7 @@ interface AddWorldElementData { ipcMain.handle( 'db:book:world:element:add', - createDbHandler2( + createHandler( function(userId: string, data: AddWorldElementData, lang: 'fr' | 'en') { return Book.addNewElementToWorld( 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( 'db:book:world:element:remove', - createDbHandler2( + createHandler( function(userId: string, elementId: string, lang: 'fr' | 'en') { 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 // ============================================================ // ipcMain.handle( // 'db:book:world:update', -// createDbHandler2( +// createHandler( // function(userId: string, world: WorldProps, lang: 'fr' | 'en') { // 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 // ============================================================ // ipcMain.handle( // 'db:book:cover:delete', -// createDbHandler2( +// createHandler( // function(userId: string, bookId: string, lang: 'fr' | 'en') { // 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( 'db:book:delete', - createDbHandler2( + createHandler( function(userId: string, bookId: string, lang: 'fr' | 'en') { 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( 'db:book:guideline:ai:get', - createDbHandler2( + createHandler( function(userId: string, bookId: string, lang: 'fr' | 'en') { 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 { bookId: string; @@ -427,7 +448,7 @@ interface SetAIGuideLineData { ipcMain.handle( 'db:book:guideline:ai:set', - createDbHandler2( + createHandler( function(userId: string, data: SetAIGuideLineData, lang: 'fr' | 'en') { return Book.setAIGuideLine( 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 +// ============================================================ // ipcMain.handle( // 'db:book:export:epub', -// createDbHandler2( +// createHandler( // function(userId: string, bookId: string, lang: 'fr' | 'en') { // 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 +// ============================================================ // ipcMain.handle( // 'db:book:export:pdf', -// createDbHandler2( +// createHandler( // function(userId: string, bookId: string, lang: 'fr' | 'en') { // 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 +// ============================================================ // ipcMain.handle( // 'db:book:export:docx', -// createDbHandler2( +// createHandler( // function(userId: string, bookId: string, lang: 'fr' | 'en') { // 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 // ============================================================ // interface BookTags { @@ -497,11 +522,12 @@ ipcMain.handle( // // ipcMain.handle( // 'db:book:tags:get', -// createDbHandler2( +// createHandler( // function(userId: string, bookId: string, lang: 'fr' | 'en') { // return Book.getTagsFromBook(userId, bookId, lang); // } // ) // ); +// // Frontend: invoke('db:book:tags:get', bookId) console.log('[IPC] Book handlers registered'); diff --git a/electron/main.ts b/electron/main.ts index eacf8fb..39e9f1f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -6,6 +6,9 @@ import Store from 'electron-store'; import * as fs from 'fs'; import { getDatabaseService } from './database/database.service.js'; +// Import IPC handlers +import './ipc/book.ipc.js'; + // Fix pour __dirname en ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/electron/preload.ts b/electron/preload.ts index d39cafa..e486a44 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,36 +1,34 @@ 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', { + // Platform info 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'), setToken: (token: string) => ipcRenderer.invoke('set-token', token), removeToken: () => ipcRenderer.invoke('remove-token'), - // Language management + // Language management (shortcuts for convenience) getLang: () => ipcRenderer.invoke('get-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), logout: () => ipcRenderer.send('logout'), - // Database operations + // Encryption key management (shortcuts for convenience) generateEncryptionKey: (userId: string) => ipcRenderer.invoke('generate-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), + + // Database initialization (shortcut for convenience) 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), }); diff --git a/lib/services/data.service.ts b/lib/services/data.service.ts deleted file mode 100644 index bc03992..0000000 --- a/lib/services/data.service.ts +++ /dev/null @@ -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 { - 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( - 'books', - this.accessToken - ); - - return response.data || []; - } - } - - /** - * Get a single book with all data - */ - static async getBook(bookId: string): Promise { - 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( - `books/${bookId}`, - this.accessToken - ); - - return response.data || null; - } - } - - /** - * Save or update a book - */ - static async saveBook(book: BookProps | BookListProps, authorId?: string): Promise { - 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 { - 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 { - 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 { - 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( - `characters?bookId=${bookId}`, - this.accessToken - ); - - return response.data || []; - } - } - - /** - * Save or update a character - */ - static async saveCharacter(character: CharacterProps, bookId: string): Promise { - 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 { - 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( - `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 { - // 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 { - 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 { - 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; diff --git a/lib/services/offline-data.service.ts b/lib/services/offline-data.service.ts deleted file mode 100644 index 56b87cd..0000000 --- a/lib/services/offline-data.service.ts +++ /dev/null @@ -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): Promise { - 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 - ): Promise { - 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, - authorId: string, - createOnServer: () => Promise - ): Promise { - 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 - ): Promise { - 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 - ): Promise { - 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, - bookId: string, - createOnServer: () => Promise - ): Promise { - 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 - ): Promise { - 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 - ): Promise { - 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, - bookId: string, - createOnServer: () => Promise - ): Promise { - 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 - ): Promise { - 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, - bookId: string, - createOnServer: () => Promise - ): Promise { - 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 - ): Promise { - 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; -} diff --git a/tsconfig.json b/tsconfig.json index 8cdf189..a727c4a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,6 +47,7 @@ "public/**/*", "fonts/**/*", "electron/**/*", + "electron.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ],