Add BooksSyncContext, refine database schema, and enhance synchronization support

- Introduce `BooksSyncContext` for managing book synchronization states (server-only, local-only, to-sync, etc.).
- Remove `UserContext` and related dependencies.
- Refine localization strings (`en.json`) with sync-related updates (e.g., "toSyncFromServer", "toSyncToServer").
- Extend database schema with additional tables and fields for syncing books, chapters, and related entities.
- Add `last_update` fields and update corresponding repository methods to support synchronization logic.
- Enhance IPC handlers with stricter typing, data validation, and sync-aware operations.
This commit is contained in:
natreex
2025-12-07 14:36:03 -05:00
parent db2c88a42d
commit bb331b5c22
22 changed files with 2594 additions and 370 deletions

View File

@@ -33,6 +33,8 @@ import OfflineProvider from "@/context/OfflineProvider";
import OfflineContext from "@/context/OfflineContext";
import OfflinePinSetup from "@/components/offline/OfflinePinSetup";
import OfflinePinVerify from "@/components/offline/OfflinePinVerify";
import {SyncedBook} from "@/lib/models/SyncedBook";
import {BooksSyncContext} from "@/context/BooksSyncContext";
const messagesMap = {
fr: frMessages,
@@ -59,6 +61,13 @@ function ScribeContent() {
const [session, setSession] = useState<SessionProps>({user: null, accessToken: '', isConnected: false});
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
const [serverSyncedBooks, setServerSyncedBooks] = useState<SyncedBook[]>([]);
const [localSyncedBooks, setLocalSyncedBooks] = useState<SyncedBook[]>([]);
const [booksToSyncFromServer, setBooksToSyncFromServer] = useState<SyncedBook[]>([]);
const [booksToSyncToServer, setBooksToSyncToServer] = useState<SyncedBook[]>([]);
const [serverOnlyBooks, setServerOnlyBooks] = useState<SyncedBook[]>([]);
const [localOnlyBooks, setLocalOnlyBooks] = useState<SyncedBook[]>([]);
const [currentCredits, setCurrentCredits] = useState<number>(160);
const [amountSpent, setAmountSpent] = useState<number>(session.user?.aiUsage || 0);
@@ -143,6 +152,7 @@ function ScribeContent() {
useEffect((): void => {
if (session.isConnected) {
getBooks().then()
setIsTermsAccepted(session.user?.termsAccepted ?? false);
setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
setIsLoading(false);
@@ -154,8 +164,46 @@ function ScribeContent() {
getLastChapter().then();
}
}, [currentBook]);
// Check for PIN setup after successful connection
useEffect(():void => {
setBooksToSyncFromServer(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => {
const localBook: SyncedBook | undefined = localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id);
console.log('localBook from setBookToSyncFromServer',localBook);
console.log('serverBook from setBookToSyncFromServer',serverBook);
return !localBook || localBook.lastUpdate < serverBook.lastUpdate;
}))
setBooksToSyncToServer(localSyncedBooks.filter((localBook: SyncedBook):boolean => {
const serverBook: SyncedBook | undefined = serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id);
return !serverBook || serverBook.lastUpdate < localBook.lastUpdate;
}))
setServerOnlyBooks(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => !localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id)))
setLocalOnlyBooks(localSyncedBooks.filter((localBook: SyncedBook):boolean => !serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id)))
}, [localSyncedBooks, serverSyncedBooks]);
async function getBooks(): Promise<void> {
try {
let localBooksResponse: SyncedBook[]
if (!isCurrentlyOffline()){
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
} else {
localBooksResponse = [];
}
const serverBooksResponse: SyncedBook[] = await System.authGetQueryToServer<SyncedBook[]>('books/synced', session.accessToken, locale);
if (serverBooksResponse) {
setServerSyncedBooks(serverBooksResponse);
}
if (localBooksResponse) {
setLocalSyncedBooks(localBooksResponse);
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("homePage.errors.fetchBooksError"));
}
}
}
useEffect(():void => {
async function checkPinSetup() {
if (session.isConnected && window.electron) {
@@ -179,6 +227,7 @@ function ScribeContent() {
}
checkPinSetup().then();
}, [session.isConnected]);
async function handlePinVerifySuccess(userId: string): Promise<void> {
@@ -186,16 +235,18 @@ function ScribeContent() {
try {
if (window.electron) {
const storedToken: string | null = await window.electron.getToken();
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
if (encryptionKey) {
await window.electron.dbInitialize(userId, encryptionKey);
const localUser:UserProps = await window.electron.invoke('db:user:info');
if (localUser && localUser.id) {
setSession({
isConnected: true,
user: localUser,
accessToken: 'offline', // Special offline token
accessToken: storedToken || '',
});
setShowPinVerify(false);
setCurrentCredits(localUser.creditsBalance || 0);
@@ -225,7 +276,7 @@ function ScribeContent() {
async function handleHomeTour(): Promise<void> {
try {
const response: boolean = await System.authPostToServer<boolean>('logs/tour', {
plateforme: 'web',
plateforme: 'desktop',
tour: 'home-basic'
},
session.accessToken,
@@ -260,7 +311,6 @@ function ScribeContent() {
const user: UserProps = await System.authGetQueryToServer<UserProps>('user/infos', token, locale);
if (!user) {
errorMessage(t("homePage.errors.userNotFound"));
// Token invalide, supprimer et logout
if (window.electron) {
await window.electron.removeToken();
window.electron.logout();
@@ -272,18 +322,18 @@ function ScribeContent() {
try {
const initResult = await window.electron.initUser(user.id);
if (!initResult.success) {
console.error('[Page] Failed to initialize user:', initResult.error);
} else {
try {
const offlineStatus = await window.electron.offlineModeGet();
if (!offlineStatus.hasPin) {
setTimeout(():void => {
setShowPinSetup(true);
}, 2000);
}
} catch (error) {
console.error('[Page] Error checking offline mode:', error);
errorMessage(initResult.error || t("homePage.errors.offlineInitError"));
return;
}
try {
const offlineStatus = await window.electron.offlineModeGet();
if (!offlineStatus.hasPin) {
setTimeout(():void => {
setShowPinSetup(true);
}, 2000);
}
} catch (error) {
console.error('[Page] Error checking offline mode:', error);
}
} catch (error) {
console.error('[Page] Error initializing user:', error);
@@ -358,7 +408,6 @@ function ScribeContent() {
} catch (error) {
console.error('[Auth] Error checking offline mode:', error);
}
window.electron.logout();
}
}
@@ -439,60 +488,62 @@ function ScribeContent() {
return (
<SessionContext.Provider value={{session: session, setSession: setSession}}>
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
<AIUsageContext.Provider value={{
totalCredits: currentCredits,
setTotalCredits: setCurrentCredits,
totalPrice: amountSpent,
setTotalPrice: setAmountSpent
}}>
<div
className="bg-background text-text-primary h-screen flex flex-col font-['Lora']">
<ScribeTopBar/>
<EditorContext.Provider value={{editor: editor}}>
<ScribeControllerBar/>
<div className="flex-1 flex overflow-hidden">
<ScribeLeftBar/>
<ScribeEditor/>
<ComposerRightBar/>
</div>
<ScribeFooterBar/>
</EditorContext.Provider>
</div>
{
homeStepsGuide && !isCurrentlyOffline() &&
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
onClose={(): void => setHomeStepsGuide(false)}/>
}
{
!isTermsAccepted && !isCurrentlyOffline() && <TermsOfUse onAccept={handleTermsAcceptance}/>
}
{
showPinSetup && window.electron && (
<OfflinePinSetup
showOnFirstLogin={true}
onClose={() => setShowPinSetup(false)}
onSuccess={() => {
setShowPinSetup(false);
console.log('[Page] PIN configured successfully');
}}
/>
)
}
{
showPinVerify && window.electron && (
<OfflinePinVerify
onSuccess={handlePinVerifySuccess}
onCancel={():void => {
//window.electron.logout();
}}
/>
)
}
</AIUsageContext.Provider>
</ChapterContext.Provider>
</BookContext.Provider>
<BooksSyncContext.Provider value={{serverSyncedBooks, localSyncedBooks, booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks}}>
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
<AIUsageContext.Provider value={{
totalCredits: currentCredits,
setTotalCredits: setCurrentCredits,
totalPrice: amountSpent,
setTotalPrice: setAmountSpent
}}>
<div
className="bg-background text-text-primary h-screen flex flex-col font-['Lora']">
<ScribeTopBar/>
<EditorContext.Provider value={{editor: editor}}>
<ScribeControllerBar/>
<div className="flex-1 flex overflow-hidden">
<ScribeLeftBar/>
<ScribeEditor/>
<ComposerRightBar/>
</div>
<ScribeFooterBar/>
</EditorContext.Provider>
</div>
{
homeStepsGuide && !isCurrentlyOffline() &&
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
onClose={(): void => setHomeStepsGuide(false)}/>
}
{
!isTermsAccepted && !isCurrentlyOffline() && <TermsOfUse onAccept={handleTermsAcceptance}/>
}
{
showPinSetup && window.electron && (
<OfflinePinSetup
showOnFirstLogin={true}
onClose={() => setShowPinSetup(false)}
onSuccess={() => {
setShowPinSetup(false);
console.log('[Page] PIN configured successfully');
}}
/>
)
}
{
showPinVerify && window.electron && (
<OfflinePinVerify
onSuccess={handlePinVerifySuccess}
onCancel={():void => {
//window.electron.logout();
}}
/>
)
}
</AIUsageContext.Provider>
</ChapterContext.Provider>
</BookContext.Provider>
</BooksSyncContext.Provider>
</SessionContext.Provider>
);
}