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:
195
app/page.tsx
195
app/page.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user