From dbbe33b19b31ac6702d8138e4e312423f7c8a4ae Mon Sep 17 00:00:00 2001 From: natreex Date: Mon, 30 Mar 2026 21:06:58 -0400 Subject: [PATCH] Refactor and extend offline synchronization logic across components and services - Integrated sync queue mechanisms with `LocalSyncQueueContext` for offline data handling. - Updated key sync-related services (e.g., book, chapter, series) to support offline-first functionality. - Removed redundant database fetch methods to optimize repository logic and improve maintainability. - Enhanced Tauri IPC usage for sync operations and removed legacy methods in Rust services. --- .../book/settings/BasicInformationSetting.tsx | 21 +++--- components/book/settings/DeleteBook.tsx | 33 ++++++--- .../settings/locations/LocationComponent.tsx | 29 ++++++++ components/book/settings/story/Act.tsx | 67 ++++++++++++------- components/book/settings/story/Issue.tsx | 30 +++++---- .../book/settings/story/MainChapter.tsx | 36 +++++----- .../book/settings/story/StorySetting.tsx | 17 +++-- .../book/settings/world/WorldElement.tsx | 11 +++ .../book/settings/world/WorldSetting.tsx | 19 +++++- components/editor/TextEditor.tsx | 20 ++++-- components/form/SyncFieldWrapper.tsx | 35 ++++++---- components/leftbar/ScribeChapterComponent.tsx | 36 ++++++---- components/series/AddNewSeriesForm.tsx | 10 ++- components/series/SeriesSettingSidebar.tsx | 18 ++++- .../settings/BasicSeriesInformation.tsx | 18 ++++- .../series/settings/SeriesBooksManager.tsx | 11 +-- src-tauri/src/crypto/key_manager.rs | 20 +++--- src-tauri/src/domains/chapter/repo.rs | 23 ------- src-tauri/src/domains/chapter_content/repo.rs | 27 -------- src-tauri/src/domains/character/repo.rs | 26 ------- src-tauri/src/domains/location/repo.rs | 54 --------------- src-tauri/src/domains/world/repo.rs | 27 -------- 22 files changed, 295 insertions(+), 293 deletions(-) diff --git a/components/book/settings/BasicInformationSetting.tsx b/components/book/settings/BasicInformationSetting.tsx index 034dd5e..2234003 100644 --- a/components/book/settings/BasicInformationSetting.tsx +++ b/components/book/settings/BasicInformationSetting.tsx @@ -20,6 +20,9 @@ import {LangContext, LangContextProps} from "@/context/LangContext"; import {BookProps} from "@/lib/types/book"; import {SettingRef} from "@/lib/types/settings"; import ImageDropZone from "@/components/form/ImageDropZone"; +import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; +import {SyncedBook} from "@/lib/types/synced-book"; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; function BasicInformationSetting(_props: object, ref: React.ForwardedRef): React.JSX.Element { const t = useTranslations(); @@ -30,6 +33,8 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef(AlertContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const bookId: string = book?.bookId ? book?.bookId.toString() : ''; const [currentImage, setCurrentImage] = useState(book?.coverImage ?? ''); @@ -120,14 +125,14 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef('book/basic-information', { - title: title, - subTitle: subTitle, - summary: summary, - publicationDate: publicationDate, - wordCount: wordCount, - bookId: bookId - }, userToken, lang); + const basicInfoData = { + title, subTitle, summary, publicationDate, wordCount, bookId + }; + response = await apiPost('book/basic-information', basicInfoData, userToken, lang); + + if (isDesktop && localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('update_book_basic_info', basicInfoData); + } } if (!response) { errorMessage(t('basicInformationSetting.error.update')); diff --git a/components/book/settings/DeleteBook.tsx b/components/book/settings/DeleteBook.tsx index b0f7954..e72de93 100644 --- a/components/book/settings/DeleteBook.tsx +++ b/components/book/settings/DeleteBook.tsx @@ -22,31 +22,44 @@ export default function DeleteBook({bookId}: DeleteBookProps) { const {lang}: LangContextProps = useContext(LangContext) const [showConfirmBox, setShowConfirmBox] = useState(false); const {errorMessage}: AlertContextProps = useContext(AlertContext) - const {setServerSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext) + const {serverOnlyBooks, setServerOnlyBooks, localOnlyBooks, setLocalOnlyBooks, localSyncedBooks, setLocalSyncedBooks, setServerSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext) + const [deleteLocalToo, setDeleteLocalToo] = useState(false); + const ifLocalOnlyBook: SyncedBook | undefined = localOnlyBooks.find((b: SyncedBook): boolean => b.id === bookId); + const ifSyncedBook: SyncedBook | undefined = localSyncedBooks.find((b: SyncedBook): boolean => b.id === bookId); const {book}: BookContextProps = useContext(BookContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); function handleConfirmation(): void { + setDeleteLocalToo(false); setShowConfirmBox(true); } async function handleDeleteBook(): Promise { try { let response: boolean; - if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { - response = await tauri.deleteBook(bookId, Date.now()); + const deletedAt: number = Math.floor(Date.now() / 1000); + + if (isDesktop && (isCurrentlyOffline() || ifLocalOnlyBook)) { + response = await tauri.deleteBook(bookId, deletedAt); } else { - response = await apiDelete('book/delete', { - id: bookId, - }, session.accessToken, lang); + response = await apiDelete('book/delete', {id: bookId, deletedAt}, session.accessToken, lang); + if (response && isDesktop && ifSyncedBook && deleteLocalToo) { + await tauri.deleteBook(bookId, deletedAt); + } } if (response) { setShowConfirmBox(false); - if (!response) { - errorMessage("Une erreur est survenue lors de la suppression du livre."); - return; + if (ifLocalOnlyBook) { + setLocalOnlyBooks(localOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId)); + } else if (ifSyncedBook) { + setLocalSyncedBooks(localSyncedBooks.filter((b: SyncedBook): boolean => b.id !== bookId)); + setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.filter((b: SyncedBook): boolean => b.id !== bookId)); + if (!deleteLocalToo) { + setLocalOnlyBooks([...localOnlyBooks, ifSyncedBook]); + } + } else { + setServerOnlyBooks(serverOnlyBooks.filter((b: SyncedBook): boolean => b.id !== bookId)); } - setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.filter((book: SyncedBook): boolean => book.id !== bookId)) } } catch (e: unknown) { if (e instanceof Error) { diff --git a/components/book/settings/locations/LocationComponent.tsx b/components/book/settings/locations/LocationComponent.tsx index 1877dc3..3914c95 100644 --- a/components/book/settings/locations/LocationComponent.tsx +++ b/components/book/settings/locations/LocationComponent.tsx @@ -5,6 +5,9 @@ import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {BookContext, BookContextProps} from "@/context/BookContext"; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {isDesktop} from '@/lib/configs'; import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client'; import * as tauri from '@/lib/tauri'; @@ -57,6 +60,8 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< const {successMessage, errorMessage}: AlertContextProps = useContext(AlertContext); const {book, setBook}: BookContextProps = useContext(BookContext); const {isCurrentlyOffline} = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const currentEntityId: string = entityId || book?.bookId || ''; const useLocal: boolean = isDesktop && (isCurrentlyOffline() || !!book?.localBook); @@ -120,6 +125,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< toolName: 'locations', enabled: enabled }, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('update_book_tool_setting', {bookId: currentEntityId, toolName: 'locations', enabled}); + } if (response && setBook && book) { setToolEnabled(enabled); setBook({ @@ -218,6 +226,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< sectionId = useLocal ? await tauri.addLocationSection(newSectionName, currentEntityId) : await apiPost('location/section/add', {bookId: currentEntityId, locationName: newSectionName}, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('add_location_section', {bookId: currentEntityId, sectionId, locationName: newSectionName}); + } if (!sectionId) { errorMessage(t('locationComponent.errorUnknownAddSection')); return; @@ -258,6 +269,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< elementId = useLocal ? await tauri.addLocationElement(sectionId, newElementNames[sectionId]) : await apiPost('location/element/add', {bookId: currentEntityId, locationId: sectionId, elementName: newElementNames[sectionId]}, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('add_location_element', {bookId: currentEntityId, locationId: sectionId, elementId, elementName: newElementNames[sectionId]}); + } if (!elementId) { errorMessage(t('locationComponent.errorUnknownAddElement')); return; @@ -325,6 +339,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< subElementId = useLocal ? await tauri.addLocationSubElement(parentElementId, newSubElementNames[elementIndex]) : await apiPost('location/sub-element/add', {elementId: parentElementId, subElementName: newSubElementNames[elementIndex]}, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('add_location_subelement', {elementId: parentElementId, subElementId, subElementName: newSubElementNames[elementIndex]}); + } if (!subElementId) { errorMessage(t('locationComponent.errorUnknownAddSubElement')); return; @@ -381,6 +398,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< success = useLocal ? await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt) : await apiDelete('location/element/delete', {elementId: elementId}, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('delete_location_element', {elementId, bookId: currentEntityId, deletedAt}); + } } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteElement')); @@ -417,6 +437,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< success = useLocal ? await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt) : await apiDelete('location/sub-element/delete', {subElementId: subElementId}, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('delete_location_subelement', {subElementId, bookId: currentEntityId, deletedAt}); + } } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteSubElement')); @@ -447,6 +470,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< success = useLocal ? await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt) : await apiDelete('location/delete', {locationId: sectionId}, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('delete_location', {locationId: sectionId, bookId: currentEntityId, deletedAt}); + } } if (!success) { errorMessage(t('locationComponent.errorUnknownDeleteSection')); @@ -468,6 +494,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref< const response: boolean = useLocal ? await tauri.updateLocations(sections) as boolean : await apiPost(`location/update`, {locations: sections}, token, lang); + if (!useLocal && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === currentEntityId)) { + addToQueue('update_locations', {locations: sections}); + } if (!response) { errorMessage(t('locationComponent.errorUnknownSave')); return; diff --git a/components/book/settings/story/Act.tsx b/components/book/settings/story/Act.tsx index 0fb48a5..fa3cabb 100644 --- a/components/book/settings/story/Act.tsx +++ b/components/book/settings/story/Act.tsx @@ -6,6 +6,9 @@ import {apiDelete, apiPost} from '@/lib/api/client'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {BookContext, BookContextProps} from '@/context/BookContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; @@ -30,6 +33,8 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const bookId: string | undefined = book?.bookId; const token: string = session.accessToken; @@ -72,10 +77,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { incidentId = await tauri.addIncident(bookId ?? '', newIncidentTitle); } else { - incidentId = await apiPost('book/incident/new', { - bookId, - name: newIncidentTitle, - }, token, lang); + const addData = {bookId, name: newIncidentTitle}; + incidentId = await apiPost('book/incident/new', addData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('add_incident', addData); + } } if (!incidentId) { errorMessage(t('errorAddIncident')); @@ -114,10 +121,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.removeIncident(bookId ?? '', incidentId, Date.now()); } else { - response = await apiDelete('book/incident/remove', { - bookId, - incidentId, - }, token, lang); + const deleteData = {bookId, incidentId}; + response = await apiDelete('book/incident/remove', deleteData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('remove_incident', {...deleteData, deletedAt: Date.now()}); + } } if (!response) { errorMessage(t('errorDeleteIncident')); @@ -151,11 +160,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { plotId = await tauri.addPlotPoint(bookId ?? '', newPlotPointTitle, selectedIncidentId); } else { - plotId = await apiPost('book/plot/new', { - bookId, - name: newPlotPointTitle, - incidentId: selectedIncidentId, - }, token, lang); + const plotData = {bookId, name: newPlotPointTitle, incidentId: selectedIncidentId}; + plotId = await apiPost('book/plot/new', plotData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('add_plot_point', plotData); + } } if (!plotId) { errorMessage(t('errorAddPlotPoint')); @@ -195,9 +205,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.removePlotPoint(plotPointId, bookId ?? '', Date.now()); } else { - response = await apiDelete('book/plot/remove', { - plotId: plotPointId, - }, token, lang); + const deleteData = {plotId: plotPointId}; + response = await apiDelete('book/plot/remove', deleteData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('remove_plot_point', {...deleteData, bookId, deletedAt: Date.now()}); + } } if (!response) { errorMessage(t('errorDeletePlotPoint')); @@ -246,13 +259,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { incidentId: destination === 'incident' ? itemId : undefined, }); } else { - linkId = await apiPost('chapter/resume/add', { - bookId, - chapterId: chapterId, - actId: actId, - plotId: destination === 'plotPoint' ? itemId : null, - incidentId: destination === 'incident' ? itemId : null, - }, token, lang); + const linkData = {bookId, chapterId, actId, plotId: destination === 'plotPoint' ? itemId : null, incidentId: destination === 'incident' ? itemId : null}; + linkId = await apiPost('chapter/resume/add', linkData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('add_chapter_information', linkData); + } } if (!linkId) { errorMessage(t('errorLinkChapter')); @@ -332,9 +344,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.removeChapterInformation(chapterInfoId, bookId ?? '', Date.now()); } else { - response = await apiDelete('chapter/resume/remove', { - chapterInfoId, - }, token, lang); + const removeData = {chapterInfoId}; + response = await apiDelete('chapter/resume/remove', removeData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('remove_chapter_information', {...removeData, bookId, deletedAt: Date.now()}); + } } if (!response) { errorMessage(t('errorUnlinkChapter')); diff --git a/components/book/settings/story/Issue.tsx b/components/book/settings/story/Issue.tsx index 93a921c..52fc38d 100644 --- a/components/book/settings/story/Issue.tsx +++ b/components/book/settings/story/Issue.tsx @@ -6,6 +6,9 @@ import {apiDelete, apiPost} from '@/lib/api/client'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {BookContext, BookContextProps} from '@/context/BookContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; @@ -27,6 +30,8 @@ export default function Issues({issues, setIssues}: IssuesProps) { const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage}: AlertContextProps = useContext(AlertContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const bookId: string | undefined = book?.bookId; const token: string = session.accessToken; @@ -43,10 +48,12 @@ export default function Issues({issues, setIssues}: IssuesProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { issueId = await tauri.addIssue(bookId ?? '', newIssueName); } else { - issueId = await apiPost('book/issue/add', { - bookId, - name: newIssueName, - }, token, lang); + const addData = {bookId, name: newIssueName}; + issueId = await apiPost('book/issue/add', addData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('add_issue', addData); + } } if (!issueId) { errorMessage(t("issues.errorAdd")); @@ -79,15 +86,12 @@ export default function Issues({issues, setIssues}: IssuesProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.removeIssue(bookId ?? '', issueId, Date.now()); } else { - response = await apiDelete( - 'book/issue/remove', - { - bookId, - issueId, - }, - token, - lang - ); + const deleteData = {bookId, issueId}; + response = await apiDelete('book/issue/remove', deleteData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('remove_issue', {...deleteData, deletedAt: Date.now()}); + } } if (response) { const updatedIssues: Issue[] = issues.filter((issue: Issue): boolean => issue.id !== issueId,); diff --git a/components/book/settings/story/MainChapter.tsx b/components/book/settings/story/MainChapter.tsx index f33695e..a1e471a 100644 --- a/components/book/settings/story/MainChapter.tsx +++ b/components/book/settings/story/MainChapter.tsx @@ -7,6 +7,9 @@ import {apiDelete, apiPost} from '@/lib/api/client'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {BookContext, BookContextProps} from '@/context/BookContext'; import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; @@ -30,6 +33,8 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) { const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const bookId: string | undefined = book?.bookId; const token: string = session.accessToken; @@ -88,15 +93,12 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.removeChapter(chapterIdToRemove, bookId ?? '', Date.now()); } else { - response = await apiDelete( - 'chapter/remove', - { - bookId, - chapterId: chapterIdToRemove, - }, - token, - lang, - ); + const deleteData = {bookId, chapterId: chapterIdToRemove}; + response = await apiDelete('chapter/remove', deleteData, token, lang); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('remove_chapter', {...deleteData, deletedAt: Date.now()}); + } } if (!response) { errorMessage(t("mainChapter.errorDelete")); @@ -126,16 +128,12 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) { chapterOrder: newChapterOrder ? newChapterOrder : 0, }); } else { - responseId = await apiPost( - 'chapter/add', - { - bookId: bookId, - wordsCount: 0, - chapterOrder: newChapterOrder ? newChapterOrder : 0, - title: newChapterTitle, - }, - token, - ); + const addData = {bookId, wordsCount: 0, chapterOrder: newChapterOrder ? newChapterOrder : 0, title: newChapterTitle}; + responseId = await apiPost('chapter/add', addData, token); + + if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('add_chapter', addData); + } } if (!responseId) { errorMessage(t("mainChapter.errorAdd")); diff --git a/components/book/settings/story/StorySetting.tsx b/components/book/settings/story/StorySetting.tsx index 6d77897..fe972ee 100644 --- a/components/book/settings/story/StorySetting.tsx +++ b/components/book/settings/story/StorySetting.tsx @@ -8,6 +8,9 @@ import {apiGet, apiPost} from '@/lib/api/client'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {Act as ActType, Incident, Issue, PlotPoint} from '@/lib/types/book'; import {ActChapter, ChapterListProps} from '@/lib/types/chapter'; import MainChapter from "@/components/book/settings/story/MainChapter"; @@ -51,6 +54,8 @@ export function Story(_props: object, ref: React.ForwardedRef): Reac const userToken: string = session.accessToken; const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const [acts, setActs] = useState([]); const [issues, setIssues] = useState([]); @@ -138,12 +143,12 @@ export function Story(_props: object, ref: React.ForwardedRef): Reac issues, }); } else { - response = await apiPost('book/story', { - bookId, - acts, - mainChapters, - issues, - }, userToken, lang); + const storyData = {bookId, acts, mainChapters, issues}; + response = await apiPost('book/story', storyData, userToken, lang); + + if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) { + addToQueue('update_book_story', storyData); + } } if (!response) { errorMessage(t("story.errorSave")) diff --git a/components/book/settings/world/WorldElement.tsx b/components/book/settings/world/WorldElement.tsx index 7a2985c..6340b38 100644 --- a/components/book/settings/world/WorldElement.tsx +++ b/components/book/settings/world/WorldElement.tsx @@ -11,6 +11,9 @@ import {apiDelete, apiPost} from "@/lib/api/client"; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {BookContext, BookContextProps} from '@/context/BookContext'; import InputField from "@/components/form/InputField"; import {useTranslations} from '@/lib/i18n'; @@ -52,6 +55,8 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World const {session}: SessionContextProps = useContext(SessionContext); const {book}: BookContextProps = useContext(BookContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const [newElementName, setNewElementName] = useState(''); @@ -69,6 +74,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World response = await apiDelete(endpoint, { elementId: elements[index].id, }, session.accessToken, lang); + if (!isSeriesMode && isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { + addToQueue('remove_world_element', {elementId: elements[index].id, bookId: book?.bookId, deletedAt: Date.now()}); + } } if (!response) { errorMessage(t("worldSetting.unknownError")) @@ -119,6 +127,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World worldId: worlds[selectedWorldIndex].id, elementName: newElementName, }, session.accessToken, lang); + if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { + addToQueue('add_world_element', {worldId: worlds[selectedWorldIndex].id, elementId, elementType: section, elementName: newElementName}); + } if (!elementId) { errorMessage(t("worldSetting.unknownError")) return; diff --git a/components/book/settings/world/WorldSetting.tsx b/components/book/settings/world/WorldSetting.tsx index a1be506..7bb6e23 100644 --- a/components/book/settings/world/WorldSetting.tsx +++ b/components/book/settings/world/WorldSetting.tsx @@ -9,6 +9,9 @@ import {apiGet, apiPatch, apiPost} from "@/lib/api/client"; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {ElementSection, WorldListResponse, WorldProps, WorldTextField} from "@/lib/types/world"; import {SettingRef} from "@/lib/types/settings"; import {elementSections} from "@/lib/constants/world"; @@ -39,6 +42,8 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef(SessionContext); const {book, setBook}: BookContextProps = useContext(BookContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const currentEntityId: string = entityId || book?.bookId || ''; const isSeriesMode: boolean = entityType === 'series'; @@ -96,6 +101,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef sb.id === currentEntityId)) { + addToQueue('update_book_tool_setting', {bookId: currentEntityId, toolName: 'worlds', enabled}); + } } if (response && setBook && book) { setToolEnabled(enabled); @@ -238,6 +246,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef sb.id === currentEntityId)) { + addToQueue('add_world', {worldName: newWorldName, bookId: currentEntityId, worldId}); + } if (!worldId) { errorMessage(t("worldSetting.addWorldError")); return; @@ -310,6 +321,9 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef sb.id === currentEntityId)) { + addToQueue('update_world', {world: currentWorld, bookId: currentEntityId}); + } if (!response) { errorMessage(t("worldSetting.updateWorldError")); return; @@ -393,7 +407,10 @@ export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef sb.id === currentEntityId)) { + addToQueue('add_world', {worldName: seriesWorld.name, bookId: currentEntityId, worldId, seriesWorldId}); + } + const newWorld: WorldProps = { id: worldId, name: seriesWorld.name, diff --git a/components/editor/TextEditor.tsx b/components/editor/TextEditor.tsx index f803e49..efe689b 100644 --- a/components/editor/TextEditor.tsx +++ b/components/editor/TextEditor.tsx @@ -26,6 +26,9 @@ import {apiPost} from '@/lib/api/client'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import DraftCompanion from "@/components/editor/DraftCompanion"; @@ -144,7 +147,9 @@ export default function TextEditor() { const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {session}: SessionContextProps = useContext(SessionContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); - + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); + const [mainTimer, setMainTimer] = useState(0); const [showDraftCompanion, setShowDraftCompanion] = useState(false); const [showGhostWriter, setShowGhostWriter] = useState(false); @@ -291,13 +296,18 @@ export default function TextEditor() { contentId: '', }); } else { - response = await apiPost(`chapter/content`, { + const saveData = { chapterId, version, content, totalWordCount: editor.getText().length, currentTime: mainTimer - }, session?.accessToken ?? ''); + }; + response = await apiPost(`chapter/content`, saveData, session?.accessToken ?? ''); + + if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { + addToQueue('save_chapter_content', saveData); + } } if (!response) { errorMessage(t('editor.error.savedFailed')); @@ -315,7 +325,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); @@ -454,7 +464,7 @@ export default function TextEditor() { onClick={handleShowUserSettings} tooltip={t("textEditor.preferences")} /> - {chapter?.chapterContent.version === 2 && book?.quillsenseEnabled !== false && ( + {chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && !book?.localBook && book?.quillsenseEnabled !== false && ( (SessionContext); const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {lang}: LangContextProps = useContext(LangContext); - + const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); + const {book}: BookContextProps = useContext(BookContext); + const [isUploading, setIsUploading] = useState(false); const isLinkedToSeries: boolean = !!seriesElementId; @@ -53,17 +64,17 @@ export default function SyncFieldWrapper({ setIsUploading(true); try { - const response: SeriesSyncUploadResponse = await apiPost( - 'series/propagate', - { - type: elementType, - bookElementId: bookElementId, - field: field, - value: currentValue - }, - session.accessToken, - lang - ); + const requestData = {type: elementType, bookElementId, field, value: currentValue}; + let response: SeriesSyncUploadResponse; + + if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { + response = await tauri.invoke('series_sync_upload', {data: requestData}); + } else { + response = await apiPost('series/propagate', requestData, session.accessToken, lang); + if (isDesktop && book?.bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book.bookId)) { + addToQueue('series_sync_upload', requestData); + } + } if (response.success) { successMessage(t('syncField.uploadSuccess', {count: response.updatedCount})); if (onSyncComplete) { diff --git a/components/leftbar/ScribeChapterComponent.tsx b/components/leftbar/ScribeChapterComponent.tsx index 82fdcc3..5f65d92 100644 --- a/components/leftbar/ScribeChapterComponent.tsx +++ b/components/leftbar/ScribeChapterComponent.tsx @@ -4,6 +4,9 @@ import {apiDelete, apiGet, apiPost} from '@/lib/api/client'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; +import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; +import {SyncedBook} from '@/lib/types/synced-book'; +import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {BookContext, BookContextProps} from "@/context/BookContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext"; @@ -26,6 +29,8 @@ export default function ScribeChapterComponent() { const {session}: SessionContextProps = useContext(SessionContext); const userToken: string = session?.accessToken ? session?.accessToken : ''; const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); + const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); + const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const router = useRouter(); const [chapters, setChapters] = useState([]) @@ -107,11 +112,12 @@ export default function ScribeChapterComponent() { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.updateChapter(chapterId, title, chapterOrder); } else { - response = await apiPost('chapter/update', { - chapterId: chapterId, - chapterOrder: chapterOrder, - title: title, - }, userToken, lang); + const updateData = {chapterId, chapterOrder, title}; + response = await apiPost('chapter/update', updateData, userToken, lang); + + if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { + addToQueue('update_chapter', updateData); + } } if (!response) { errorMessage(t("scribeChapterComponent.errorChapterUpdate")); @@ -148,9 +154,12 @@ export default function ScribeChapterComponent() { if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.removeChapter(removeChapterId, book?.bookId ?? '', Date.now()); } else { - response = await apiDelete('chapter/remove', { - chapterId: removeChapterId, - }, userToken, lang); + const deleteData = {bookId: book?.bookId, chapterId: removeChapterId}; + response = await apiDelete('chapter/remove', deleteData, userToken, lang); + + if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { + addToQueue('remove_chapter', {...deleteData, deletedAt: Date.now()}); + } } if (!response) { errorMessage(t("scribeChapterComponent.errorChapterDelete")); @@ -184,11 +193,12 @@ export default function ScribeChapterComponent() { chapterOrder: chapterOrder, }); } else { - chapterId = await apiPost('chapter/add', { - bookId: book?.bookId, - chapterOrder: chapterOrder, - title: chapterTitle - }, userToken, lang); + const addData = {bookId: book?.bookId, chapterOrder, title: chapterTitle}; + chapterId = await apiPost('chapter/add', addData, userToken, lang); + + if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { + addToQueue('add_chapter', addData); + } } if (!chapterId) { errorMessage(t("scribeChapterComponent.errorChapterSubmit", {chapterName: newChapterName})); diff --git a/components/series/AddNewSeriesForm.tsx b/components/series/AddNewSeriesForm.tsx index 979e2bb..38324d3 100644 --- a/components/series/AddNewSeriesForm.tsx +++ b/components/series/AddNewSeriesForm.tsx @@ -30,8 +30,10 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew const {isCurrentlyOffline} = useContext(OfflineContext); const { serverSyncedBooks, - setServerSyncedBooks + setServerSyncedBooks, + localSyncedBooks }: BooksSyncContextProps = useContext(BooksSyncContext); + const useLocal: boolean = isDesktop && isCurrentlyOffline(); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [selectedBookIds, setSelectedBookIds] = useState([]); @@ -83,6 +85,8 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew : book ) ); + } else { + setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev]); } if (onSeriesCreated) { @@ -150,7 +154,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew )} - {serverSyncedBooks.length === 0 ? ( + {(useLocal ? localSyncedBooks : serverSyncedBooks).length === 0 ? (

{t("addNewSeriesForm.noBooks")}

@@ -158,7 +162,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew ) : (
- {serverSyncedBooks.map((book: SyncedBook) => { + {(useLocal ? localSyncedBooks : serverSyncedBooks).map((book: SyncedBook) => { const isSelected: boolean = selectedBookIds.includes(book.id); return (