From 64c7cb624364f0eec8a6d494bb9fd58ce0f2b5d4 Mon Sep 17 00:00:00 2001 From: natreex Date: Mon, 15 Dec 2025 20:55:24 -0500 Subject: [PATCH] Update database schema and synchronization logic - Add `useEffect` in `ScribeLeftBar` for handling book state changes. - Extend `BooksSyncContext` with new properties and stricter typings. - Refine `Repositories` to include `lastUpdate` handling for synchronization processes. - Add comprehensive `fetchComplete*` repository methods for retrieving entity-specific sync data. - Enhance offline logic for chapters, characters, locations, and world synchronization. - Improve error handling across IPC handlers and repositories. --- app/page.tsx | 49 +- components/ScribeControllerBar.tsx | 4 +- components/ScribeFooterBar.tsx | 5 +- components/SyncBook.tsx | 64 ++- components/book/BookList.tsx | 6 +- components/leftbar/ScribeChapterComponent.tsx | 1 + components/leftbar/ScribeLeftBar.tsx | 10 +- components/rightbar/ComposerRightBar.tsx | 11 +- context/BooksSyncContext.ts | 12 +- electron/database/models/Book.ts | 478 +++++++++++++++++- electron/database/models/Chapter.ts | 6 +- electron/database/models/Character.ts | 7 +- electron/database/models/Location.ts | 6 +- .../database/repositories/book.repository.ts | 327 +++++++++++- .../repositories/chapter.repository.ts | 17 +- .../repositories/character.repository.ts | 19 +- .../repositories/location.repository.ts | 12 +- electron/ipc/book.ipc.ts | 18 +- lib/errors.ts | 14 + lib/models/Book.ts | 8 +- lib/models/BookTables.ts | 21 +- lib/models/SyncedBook.ts | 228 +++++++++ lib/utils/syncComparison.ts | 365 +++++++++++++ 23 files changed, 1609 insertions(+), 79 deletions(-) create mode 100644 lib/errors.ts create mode 100644 lib/utils/syncComparison.ts diff --git a/app/page.tsx b/app/page.tsx index 8d35966..41398bf 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -33,7 +33,7 @@ import OfflineProvider from "@/context/OfflineProvider"; import OfflineContext from "@/context/OfflineContext"; import OfflinePinSetup from "@/components/offline/OfflinePinSetup"; import OfflinePinVerify from "@/components/offline/OfflinePinVerify"; -import {SyncedBook} from "@/lib/models/SyncedBook"; +import {SyncedBook, BookSyncCompare, compareBookSyncs} from "@/lib/models/SyncedBook"; import {BooksSyncContext} from "@/context/BooksSyncContext"; const messagesMap = { @@ -64,8 +64,8 @@ function ScribeContent() { const [serverSyncedBooks, setServerSyncedBooks] = useState([]); const [localSyncedBooks, setLocalSyncedBooks] = useState([]); - const [booksToSyncFromServer, setBooksToSyncFromServer] = useState([]); - const [booksToSyncToServer, setBooksToSyncToServer] = useState([]); + const [bookSyncDiffsFromServer, setBookSyncDiffsFromServer] = useState([]); + const [bookSyncDiffsToServer, setBookSyncDiffsToServer] = useState([]); const [serverOnlyBooks, setServerOnlyBooks] = useState([]); const [localOnlyBooks, setLocalOnlyBooks] = useState([]); @@ -165,17 +165,36 @@ function ScribeContent() { } }, [currentBook]); - useEffect(():void => { - setBooksToSyncFromServer(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => { - const localBook: SyncedBook | undefined = localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id); - console.log('localBook from setBookToSyncFromServer',localBook); - console.log('serverBook from setBookToSyncFromServer',serverBook); - return !localBook || localBook.lastUpdate < serverBook.lastUpdate; - })) - setBooksToSyncToServer(localSyncedBooks.filter((localBook: SyncedBook):boolean => { - const serverBook: SyncedBook | undefined = serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id); - return !serverBook || serverBook.lastUpdate < localBook.lastUpdate; - })) + useEffect((): void => { + const diffsFromServer: BookSyncCompare[] = []; + const diffsToServer: BookSyncCompare[] = []; + + serverSyncedBooks.forEach((serverBook: SyncedBook): void => { + const localBook: SyncedBook | undefined = localSyncedBooks.find((book: SyncedBook): boolean => book.id === serverBook.id); + if (!localBook) { + return; + } + + const diff: BookSyncCompare | null = compareBookSyncs(serverBook, localBook); + if (diff) { + diffsFromServer.push(diff); + } + }); + + localSyncedBooks.forEach((localBook: SyncedBook): void => { + const serverBook: SyncedBook | undefined = serverSyncedBooks.find((book: SyncedBook): boolean => book.id === localBook.id); + if (!serverBook) { + return; + } + + const diff: BookSyncCompare | null = compareBookSyncs(localBook, serverBook); + if (diff) { + diffsToServer.push(diff); + } + }); + + setBookSyncDiffsFromServer(diffsFromServer); + setBookSyncDiffsToServer(diffsToServer); setServerOnlyBooks(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => !localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id))) setLocalOnlyBooks(localSyncedBooks.filter((localBook: SyncedBook):boolean => !serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id))) }, [localSyncedBooks, serverSyncedBooks]); @@ -488,7 +507,7 @@ function ScribeContent() { return ( - + (LangContext); const {isCurrentlyOffline} = useContext(OfflineContext) + const {serverOnlyBooks,localOnlyBooks} = useContext(BooksSyncContext); const isGPTEnabled: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session); const isGemini: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session); @@ -120,7 +122,7 @@ export default function ScribeControllerBar() {
getBook(e.target.value)} - data={Book.booksToSelectBox(session.user?.books ?? [])} defaultValue={book?.bookId} + data={Book.booksToSelectBox([...serverOnlyBooks, ...localOnlyBooks])} defaultValue={book?.bookId} placeholder={t("controllerBar.selectBook")}/>
{chapter && ( diff --git a/components/ScribeFooterBar.tsx b/components/ScribeFooterBar.tsx index cbbd285..777926d 100644 --- a/components/ScribeFooterBar.tsx +++ b/components/ScribeFooterBar.tsx @@ -8,14 +8,15 @@ import {SessionContext} from "@/context/SessionContext"; import {useTranslations} from "next-intl"; import {AlertContext} from "@/context/AlertContext"; import {BookContext} from "@/context/BookContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; export default function ScribeFooterBar() { const t = useTranslations(); const {chapter} = useContext(ChapterContext); const {book} = useContext(BookContext); const editor: Editor | null = useContext(EditorContext).editor; - const {session} = useContext(SessionContext); const {errorMessage} = useContext(AlertContext) + const {serverOnlyBooks,localOnlyBooks} = useContext(BooksSyncContext); const [wordsCount, setWordsCount] = useState(0); @@ -91,7 +92,7 @@ export default function ScribeFooterBar() { className="flex items-center gap-2 bg-secondary/50 px-4 py-2 rounded-xl border border-secondary shadow-sm"> {t('scribeFooterBar.books')}: - {session.user?.books?.length} + {(serverOnlyBooks.length+localOnlyBooks.length)} ) diff --git a/components/SyncBook.tsx b/components/SyncBook.tsx index 8aba075..5751c69 100644 --- a/components/SyncBook.tsx +++ b/components/SyncBook.tsx @@ -7,8 +7,9 @@ import System from "@/lib/models/System"; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {LangContext} from "@/context/LangContext"; import {CompleteBook} from "@/lib/models/Book"; -import {SyncType} from "@/context/BooksSyncContext"; +import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; +import {BookSyncCompare} from "@/lib/models/SyncedBook"; interface SyncBookProps { bookId: string; @@ -23,6 +24,7 @@ export default function SyncBook({bookId, status}: SyncBookProps) { const {isCurrentlyOffline} = useContext(OfflineContext); const [isLoading, setIsLoading] = useState(false); const [currentStatus, setCurrentStatus] = useState(status); + const {booksToSyncToServer, booksToSyncFromServer} = useContext(BooksSyncContext) const isOffline: boolean = isCurrentlyOffline(); @@ -53,11 +55,67 @@ export default function SyncBook({bookId, status}: SyncBookProps) { } async function syncFromServer(): Promise { - // TODO: Implement sync from server (server has newer version) + if (isOffline) { + return; + } + try { + const bookToFetch:BookSyncCompare|undefined = booksToSyncFromServer.find((book:BookSyncCompare):boolean => book.id === bookId); + if (!bookToFetch) { + errorMessage(t("bookCard.syncFromServerError")); + return; + } + const response: CompleteBook = await System.authPostToServer('book/sync/server-to-client', { + bookToSync: bookToFetch + }, session.accessToken, lang); + if (!response) { + errorMessage(t("bookCard.syncFromServerError")); + return; + } + const syncStatus:boolean = await window.electron.invoke('db:book:sync:toClient', response); + if (!syncStatus) { + errorMessage(t("bookCard.syncFromServerError")); + return; + } + setCurrentStatus('synced'); + } catch (e:unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } else { + errorMessage(t("bookCard.syncFromServerError")); + } + } } async function syncToServer(): Promise { - // TODO: Implement sync to server (local has newer version) + if (isOffline) { + return; + } + try { + const bookToFetch:BookSyncCompare|undefined = booksToSyncToServer.find((book:BookSyncCompare):boolean => book.id === bookId); + if (!bookToFetch) { + errorMessage(t("bookCard.syncToServerError")); + return; + } + const bookToSync: CompleteBook = await window.electron.invoke('db:book:sync:toServer', bookToFetch); + if (!bookToSync) { + errorMessage(t("bookCard.syncToServerError")); + return; + } + const response: boolean = await System.authPutToServer('book/sync/client-to-server', { + book: bookToSync + }, session.accessToken, lang); + if (!response) { + errorMessage(t("bookCard.syncToServerError")); + return; + } + setCurrentStatus('synced'); + } catch (e:unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } else { + errorMessage(t("bookCard.syncToServerError")); + } + } } if (isLoading) { diff --git a/components/book/BookList.tsx b/components/book/BookList.tsx index d554747..d877539 100644 --- a/components/book/BookList.tsx +++ b/components/book/BookList.tsx @@ -15,7 +15,7 @@ import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext"; -import {SyncedBook} from "@/lib/models/SyncedBook"; +import {BookSyncCompare, SyncedBook} from "@/lib/models/SyncedBook"; export default function BookList() { const {session, setSession} = useContext(SessionContext); @@ -190,10 +190,10 @@ export default function BookList() { if (localOnlyBooks.find((book: SyncedBook):boolean => book.id === bookId)) { return 'local-only'; } - if (booksToSyncFromServer.find((book: SyncedBook):boolean => book.id === bookId)) { + if (booksToSyncFromServer.find((book: BookSyncCompare):boolean => book.id === bookId)) { return 'to-sync-from-server'; } - if (booksToSyncToServer.find((book: SyncedBook):boolean => book.id === bookId)) { + if (booksToSyncToServer.find((book: BookSyncCompare):boolean => book.id === bookId)) { return 'to-sync-to-server'; } return 'synced'; diff --git a/components/leftbar/ScribeChapterComponent.tsx b/components/leftbar/ScribeChapterComponent.tsx index 759d284..b893788 100644 --- a/components/leftbar/ScribeChapterComponent.tsx +++ b/components/leftbar/ScribeChapterComponent.tsx @@ -36,6 +36,7 @@ export default function ScribeChapterComponent() { const scrollContainerRef = useRef(null); useEffect((): void => { + if (book) getChapterList().then(); }, [book]); diff --git a/components/leftbar/ScribeLeftBar.tsx b/components/leftbar/ScribeLeftBar.tsx index 6aa8d7d..7d031b4 100644 --- a/components/leftbar/ScribeLeftBar.tsx +++ b/components/leftbar/ScribeLeftBar.tsx @@ -1,6 +1,6 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faBookMedical, faBookOpen, faFeather} from "@fortawesome/free-solid-svg-icons"; -import {useContext, useState} from "react"; +import {useContext, useEffect, useState} from "react"; import {BookContext} from "@/context/BookContext"; import ScribeChapterComponent from "@/components/leftbar/ScribeChapterComponent"; import PanelHeader from "@/components/PanelHeader"; @@ -75,6 +75,14 @@ export default function ScribeLeftBar() { } } + useEffect(():void => { + if (!book){ + setCurrentPanel(undefined); + setPanelHidden(false); + return; + } + }, [book]); + return (
diff --git a/components/rightbar/ComposerRightBar.tsx b/components/rightbar/ComposerRightBar.tsx index 4ec7ba8..aee983b 100644 --- a/components/rightbar/ComposerRightBar.tsx +++ b/components/rightbar/ComposerRightBar.tsx @@ -1,8 +1,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faFeather, faGlobe, faInfoCircle, faMapMarkerAlt, faUsers} from "@fortawesome/free-solid-svg-icons"; -import {RefObject, useContext, useRef, useState} from "react"; +import {RefObject, useContext, useEffect, useRef, useState} from "react"; import {BookContext} from "@/context/BookContext"; -import {ChapterContext} from "@/context/ChapterContext"; import {PanelComponent} from "@/lib/models/Editor"; import PanelHeader from "@/components/PanelHeader"; import AboutEditors from "@/components/rightbar/AboutERitors"; @@ -57,6 +56,14 @@ export default function ComposerRightBar() { } } + useEffect(():void => { + if (!book){ + setCurrentPanel(undefined); + setPanelHidden(false); + return; + } + }, [book]); + const editorComponents: PanelComponent[] = [ { id: 1, diff --git a/context/BooksSyncContext.ts b/context/BooksSyncContext.ts index 6908ec8..643bb5d 100644 --- a/context/BooksSyncContext.ts +++ b/context/BooksSyncContext.ts @@ -1,13 +1,15 @@ -import {SyncedBook} from "@/lib/models/SyncedBook"; -import {Context, createContext} from "react"; +import {BookSyncCompare, SyncedBook} from "@/lib/models/SyncedBook"; +import {Context, createContext, Dispatch, SetStateAction} from "react"; export type SyncType = 'server-only' | 'local-only' | 'to-sync-from-server' | 'to-sync-to-server' | 'synced' export interface BooksSyncContextProps { serverSyncedBooks:SyncedBook[]; localSyncedBooks:SyncedBook[]; - booksToSyncFromServer:SyncedBook[]; - booksToSyncToServer:SyncedBook[]; + booksToSyncFromServer:BookSyncCompare[]; + booksToSyncToServer:BookSyncCompare[]; + setServerOnlyBooks:Dispatch>; + setLocalOnlyBooks:Dispatch>; serverOnlyBooks:SyncedBook[]; localOnlyBooks:SyncedBook[]; } @@ -17,6 +19,8 @@ export const BooksSyncContext:Context = createContext {}, + setLocalOnlyBooks:():void => {}, serverOnlyBooks:[], localOnlyBooks:[] }) \ No newline at end of file diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index 3ef3ac7..d4e6f64 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -43,6 +43,8 @@ import fs from "fs"; import Chapter, {ActChapter, ChapterContentData, ChapterProps} from "./Chapter.js"; import UserRepo from "../repositories/user.repository.js"; import ChapterRepo from "../repositories/chapter.repository.js"; +import CharacterRepo from "../repositories/character.repository.js"; +import LocationRepo from "../repositories/location.repository.js"; export interface BookProps{ id:string; @@ -97,6 +99,26 @@ export interface SyncedBook { aiGuideLine: SyncedAIGuideLine | null; } +export interface BookSyncCompare { + id: string; + chapters: string[]; + chapterContents: string[]; + chapterInfos: string[]; + characters: string[]; + characterAttributes: string[]; + locations: string[]; + locationElements: string[]; + locationSubElements: string[]; + worlds: string[]; + worldElements: string[]; + incidents: string[]; + plotPoints: string[]; + issues: string[]; + actSummaries: string[]; + guideLine: boolean; + aiGuideLine: boolean; +} + export interface SyncedChapter { id: string; name: string; @@ -617,7 +639,7 @@ export default class Book { if (actId === 1 || actId === 4 || actId === 5) { const actSummary: string = act.summary ? System.encryptDataWithUserKey(act.summary, userKey) : ''; try { - BookRepo.updateActSummary(userId, bookId, actId, actSummary,lang); + BookRepo.updateActSummary(userId, bookId, actId, actSummary,System.timeStampInSeconds(),lang); } catch (e: unknown) { const actSummaryId: string = System.createUniqueId(); BookRepo.insertActSummary(actSummaryId, userId, bookId, actId, actSummary,lang); @@ -632,7 +654,7 @@ export default class Book { const incidentName: string = incident.title; const incidentHashedName: string = System.hashElement(incidentName); const encryptedIncidentName: string = System.encryptDataWithUserKey(incidentName, userKey); - BookRepo.updateIncident(userId, bookId, incidentId, encryptedIncidentName, incidentHashedName, incidentSummary, lang); + BookRepo.updateIncident(userId, bookId, incidentId, encryptedIncidentName, incidentHashedName, incidentSummary, System.timeStampInSeconds(), lang); if (incident.chapters) { Chapter.updateChapterInfos(incident.chapters, userId, actId, bookId, incidentId, null, lang); } @@ -645,7 +667,7 @@ export default class Book { const plotPointName: string = plotPoint.title; const plotPointHashedName: string = System.hashElement(plotPointName); const encryptedPlotPointName: string = System.encryptDataWithUserKey(plotPointName, userKey); - BookRepo.updatePlotPoint(userId, bookId, plotPointId, encryptedPlotPointName, plotPointHashedName, plotPointSummary, lang); + BookRepo.updatePlotPoint(userId, bookId, plotPointId, encryptedPlotPointName, plotPointHashedName, plotPointSummary, System.timeStampInSeconds(), lang); if (plotPoint.chapters) { Chapter.updateChapterInfos(plotPoint.chapters, userId, actId, bookId, null, plotPointId, lang); } @@ -664,7 +686,7 @@ export default class Book { const chapterHashedTitle: string = System.hashElement(chapterTitle); const encryptedTitle: string = System.encryptDataWithUserKey(chapterTitle, userKey); const chapterOrder: number = chapter.chapterOrder; - ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, chapterHashedTitle, chapterOrder, lang); + ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, chapterHashedTitle, chapterOrder, System.timeStampInSeconds(), lang); } return true; } @@ -849,7 +871,7 @@ export default class Book { })); }); - BookRepo.updateWorld(userId, world.id, encryptName, System.hashElement(world.name), encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, lang); + BookRepo.updateWorld(userId, world.id, encryptName, System.hashElement(world.name), encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, System.timeStampInSeconds(), lang); return BookRepo.updateWorldElements(userId, elements, lang); } @@ -1466,4 +1488,450 @@ export default class Book { return BookRepo.insertSyncIssue(issue.issue_id, userId, issue.book_id, encryptedIssueName, issue.hashed_issue_name, issue.last_update, lang); }); } + + static async getCompleteSyncBook(userId: string, data: BookSyncCompare, lang: "fr" | "en"):Promise { + const userKey: string = getUserEncryptionKey(userId); + const bookData: EritBooksTable[] = []; + const chaptersData: BookChaptersTable[] = []; + const plotPointsData: BookPlotPointsTable[] = []; + const incidentsData: BookIncidentsTable[] = []; + const chapterContentsData: BookChapterContentTable[] = []; + const chapterInfosData: BookChapterInfosTable[] = []; + const charactersData: BookCharactersTable[] = []; + const characterAttributesData: BookCharactersAttributesTable[] = []; + const locationsData: BookLocationTable[] = []; + const locationElementsData: LocationElementTable[] = []; + const locationSubElementsData: LocationSubElementTable[] = []; + const worldsData: BookWorldTable[] = []; + const worldElementsData: BookWorldElementsTable[] = []; + const actSummariesData: BookActSummariesTable[] = []; + const guideLineData: BookGuideLineTable[] = []; + const aiGuideLineData: BookAIGuideLineTable[] = []; + const issuesData: BookIssuesTable[] = []; + + const actSummaries: string[] = data.actSummaries; + const chapters: string[] = data.chapters; + const plotPoints: string[] = data.plotPoints; + const incidents: string[] = data.incidents; + const chapterContents: string[] = data.chapterContents; + const chapterInfos: string[] = data.chapterInfos; + const characters: string[] = data.characters; + const characterAttributes: string[] = data.characterAttributes; + const locations: string[] = data.locations; + const locationElements: string[] = data.locationElements; + const locationSubElements: string[] = data.locationSubElements; + const worlds: string[] = data.worlds; + const worldElements: string[] = data.worldElements; + const issues: string[] = data.issues; + + if (actSummaries && actSummaries.length > 0) { + for (const id of actSummaries) { + const actSummary: BookActSummariesTable[] = await BookRepo.fetchCompleteActSummaryById(id, lang); + if (actSummary.length>0) { + const actSummaryData: BookActSummariesTable = actSummary[0]; + actSummariesData.push({ + ...actSummaryData, + summary: actSummaryData.summary ? System.decryptDataWithUserKey(actSummaryData.summary, userKey) : null + }); + } + } + } + + if (chapters && chapters.length > 0) { + for (const id of chapters) { + const chapter: BookChaptersTable[] = await BookRepo.fetchCompleteChapterById(id, lang); + if (chapter.length>0) { + const chapterData: BookChaptersTable = chapter[0]; + chaptersData.push({ + ...chapterData, + title: System.decryptDataWithUserKey(chapterData.title, userKey) + }); + } + } + } + + if (plotPoints && plotPoints.length > 0) { + for (const id of plotPoints) { + const plotPoint: BookPlotPointsTable[] = await BookRepo.fetchCompletePlotPointById(id, lang); + if (plotPoint.length>0) { + const plotPointData: BookPlotPointsTable = plotPoint[0]; + plotPointsData.push({ + ...plotPointData, + title: System.decryptDataWithUserKey(plotPointData.title, userKey), + summary: plotPointData.summary ? System.decryptDataWithUserKey(plotPointData.summary, userKey) : null + }); + } + } + } + + if (incidents && incidents.length > 0) { + for (const id of incidents) { + const incident: BookIncidentsTable[] = await BookRepo.fetchCompleteIncidentById(id, lang); + if (incident.length>0) { + const incidentData: BookIncidentsTable = incident[0]; + incidentsData.push({ + ...incidentData, + title: System.decryptDataWithUserKey(incidentData.title, userKey), + summary: incidentData.summary ? System.decryptDataWithUserKey(incidentData.summary, userKey) : null + }); + } + } + } + + if (chapterContents && chapterContents.length > 0) { + for (const id of chapterContents) { + const chapterContent: BookChapterContentTable[] = await BookRepo.fetchCompleteChapterContentById(id, lang); + if (chapterContent.length>0) { + const chapterContentData: BookChapterContentTable = chapterContent[0]; + chapterContentsData.push({ + ...chapterContentData, + content: chapterContentData.content ? JSON.parse(System.decryptDataWithUserKey(chapterContentData.content, userKey)) : null + }); + } + } + } + + if (chapterInfos && chapterInfos.length > 0) { + for (const id of chapterInfos) { + const chapterInfo: BookChapterInfosTable[] = await BookRepo.fetchCompleteChapterInfoById(id, lang); + if (chapterInfo.length>0) { + const chapterInfoData: BookChapterInfosTable = chapterInfo[0]; + chapterInfosData.push({ + ...chapterInfoData, + summary: chapterInfoData.summary ? System.decryptDataWithUserKey(chapterInfoData.summary, userKey) : null, + goal: chapterInfoData.goal ? System.decryptDataWithUserKey(chapterInfoData.goal, userKey) : null + }); + } + } + } + + if (characters && characters.length > 0) { + for (const id of characters) { + const character: BookCharactersTable[] = await BookRepo.fetchCompleteCharacterById(id, lang); + if (character.length>0) { + const characterData: BookCharactersTable = character[0]; + charactersData.push({ + ...characterData, + first_name: System.decryptDataWithUserKey(characterData.first_name, userKey), + last_name: characterData.last_name ? System.decryptDataWithUserKey(characterData.last_name, userKey) : null, + category: System.decryptDataWithUserKey(characterData.category, userKey), + title: characterData.title ? System.decryptDataWithUserKey(characterData.title, userKey) : null, + role: characterData.role ? System.decryptDataWithUserKey(characterData.role, userKey) : null, + biography: characterData.biography ? System.decryptDataWithUserKey(characterData.biography, userKey) : null, + history: characterData.history ? System.decryptDataWithUserKey(characterData.history, userKey) : null + }); + } + } + } + + if (characterAttributes && characterAttributes.length > 0) { + for (const id of characterAttributes) { + const characterAttribute: BookCharactersAttributesTable[] = await BookRepo.fetchCompleteCharacterAttributeById(id, lang); + if (characterAttribute.length>0) { + const characterAttributeData: BookCharactersAttributesTable = characterAttribute[0]; + characterAttributesData.push({ + ...characterAttributeData, + attribute_name: System.decryptDataWithUserKey(characterAttributeData.attribute_name, userKey), + attribute_value: System.decryptDataWithUserKey(characterAttributeData.attribute_value, userKey) + }); + } + } + } + + if (locations && locations.length > 0) { + for (const id of locations) { + const location: BookLocationTable[] = await BookRepo.fetchCompleteLocationById(id, lang); + if (location.length>0) { + const locationData: BookLocationTable = location[0]; + locationsData.push({ + ...locationData, + loc_name: System.decryptDataWithUserKey(locationData.loc_name, userKey) + }); + } + } + } + + if (locationElements && locationElements.length > 0) { + for (const id of locationElements) { + const locationElement: LocationElementTable[] = await BookRepo.fetchCompleteLocationElementById(id, lang); + if (locationElement.length>0) { + const locationElementData: LocationElementTable = locationElement[0]; + locationElementsData.push({ + ...locationElementData, + element_name: System.decryptDataWithUserKey(locationElementData.element_name, userKey), + element_description: locationElementData.element_description ? System.decryptDataWithUserKey(locationElementData.element_description, userKey) : null + }); + } + } + } + + if (locationSubElements && locationSubElements.length > 0) { + for (const id of locationSubElements) { + const locationSubElement: LocationSubElementTable[] = await BookRepo.fetchCompleteLocationSubElementById(id, lang); + if (locationSubElement.length>0) { + const locationSubElementData: LocationSubElementTable = locationSubElement[0]; + locationSubElementsData.push({ + ...locationSubElementData, + sub_elem_name: System.decryptDataWithUserKey(locationSubElementData.sub_elem_name, userKey), + sub_elem_description: locationSubElementData.sub_elem_description ? System.decryptDataWithUserKey(locationSubElementData.sub_elem_description, userKey) : null + }); + } + } + } + + if (worlds && worlds.length > 0) { + for (const id of worlds) { + const world: BookWorldTable[] = await BookRepo.fetchCompleteWorldById(id, lang); + if (world.length>0) { + const worldData: BookWorldTable = world[0]; + worldsData.push({ + ...worldData, + name: System.decryptDataWithUserKey(worldData.name, userKey), + history: worldData.history ? System.decryptDataWithUserKey(worldData.history, userKey) : null, + politics: worldData.politics ? System.decryptDataWithUserKey(worldData.politics, userKey) : null, + economy: worldData.economy ? System.decryptDataWithUserKey(worldData.economy, userKey) : null, + religion: worldData.religion ? System.decryptDataWithUserKey(worldData.religion, userKey) : null, + languages: worldData.languages ? System.decryptDataWithUserKey(worldData.languages, userKey) : null + }); + } + } + } + + if (worldElements && worldElements.length > 0) { + for (const id of worldElements) { + const worldElement: BookWorldElementsTable[] = await BookRepo.fetchCompleteWorldElementById(id, lang); + if (worldElement.length>0) { + const worldElementData: BookWorldElementsTable = worldElement[0]; + worldElementsData.push({ + ...worldElementData, + name: System.decryptDataWithUserKey(worldElementData.name, userKey), + description: worldElementData.description ? System.decryptDataWithUserKey(worldElementData.description, userKey) : null + }); + } + } + } + + if (issues && issues.length > 0) { + for (const id of issues) { + const issue: BookIssuesTable[] = await BookRepo.fetchCompleteIssueById(id, lang); + if (issue.length>0) { + const issueData: BookIssuesTable = issue[0]; + issuesData.push({ + ...issueData, + name: System.decryptDataWithUserKey(issueData.name, userKey) + }); + } + } + } + console.log(data.id) + const book: EritBooksTable[] = await BookRepo.fetchCompleteBookById(data.id, lang); + if (book.length>0) { + const bookDataItem: EritBooksTable = book[0]; + bookData.push({ + ...bookDataItem, + title: System.decryptDataWithUserKey(bookDataItem.title, userKey), + sub_title: bookDataItem.sub_title ? System.decryptDataWithUserKey(bookDataItem.sub_title, userKey) : null, + summary: bookDataItem.summary ? System.decryptDataWithUserKey(bookDataItem.summary, userKey) : null, + cover_image: bookDataItem.cover_image ? System.decryptDataWithUserKey(bookDataItem.cover_image, userKey) : null + }); + } + return { + eritBooks: bookData, + chapters: chaptersData, + plotPoints: plotPointsData, + incidents: incidentsData, + chapterContents: chapterContentsData, + chapterInfos: chapterInfosData, + characters: charactersData, + characterAttributes: characterAttributesData, + locations: locationsData, + locationElements: locationElementsData, + locationSubElements: locationSubElementsData, + worlds: worldsData, + worldElements: worldElementsData, + actSummaries: actSummariesData, + guideLine: guideLineData, + aiGuideLine: aiGuideLineData, + issues: issuesData + }; + } + + static async syncBookFromServerToClient(userId:string,completeBook: CompleteBook,lang:"fr"|"en"):Promise { + const userKey: string = getUserEncryptionKey(userId); + const actSummaries: BookActSummariesTable[] = completeBook.actSummaries; + const chapters: BookChaptersTable[] = completeBook.chapters; + const plotPoints: BookPlotPointsTable[] = completeBook.plotPoints; + const incidents: BookIncidentsTable[] = completeBook.incidents; + const chapterContents: BookChapterContentTable[] = completeBook.chapterContents; + const chapterInfos: BookChapterInfosTable[] = completeBook.chapterInfos; + const characters: BookCharactersTable[] = completeBook.characters; + const characterAttributes: BookCharactersAttributesTable[] = completeBook.characterAttributes; + const locations: BookLocationTable[] = completeBook.locations; + const locationElements: LocationElementTable[] = completeBook.locationElements; + const locationSubElements: LocationSubElementTable[] = completeBook.locationSubElements; + const worlds: BookWorldTable[] = completeBook.worlds; + const worldElements: BookWorldElementsTable[] = completeBook.worldElements; + const issues: BookIssuesTable[] = completeBook.issues; + + const bookId: string = completeBook.eritBooks.length > 0 ? completeBook.eritBooks[0].book_id : ''; + + if (actSummaries && actSummaries.length > 0) { + for (const actSummary of actSummaries) { + const summary: string = System.encryptDataWithUserKey(actSummary.summary ? actSummary.summary : '', userKey) + const updated: boolean = BookRepo.updateActSummary(userId, bookId, actSummary.act_index, summary, actSummary.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (chapters && chapters.length > 0) { + for (const chapter of chapters) { + const title: string = System.encryptDataWithUserKey(chapter.title, userKey) + const updated: boolean = ChapterRepo.updateChapter(userId, chapter.chapter_id, title, chapter.hashed_title, chapter.chapter_order, chapter.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (plotPoints && plotPoints.length > 0) { + for (const plotPoint of plotPoints) { + const title: string = System.encryptDataWithUserKey(plotPoint.title, userKey); + const summary: string = System.encryptDataWithUserKey(plotPoint.summary ? plotPoint.summary : '', userKey); + const updated: boolean = BookRepo.updatePlotPoint(userId, bookId, plotPoint.plot_point_id, title, plotPoint.hashed_title, summary, plotPoint.last_update,lang); + if (!updated) { + return false; + } + } + } + + if (incidents && incidents.length > 0) { + for (const incident of incidents) { + const title: string = System.encryptDataWithUserKey(incident.title, userKey); + const summary: string = System.encryptDataWithUserKey(incident.summary ? incident.summary : '', userKey); + const updated: boolean = BookRepo.updateIncident(userId, bookId, incident.incident_id, title, incident.hashed_title, summary, incident.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (chapterContents && chapterContents.length > 0) { + for (const chapterContent of chapterContents) { + const content: string = System.encryptDataWithUserKey(chapterContent.content ? JSON.stringify(chapterContent.content) : '', userKey); + const updated: boolean = ChapterRepo.updateChapterContent(userId, chapterContent.chapter_id, chapterContent.version, content, chapterContent.words_count, chapterContent.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (chapterInfos && chapterInfos.length > 0) { + for (const chapterInfo of chapterInfos) { + const summary: string = System.encryptDataWithUserKey(chapterInfo.summary ? chapterInfo.summary : '', userKey); + const goal: string = System.encryptDataWithUserKey(chapterInfo.goal ? chapterInfo.goal : '', userKey); + const updated: boolean = ChapterRepo.updateChapterInfos(userId, chapterInfo.chapter_id, chapterInfo.act_id, bookId,chapterInfo.incident_id, chapterInfo.plot_point_id, summary, goal, chapterInfo.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (characters && characters.length > 0) { + for (const character of characters) { + const firstName: string = System.encryptDataWithUserKey(character.first_name, userKey); + const lastName: string = System.encryptDataWithUserKey(character.last_name ? character.last_name : '', userKey); + const category: string = System.encryptDataWithUserKey(character.category, userKey); + const title: string = System.encryptDataWithUserKey(character.title ? character.title : '', userKey); + const role: string = System.encryptDataWithUserKey(character.role ? character.role : '', userKey); + const biography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); + const history: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); + const updated: boolean = CharacterRepo.updateCharacter(userId, character.character_id, firstName, lastName, title, category, character.image || '', role, biography, history, character.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (characterAttributes && characterAttributes.length > 0) { + for (const characterAttribute of characterAttributes) { + const attributeName: string = System.encryptDataWithUserKey(characterAttribute.attribute_name, userKey); + const attributeValue: string = System.encryptDataWithUserKey(characterAttribute.attribute_value, userKey); + const updated: boolean = CharacterRepo.updateCharacterAttribute(userId, characterAttribute.attr_id, attributeName, attributeValue, characterAttribute.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (locations && locations.length > 0) { + for (const location of locations) { + const locName: string = System.encryptDataWithUserKey(location.loc_name, userKey); + const updated: boolean = LocationRepo.updateLocationSection(userId, location.loc_id, locName, location.loc_original_name, location.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (locationElements && locationElements.length > 0) { + for (const locationElement of locationElements) { + const elementName: string = System.encryptDataWithUserKey(locationElement.element_name, userKey); + const elementDescription: string = System.encryptDataWithUserKey(locationElement.element_description ? locationElement.element_description : '', userKey); + const updated: boolean = LocationRepo.updateLocationElement(userId, locationElement.element_id, elementName,locationElement.original_name, elementDescription, locationElement.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (locationSubElements && locationSubElements.length > 0) { + for (const locationSubElement of locationSubElements) { + const subElemName: string = System.encryptDataWithUserKey(locationSubElement.sub_elem_name, userKey); + const subElemDescription: string = System.encryptDataWithUserKey(locationSubElement.sub_elem_description ? locationSubElement.sub_elem_description : '', userKey); + const updated: boolean = LocationRepo.updateLocationSubElement(userId, locationSubElement.sub_element_id, subElemName, locationSubElement.original_name, subElemDescription, locationSubElement.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (worlds && worlds.length > 0) { + for (const world of worlds) { + const name: string = System.encryptDataWithUserKey(world.name, userKey); + const history: string = System.encryptDataWithUserKey(world.history ? world.history : '', userKey); + const politics: string = System.encryptDataWithUserKey(world.politics ? world.politics : '', userKey); + const economy: string = System.encryptDataWithUserKey(world.economy ? world.economy : '', userKey); + const religion: string = System.encryptDataWithUserKey(world.religion ? world.religion : '', userKey); + const languages: string = System.encryptDataWithUserKey(world.languages ? world.languages : '', userKey); + const updated: boolean = BookRepo.updateWorld(userId, world.world_id, name, world.hashed_name, history, politics, economy, religion, languages, world.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (worldElements && worldElements.length > 0) { + for (const worldElement of worldElements) { + const name: string = System.encryptDataWithUserKey(worldElement.name, userKey); + const description: string = System.encryptDataWithUserKey(worldElement.description ? worldElement.description : '', userKey); + const updated: boolean = BookRepo.updateWorldElement(userId, worldElement.element_id, name, description, worldElement.last_update, lang); + if (!updated) { + return false; + } + } + } + + if (issues && issues.length > 0) { + for (const issue of issues) { + const name: string = System.encryptDataWithUserKey(issue.name, userKey); + const updated: boolean = BookRepo.updateIssue(userId, bookId, issue.issue_id, name, issue.hashed_issue_name, issue.last_update, lang); + if (!updated) { + return false; + } + } + } + return true; + } } \ No newline at end of file diff --git a/electron/database/models/Chapter.ts b/electron/database/models/Chapter.ts index c1cd358..2236bdd 100644 --- a/electron/database/models/Chapter.ts +++ b/electron/database/models/Chapter.ts @@ -149,7 +149,7 @@ export default class Chapter { const response:string = await QS.request(prompt,'summary-chapter'); console.log(response); }*/ - return ChapterRepo.updateChapterContent(userId, chapterId, version, encryptContent, wordsCount, lang); + return ChapterRepo.updateChapterContent(userId, chapterId, version, encryptContent, wordsCount, System.timeStampInSeconds(), lang); } public static getLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps | null { @@ -200,7 +200,7 @@ export default class Chapter { const hashedTitle: string = System.hashElement(title); const userKey: string = getUserEncryptionKey(userId); const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); - return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, lang); + return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, System.timeStampInSeconds(), lang); } static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr') { @@ -209,7 +209,7 @@ export default class Chapter { const summary: string = chapter.summary ? System.encryptDataWithUserKey(chapter.summary, userKey) : ''; const goal: string = chapter.goal ? System.encryptDataWithUserKey(chapter.goal, userKey) : ''; const chapterId: string = chapter.chapterId; - ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, summary, goal, lang); + ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, summary, goal, System.timeStampInSeconds(), lang); } } diff --git a/electron/database/models/Character.ts b/electron/database/models/Character.ts index eebcd71..598efcb 100644 --- a/electron/database/models/Character.ts +++ b/electron/database/models/Character.ts @@ -9,7 +9,7 @@ import {getUserEncryptionKey} from "../keyManager.js"; export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring'; export interface CharacterPropsPost { - id: number | null; + id: string | null; name: string; lastName: string; category: CharacterCategory; @@ -118,6 +118,9 @@ export default class Character { static updateCharacter(userId: string, character: CharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean { const userKey: string = getUserEncryptionKey(userId); + if (!character.id) { + return false; + } const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey); const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey); @@ -126,7 +129,7 @@ export default class Character { const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey); const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); - return CharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, lang); + return CharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, System.timeStampInSeconds(), lang); } static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { diff --git a/electron/database/models/Location.ts b/electron/database/models/Location.ts index 248016a..226884a 100644 --- a/electron/database/models/Location.ts +++ b/electron/database/models/Location.ts @@ -118,17 +118,17 @@ export default class Location { for (const location of locations) { const originalName: string = System.hashElement(location.name); const encryptedName: string = System.encryptDataWithUserKey(location.name, userKey); - LocationRepo.updateLocationSection(userId, location.id, encryptedName, originalName, lang) + LocationRepo.updateLocationSection(userId, location.id, encryptedName, originalName, System.timeStampInSeconds(),lang) for (const element of location.elements) { const originalName: string = System.hashElement(element.name); const encryptedName: string = System.encryptDataWithUserKey(element.name, userKey); const encryptDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : ''; - LocationRepo.updateLocationElement(userId, element.id, encryptedName, originalName, encryptDescription, lang) + LocationRepo.updateLocationElement(userId, element.id, encryptedName, originalName, encryptDescription, System.timeStampInSeconds(), lang) for (const subElement of element.subElements) { const originalName: string = System.hashElement(subElement.name); const encryptedName: string = System.encryptDataWithUserKey(subElement.name, userKey); const encryptDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : ''; - LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedName, originalName, encryptDescription, lang) + LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedName, originalName, encryptDescription,System.timeStampInSeconds(),lang) } } } diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts index 031ef87..491674c 100644 --- a/electron/database/repositories/book.repository.ts +++ b/electron/database/repositories/book.repository.ts @@ -63,9 +63,9 @@ export interface BookChaptersTable extends Record { book_id: string; author_id: string; title: string; - hashed_title: string | null; + hashed_title: string; words_count: number | null; - chapter_order: number | null; + chapter_order: number; last_update: number; } @@ -83,7 +83,7 @@ export interface BookChapterContentTable extends Record { export interface BookChapterInfosTable extends Record { chapter_info_id: string; chapter_id: string; - act_id: number | null; + act_id: number; incident_id: string | null; plot_point_id: string | null; book_id: string; @@ -626,10 +626,10 @@ export default class BookRepo { } } - public static updateActSummary(userId: string, bookId: string, actId: number, summary: string, lang: 'fr' | 'en'): boolean { + public static updateActSummary(userId: string, bookId: string, actId: number, summary: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_act_summaries SET summary=?, last_update=? WHERE user_id=? AND book_id=? AND act_sum_id=?', [summary, System.timeStampInSeconds(), userId, bookId, actId]); + const result: RunResult = db.run('UPDATE book_act_summaries SET summary=?, last_update=? WHERE user_id=? AND book_id=? AND act_sum_id=?', [summary, lastUpdate, userId, bookId, actId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -782,10 +782,10 @@ export default class BookRepo { } } - public static updateIncident(userId: string, bookId: string, incidentId: string, encryptedIncidentName: string, incidentHashedName: string, incidentSummary: string, lang: 'fr' | 'en'): boolean { + public static updateIncident(userId: string, bookId: string, incidentId: string, encryptedIncidentName: string, incidentHashedName: string, incidentSummary: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_incidents SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND incident_id=?', [encryptedIncidentName, incidentHashedName, incidentSummary, System.timeStampInSeconds(), userId, bookId, incidentId]); + const result: RunResult = db.run('UPDATE book_incidents SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND incident_id=?', [encryptedIncidentName, incidentHashedName, incidentSummary, lastUpdate, userId, bookId, incidentId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -798,10 +798,10 @@ export default class BookRepo { } } - public static updatePlotPoint(userId: string, bookId: string, plotPointId: string, encryptedPlotPointName: string, plotPointHashedName: string, plotPointSummary: string, lang: 'fr' | 'en'): boolean { + public static updatePlotPoint(userId: string, bookId: string, plotPointId: string, encryptedPlotPointName: string, plotPointHashedName: string, plotPointSummary: string, lastUpdate:number, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_plot_points SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND plot_point_id=?', [encryptedPlotPointName, plotPointHashedName, plotPointSummary, System.timeStampInSeconds(), userId, bookId, plotPointId]); + const result: RunResult = db.run('UPDATE book_plot_points SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND plot_point_id=?', [encryptedPlotPointName, plotPointHashedName, plotPointSummary, lastUpdate, userId, bookId, plotPointId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -863,10 +863,10 @@ export default class BookRepo { } } - public static updateWorld(userId: string, worldId: string, encryptName: string, hashedName: string, encryptHistory: string, encryptPolitics: string, encryptEconomy: string, encryptReligion: string, encryptLanguages: string, lang: 'fr' | 'en'): boolean { + public static updateWorld(userId: string, worldId: string, encryptName: string, hashedName: string, encryptHistory: string, encryptPolitics: string, encryptEconomy: string, encryptReligion: string, encryptLanguages: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE author_id=? AND world_id=?', [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, System.timeStampInSeconds(), userId, worldId]); + const result: RunResult = db.run('UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE author_id=? AND world_id=?', [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, lastUpdate, userId, worldId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -1809,4 +1809,309 @@ export default class BookRepo { } } } + + static async fetchCompleteActSummaryById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT act_sum_id, book_id, user_id, act_index, summary, last_update + FROM book_act_summaries + WHERE act_sum_id = ?`, + [id] + ) as BookActSummariesTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le résumé d'acte complet.` : `Unable to retrieve complete act summary.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteChapterById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update + FROM book_chapters + WHERE chapter_id = ?`, + [id] + ) as BookChaptersTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le chapitre complet.` : `Unable to retrieve complete chapter.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompletePlotPointById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update + FROM book_plot_points + WHERE plot_point_id = ?`, + [id] + ) as BookPlotPointsTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le point d'intrigue complet.` : `Unable to retrieve complete plot point.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteIncidentById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT incident_id, author_id, book_id, title, hashed_title, summary, last_update + FROM book_incidents + WHERE incident_id = ?`, + [id] + ) as BookIncidentsTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer l'incident complet.` : `Unable to retrieve complete incident.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteChapterContentById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update + FROM book_chapter_content + WHERE content_id = ?`, + [id] + ) as BookChapterContentTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu de chapitre complet.` : `Unable to retrieve complete chapter content.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteChapterInfoById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update + FROM book_chapter_infos + WHERE chapter_info_id = ?`, + [id] + ) as BookChapterInfosTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer les informations de chapitre complètes.` : `Unable to retrieve complete chapter info.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteCharacterById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history, last_update + FROM book_characters + WHERE character_id = ?`, + [id] + ) as BookCharactersTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le personnage complet.` : `Unable to retrieve complete character.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteCharacterAttributeById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update + FROM book_characters_attributes + WHERE attr_id = ?`, + [id] + ) as BookCharactersAttributesTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer l'attribut de personnage complet.` : `Unable to retrieve complete character attribute.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteLocationById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT loc_id, book_id, user_id, loc_name, loc_original_name, last_update + FROM book_location + WHERE loc_id = ?`, + [id] + ) as BookLocationTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le lieu complet.` : `Unable to retrieve complete location.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteLocationElementById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT element_id, location, user_id, element_name, original_name, element_description, last_update + FROM location_element + WHERE element_id = ?`, + [id] + ) as LocationElementTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de lieu complet.` : `Unable to retrieve complete location element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteLocationSubElementById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update + FROM location_sub_element + WHERE sub_element_id = ?`, + [id] + ) as LocationSubElementTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le sous-élément de lieu complet.` : `Unable to retrieve complete location sub-element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteWorldById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT world_id, name, hashed_name, author_id, book_id, history, politics, economy, religion, languages, last_update + FROM book_world + WHERE world_id = ?`, + [id] + ) as BookWorldTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le monde complet.` : `Unable to retrieve complete world.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteWorldElementById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update + FROM book_world_elements + WHERE element_id = ?`, + [id] + ) as BookWorldElementsTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de monde complet.` : `Unable to retrieve complete world element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteIssueById(id: string, lang: "fr" | "en"):Promise { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT issue_id, author_id, book_id, name, hashed_issue_name, last_update + FROM book_issues + WHERE issue_id = ?`, + [id] + ) as BookIssuesTable[]; + } catch (e:unknown){ + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le problème complet.` : `Unable to retrieve complete issue.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static async fetchCompleteBookById(bookId: string, lang: "fr" | "en") { + try { + const db: Database = System.getDb(); + return db.all( + `SELECT * FROM erit_books + WHERE book_id = ?`, + [bookId] + ) as EritBooksTable[]; + } catch (e: unknown) { + if (e instanceof Error) { + throw new Error(lang === 'fr' ? `Impossible de récupérer le livre complet.` : `Unable to retrieve complete book.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static updateWorldElement(userId: string, elementId: string, name: string, description: string, lastUpdate: number,lang: "fr" | "en"):boolean { + try { + const db: Database = System.getDb(); + const query:string = `UPDATE book_world_elements SET name = ?, description = ?, last_update = FROM_UNIXTIME(?) WHERE element_id = UUID_TO_BIN(?) AND user_id = UUID_TO_BIN(?)`; + const params:(string|number)[] = [name, description, lastUpdate, elementId, userId]; + const result:RunResult = db.run(query, params); + return result.changes > 0; + } catch (e:unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément du monde.` : `Unable to update world element.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static updateIssue(userId: string, bookId: string, issueId: string, name: string, hashedName: string, lastUpdate: number, lang: "fr" | "en"):boolean { + try { + const db: Database = System.getDb(); + const query:string = `UPDATE book_issues SET name = ?, hashed_issue_name = ?, last_update = FROM_UNIXTIME(?) WHERE issue_id = UUID_TO_BIN(?) AND author_id = UUID_TO_BIN(?) AND book_id = UUID_TO_BIN(?)`; + const params:(string|number)[] = [name, hashedName, lastUpdate, issueId, userId, bookId]; + const result:RunResult = db.run(query, params); + return result.changes > 0; + } catch (e:unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour la problématique.` : `Unable to update issue.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } } diff --git a/electron/database/repositories/chapter.repository.ts b/electron/database/repositories/chapter.repository.ts index 0e9762a..016f9aa 100644 --- a/electron/database/repositories/chapter.repository.ts +++ b/electron/database/repositories/chapter.repository.ts @@ -212,10 +212,10 @@ export default class ChapterRepo{ return chapterInfoId; } - public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { + public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, lastUpdate:number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=?, last_update=? WHERE author_id=? AND chapter_id=?', [encryptedTitle, hashTitle, chapterOrder, System.timeStampInSeconds(), userId, chapterId]); + const result: RunResult = db.run('UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=?, last_update=? WHERE author_id=? AND chapter_id=?', [encryptedTitle, hashTitle, chapterOrder, lastUpdate, userId, chapterId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -228,16 +228,17 @@ export default class ChapterRepo{ } } - public static updateChapterInfos(userId: string, chapterId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, summary: string, goal: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + public static updateChapterInfos(userId: string, chapterId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, summary: string, goal: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); let sql: string = `UPDATE book_chapter_infos SET summary=?, - goal=? + goal=?, + last_update=? WHERE chapter_id = ? AND act_id = ? AND book_id = ?`; - const params: any[] = [summary, goal, chapterId, actId, bookId]; + const params: any[] = [summary, goal, lastUpdate, chapterId, actId, bookId]; if (incidentId) { sql += ` AND incident_id=?`; params.push(incidentId); @@ -265,15 +266,15 @@ export default class ChapterRepo{ } } - public static updateChapterContent(userId: string, chapterId: string, version: number, encryptContent: string, wordsCount: number, lang: 'fr' | 'en' = 'fr'): boolean { + public static updateChapterContent(userId: string, chapterId: string, version: number, encryptContent: string, wordsCount: number, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_chapter_content SET content=?, words_count=?, last_update=? WHERE chapter_id=? AND author_id=? AND version=?', [encryptContent, wordsCount, System.timeStampInSeconds(), chapterId, userId, version]); + const result: RunResult = db.run('UPDATE book_chapter_content SET content=?, words_count=?, last_update=? WHERE chapter_id=? AND author_id=? AND version=?', [encryptContent, wordsCount, lastUpdate, chapterId, userId, version]); if (result.changes > 0) { return true; } else { const contentId:string = System.createUniqueId(); - const insertResult: RunResult = db.run('INSERT INTO book_chapter_content (content_id,chapter_id, author_id, version, content, words_count, last_update) VALUES (?,?,?,?,?,?,?)', [contentId, chapterId, userId, version, encryptContent, wordsCount, System.timeStampInSeconds()]); + const insertResult: RunResult = db.run('INSERT INTO book_chapter_content (content_id,chapter_id, author_id, version, content, words_count, last_update) VALUES (?,?,?,?,?,?,?)', [contentId, chapterId, userId, version, encryptContent, wordsCount, lastUpdate]); return insertResult.changes > 0; } } catch (e: unknown) { diff --git a/electron/database/repositories/character.repository.ts b/electron/database/repositories/character.repository.ts index adeb851..e942534 100644 --- a/electron/database/repositories/character.repository.ts +++ b/electron/database/repositories/character.repository.ts @@ -90,10 +90,10 @@ export default class CharacterRepo { return attributeId; } - static updateCharacter(userId: string, id: number | null, encryptedName: string, encryptedLastName: string, encryptedTitle: string, encryptedCategory: string, encryptedImage: string, encryptedRole: string, encryptedBiography: string, encryptedHistory: string, lang: 'fr' | 'en' = 'fr'): boolean { + static updateCharacter(userId: string, id: string, encryptedName: string, encryptedLastName: string, encryptedTitle: string, encryptedCategory: string, encryptedImage: string, encryptedRole: string, encryptedBiography: string, encryptedHistory: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE `book_characters` SET `first_name`=?,`last_name`=?,`title`=?,`category`=?,`image`=?,`role`=?,`biography`=?,`history`=?,`last_update`=? WHERE `character_id`=? AND `user_id`=?', [encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, System.timeStampInSeconds(), id, userId]); + const result: RunResult = db.run('UPDATE `book_characters` SET `first_name`=?,`last_name`=?,`title`=?,`category`=?,`image`=?,`role`=?,`biography`=?,`history`=?,`last_update`=? WHERE `character_id`=? AND `user_id`=?', [encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, lastUpdate, id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -165,4 +165,19 @@ export default class CharacterRepo { } return result; } + static updateCharacterAttribute(userId: string, characterAttributeId: string, attributeName: string, attributeValue: string, lastUpdate: number,lang: "fr" | "en"):boolean { + try { + const db: Database = System.getDb(); + const result:RunResult = db.run('UPDATE `book_characters_attributes` SET `attribute_name`=?,`attribute_value`=?, last_update=FROM_UNIXTIME(?) WHERE `attr_id`=UUID_TO_BIN(?) AND `user_id`=UUID_TO_BIN(?)', [attributeName, attributeValue, lastUpdate, characterAttributeId, userId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'attribut du personnage.` : `Unable to update character attribute.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } } diff --git a/electron/database/repositories/location.repository.ts b/electron/database/repositories/location.repository.ts index d27be7f..2fa4bf5 100644 --- a/electron/database/repositories/location.repository.ts +++ b/electron/database/repositories/location.repository.ts @@ -107,10 +107,10 @@ export default class LocationRepo { return subElementId; } - static updateLocationSubElement(userId: string, id: string, encryptedName: string, originalName: string, encryptDescription: string, lang: 'fr' | 'en' = 'fr'): boolean { + static updateLocationSubElement(userId: string, id: string, encryptedName: string, originalName: string, encryptDescription: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE location_sub_element SET sub_elem_name=?, original_name=?, sub_elem_description=?, last_update=? WHERE sub_element_id=? AND user_id=?', [encryptedName, originalName, encryptDescription, System.timeStampInSeconds(), id, userId]); + const result: RunResult = db.run('UPDATE location_sub_element SET sub_elem_name=?, original_name=?, sub_elem_description=?, last_update=? WHERE sub_element_id=? AND user_id=?', [encryptedName, originalName, encryptDescription, lastUpdate, id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -123,10 +123,10 @@ export default class LocationRepo { } } - static updateLocationElement(userId: string, id: string, encryptedName: string, originalName: string, encryptedDescription: string, lang: 'fr' | 'en' = 'fr'): boolean { + static updateLocationElement(userId: string, id: string, encryptedName: string, originalName: string, encryptedDescription: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE location_element SET element_name=?, original_name=?, element_description=?, last_update=? WHERE element_id=? AND user_id=?', [encryptedName, originalName, encryptedDescription, System.timeStampInSeconds(), id, userId]); + const result: RunResult = db.run('UPDATE location_element SET element_name=?, original_name=?, element_description=?, last_update=? WHERE element_id=? AND user_id=?', [encryptedName, originalName, encryptedDescription, lastUpdate, id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { @@ -139,10 +139,10 @@ export default class LocationRepo { } } - static updateLocationSection(userId: string, id: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): boolean { + static updateLocationSection(userId: string, id: string, encryptedName: string, originalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); - const result: RunResult = db.run('UPDATE book_location SET loc_name=?, loc_original_name=?, last_update=? WHERE loc_id=? AND user_id=?', [encryptedName, originalName, System.timeStampInSeconds(), id, userId]); + const result: RunResult = db.run('UPDATE book_location SET loc_name=?, loc_original_name=?, last_update=? WHERE loc_id=? AND user_id=?', [encryptedName, originalName, lastUpdate, id, userId]); return result.changes > 0; } catch (e: unknown) { if (e instanceof Error) { diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts index 186c7a2..1349e74 100644 --- a/electron/ipc/book.ipc.ts +++ b/electron/ipc/book.ipc.ts @@ -1,6 +1,6 @@ import { ipcMain } from 'electron'; import { createHandler } from '../database/LocalSystem.js'; -import Book, {CompleteBook, SyncedBook} from '../database/models/Book.js'; +import Book, {BookSyncCompare, CompleteBook, SyncedBook} from '../database/models/Book.js'; import type { BookProps, GuideLine, GuideLineAI, Act, Issue, WorldProps } from '../database/models/Book.js'; import Chapter from '../database/models/Chapter.js'; import type { ChapterProps } from '../database/models/Chapter.js'; @@ -130,6 +130,22 @@ ipcMain.handle('db:book:updateBasicInformation', createHandler( + async function(userId: string, data:BookSyncCompare, lang: 'fr' | 'en'):Promise { + return await Book.getCompleteSyncBook(userId, data, lang); + } + ) +); + +// GET /book/sync/from-server - Get book data to sync from server +ipcMain.handle('db:book:sync:toClient', createHandler( + async function(userId: string, data:CompleteBook, lang: 'fr' | 'en'):Promise { + return await Book.syncBookFromServerToClient(userId, data, lang); + } + ) +); + // GET /book/guide-line - Get guideline ipcMain.handle('db:book:guideline:get', createHandler(async function(userId: string, data: GetGuidelineData, lang: 'fr' | 'en') { diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 0000000..cbf3024 --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,14 @@ +/** + * Clean error messages from Electron IPC prefix + * Transforms: "Error invoking remote method 'channel': Error: Message" + * Into: "Message" + */ +export function cleanErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message.replace(/^Error invoking remote method '[^']+': Error: /, ''); + } + if (typeof error === 'string') { + return error.replace(/^Error invoking remote method '[^']+': Error: /, ''); + } + return 'An unknown error occurred'; +} diff --git a/lib/models/Book.ts b/lib/models/Book.ts index 66bd300..59fd4b7 100644 --- a/lib/models/Book.ts +++ b/lib/models/Book.ts @@ -163,12 +163,12 @@ export const bookTypes: SelectBoxProps[] = [ export default class Book { constructor() { } - - static booksToSelectBox(books: BookProps[]): SelectBoxProps[] { - return books.map((book: BookProps) => { + + static booksToSelectBox(books: SyncedBook[]): SelectBoxProps[] { + return books.map((book: SyncedBook):SelectBoxProps => { return { label: book.title, - value: book.bookId, + value: book.id, } }); } diff --git a/lib/models/BookTables.ts b/lib/models/BookTables.ts index 868fdde..d6632cf 100644 --- a/lib/models/BookTables.ts +++ b/lib/models/BookTables.ts @@ -12,6 +12,7 @@ export interface EritBooksTable { desired_word_count:number|null; words_count:number|null; cover_image:string|null; + last_update:number; } export interface BookActSummariesTable { @@ -20,6 +21,7 @@ export interface BookActSummariesTable { user_id: string; act_index: number; summary: string | null; + last_update: number; } export interface BookAIGuideLineTable { @@ -41,9 +43,10 @@ export interface BookChaptersTable { book_id: string; author_id: string; title: string; - hashed_title: string | null; + hashed_title: string; words_count: number | null; - chapter_order: number | null; + chapter_order: number; + last_update: number; } export interface BookChapterContentTable { @@ -54,18 +57,20 @@ export interface BookChapterContentTable { content: string | null; words_count: number; time_on_it: number; + last_update: number; } export interface BookChapterInfosTable { chapter_info_id: string; chapter_id: string; - act_id: number | null; + act_id: number; incident_id: string | null; plot_point_id: string | null; book_id: string; author_id: string; summary: string | null; goal: string | null; + last_update: number; } export interface BookCharactersTable { @@ -80,6 +85,7 @@ export interface BookCharactersTable { role: string | null; biography: string | null; history: string | null; + last_update: number; } export interface BookCharactersAttributesTable { @@ -88,6 +94,7 @@ export interface BookCharactersAttributesTable { user_id: string; attribute_name: string; attribute_value: string; + last_update: number; } export interface BookGuideLineTable { @@ -112,6 +119,7 @@ export interface BookIncidentsTable { title: string; hashed_title: string; summary: string | null; + last_update: number; } export interface BookIssuesTable { @@ -120,6 +128,7 @@ export interface BookIssuesTable { book_id: string; name: string; hashed_issue_name: string; + last_update: number; } export interface BookLocationTable { @@ -128,6 +137,7 @@ export interface BookLocationTable { user_id: string; loc_name: string; loc_original_name: string; + last_update: number; } export interface BookPlotPointsTable { @@ -138,6 +148,7 @@ export interface BookPlotPointsTable { linked_incident_id: string | null; author_id: string; book_id: string; + last_update: number; } export interface BookWorldTable { @@ -151,6 +162,7 @@ export interface BookWorldTable { economy: string | null; religion: string | null; languages: string | null; + last_update: number; } export interface BookWorldElementsTable { @@ -161,6 +173,7 @@ export interface BookWorldElementsTable { name: string; original_name: string; description: string | null; + last_update: number; } export interface LocationElementTable { @@ -170,6 +183,7 @@ export interface LocationElementTable { element_name: string; original_name: string; element_description: string | null; + last_update: number; } export interface LocationSubElementTable { @@ -179,4 +193,5 @@ export interface LocationSubElementTable { sub_elem_name: string; original_name: string; sub_elem_description: string | null; + last_update: number; } \ No newline at end of file diff --git a/lib/models/SyncedBook.ts b/lib/models/SyncedBook.ts index 098b5c3..3063a55 100644 --- a/lib/models/SyncedBook.ts +++ b/lib/models/SyncedBook.ts @@ -109,4 +109,232 @@ export interface SyncedGuideLine { export interface SyncedAIGuideLine { lastUpdate: number; +} + +export interface BookSyncCompare { + id: string; + chapters: string[]; + chapterContents: string[]; + chapterInfos: string[]; + characters: string[]; + characterAttributes: string[]; + locations: string[]; + locationElements: string[]; + locationSubElements: string[]; + worlds: string[]; + worldElements: string[]; + incidents: string[]; + plotPoints: string[]; + issues: string[]; + actSummaries: string[]; + guideLine: boolean; + aiGuideLine: boolean; +} + +export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null { + const changedChapterIds: string[] = []; + const changedChapterContentIds: string[] = []; + const changedChapterInfoIds: string[] = []; + const changedCharacterIds: string[] = []; + const changedCharacterAttributeIds: string[] = []; + const changedLocationIds: string[] = []; + const changedLocationElementIds: string[] = []; + const changedLocationSubElementIds: string[] = []; + const changedWorldIds: string[] = []; + const changedWorldElementIds: string[] = []; + const changedIncidentIds: string[] = []; + const changedPlotPointIds: string[] = []; + const changedIssueIds: string[] = []; + const changedActSummaryIds: string[] = []; + let guideLineChanged: boolean = false; + let aiGuideLineChanged: boolean = false; + + newerBook.chapters.forEach((newerChapter: SyncedChapter): void => { + const olderChapter: SyncedChapter | undefined = olderBook.chapters.find((chapter: SyncedChapter): boolean => chapter.id === newerChapter.id); + + if (!olderChapter) { + changedChapterIds.push(newerChapter.id); + newerChapter.contents.forEach((content: SyncedChapterContent): void => { + changedChapterContentIds.push(content.id); + }); + if (newerChapter.info) { + changedChapterInfoIds.push(newerChapter.info.id); + } + } else if (newerChapter.lastUpdate > olderChapter.lastUpdate) { + changedChapterIds.push(newerChapter.id); + } else { + newerChapter.contents.forEach((newerContent: SyncedChapterContent): void => { + const olderContent: SyncedChapterContent | undefined = olderChapter.contents.find((content: SyncedChapterContent): boolean => content.id === newerContent.id); + if (!olderContent || newerContent.lastUpdate > olderContent.lastUpdate) { + changedChapterContentIds.push(newerContent.id); + } + }); + + if (newerChapter.info && olderChapter.info) { + if (newerChapter.info.lastUpdate > olderChapter.info.lastUpdate) { + changedChapterInfoIds.push(newerChapter.info.id); + } + } else if (newerChapter.info && !olderChapter.info) { + changedChapterInfoIds.push(newerChapter.info.id); + } + } + }); + + newerBook.characters.forEach((newerCharacter: SyncedCharacter): void => { + const olderCharacter: SyncedCharacter | undefined = olderBook.characters.find((character: SyncedCharacter): boolean => character.id === newerCharacter.id); + + if (!olderCharacter) { + changedCharacterIds.push(newerCharacter.id); + newerCharacter.attributes.forEach((attribute: SyncedCharacterAttribute): void => { + changedCharacterAttributeIds.push(attribute.id); + }); + } else if (newerCharacter.lastUpdate > olderCharacter.lastUpdate) { + changedCharacterIds.push(newerCharacter.id); + } else { + newerCharacter.attributes.forEach((newerAttribute: SyncedCharacterAttribute): void => { + const olderAttribute: SyncedCharacterAttribute | undefined = olderCharacter.attributes.find((attribute: SyncedCharacterAttribute): boolean => attribute.id === newerAttribute.id); + if (!olderAttribute || newerAttribute.lastUpdate > olderAttribute.lastUpdate) { + changedCharacterAttributeIds.push(newerAttribute.id); + } + }); + } + }); + + newerBook.locations.forEach((newerLocation: SyncedLocation): void => { + const olderLocation: SyncedLocation | undefined = olderBook.locations.find((location: SyncedLocation): boolean => location.id === newerLocation.id); + + if (!olderLocation) { + changedLocationIds.push(newerLocation.id); + newerLocation.elements.forEach((element: SyncedLocationElement): void => { + changedLocationElementIds.push(element.id); + element.subElements.forEach((subElement: SyncedLocationSubElement): void => { + changedLocationSubElementIds.push(subElement.id); + }); + }); + } else if (newerLocation.lastUpdate > olderLocation.lastUpdate) { + changedLocationIds.push(newerLocation.id); + } else { + newerLocation.elements.forEach((newerElement: SyncedLocationElement): void => { + const olderElement: SyncedLocationElement | undefined = olderLocation.elements.find((element: SyncedLocationElement): boolean => element.id === newerElement.id); + + if (!olderElement) { + changedLocationElementIds.push(newerElement.id); + newerElement.subElements.forEach((subElement: SyncedLocationSubElement): void => { + changedLocationSubElementIds.push(subElement.id); + }); + } else if (newerElement.lastUpdate > olderElement.lastUpdate) { + changedLocationElementIds.push(newerElement.id); + } else { + newerElement.subElements.forEach((newerSubElement: SyncedLocationSubElement): void => { + const olderSubElement: SyncedLocationSubElement | undefined = olderElement.subElements.find((subElement: SyncedLocationSubElement): boolean => subElement.id === newerSubElement.id); + if (!olderSubElement || newerSubElement.lastUpdate > olderSubElement.lastUpdate) { + changedLocationSubElementIds.push(newerSubElement.id); + } + }); + } + }); + } + }); + + newerBook.worlds.forEach((newerWorld: SyncedWorld): void => { + const olderWorld: SyncedWorld | undefined = olderBook.worlds.find((world: SyncedWorld): boolean => world.id === newerWorld.id); + + if (!olderWorld) { + changedWorldIds.push(newerWorld.id); + newerWorld.elements.forEach((element: SyncedWorldElement): void => { + changedWorldElementIds.push(element.id); + }); + } else if (newerWorld.lastUpdate > olderWorld.lastUpdate) { + changedWorldIds.push(newerWorld.id); + } else { + newerWorld.elements.forEach((newerElement: SyncedWorldElement): void => { + const olderElement: SyncedWorldElement | undefined = olderWorld.elements.find((element: SyncedWorldElement): boolean => element.id === newerElement.id); + if (!olderElement || newerElement.lastUpdate > olderElement.lastUpdate) { + changedWorldElementIds.push(newerElement.id); + } + }); + } + }); + + newerBook.incidents.forEach((newerIncident: SyncedIncident): void => { + const olderIncident: SyncedIncident | undefined = olderBook.incidents.find((incident: SyncedIncident): boolean => incident.id === newerIncident.id); + if (!olderIncident || newerIncident.lastUpdate > olderIncident.lastUpdate) { + changedIncidentIds.push(newerIncident.id); + } + }); + + newerBook.plotPoints.forEach((newerPlotPoint: SyncedPlotPoint): void => { + const olderPlotPoint: SyncedPlotPoint | undefined = olderBook.plotPoints.find((plotPoint: SyncedPlotPoint): boolean => plotPoint.id === newerPlotPoint.id); + if (!olderPlotPoint || newerPlotPoint.lastUpdate > olderPlotPoint.lastUpdate) { + changedPlotPointIds.push(newerPlotPoint.id); + } + }); + + newerBook.issues.forEach((newerIssue: SyncedIssue): void => { + const olderIssue: SyncedIssue | undefined = olderBook.issues.find((issue: SyncedIssue): boolean => issue.id === newerIssue.id); + if (!olderIssue || newerIssue.lastUpdate > olderIssue.lastUpdate) { + changedIssueIds.push(newerIssue.id); + } + }); + + newerBook.actSummaries.forEach((newerActSummary: SyncedActSummary): void => { + const olderActSummary: SyncedActSummary | undefined = olderBook.actSummaries.find((actSummary: SyncedActSummary): boolean => actSummary.id === newerActSummary.id); + if (!olderActSummary || newerActSummary.lastUpdate > olderActSummary.lastUpdate) { + changedActSummaryIds.push(newerActSummary.id); + } + }); + + if (newerBook.guideLine && olderBook.guideLine) { + guideLineChanged = newerBook.guideLine.lastUpdate > olderBook.guideLine.lastUpdate; + } else if (newerBook.guideLine && !olderBook.guideLine) { + guideLineChanged = true; + } + + if (newerBook.aiGuideLine && olderBook.aiGuideLine) { + aiGuideLineChanged = newerBook.aiGuideLine.lastUpdate > olderBook.aiGuideLine.lastUpdate; + } else if (newerBook.aiGuideLine && !olderBook.aiGuideLine) { + aiGuideLineChanged = true; + } + + const hasChanges: boolean = + changedChapterIds.length > 0 || + changedChapterContentIds.length > 0 || + changedChapterInfoIds.length > 0 || + changedCharacterIds.length > 0 || + changedCharacterAttributeIds.length > 0 || + changedLocationIds.length > 0 || + changedLocationElementIds.length > 0 || + changedLocationSubElementIds.length > 0 || + changedWorldIds.length > 0 || + changedWorldElementIds.length > 0 || + changedIncidentIds.length > 0 || + changedPlotPointIds.length > 0 || + changedIssueIds.length > 0 || + changedActSummaryIds.length > 0 || + guideLineChanged || + aiGuideLineChanged; + + if (!hasChanges) { + return null; + } + + return { + id: newerBook.id, + chapters: changedChapterIds, + chapterContents: changedChapterContentIds, + chapterInfos: changedChapterInfoIds, + characters: changedCharacterIds, + characterAttributes: changedCharacterAttributeIds, + locations: changedLocationIds, + locationElements: changedLocationElementIds, + locationSubElements: changedLocationSubElementIds, + worlds: changedWorldIds, + worldElements: changedWorldElementIds, + incidents: changedIncidentIds, + plotPoints: changedPlotPointIds, + issues: changedIssueIds, + actSummaries: changedActSummaryIds, + guideLine: guideLineChanged, + aiGuideLine: aiGuideLineChanged + }; } \ No newline at end of file diff --git a/lib/utils/syncComparison.ts b/lib/utils/syncComparison.ts new file mode 100644 index 0000000..be43771 --- /dev/null +++ b/lib/utils/syncComparison.ts @@ -0,0 +1,365 @@ +import { + SyncedBook, + SyncedChapter, + SyncedChapterContent, + SyncedChapterInfo, + SyncedCharacter, + SyncedCharacterAttribute, + SyncedLocation, + SyncedLocationElement, + SyncedLocationSubElement, + SyncedWorld, + SyncedWorldElement, + SyncedIncident, + SyncedPlotPoint, + SyncedIssue, + SyncedActSummary, + SyncedGuideLine, + SyncedAIGuideLine +} from "@/lib/models/SyncedBook"; + +/** + * Résultat de comparaison pour un livre + */ +export interface BookSyncDiff { + id: string; + type: string; + title: string; + subTitle: string | null; + bookNeedsUpdate: boolean; // Le livre lui-même a changé + lastUpdate: number; + chapters: SyncedChapter[]; + characters: SyncedCharacter[]; + locations: SyncedLocation[]; + worlds: SyncedWorld[]; + incidents: SyncedIncident[]; + plotPoints: SyncedPlotPoint[]; + issues: SyncedIssue[]; + actSummaries: SyncedActSummary[]; + guideLine: SyncedGuideLine | null; + aiGuideLine: SyncedAIGuideLine | null; + hasAnyChanges: boolean; // Indique si des changements existent +} + +/** + * Compare en profondeur deux livres synchronisés et retourne les différences + */ +export function compareBooks(serverBook: SyncedBook, localBook: SyncedBook): BookSyncDiff { + const bookNeedsUpdate = serverBook.lastUpdate > localBook.lastUpdate; + + // Comparer les chapitres + const chaptersWithChanges: SyncedChapter[] = []; + for (const serverChapter of serverBook.chapters) { + const localChapter = localBook.chapters.find(c => c.id === serverChapter.id); + + if (!localChapter) { + // Chapitre n'existe pas localement + chaptersWithChanges.push(serverChapter); + } else if (serverChapter.lastUpdate > localChapter.lastUpdate) { + // Chapitre a changé + chaptersWithChanges.push(serverChapter); + } else { + // Vérifier les contenus du chapitre + const contentsWithChanges: SyncedChapterContent[] = []; + for (const serverContent of serverChapter.contents) { + const localContent = localChapter.contents.find(c => c.id === serverContent.id); + if (!localContent || serverContent.lastUpdate > localContent.lastUpdate) { + contentsWithChanges.push(serverContent); + } + } + + // Vérifier l'info du chapitre + let infoNeedsUpdate = false; + if (serverChapter.info && localChapter.info) { + infoNeedsUpdate = serverChapter.info.lastUpdate > localChapter.info.lastUpdate; + } else if (serverChapter.info && !localChapter.info) { + infoNeedsUpdate = true; + } + + // Si des contenus ou info ont changé, inclure le chapitre + if (contentsWithChanges.length > 0 || infoNeedsUpdate) { + chaptersWithChanges.push({ + ...serverChapter, + contents: contentsWithChanges.length > 0 ? contentsWithChanges : serverChapter.contents, + info: infoNeedsUpdate ? serverChapter.info : null + }); + } + } + } + + // Comparer les personnages + const charactersWithChanges: SyncedCharacter[] = []; + for (const serverChar of serverBook.characters) { + const localChar = localBook.characters.find(c => c.id === serverChar.id); + + if (!localChar) { + charactersWithChanges.push(serverChar); + } else if (serverChar.lastUpdate > localChar.lastUpdate) { + charactersWithChanges.push(serverChar); + } else { + // Vérifier les attributs + const attributesWithChanges: SyncedCharacterAttribute[] = []; + for (const serverAttr of serverChar.attributes) { + const localAttr = localChar.attributes.find(a => a.id === serverAttr.id); + if (!localAttr || serverAttr.lastUpdate > localAttr.lastUpdate) { + attributesWithChanges.push(serverAttr); + } + } + + if (attributesWithChanges.length > 0) { + charactersWithChanges.push({ + ...serverChar, + attributes: attributesWithChanges + }); + } + } + } + + // Comparer les locations + const locationsWithChanges: SyncedLocation[] = []; + for (const serverLoc of serverBook.locations) { + const localLoc = localBook.locations.find(l => l.id === serverLoc.id); + + if (!localLoc) { + locationsWithChanges.push(serverLoc); + } else if (serverLoc.lastUpdate > localLoc.lastUpdate) { + locationsWithChanges.push(serverLoc); + } else { + // Vérifier les éléments + const elementsWithChanges: SyncedLocationElement[] = []; + for (const serverElem of serverLoc.elements) { + const localElem = localLoc.elements.find(e => e.id === serverElem.id); + + if (!localElem) { + elementsWithChanges.push(serverElem); + } else if (serverElem.lastUpdate > localElem.lastUpdate) { + elementsWithChanges.push(serverElem); + } else { + // Vérifier les sous-éléments + const subElementsWithChanges: SyncedLocationSubElement[] = []; + for (const serverSubElem of serverElem.subElements) { + const localSubElem = localElem.subElements.find(s => s.id === serverSubElem.id); + if (!localSubElem || serverSubElem.lastUpdate > localSubElem.lastUpdate) { + subElementsWithChanges.push(serverSubElem); + } + } + + if (subElementsWithChanges.length > 0) { + elementsWithChanges.push({ + ...serverElem, + subElements: subElementsWithChanges + }); + } + } + } + + if (elementsWithChanges.length > 0) { + locationsWithChanges.push({ + ...serverLoc, + elements: elementsWithChanges + }); + } + } + } + + // Comparer les mondes + const worldsWithChanges: SyncedWorld[] = []; + for (const serverWorld of serverBook.worlds) { + const localWorld = localBook.worlds.find(w => w.id === serverWorld.id); + + if (!localWorld) { + worldsWithChanges.push(serverWorld); + } else if (serverWorld.lastUpdate > localWorld.lastUpdate) { + worldsWithChanges.push(serverWorld); + } else { + // Vérifier les éléments du monde + const elementsWithChanges: SyncedWorldElement[] = []; + for (const serverElem of serverWorld.elements) { + const localElem = localWorld.elements.find(e => e.id === serverElem.id); + if (!localElem || serverElem.lastUpdate > localElem.lastUpdate) { + elementsWithChanges.push(serverElem); + } + } + + if (elementsWithChanges.length > 0) { + worldsWithChanges.push({ + ...serverWorld, + elements: elementsWithChanges + }); + } + } + } + + // Comparer les incidents + const incidentsWithChanges: SyncedIncident[] = []; + for (const serverIncident of serverBook.incidents) { + const localIncident = localBook.incidents.find(i => i.id === serverIncident.id); + if (!localIncident || serverIncident.lastUpdate > localIncident.lastUpdate) { + incidentsWithChanges.push(serverIncident); + } + } + + // Comparer les plot points + const plotPointsWithChanges: SyncedPlotPoint[] = []; + for (const serverPlot of serverBook.plotPoints) { + const localPlot = localBook.plotPoints.find(p => p.id === serverPlot.id); + if (!localPlot || serverPlot.lastUpdate > localPlot.lastUpdate) { + plotPointsWithChanges.push(serverPlot); + } + } + + // Comparer les issues + const issuesWithChanges: SyncedIssue[] = []; + for (const serverIssue of serverBook.issues) { + const localIssue = localBook.issues.find(i => i.id === serverIssue.id); + if (!localIssue || serverIssue.lastUpdate > localIssue.lastUpdate) { + issuesWithChanges.push(serverIssue); + } + } + + // Comparer les act summaries + const actSummariesWithChanges: SyncedActSummary[] = []; + for (const serverAct of serverBook.actSummaries) { + const localAct = localBook.actSummaries.find(a => a.id === serverAct.id); + if (!localAct || serverAct.lastUpdate > localAct.lastUpdate) { + actSummariesWithChanges.push(serverAct); + } + } + + // Comparer guideline + let guideLineNeedsUpdate: SyncedGuideLine | null = null; + if (serverBook.guideLine && localBook.guideLine) { + if (serverBook.guideLine.lastUpdate > localBook.guideLine.lastUpdate) { + guideLineNeedsUpdate = serverBook.guideLine; + } + } else if (serverBook.guideLine && !localBook.guideLine) { + guideLineNeedsUpdate = serverBook.guideLine; + } + + // Comparer AI guideline + let aiGuideLineNeedsUpdate: SyncedAIGuideLine | null = null; + if (serverBook.aiGuideLine && localBook.aiGuideLine) { + if (serverBook.aiGuideLine.lastUpdate > localBook.aiGuideLine.lastUpdate) { + aiGuideLineNeedsUpdate = serverBook.aiGuideLine; + } + } else if (serverBook.aiGuideLine && !localBook.aiGuideLine) { + aiGuideLineNeedsUpdate = serverBook.aiGuideLine; + } + + // Déterminer s'il y a des changements + const hasAnyChanges = bookNeedsUpdate || + chaptersWithChanges.length > 0 || + charactersWithChanges.length > 0 || + locationsWithChanges.length > 0 || + worldsWithChanges.length > 0 || + incidentsWithChanges.length > 0 || + plotPointsWithChanges.length > 0 || + issuesWithChanges.length > 0 || + actSummariesWithChanges.length > 0 || + guideLineNeedsUpdate !== null || + aiGuideLineNeedsUpdate !== null; + + return { + id: serverBook.id, + type: serverBook.type, + title: serverBook.title, + subTitle: serverBook.subTitle, + bookNeedsUpdate, + lastUpdate: serverBook.lastUpdate, + chapters: chaptersWithChanges, + characters: charactersWithChanges, + locations: locationsWithChanges, + worlds: worldsWithChanges, + incidents: incidentsWithChanges, + plotPoints: plotPointsWithChanges, + issues: issuesWithChanges, + actSummaries: actSummariesWithChanges, + guideLine: guideLineNeedsUpdate, + aiGuideLine: aiGuideLineNeedsUpdate, + hasAnyChanges + }; +} + +/** + * Compare tous les livres serveur vs locaux et retourne ceux qui ont des changements + */ +export function getBooksToSyncFromServer(serverBooks: SyncedBook[], localBooks: SyncedBook[]): BookSyncDiff[] { + const booksWithChanges: BookSyncDiff[] = []; + + for (const serverBook of serverBooks) { + const localBook = localBooks.find(b => b.id === serverBook.id); + + if (!localBook) { + // Livre n'existe pas localement - tout le livre doit être synchronisé + booksWithChanges.push({ + id: serverBook.id, + type: serverBook.type, + title: serverBook.title, + subTitle: serverBook.subTitle, + bookNeedsUpdate: true, + lastUpdate: serverBook.lastUpdate, + chapters: serverBook.chapters, + characters: serverBook.characters, + locations: serverBook.locations, + worlds: serverBook.worlds, + incidents: serverBook.incidents, + plotPoints: serverBook.plotPoints, + issues: serverBook.issues, + actSummaries: serverBook.actSummaries, + guideLine: serverBook.guideLine, + aiGuideLine: serverBook.aiGuideLine, + hasAnyChanges: true + }); + } else { + // Comparer en profondeur + const diff = compareBooks(serverBook, localBook); + if (diff.hasAnyChanges) { + booksWithChanges.push(diff); + } + } + } + + return booksWithChanges; +} + +/** + * Compare tous les livres locaux vs serveur et retourne ceux qui ont des changements locaux + */ +export function getBooksToSyncToServer(localBooks: SyncedBook[], serverBooks: SyncedBook[]): BookSyncDiff[] { + const booksWithChanges: BookSyncDiff[] = []; + + for (const localBook of localBooks) { + const serverBook = serverBooks.find(b => b.id === localBook.id); + + if (!serverBook) { + // Livre n'existe pas sur le serveur - tout le livre doit être envoyé + booksWithChanges.push({ + id: localBook.id, + type: localBook.type, + title: localBook.title, + subTitle: localBook.subTitle, + bookNeedsUpdate: true, + lastUpdate: localBook.lastUpdate, + chapters: localBook.chapters, + characters: localBook.characters, + locations: localBook.locations, + worlds: localBook.worlds, + incidents: localBook.incidents, + plotPoints: localBook.plotPoints, + issues: localBook.issues, + actSummaries: localBook.actSummaries, + guideLine: localBook.guideLine, + aiGuideLine: localBook.aiGuideLine, + hasAnyChanges: true + }); + } else { + // Comparer en profondeur (local vs server) + const diff = compareBooks(localBook, serverBook); + if (diff.hasAnyChanges) { + booksWithChanges.push(diff); + } + } + } + + return booksWithChanges; +}