Enhance synchronization logic and offline handling

- Refactor components to support conditional offline and online CRUD operations.
- Introduce `addToQueue` mechanism for syncing offline changes to the server.
- Add `isChapterContentExist` method and related existence checks in repositories.
- Consolidate data structures and streamline book, chapter, character, and guideline synchronization workflows.
- Encrypt additional character fields and adjust repository inserts for offline data.
This commit is contained in:
natreex
2026-01-07 20:43:34 -05:00
parent fa05d6dbae
commit 8eab6fd771
21 changed files with 557 additions and 578 deletions

View File

@@ -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<boolean>(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<boolean>(false);
const [showPinSetup, setShowPinSetup] = useState<boolean>(false);
const [showPinVerify, setShowPinVerify] = useState<boolean>(false);
const [localSyncQueue, setLocalSyncQueue] = useState<LocalSyncOperation[]>([]);
const [isQueueProcessing, setIsQueueProcessing] = useState<boolean>(false);
function addToLocalSyncQueue(channel: string, data: Record<string, unknown>): 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<void> {
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<void> {
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 (
<SessionContext.Provider value={{session: session, setSession: setSession}}>
<BooksSyncContext.Provider value={{serverSyncedBooks, localSyncedBooks, booksToSyncFromServer:bookSyncDiffsFromServer, booksToSyncToServer:bookSyncDiffsToServer, setServerOnlyBooks, setLocalOnlyBooks, serverOnlyBooks, localOnlyBooks}}>
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
<AIUsageContext.Provider value={{
<LocalSyncQueueContext.Provider value={{
queue: localSyncQueue,
setQueue: setLocalSyncQueue,
addToQueue: addToLocalSyncQueue,
isProcessing: isQueueProcessing,
}}>
<BooksSyncContext.Provider value={{serverSyncedBooks, localSyncedBooks, booksToSyncFromServer:bookSyncDiffsFromServer, booksToSyncToServer:bookSyncDiffsToServer, setServerSyncedBooks, setLocalSyncedBooks, setServerOnlyBooks, setLocalOnlyBooks, serverOnlyBooks, localOnlyBooks}}>
<AutoSyncOnReconnect/>
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
<AIUsageContext.Provider value={{
totalCredits: currentCredits,
setTotalCredits: setCurrentCredits,
totalPrice: amountSpent,
@@ -586,10 +658,11 @@ function ScribeContent() {
/>
)
}
</AIUsageContext.Provider>
</ChapterContext.Provider>
</BookContext.Provider>
</BooksSyncContext.Provider>
</AIUsageContext.Provider>
</ChapterContext.Provider>
</BookContext.Provider>
</BooksSyncContext.Provider>
</LocalSyncQueueContext.Provider>
</SessionContext.Provider>
);
}