diff --git a/app/page.tsx b/app/page.tsx index 261c597..b834cb9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -35,12 +35,36 @@ import OfflinePinSetup from "@/components/offline/OfflinePinSetup"; import OfflinePinVerify from "@/components/offline/OfflinePinVerify"; import {SyncedBook, BookSyncCompare, compareBookSyncs} from "@/lib/models/SyncedBook"; import {BooksSyncContext} from "@/context/BooksSyncContext"; +import useSyncBooks from "@/hooks/useSyncBooks"; +import {LocalSyncQueueContext, LocalSyncOperation} from "@/context/SyncQueueContext"; const messagesMap = { fr: frMessages, en: enMessages }; +function AutoSyncOnReconnect() { + const {offlineMode} = useContext(OfflineContext); + const {syncAllToServer, refreshBooks, booksToSyncToServer} = useSyncBooks(); + const [pendingSync, setPendingSync] = useState(false); + + useEffect((): void => { + if (!offlineMode.isOffline) { + setPendingSync(true); + refreshBooks(); + } + }, [offlineMode.isOffline]); + + useEffect((): void => { + if (pendingSync && booksToSyncToServer.length > 0) { + syncAllToServer(); + setPendingSync(false); + } + }, [booksToSyncToServer, pendingSync]); + + return null; +} + function ScribeContent() { const t = useTranslations(); const {lang: locale} = useContext(LangContext); @@ -79,7 +103,48 @@ function ScribeContent() { const [homeStepsGuide, setHomeStepsGuide] = useState(false); const [showPinSetup, setShowPinSetup] = useState(false); const [showPinVerify, setShowPinVerify] = useState(false); - + + const [localSyncQueue, setLocalSyncQueue] = useState([]); + const [isQueueProcessing, setIsQueueProcessing] = useState(false); + + + function addToLocalSyncQueue(channel: string, data: Record): void { + const operation: LocalSyncOperation = { + id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, + channel, + data, + timestamp: Date.now(), + }; + setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] => [...prev, operation]); + } + + useEffect((): void => { + if (localSyncQueue.length === 0 || isQueueProcessing) { + return; + } + + async function processQueue(): Promise { + setIsQueueProcessing(true); + + const queueCopy: LocalSyncOperation[] = [...localSyncQueue]; + + for (const operation of queueCopy) { + try { + await window.electron.invoke(operation.channel, operation.data); + setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] => + prev.filter((op: LocalSyncOperation): boolean => op.id !== operation.id) + ); + } catch (error) { + console.error(`[LocalSyncQueue] Failed to process operation ${operation.channel}:`, error); + } + } + + setIsQueueProcessing(false); + } + + processQueue().then(); + }, [localSyncQueue, isQueueProcessing]); + const homeSteps: GuideStep[] = [ { id: 0, @@ -212,6 +277,7 @@ function ScribeContent() { console.log('bookSyncDiffsFromServer', bookSyncDiffsFromServer); console.log('bookSyncDiffsToServer', bookSyncDiffsToServer); }, [localSyncedBooks, serverSyncedBooks,localOnlyBooks, bookSyncDiffsFromServer, bookSyncDiffsToServer]); + async function getBooks(): Promise { try { @@ -313,7 +379,6 @@ function ScribeContent() { setHomeStepsGuide(false); } } else { - // Mode offline: stocker dans localStorage const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]'); if (!completedGuides.includes('home-basic')) { completedGuides.push('home-basic'); @@ -535,10 +600,17 @@ function ScribeContent() { return ( - - - - + + + + + ) } - - - - + + + + + ); } diff --git a/components/ScribeFooterBar.tsx b/components/ScribeFooterBar.tsx index 45b4831..5ccf2d7 100644 --- a/components/ScribeFooterBar.tsx +++ b/components/ScribeFooterBar.tsx @@ -26,7 +26,6 @@ export default function ScribeFooterBar() { }, [editor?.state.doc.textContent]); function getWordCount(): void { - console.log(editor) if (editor) { try { const content: string = editor?.state.doc.textContent; diff --git a/components/SyncBook.tsx b/components/SyncBook.tsx index 664db32..6f98c49 100644 --- a/components/SyncBook.tsx +++ b/components/SyncBook.tsx @@ -3,13 +3,8 @@ import {faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner} from "@fortawesome import {useTranslations} from "next-intl"; import {useState, useContext} from "react"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; -import System from "@/lib/models/System"; -import {SessionContext, SessionContextProps} from "@/context/SessionContext"; -import {LangContext} from "@/context/LangContext"; -import {CompleteBook} from "@/lib/models/Book"; -import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext"; -import {AlertContext, AlertContextProps} from "@/context/AlertContext"; -import {BookSyncCompare, SyncedBook} from "@/lib/models/SyncedBook"; +import {SyncType} from "@/context/BooksSyncContext"; +import useSyncBooks from "@/hooks/useSyncBooks"; interface SyncBookProps { bookId: string; @@ -18,148 +13,43 @@ interface SyncBookProps { export default function SyncBook({bookId, status}: SyncBookProps) { const t = useTranslations(); - const {session} = useContext(SessionContext); - const {lang} = useContext(LangContext); - const {errorMessage} = useContext(AlertContext); const {isCurrentlyOffline} = useContext(OfflineContext); const [isLoading, setIsLoading] = useState(false); const [currentStatus, setCurrentStatus] = useState(status); - const {booksToSyncToServer, booksToSyncFromServer,serverSyncedBooks,localSyncedBooks,setLocalOnlyBooks, setServerOnlyBooks} = useContext(BooksSyncContext) + const {upload: hookUpload, download: hookDownload, syncFromServer: hookSyncFromServer, syncToServer: hookSyncToServer} = useSyncBooks(); const isOffline: boolean = isCurrentlyOffline(); async function upload(): Promise { - if (isOffline) { - return; - } + if (isOffline) return; setIsLoading(true); - try { - const bookToSync: CompleteBook = await window.electron.invoke('db:book:uploadToServer', bookId); - if (!bookToSync) { - errorMessage(t("bookCard.uploadError")); - return; - } - const response: boolean = await System.authPostToServer('book/sync/upload', { - book: bookToSync - }, session.accessToken, lang); - if (!response) { - errorMessage(t("bookCard.uploadError")); - return; - } - setCurrentStatus('synced'); - setLocalOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => { - return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId) - }); - } catch (e:unknown) { - if (e instanceof Error) { - errorMessage(e.message); - } else { - errorMessage(t("bookCard.uploadError")); - } - } finally { - setIsLoading(false); - } + const success = await hookUpload(bookId); + if (success) setCurrentStatus('synced'); + setIsLoading(false); } async function download(): Promise { - if (isOffline) { - return; - } + if (isOffline) return; setIsLoading(true); - try { - const response: CompleteBook = await System.authGetQueryToServer('book/sync/download', session.accessToken, lang, {bookId}); - if (!response) { - errorMessage(t("bookCard.downloadError")); - return; - } - const syncStatus:boolean = await window.electron.invoke('db:book:syncSave', response); - if (!syncStatus) { - errorMessage(t("bookCard.downloadError")); - return; - } - setCurrentStatus('synced'); - setServerOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => { - return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId) - }); - } catch (e:unknown) { - if (e instanceof Error) { - errorMessage(e.message); - } else { - errorMessage(t("bookCard.downloadError")); - } - } finally { - setIsLoading(false); - } + const success = await hookDownload(bookId); + if (success) setCurrentStatus('synced'); + setIsLoading(false); } async function syncFromServer(): Promise { - if (isOffline) { - return; - } + if (isOffline) return; setIsLoading(true); - 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")); - } - } finally { - setIsLoading(false); - } + const success = await hookSyncFromServer(bookId); + if (success) setCurrentStatus('synced'); + setIsLoading(false); } async function syncToServer(): Promise { - if (isOffline) { - return; - } + if (isOffline) return; setIsLoading(true); - 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.authPatchToServer('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")); - } - } finally { - setIsLoading(false); - } + const success = await hookSyncToServer(bookId); + if (success) setCurrentStatus('synced'); + setIsLoading(false); } if (isLoading) { diff --git a/components/book/AddNewBookForm.tsx b/components/book/AddNewBookForm.tsx index 5e5135d..259eaf1 100644 --- a/components/book/AddNewBookForm.tsx +++ b/components/book/AddNewBookForm.tsx @@ -138,19 +138,11 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch< }; let bookId: string; - if (!isCurrentlyOffline()) { - // Online - call API server - bookId = await System.authPostToServer('book/add', { - title: title, - subTitle: subtitle, - type: selectedBookType, - summary: summary, - serie: 0, - publicationDate: publicationDate, - desiredWordCount: wordCount, - }, token, lang); - } else { + if (isCurrentlyOffline()) { bookId = await window.electron.invoke('db:book:create', bookData); + } else { + // Online - call API server + bookId = await System.authPostToServer('book/add', bookData, token, lang); } if (!bookId) { diff --git a/components/book/settings/BasicInformationSetting.tsx b/components/book/settings/BasicInformationSetting.tsx index 410f181..7b9adc2 100644 --- a/components/book/settings/BasicInformationSetting.tsx +++ b/components/book/settings/BasicInformationSetting.tsx @@ -17,11 +17,16 @@ import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import {BookProps} from "@/lib/models/Book"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; function BasicInformationSetting(props: any, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext) const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {session} = useContext(SessionContext); const {book, setBook} = useContext(BookContext); @@ -117,34 +122,21 @@ function BasicInformationSetting(props: any, ref: any) { } try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:book:updateBasicInformation', { - title: title, - subTitle: subTitle, - summary: summary, - publicationDate: publicationDate, - wordCount: wordCount, - bookId: bookId - }); + const basicInfoData = { + title: title, + subTitle: subTitle, + summary: summary, + publicationDate: publicationDate, + wordCount: wordCount, + bookId: bookId + }; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:updateBasicInformation', basicInfoData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:updateBasicInformation', { - title: title, - subTitle: subTitle, - summary: summary, - publicationDate: publicationDate, - wordCount: wordCount, - bookId: bookId - }); - } else { - response = await System.authPostToServer('book/basic-information', { - title: title, - subTitle: subTitle, - summary: summary, - publicationDate: publicationDate, - wordCount: wordCount, - bookId: bookId - }, userToken, lang); + response = await System.authPostToServer('book/basic-information', basicInfoData, userToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:updateBasicInformation', basicInfoData); } } if (!response) { diff --git a/components/book/settings/DeleteBook.tsx b/components/book/settings/DeleteBook.tsx index 11c8c17..73f3484 100644 --- a/components/book/settings/DeleteBook.tsx +++ b/components/book/settings/DeleteBook.tsx @@ -7,7 +7,6 @@ import {LangContext, LangContextProps} from "@/context/LangContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import AlertBox from "@/components/AlertBox"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; -import {BookContext} from "@/context/BookContext"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {SyncedBook} from "@/lib/models/SyncedBook"; @@ -19,7 +18,6 @@ export default function DeleteBook({bookId}: DeleteBookProps) { const {session} = useContext(SessionContext); const {lang} = useContext(LangContext) const {isCurrentlyOffline} = useContext(OfflineContext); - const {book} = useContext(BookContext); const [showConfirmBox, setShowConfirmBox] = useState(false); const {errorMessage} = useContext(AlertContext) const {serverOnlyBooks,setServerOnlyBooks,localOnlyBooks,setLocalOnlyBooks} = useContext(BooksSyncContext); @@ -31,38 +29,26 @@ export default function DeleteBook({bookId}: DeleteBookProps) { async function handleDeleteBook(): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:book:delete', { - id: bookId, - }); + const deleteData = { id: bookId }; + const ifLocalBook: SyncedBook | undefined = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId); + + if (isCurrentlyOffline() || ifLocalBook) { + response = await window.electron.invoke('db:book:delete', deleteData); } else { - const ifLocalBook:SyncedBook|undefined = localOnlyBooks.find((book: SyncedBook):boolean => book.id === bookId); - if (ifLocalBook) { - response = await window.electron.invoke('db:book:delete', { - id: bookId, - }); - } else { - response = await window.electron.invoke('db:book:delete', { - id: bookId, - }); - response = await System.authDeleteToServer( - `book/delete`, - { - id: bookId, - }, - session.accessToken, - lang - ); - } + response = await System.authDeleteToServer( + `book/delete`, + deleteData, + session.accessToken, + lang + ); } if (response) { setShowConfirmBox(false); - if (book?.localBook){ - setLocalOnlyBooks(localOnlyBooks.filter((book:SyncedBook):boolean => book.id !== bookId)); - return; + if (ifLocalBook) { + setLocalOnlyBooks(localOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId)); + } else { + setServerOnlyBooks(serverOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId)); } - setServerOnlyBooks(serverOnlyBooks.filter((book:SyncedBook):boolean => book.id !== bookId)); - setShowConfirmBox(false); } } catch (e: unknown) { if (e instanceof Error) { diff --git a/components/book/settings/characters/CharacterComponent.tsx b/components/book/settings/characters/CharacterComponent.tsx index 59a36fe..ffe730a 100644 --- a/components/book/settings/characters/CharacterComponent.tsx +++ b/components/book/settings/characters/CharacterComponent.tsx @@ -10,6 +10,9 @@ import CharacterDetail from "@/components/book/settings/characters/CharacterDeta import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; interface CharacterDetailProps { selectedCharacter: CharacterProps | null; @@ -48,6 +51,8 @@ export function CharacterComponent(props: any, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext) const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {session} = useContext(SessionContext); const {book} = useContext(BookContext); const {errorMessage, successMessage} = useContext(AlertContext); @@ -120,22 +125,23 @@ export function CharacterComponent(props: any, ref: any) { } try { let characterId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { characterId = await window.electron.invoke('db:character:create', { bookId: book?.bookId, character: updatedCharacter, }); } else { - if (book?.localBook) { - characterId = await window.electron.invoke('db:character:create', { + characterId = await System.authPostToServer(`character/add`, { + bookId: book?.bookId, + character: updatedCharacter, + }, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:character:create', { bookId: book?.bookId, + characterId, character: updatedCharacter, }); - } else { - characterId = await System.authPostToServer(`character/add`, { - bookId: book?.bookId, - character: updatedCharacter, - }, session.accessToken, lang); } } if (!characterId) { @@ -157,19 +163,19 @@ export function CharacterComponent(props: any, ref: any) { async function updateCharacter(updatedCharacter: CharacterProps,): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:character:update', { character: updatedCharacter, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:character:update', { + response = await System.authPostToServer(`character/update`, { + character: updatedCharacter, + }, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:character:update', { character: updatedCharacter, }); - } else { - response = await System.authPostToServer(`character/update`, { - character: updatedCharacter, - }, session.accessToken, lang); } } if (!response) { @@ -215,25 +221,26 @@ export function CharacterComponent(props: any, ref: any) { } else { try { let attributeId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { attributeId = await window.electron.invoke('db:character:attribute:add', { characterId: selectedCharacter.id, type: section, name: value.name, }); } else { - if (book?.localBook) { - attributeId = await window.electron.invoke('db:character:attribute:add', { + attributeId = await System.authPostToServer(`character/attribute/add`, { + characterId: selectedCharacter.id, + type: section, + name: value.name, + }, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:character:attribute:add', { characterId: selectedCharacter.id, + attributeId, type: section, name: value.name, }); - } else { - attributeId = await System.authPostToServer(`character/attribute/add`, { - characterId: selectedCharacter.id, - type: section, - name: value.name, - }, session.accessToken, lang); } } if (!attributeId) { @@ -271,19 +278,19 @@ export function CharacterComponent(props: any, ref: any) { } else { try { let response: boolean; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:character:attribute:delete', { attributeId: attrId, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:character:attribute:delete', { + response = await System.authDeleteToServer(`character/attribute/delete`, { + attributeId: attrId, + }, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:character:attribute:delete', { attributeId: attrId, }); - } else { - response = await System.authDeleteToServer(`character/attribute/delete`, { - attributeId: attrId, - }, session.accessToken, lang); } } if (!response) { diff --git a/components/book/settings/characters/CharacterDetail.tsx b/components/book/settings/characters/CharacterDetail.tsx index 004e1b3..d755adf 100644 --- a/components/book/settings/characters/CharacterDetail.tsx +++ b/components/book/settings/characters/CharacterDetail.tsx @@ -6,6 +6,7 @@ import SelectBox from "@/components/form/SelectBox"; import {AlertContext} from "@/context/AlertContext"; import {SessionContext} from "@/context/SessionContext"; import { + Attribute, CharacterAttribute, characterCategories, CharacterElement, @@ -32,6 +33,8 @@ import {LangContext} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {BookContext} from "@/context/BookContext"; +type AttributeResponse = { type: string; values: Attribute[] }[]; + interface CharacterDetailProps { selectedCharacter: CharacterProps | null; setSelectedCharacter: Dispatch>; @@ -70,23 +73,31 @@ export default function CharacterDetail( async function getAttributes(): Promise { try { - let response: CharacterAttribute; + let response: AttributeResponse; if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:character:attributes', { + response = await window.electron.invoke('db:character:attributes', { characterId: selectedCharacter?.id, }); } else { if (book?.localBook) { - response = await window.electron.invoke('db:character:attributes', { + response = await window.electron.invoke('db:character:attributes', { characterId: selectedCharacter?.id, }); } else { - response = await System.authGetQueryToServer(`character/attribute`, session.accessToken, lang, { + response = await System.authGetQueryToServer(`character/attribute`, session.accessToken, lang, { characterId: selectedCharacter?.id, }); } } if (response) { + const attributes: CharacterAttribute = {}; + response.forEach((item: { + type: string + values: Attribute[] + }):void => { + attributes[item.type] = item.values; + }); + setSelectedCharacter({ id: selectedCharacter?.id ?? '', name: selectedCharacter?.name ?? '', @@ -97,14 +108,14 @@ export default function CharacterDetail( biography: selectedCharacter?.biography, history: selectedCharacter?.history, role: selectedCharacter?.role ?? '', - physical: response.physical ?? [], - psychological: response.psychological ?? [], - relations: response.relations ?? [], - skills: response.skills ?? [], - weaknesses: response.weaknesses ?? [], - strengths: response.strengths ?? [], - goals: response.goals ?? [], - motivations: response.motivations ?? [], + physical: attributes.physical ?? [], + psychological: attributes.psychological ?? [], + relations: attributes.relations ?? [], + skills: attributes.skills ?? [], + weaknesses: attributes.weaknesses ?? [], + strengths: attributes.strengths ?? [], + goals: attributes.goals ?? [], + motivations: attributes.motivations ?? [], }); } } catch (e: unknown) { diff --git a/components/book/settings/guide-line/GuideLineSetting.tsx b/components/book/settings/guide-line/GuideLineSetting.tsx index 9807e22..a835db6 100644 --- a/components/book/settings/guide-line/GuideLineSetting.tsx +++ b/components/book/settings/guide-line/GuideLineSetting.tsx @@ -22,11 +22,16 @@ import { import {useTranslations} from "next-intl"; import {LangContext} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; function GuideLineSetting(props: any, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {book} = useContext(BookContext); const {session} = useContext(SessionContext); const userToken: string = session?.accessToken ? session?.accessToken : ''; @@ -159,18 +164,18 @@ function GuideLineSetting(props: any, ref: any) { intendedAudience: intendedAudience, keyMessages: keyMessages, }; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:book:guideline:update', guidelineData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:guideline:update', guidelineData); - } else { - response = await System.authPostToServer( - 'book/guide-line', - guidelineData, - userToken, - lang, - ); + response = await System.authPostToServer( + 'book/guide-line', + guidelineData, + userToken, + lang, + ); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:guideline:update', guidelineData); } } if (!response) { @@ -190,45 +195,28 @@ function GuideLineSetting(props: any, ref: any) { async function saveQuillSense(): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:book:guideline:ai:update', { - bookId: bookId, - plotSummary: plotSummary, - verbTense: verbTense, - narrativeType: narrativeType, - dialogueType: dialogueType, - toneAtmosphere: toneAtmosphere, - language: language, - themes: themes, - }); + const aiGuidelineData = { + bookId: bookId, + plotSummary: plotSummary, + verbTense: verbTense, + narrativeType: narrativeType, + dialogueType: dialogueType, + toneAtmosphere: toneAtmosphere, + language: language, + themes: themes, + }; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:guideline:ai:update', aiGuidelineData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:guideline:ai:update', { - bookId: bookId, - plotSummary: plotSummary, - verbTense: verbTense, - narrativeType: narrativeType, - dialogueType: dialogueType, - toneAtmosphere: toneAtmosphere, - language: language, - themes: themes, - }); - } else { - response = await System.authPostToServer( - 'quillsense/book/guide-line', - { - bookId: bookId, - plotSummary: plotSummary, - verbTense: verbTense, - narrativeType: narrativeType, - dialogueType: dialogueType, - toneAtmosphere: toneAtmosphere, - language: language, - themes: themes, - }, - userToken, - lang, - ); + response = await System.authPostToServer( + 'quillsense/book/guide-line', + aiGuidelineData, + userToken, + lang, + ); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:guideline:ai:update', aiGuidelineData); } } if (response) { diff --git a/components/book/settings/locations/LocationComponent.tsx b/components/book/settings/locations/LocationComponent.tsx index b51923d..304f29e 100644 --- a/components/book/settings/locations/LocationComponent.tsx +++ b/components/book/settings/locations/LocationComponent.tsx @@ -12,6 +12,9 @@ import TexteAreaInput from "@/components/form/TexteAreaInput"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; interface SubElement { id: string; @@ -36,6 +39,8 @@ export function LocationComponent(props: any, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {session} = useContext(SessionContext); const {successMessage, errorMessage} = useContext(AlertContext); const {book} = useContext(BookContext); @@ -91,22 +96,23 @@ export function LocationComponent(props: any, ref: any) { } try { let sectionId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { sectionId = await window.electron.invoke('db:location:section:add', { bookId: bookId, locationName: newSectionName, }); } else { - if (book?.localBook) { - sectionId = await window.electron.invoke('db:location:section:add', { + sectionId = await System.authPostToServer(`location/section/add`, { + bookId: bookId, + locationName: newSectionName, + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:location:section:add', { bookId: bookId, + sectionId, locationName: newSectionName, }); - } else { - sectionId = await System.authPostToServer(`location/section/add`, { - bookId: bookId, - locationName: newSectionName, - }, token, lang); } } if (!sectionId) { @@ -136,26 +142,27 @@ export function LocationComponent(props: any, ref: any) { } try { let elementId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { elementId = await window.electron.invoke('db:location:element:add', { bookId: bookId, locationId: sectionId, elementName: newElementNames[sectionId], }); } else { - if (book?.localBook) { - elementId = await window.electron.invoke('db:location:element:add', { + elementId = await System.authPostToServer(`location/element/add`, { bookId: bookId, locationId: sectionId, elementName: newElementNames[sectionId], + }, + token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:location:element:add', { + bookId: bookId, + locationId: sectionId, + elementId, + elementName: newElementNames[sectionId], }); - } else { - elementId = await System.authPostToServer(`location/element/add`, { - bookId: bookId, - locationId: sectionId, - elementName: newElementNames[sectionId], - }, - token, lang); } } if (!elementId) { @@ -211,22 +218,24 @@ export function LocationComponent(props: any, ref: any) { ); try { let subElementId: string; - if (isCurrentlyOffline()) { + const elementId = sections[sectionIndex].elements[elementIndex].id; + if (isCurrentlyOffline() || book?.localBook) { subElementId = await window.electron.invoke('db:location:subelement:add', { - elementId: sections[sectionIndex].elements[elementIndex].id, + elementId: elementId, subElementName: newSubElementNames[elementIndex], }); } else { - if (book?.localBook) { - subElementId = await window.electron.invoke('db:location:subelement:add', { - elementId: sections[sectionIndex].elements[elementIndex].id, + subElementId = await System.authPostToServer(`location/sub-element/add`, { + elementId: elementId, + subElementName: newSubElementNames[elementIndex], + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:location:subelement:add', { + elementId: elementId, + subElementId, subElementName: newSubElementNames[elementIndex], }); - } else { - subElementId = await System.authPostToServer(`location/sub-element/add`, { - elementId: sections[sectionIndex].elements[elementIndex].id, - subElementName: newSubElementNames[elementIndex], - }, token, lang); } } if (!subElementId) { @@ -275,19 +284,19 @@ export function LocationComponent(props: any, ref: any) { let response: boolean; const elementId = sections.find((section: LocationProps): boolean => section.id === sectionId) ?.elements[elementIndex].id; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:location:element:delete', { elementId: elementId, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:location:element:delete', { + response = await System.authDeleteToServer(`location/element/delete`, { + elementId: elementId, + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:location:element:delete', { elementId: elementId, }); - } else { - response = await System.authDeleteToServer(`location/element/delete`, { - elementId: elementId, - }, token, lang); } } if (!response) { @@ -315,19 +324,19 @@ export function LocationComponent(props: any, ref: any) { try { let response: boolean; const subElementId = sections.find((section: LocationProps): boolean => section.id === sectionId)?.elements[elementIndex].subElements[subElementIndex].id; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:location:subelement:delete', { subElementId: subElementId, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:location:subelement:delete', { + response = await System.authDeleteToServer(`location/sub-element/delete`, { + subElementId: subElementId, + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:location:subelement:delete', { subElementId: subElementId, }); - } else { - response = await System.authDeleteToServer(`location/sub-element/delete`, { - subElementId: subElementId, - }, token, lang); } } if (!response) { @@ -350,19 +359,19 @@ export function LocationComponent(props: any, ref: any) { async function handleRemoveSection(sectionId: string): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:location:delete', { locationId: sectionId, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:location:delete', { + response = await System.authDeleteToServer(`location/delete`, { + locationId: sectionId, + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:location:delete', { locationId: sectionId, }); - } else { - response = await System.authDeleteToServer(`location/delete`, { - locationId: sectionId, - }, token, lang); } } if (!response) { @@ -383,19 +392,19 @@ export function LocationComponent(props: any, ref: any) { async function handleSave(): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:location:update', { locations: sections, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:location:update', { + response = await System.authPostToServer(`location/update`, { + locations: sections, + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:location:update', { locations: sections, }); - } else { - response = await System.authPostToServer(`location/update`, { - locations: sections, - }, token, lang); } } if (!response) { diff --git a/components/book/settings/story/Act.tsx b/components/book/settings/story/Act.tsx index ee29115..64be161 100644 --- a/components/book/settings/story/Act.tsx +++ b/components/book/settings/story/Act.tsx @@ -21,6 +21,9 @@ import ActPlotPoints from '@/components/book/settings/story/act/ActPlotPoints'; import {useTranslations} from 'next-intl'; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; interface ActProps { acts: ActType[]; @@ -32,6 +35,8 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { const t = useTranslations('actComponent'); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {book} = useContext(BookContext); const {session} = useContext(SessionContext); const {errorMessage, successMessage} = useContext(AlertContext); @@ -74,22 +79,23 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { try { let incidentId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { incidentId = await window.electron.invoke('db:book:incident:add', { bookId, name: newIncidentTitle, }); } else { - if (book?.localBook) { - incidentId = await window.electron.invoke('db:book:incident:add', { + incidentId = await System.authPostToServer('book/incident/new', { + bookId, + name: newIncidentTitle, + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:incident:add', { bookId, + incidentId, name: newIncidentTitle, }); - } else { - incidentId = await System.authPostToServer('book/incident/new', { - bookId, - name: newIncidentTitle, - }, token, lang); } } if (!incidentId) { @@ -104,7 +110,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { summary: '', chapters: [], }; - + return { ...act, incidents: [...(act.incidents || []), newIncident], @@ -126,22 +132,14 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { async function deleteIncident(actId: number, incidentId: string): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:book:incident:remove', { - bookId, - incidentId, - }); + const deleteData = { bookId, incidentId }; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:incident:remove', deleteData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:incident:remove', { - bookId, - incidentId, - }); - } else { - response = await System.authDeleteToServer('book/incident/remove', { - bookId, - incidentId, - }, token, lang); + response = await System.authDeleteToServer('book/incident/remove', deleteData, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:incident:remove', deleteData); } } if (!response) { @@ -173,25 +171,21 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { if (newPlotPointTitle.trim() === '') return; try { let plotId: string; - if (isCurrentlyOffline()) { - plotId = await window.electron.invoke('db:book:plot:add', { - bookId, - name: newPlotPointTitle, - incidentId: selectedIncidentId, - }); + const plotData = { + bookId, + name: newPlotPointTitle, + incidentId: selectedIncidentId, + }; + if (isCurrentlyOffline() || book?.localBook) { + plotId = await window.electron.invoke('db:book:plot:add', plotData); } else { - if (book?.localBook) { - plotId = await window.electron.invoke('db:book:plot:add', { - bookId, - name: newPlotPointTitle, - incidentId: selectedIncidentId, + plotId = await System.authPostToServer('book/plot/new', plotData, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:plot:add', { + ...plotData, + plotId, }); - } else { - plotId = await System.authPostToServer('book/plot/new', { - bookId, - name: newPlotPointTitle, - incidentId: selectedIncidentId, - }, token, lang); } } if (!plotId) { @@ -229,19 +223,14 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { async function deletePlotPoint(actId: number, plotPointId: string): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:book:plot:remove', { - plotId: plotPointId, - }); + const deleteData = { plotId: plotPointId }; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:plot:remove', deleteData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:plot:remove', { - plotId: plotPointId, - }); - } else { - response = await System.authDeleteToServer('book/plot/remove', { - plotId: plotPointId, - }, token, lang); + response = await System.authDeleteToServer('book/plot/remove', deleteData, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:plot:remove', deleteData); } } if (!response) { @@ -289,13 +278,16 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { plotId: destination === 'plotPoint' ? itemId : null, incidentId: destination === 'incident' ? itemId : null, }; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { linkId = await window.electron.invoke('db:chapter:information:add', linkData); } else { - if (book?.localBook) { - linkId = await window.electron.invoke('db:chapter:information:add', linkData); - } else { - linkId = await System.authPostToServer('chapter/resume/add', linkData, token, lang); + linkId = await System.authPostToServer('chapter/resume/add', linkData, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:chapter:information:add', { + ...linkData, + chapterInfoId: linkId, + }); } } if (!linkId) { @@ -373,19 +365,14 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { ): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:chapter:information:remove', { - chapterInfoId, - }); + const unlinkData = { chapterInfoId }; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:chapter:information:remove', unlinkData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:chapter:information:remove', { - chapterInfoId, - }); - } else { - response = await System.authDeleteToServer('chapter/resume/remove', { - chapterInfoId, - }, token, lang); + response = await System.authDeleteToServer('chapter/resume/remove', unlinkData, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:chapter:information:remove', unlinkData); } } if (!response) { diff --git a/components/book/settings/story/Issue.tsx b/components/book/settings/story/Issue.tsx index cdfbbe8..de2b001 100644 --- a/components/book/settings/story/Issue.tsx +++ b/components/book/settings/story/Issue.tsx @@ -10,6 +10,9 @@ import CollapsableArea from "@/components/CollapsableArea"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; interface IssuesProps { issues: Issue[]; @@ -20,6 +23,8 @@ export default function Issues({issues, setIssues}: IssuesProps) { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {book} = useContext(BookContext); const {session} = useContext(SessionContext); const {errorMessage} = useContext(AlertContext); @@ -36,22 +41,23 @@ export default function Issues({issues, setIssues}: IssuesProps) { } try { let issueId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { issueId = await window.electron.invoke('db:book:issue:add', { bookId, name: newIssueName, }); } else { - if (book?.localBook) { - issueId = await window.electron.invoke('db:book:issue:add', { + issueId = await System.authPostToServer('book/issue/add', { + bookId, + name: newIssueName, + }, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:issue:add', { bookId, + issueId, name: newIssueName, }); - } else { - issueId = await System.authPostToServer('book/issue/add', { - bookId, - name: newIssueName, - }, token, lang); } } if (!issueId) { @@ -62,7 +68,7 @@ export default function Issues({issues, setIssues}: IssuesProps) { name: newIssueName, id: issueId, }; - + setIssues([...issues, newIssue]); setNewIssueName(''); } catch (e: unknown) { @@ -77,32 +83,32 @@ export default function Issues({issues, setIssues}: IssuesProps) { async function deleteIssue(issueId: string): Promise { if (issueId === undefined) { errorMessage(t("issues.errorInvalidId")); + return; } - try { let response: boolean; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:book:issue:remove', { bookId, issueId, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:issue:remove', { + response = await System.authDeleteToServer( + 'book/issue/remove', + { + bookId, + issueId, + }, + token, + lang + ); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:issue:remove', { bookId, issueId, }); - } else { - response = await System.authDeleteToServer( - 'book/issue/remove', - { - bookId, - issueId, - }, - token, - lang - ); } } if (response) { diff --git a/components/book/settings/story/MainChapter.tsx b/components/book/settings/story/MainChapter.tsx index c9b061e..31ec772 100644 --- a/components/book/settings/story/MainChapter.tsx +++ b/components/book/settings/story/MainChapter.tsx @@ -13,6 +13,9 @@ import CollapsableArea from "@/components/CollapsableArea"; import {useTranslations} from "next-intl"; import {LangContext} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; interface MainChapterProps { chapters: ChapterListProps[]; @@ -23,6 +26,8 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) { const t = useTranslations(); const {lang} = useContext(LangContext) const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {book} = useContext(BookContext); const {session} = useContext(SessionContext); const {errorMessage, successMessage} = useContext(AlertContext); @@ -85,13 +90,13 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) { bookId, chapterId: chapterIdToRemove, }; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:chapter:remove', deleteData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:chapter:remove', deleteData); - } else { - response = await System.authDeleteToServer('chapter/remove', deleteData, token, lang); + response = await System.authDeleteToServer('chapter/remove', deleteData, token, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:chapter:remove', deleteData); } } if (!response) { @@ -121,13 +126,16 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) { chapterOrder: newChapterOrder ? newChapterOrder : 0, title: newChapterTitle, }; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { responseId = await window.electron.invoke('db:chapter:add', chapterData); } else { - if (book?.localBook) { - responseId = await window.electron.invoke('db:chapter:add', chapterData); - } else { - responseId = await System.authPostToServer('chapter/add', chapterData, token); + responseId = await System.authPostToServer('chapter/add', chapterData, token); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:chapter:add', { + ...chapterData, + chapterId: responseId, + }); } } if (!responseId) { diff --git a/components/book/settings/story/StorySetting.tsx b/components/book/settings/story/StorySetting.tsx index ce4dec5..22748cc 100644 --- a/components/book/settings/story/StorySetting.tsx +++ b/components/book/settings/story/StorySetting.tsx @@ -13,6 +13,9 @@ import Act from "@/components/book/settings/story/Act"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; export const StoryContext = createContext<{ acts: ActType[]; @@ -43,6 +46,8 @@ export function Story(props: any, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {book} = useContext(BookContext); const bookId: string = book?.bookId ? book.bookId.toString() : ''; const {session} = useContext(SessionContext); @@ -137,13 +142,13 @@ export function Story(props: any, ref: any) { mainChapters, issues, }; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:book:story:update', storyData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:story:update', storyData); - } else { - response = await System.authPostToServer('book/story', storyData, userToken, lang); + response = await System.authPostToServer('book/story', storyData, userToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:story:update', storyData); } } if (!response) { diff --git a/components/book/settings/world/WorldElement.tsx b/components/book/settings/world/WorldElement.tsx index 7f45d17..8df74c9 100644 --- a/components/book/settings/world/WorldElement.tsx +++ b/components/book/settings/world/WorldElement.tsx @@ -13,6 +13,9 @@ import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {BookContext} from "@/context/BookContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; interface WorldElementInputProps { sectionLabel: string; @@ -23,6 +26,8 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {book} = useContext(BookContext); const {worlds, setWorlds, selectedWorldIndex} = useContext(WorldContext); const {errorMessage, successMessage} = useContext(AlertContext); @@ -37,19 +42,19 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World try { let response: boolean; const elementId = (worlds[selectedWorldIndex][section] as WorldElement[])[index].id; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:book:world:element:remove', { elementId: elementId, }); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:world:element:remove', { + response = await System.authDeleteToServer('book/world/element/delete', { + elementId: elementId, + }, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:book:world:element:remove', { elementId: elementId, }); - } else { - response = await System.authDeleteToServer('book/world/element/delete', { - elementId: elementId, - }, session.accessToken, lang); } } if (!response) { @@ -77,25 +82,26 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World } try { let elementId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { elementId = await window.electron.invoke('db:book:world:element:add', { elementType: section, worldId: worlds[selectedWorldIndex].id, elementName: newElementName, }); } else { - if (book?.localBook) { - elementId = await window.electron.invoke('db:book:world:element:add', { + elementId = await System.authPostToServer('book/world/element/add', { + elementType: section, + worldId: worlds[selectedWorldIndex].id, + elementName: newElementName, + }, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:book:world:element:add', { elementType: section, worldId: worlds[selectedWorldIndex].id, + elementId, elementName: newElementName, }); - } else { - elementId = await System.authPostToServer('book/world/element/add', { - elementType: section, - worldId: worlds[selectedWorldIndex].id, - elementName: newElementName, - }, session.accessToken, lang); } } if (!elementId) { diff --git a/components/book/settings/world/WorldSetting.tsx b/components/book/settings/world/WorldSetting.tsx index 9f5eb52..16e9094 100644 --- a/components/book/settings/world/WorldSetting.tsx +++ b/components/book/settings/world/WorldSetting.tsx @@ -17,6 +17,9 @@ import SelectBox from "@/components/form/SelectBox"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; export interface ElementSection { title: string; @@ -28,6 +31,8 @@ export function WorldSetting(props: any, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {errorMessage, successMessage} = useContext(AlertContext); const {session} = useContext(SessionContext); const {book} = useContext(BookContext); @@ -89,22 +94,23 @@ export function WorldSetting(props: any, ref: any) { } try { let worldId: string; - if (isCurrentlyOffline()) { + if (isCurrentlyOffline() || book?.localBook) { worldId = await window.electron.invoke('db:book:world:add', { worldName: newWorldName, bookId: bookId, }); } else { - if (book?.localBook) { - worldId = await window.electron.invoke('db:book:world:add', { + worldId = await System.authPostToServer('book/world/add', { + worldName: newWorldName, + bookId: bookId, + }, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:world:add', { worldName: newWorldName, + worldId, bookId: bookId, }); - } else { - worldId = await System.authPostToServer('book/world/add', { - worldName: newWorldName, - bookId: bookId, - }, session.accessToken, lang); } } if (!worldId) { @@ -152,22 +158,17 @@ export function WorldSetting(props: any, ref: any) { async function handleUpdateWorld(): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:book:world:update', { - world: worlds[selectedWorldIndex], - bookId: bookId, - }); + const worldData = { + world: worlds[selectedWorldIndex], + bookId: bookId, + }; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:world:update', worldData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:book:world:update', { - world: worlds[selectedWorldIndex], - bookId: bookId, - }); - } else { - response = await System.authPutToServer('book/world/update', { - world: worlds[selectedWorldIndex], - bookId: bookId, - }, session.accessToken, lang); + response = await System.authPutToServer('book/world/update', worldData, session.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:world:update', worldData); } } if (!response) { diff --git a/components/editor/TextEditor.tsx b/components/editor/TextEditor.tsx index 4027a0f..faea429 100644 --- a/components/editor/TextEditor.tsx +++ b/components/editor/TextEditor.tsx @@ -33,6 +33,9 @@ import UserEditorSettings, {EditorDisplaySettings} from "@/components/editor/Use import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; interface ToolbarButton { action: () => void; @@ -141,7 +144,9 @@ export default function TextEditor() { const {errorMessage, successMessage} = useContext(AlertContext); const {session} = useContext(SessionContext); const {isCurrentlyOffline} = useContext(OfflineContext); - + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); + const [mainTimer, setMainTimer] = useState(0); const [showDraftCompanion, setShowDraftCompanion] = useState(false); const [showGhostWriter, setShowGhostWriter] = useState(false); @@ -281,39 +286,28 @@ export default function TextEditor() { const saveContent: () => Promise = useCallback(async (): Promise => { if (!editor || !chapter) return; - + setIsSaving(true); const content = editor.state.doc.toJSON(); const chapterId: string = chapter.chapterId || ''; const version: number = chapter.chapterContent.version || 0; - + try { let response: boolean; - if (isCurrentlyOffline()){ - response = await window.electron.invoke('db:chapter:content:save',{ - chapterId, - version, - content, - totalWordCount: editor.getText().length, - currentTime: mainTimer - }) + const saveData = { + chapterId, + version, + content, + totalWordCount: editor.getText().length, + currentTime: mainTimer + }; + if (isCurrentlyOffline() || book?.localBook){ + response = await window.electron.invoke('db:chapter:content:save', saveData); } else { - if (book?.localBook) { - response = await window.electron.invoke('db:chapter:content:save',{ - chapterId, - version, - content, - totalWordCount: editor.getText().length, - currentTime: mainTimer - }) - } else { - response = await System.authPostToServer(`chapter/content`, { - chapterId, - version, - content, - totalWordCount: editor.getText().length, - currentTime: mainTimer - }, session?.accessToken, lang); + response = await System.authPostToServer(`chapter/content`, saveData, session?.accessToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:chapter:content:save', saveData); } } if (!response) { @@ -332,7 +326,7 @@ export default function TextEditor() { } setIsSaving(false); } - }, [editor, chapter, mainTimer, session?.accessToken, successMessage, errorMessage]); + }, [editor, chapter, mainTimer, session?.accessToken, successMessage, errorMessage, addToQueue, book?.localBook, isCurrentlyOffline]); const handleShowDraftCompanion: () => void = useCallback((): void => { setShowDraftCompanion((prev: boolean): boolean => !prev); diff --git a/components/leftbar/ScribeChapterComponent.tsx b/components/leftbar/ScribeChapterComponent.tsx index 0315786..1841a92 100644 --- a/components/leftbar/ScribeChapterComponent.tsx +++ b/components/leftbar/ScribeChapterComponent.tsx @@ -12,11 +12,16 @@ import {useTranslations} from "next-intl"; import InlineAddInput from "@/components/form/InlineAddInput"; import {LangContext} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/models/SyncedBook"; export default function ScribeChapterComponent() { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue} = useContext(LocalSyncQueueContext); + const {localSyncedBooks} = useContext(BooksSyncContext); const {book} = useContext(BookContext); const {chapter, setChapter} = useContext(ChapterContext); @@ -137,25 +142,18 @@ export default function ScribeChapterComponent() { async function handleChapterUpdate(chapterId: string, title: string, chapterOrder: number): Promise { try { let response: boolean; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:chapter:update',{ - chapterId: chapterId, - chapterOrder: chapterOrder, - title: title, - }) + const updateData = { + chapterId: chapterId, + chapterOrder: chapterOrder, + title: title, + }; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:chapter:update', updateData); } else { - if (book?.localBook){ - response = await window.electron.invoke('db:chapter:update',{ - chapterId: chapterId, - chapterOrder: chapterOrder, - title: title, - }) - } else { - response = await System.authPostToServer('chapter/update', { - chapterId: chapterId, - chapterOrder: chapterOrder, - title: title, - }, userToken, lang); + response = await System.authPostToServer('chapter/update', updateData, userToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:chapter:update', updateData); } } if (!response) { @@ -190,15 +188,17 @@ export default function ScribeChapterComponent() { try { setDeleteConfirmationMessage(false); let response:boolean = false; - if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:chapter:remove', removeChapterId) + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:chapter:remove', removeChapterId); } else { - if (book?.localBook){ - response = await window.electron.invoke('db:chapter:remove', removeChapterId) - } else { - response = await System.authDeleteToServer('chapter/remove', { + response = await System.authDeleteToServer('chapter/remove', { + chapterId: removeChapterId, + }, userToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:chapter:remove', { chapterId: removeChapterId, - }, userToken, lang); + }); } } if (!response) { @@ -226,25 +226,21 @@ export default function ScribeChapterComponent() { const chapterTitle: string = chapterOrder >= 0 ? newChapterName : book?.title as string; try { let chapterId:string|null = null; - if (isCurrentlyOffline()){ - chapterId = await window.electron.invoke('db:chapter:add', { - bookId: book?.bookId, - chapterOrder: chapterOrder, - title: chapterTitle - }) + const addData = { + bookId: book?.bookId, + chapterOrder: chapterOrder, + title: chapterTitle + }; + if (isCurrentlyOffline() || book?.localBook){ + chapterId = await window.electron.invoke('db:chapter:add', addData); } else { - if (book?.localBook){ - chapterId = await window.electron.invoke('db:chapter:add', { - bookId: book?.bookId, - chapterOrder: chapterOrder, - title: chapterTitle - }) - } else { - chapterId = await System.authPostToServer('chapter/add', { - bookId: book?.bookId, - chapterOrder: chapterOrder, - title: chapterTitle - }, userToken, lang); + chapterId = await System.authPostToServer('chapter/add', addData, userToken, lang); + + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:chapter:add', { + ...addData, + chapterId, + }); } } if (!chapterId) { diff --git a/context/BooksSyncContext.ts b/context/BooksSyncContext.ts index 643bb5d..3ef19fa 100644 --- a/context/BooksSyncContext.ts +++ b/context/BooksSyncContext.ts @@ -8,6 +8,8 @@ export interface BooksSyncContextProps { localSyncedBooks:SyncedBook[]; booksToSyncFromServer:BookSyncCompare[]; booksToSyncToServer:BookSyncCompare[]; + setServerSyncedBooks:Dispatch>; + setLocalSyncedBooks:Dispatch>; setServerOnlyBooks:Dispatch>; setLocalOnlyBooks:Dispatch>; serverOnlyBooks:SyncedBook[]; @@ -19,6 +21,8 @@ export const BooksSyncContext:Context = createContext {}, + setLocalSyncedBooks:():void => {}, setServerOnlyBooks:():void => {}, setLocalOnlyBooks:():void => {}, serverOnlyBooks:[], diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index 08845ff..cad7d20 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -1611,10 +1611,11 @@ export default class Book { const encryptedLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userKey) : null; const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); const encryptedCharTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null; + const encryptedImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userKey) : null; const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null; const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null; const encryptedCharHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null; - return BookRepo.insertSyncCharacter(character.character_id, character.book_id, userId, encryptedFirstName, encryptedLastName, encryptedCategory, encryptedCharTitle, character.image, encryptedRole, encryptedBiography, encryptedCharHistory, character.last_update, lang); + return BookRepo.insertSyncCharacter(character.character_id, character.book_id, userId, encryptedFirstName, encryptedLastName, encryptedCategory, encryptedCharTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedCharHistory, character.last_update, lang); }); if (!charactersInserted) return false; @@ -2064,10 +2065,18 @@ export default class Book { if (chapterContents && chapterContents.length > 0) { for (const chapterContent of chapterContents) { + const isExist: boolean = ChapterRepo.isChapterContentExist(userId, chapterContent.content_id, lang); 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); - if (!updated) { - return false; + if (isExist) { + const updated: boolean = ChapterRepo.updateChapterContent(userId, chapterContent.chapter_id, chapterContent.version, content, chapterContent.words_count, chapterContent.last_update); + if (!updated) { + return false; + } + } else { + const insert: boolean = BookRepo.insertSyncChapterContent(chapterContent.content_id, chapterContent.chapter_id, userId, chapterContent.version, content, chapterContent.words_count, chapterContent.time_on_it, chapterContent.last_update, lang); + if (!insert) { + return false; + } } } } @@ -2083,7 +2092,7 @@ export default class Book { return false; } } else { - const insert: boolean = BookRepo.insertSyncChapterInfo(chapterInfo.chapter_info_id, chapterInfo.chapter_id, chapterInfo.act_id, chapterInfo.incident_id, chapterInfo.plot_point_id, bookId, chapterInfo.author_id, chapterInfo.summary, chapterInfo.goal, chapterInfo.last_update,lang); + const insert: boolean = BookRepo.insertSyncChapterInfo(chapterInfo.chapter_info_id, chapterInfo.chapter_id, chapterInfo.act_id, chapterInfo.incident_id, chapterInfo.plot_point_id, bookId, chapterInfo.author_id, summary, goal, chapterInfo.last_update,lang); if (!insert) { return false; } diff --git a/electron/database/repositories/chapter.repository.ts b/electron/database/repositories/chapter.repository.ts index 3c61fb5..857b51c 100644 --- a/electron/database/repositories/chapter.repository.ts +++ b/electron/database/repositories/chapter.repository.ts @@ -441,4 +441,20 @@ export default class ChapterRepo{ } } } + + static isChapterContentExist(userId: string, content_id: string, lang: "fr" | "en"): boolean { + try { + const db: Database = System.getDb(); + const result: QueryResult | null = db.get('SELECT 1 FROM `book_chapter_content` WHERE `content_id`=? AND `author_id`=?', [content_id, userId]) || null; + return result !== null; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du contenu du chapitre.` : `Unable to check chapter content existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } }