Update book handling and improve offline/online sync logic

- Enhanced the `BookProps` struct with updated field mappings for better API compatibility.
- Improved offline/online sync workflows in `BookList` by adding `localBook` property handling and new item count methods for segmented tracking of local/online items.
- Updated click handlers in `BookList` to fetch data based on connectivity state and prioritize local data when offline.
- Refactored the decryption and vault handling logic in Rust to remove obsolete legacy methods and standardize debug behavior.
- Introduced `ScribeShell` layout component with foundational logic for book/chapter syncing and offline queue handling.
- Added `init_panic_hook` to improve crash reporting during Rust app initialization.
This commit is contained in:
natreex
2026-04-05 11:36:12 -04:00
parent b9bc024e91
commit 2b6d4cc48b
6 changed files with 585 additions and 50 deletions

View File

@@ -6,9 +6,10 @@ import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import SearchBook from "./SearchBook";
import {useRouter} from "@/lib/navigation";
import {Book, ChevronLeft, ChevronRight, Download, Settings, Trash2} from 'lucide-react';
import {Book, ChevronLeft, ChevronRight, Cloud, Download, HardDrive, Settings, Trash2} from 'lucide-react';
import Badge from "@/components/ui/Badge";
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {BookContext, BookContextProps} from "@/context/BookContext";
import {BookProps, BookTypeLimit} from "@/lib/types/book";
import {getBookTypeLabel} from "@/lib/utils/book";
import BookCard from "@/components/book/BookCard";
@@ -41,6 +42,7 @@ export default function BookList() {
serverOnlyBooks, localOnlyBooks
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const {isCurrentlyOffline, offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
const [searchQuery, setSearchQuery] = useState<string>('');
const [groupedItems, setGroupedItems] = useState<Record<string, CategoryItem[]>>({});
@@ -215,11 +217,11 @@ export default function BookList() {
});
// Transformer les livres avec leur image
const transformedBooks: BookProps[] = booksResponse.map((book: BookProps & { bookType?: string }): BookProps => {
const transformedBooks: BookProps[] = booksResponse.map((book: BookProps): BookProps => {
const imageDataUrl: string = book.coverImage ? 'data:image/jpeg;base64,' + book.coverImage : '';
return {
bookId: book.bookId,
type: book.type || book.bookType || '',
type: book.type,
title: book.title,
subTitle: book.subTitle,
summary: book.summary,
@@ -228,6 +230,7 @@ export default function BookList() {
desiredWordCount: book.desiredWordCount,
totalWordCount: 0,
coverImage: imageDataUrl,
localBook: book.localBook,
};
});
@@ -338,13 +341,21 @@ export default function BookList() {
return filtered;
}
function getTotalItemsCount(items: CategoryItem[]): number {
function getOnlineItemsCount(items: CategoryItem[]): number {
return items.reduce((count: number, item: CategoryItem): number => {
if (item.type === 'book') {
return count + 1;
}
if (item.type === 'book' && !item.book?.localBook) return count + 1;
if (item.type === 'series' && item.series) {
return count + item.series.books.length;
return count + item.series.books.filter((b: BookProps): boolean => !b.localBook).length;
}
return count;
}, 0);
}
function getLocalItemsCount(items: CategoryItem[]): number {
return items.reduce((count: number, item: CategoryItem): number => {
if (item.type === 'book' && item.book?.localBook) return count + 1;
if (item.type === 'series' && item.series) {
return count + item.series.books.filter((b: BookProps): boolean => !!b.localBook).length;
}
return count;
}, 0);
@@ -367,8 +378,60 @@ export default function BookList() {
return 'synced';
}
function handleBookClick(bookId: string): void {
router.push(`/book/${bookId}`);
async function handleBookClick(bookId: string): Promise<void> {
try {
let localBookOnly: boolean = false;
let bookResponse: BookProps | null = null;
if (isCurrentlyOffline()) {
if (!offlineMode.isDatabaseInitialized) {
errorMessage(t("bookList.errorBookDetails"));
return;
}
bookResponse = await tauri.getBookBasicInformation(bookId);
if (bookResponse) localBookOnly = true;
} else {
const isOfflineBook = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId);
if (isOfflineBook) {
bookResponse = await tauri.getBookBasicInformation(bookId);
localBookOnly = true;
}
if (!bookResponse) {
bookResponse = await apiGet<BookProps>(
'book/basic-information', accessToken, lang, {id: bookId}
);
}
}
if (!bookResponse) {
errorMessage(t("bookList.errorBookDetails"));
return;
}
setBook({
bookId: bookId,
type: bookResponse.type,
title: bookResponse.title || '',
subTitle: bookResponse.subTitle || '',
summary: bookResponse.summary || '',
serie: bookResponse.serie,
seriesId: bookResponse.seriesId,
publicationDate: bookResponse.publicationDate || '',
desiredWordCount: bookResponse.desiredWordCount || 0,
totalWordCount: bookResponse.totalWordCount ?? 0,
localBook: localBookOnly,
coverImage: bookResponse.coverImage ? 'data:image/jpeg;base64,' + bookResponse.coverImage : '',
quillsenseEnabled: bookResponse.quillsenseEnabled,
tools: bookResponse.tools,
});
router.push(`/book/${bookId}`);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("bookList.errorBookDetails"));
}
}
}
function handleSeriesSettingsClick(seriesId: string): void {
@@ -429,7 +492,8 @@ export default function BookList() {
</div>
{Object.entries(filteredItems).map(([category, items]: [string, CategoryItem[]], index: number) => {
const itemCount: number = getTotalItemsCount(items);
const onlineCount: number = getOnlineItemsCount(items);
const localCount: number = getLocalItemsCount(items);
const typeLimit: BookTypeLimit | undefined = bookLimits?.[category] ?? undefined;
const isLimitReached: boolean = typeLimit !== undefined && typeLimit.current >= typeLimit.max;
const categoryLabel: string = t(getBookTypeLabel(category));
@@ -442,12 +506,21 @@ export default function BookList() {
<span className="w-1 h-8 bg-primary rounded-full"></span>
{categoryLabel}
</h2>
<Badge variant={isLimitReached ? "error" : "muted"} size="md">
{typeLimit !== undefined
? `${typeLimit.current}/${typeLimit.max}`
: itemCount
} {t("bookList.works")}
</Badge>
<div className="flex items-center gap-2">
{!isCurrentlyOffline() && (
<Badge variant={isLimitReached ? "error" : "muted"} size="md" icon={Cloud}>
{typeLimit !== undefined
? `${typeLimit.current}/${typeLimit.max}`
: onlineCount
}
</Badge>
)}
{isDesktop && localCount > 0 && (
<Badge variant="muted" size="md" icon={HardDrive}>
{localCount}
</Badge>
)}
</div>
</div>
<div className="group/carousel relative w-full">

View File

@@ -0,0 +1,470 @@
import React, {ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react';
import {BookContext} from '@/context/BookContext';
import {ChapterProps} from '@/lib/types/chapter';
import {ChapterContext} from '@/context/ChapterContext';
import {EditorContext} from '@/context/EditorContext';
import {Editor, useEditor} from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import {AlertContext, AlertContextProps, AlertProvider} from '@/context/AlertContext';
import {usePathname} from '@/lib/navigation';
import {apiPost} from '@/lib/api/client';
import {getCookie} from '@/lib/utils/cookies';
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {BookProps} from '@/lib/types/book';
import ScribeControllerBar from '@/components/layout/ScribeControllerBar';
import ScribeLeftBar from '@/components/leftbar/ScribeLeftBar';
import ComposerRightBar from '@/components/rightbar/ComposerRightBar';
import ScribeFooterBar from '@/components/layout/ScribeFooterBar';
import GuideTour from '@/components/GuideTour';
import TermsOfUse from '@/components/TermsOfUse';
import {useTranslations, changeLanguage} from '@/lib/i18n';
import {isSupportedLocale, LangContext, LangContextProps, SupportedLocale} from '@/context/LangContext';
import {ThemeContext} from '@/context/ThemeContext';
import useTheme from '@/hooks/useTheme';
import {AIUsageContext} from '@/context/AIUsageContext';
import {BookSyncCompare, compareBookSyncs, SyncedBook} from '@/lib/types/synced-book';
import {SeriesSyncCompare, compareSeriesSyncs, SyncedSeries} from '@/lib/types/synced-series';
import {BooksSyncContext} from '@/context/BooksSyncContext';
import {SeriesSyncContext} from '@/context/SeriesSyncContext';
import {LocalSyncQueueContext, LocalSyncOperation} from '@/context/SyncQueueContext';
import useAuthentication from '@/hooks/useAuthentication';
import useOnboarding from '@/hooks/useOnboarding';
import {SettingBookContext} from '@/context/SettingBookContext';
import PulseLoader from '@/components/ui/PulseLoader';
import {initCrashReporter} from '@/lib/crashReporter';
import OfflineProvider from '@/context/OfflineProvider';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import OfflinePinSetup from '@/components/offline/OfflinePinSetup';
import OfflinePinVerify from '@/components/offline/OfflinePinVerify';
import {isDesktop} from '@/lib/configs';
import * as tauri from '@/lib/tauri';
import useSyncBooks from '@/hooks/useSyncBooks';
import useSyncSeries from '@/hooks/useSyncSeries';
function AutoSyncOnReconnect() {
const {offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {syncAllToServer: syncAllBooksToServer, syncAllFromServer: syncAllBooksFromServer, refreshBooks, booksToSyncToServer, booksToSyncFromServer} = useSyncBooks();
const {syncAllToServer: syncAllSeriesToServer, syncAllFromServer: syncAllSeriesFromServer, refreshSeries, seriesToSyncToServer, seriesToSyncFromServer} = useSyncSeries();
const isSyncingRef = useRef<boolean>(false);
const hasRefreshedRef = useRef<boolean>(false);
const saveLastOnlineTimestamp = useCallback((): void => {
const timestamp: number = Math.floor(Date.now() / 1000);
localStorage.setItem('lastOnlineTimestamp', timestamp.toString());
}, []);
// Refresh sync data when online + authenticated + DB ready
useEffect((): void => {
if (!offlineMode.isOffline && session.isConnected && offlineMode.isDatabaseInitialized) {
hasRefreshedRef.current = true;
Promise.all([refreshBooks(), refreshSeries()]);
}
}, [offlineMode.isOffline, session.isConnected, offlineMode.isDatabaseInitialized]);
// Auto-sync when diffs become available
useEffect((): void => {
if (offlineMode.isOffline || !session.isConnected || isSyncingRef.current || !hasRefreshedRef.current) return;
const syncPromises: Promise<void>[] = [];
if (booksToSyncToServer.length > 0) syncPromises.push(syncAllBooksToServer());
if (booksToSyncFromServer.length > 0) syncPromises.push(syncAllBooksFromServer());
if (seriesToSyncToServer.length > 0) syncPromises.push(syncAllSeriesToServer());
if (seriesToSyncFromServer.length > 0) syncPromises.push(syncAllSeriesFromServer());
if (syncPromises.length > 0) {
isSyncingRef.current = true;
Promise.all(syncPromises).then((): void => {
saveLastOnlineTimestamp();
isSyncingRef.current = false;
}).catch((): void => {
isSyncingRef.current = false;
});
}
}, [booksToSyncToServer, booksToSyncFromServer, seriesToSyncToServer, seriesToSyncFromServer]);
// Update lastOnlineTimestamp every 5 minutes while online
useEffect((): (() => void) | void => {
if (!offlineMode.isOffline && session.isConnected) {
const intervalId: ReturnType<typeof setInterval> = setInterval((): void => {
saveLastOnlineTimestamp();
}, 5 * 60 * 1000);
return (): void => clearInterval(intervalId);
}
}, [offlineMode.isOffline, session.isConnected, saveLastOnlineTimestamp]);
return null;
}
function ScribeContent({children}: { children: ReactNode }) {
const t = useTranslations();
const {lang: locale}: LangContextProps = useContext<LangContextProps>(LangContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {isCurrentlyOffline, offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {
session, setSession, isLoading,
currentCredits, setCurrentCredits,
amountSpent, setAmountSpent,
showPinSetup, setShowPinSetup,
showPinVerify, setShowPinVerify,
handlePinVerifySuccess
} = useAuthentication();
const {
isTermsAccepted, homeStepsGuide, setHomeStepsGuide,
handleTermsAcceptance, handleHomeTour, homeSteps
} = useOnboarding({session, setSession});
const editor: Editor | null = useEditor({
extensions: [
StarterKit,
Underline,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
],
injectCSS: false,
immediatelyRender: false,
shouldRerenderOnTransaction: true,
});
const pathname: string = usePathname();
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
const [bookSettingId, setBookSettingId] = useState<string>('');
const [serverSyncedBooks, setServerSyncedBooks] = useState<SyncedBook[]>([]);
const [localSyncedBooks, setLocalSyncedBooks] = useState<SyncedBook[]>([]);
const [bookSyncDiffsFromServer, setBookSyncDiffsFromServer] = useState<BookSyncCompare[]>([]);
const [bookSyncDiffsToServer, setBookSyncDiffsToServer] = useState<BookSyncCompare[]>([]);
const [serverOnlyBooks, setServerOnlyBooks] = useState<SyncedBook[]>([]);
const [localOnlyBooks, setLocalOnlyBooks] = useState<SyncedBook[]>([]);
const [serverSyncedSeries, setServerSyncedSeries] = useState<SyncedSeries[]>([]);
const [localSyncedSeries, setLocalSyncedSeries] = useState<SyncedSeries[]>([]);
const [seriesSyncDiffsFromServer, setSeriesSyncDiffsFromServer] = useState<SeriesSyncCompare[]>([]);
const [seriesSyncDiffsToServer, setSeriesSyncDiffsToServer] = useState<SeriesSyncCompare[]>([]);
const [serverOnlySeries, setServerOnlySeries] = useState<SyncedSeries[]>([]);
const [localOnlySeries, setLocalOnlySeries] = useState<SyncedSeries[]>([]);
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 tauri.invoke(operation.channel, {data: operation.data});
setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] =>
prev.filter((op: LocalSyncOperation): boolean => op.id !== operation.id)
);
} catch (_e) { /* queue retry on next cycle */ }
}
setIsQueueProcessing(false);
}
processQueue().then();
}, [localSyncQueue, isQueueProcessing]);
useEffect((): void => {
if (pathname === '/') {
setCurrentBook(null);
setCurrentChapter(undefined);
}
}, [pathname]);
useEffect((): void => {
if (!session.isConnected) return;
// On desktop with DB initialized, AutoSyncOnReconnect handles fetching + tombstone exchange
if (isDesktop && offlineMode.isDatabaseInitialized) return;
getBooks().then();
getSeries().then();
}, [session, offlineMode.isDatabaseInitialized]);
useEffect((): void => {
const diffsFromServer: BookSyncCompare[] = [];
const diffsToServer: BookSyncCompare[] = [];
serverSyncedBooks.forEach((serverBook: SyncedBook): void => {
const localBook: SyncedBook | undefined = localSyncedBooks.find((book: SyncedBook): boolean => book.id === serverBook.id);
if (!localBook) return;
const diff: BookSyncCompare | null = compareBookSyncs(serverBook, localBook);
if (diff) diffsFromServer.push(diff);
});
localSyncedBooks.forEach((localBook: SyncedBook): void => {
const serverBook: SyncedBook | undefined = serverSyncedBooks.find((book: SyncedBook): boolean => book.id === localBook.id);
if (!serverBook) return;
const diff: BookSyncCompare | null = compareBookSyncs(localBook, serverBook);
if (diff) diffsToServer.push(diff);
});
setBookSyncDiffsFromServer(diffsFromServer);
setBookSyncDiffsToServer(diffsToServer);
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]);
useEffect((): void => {
const diffsFromServer: SeriesSyncCompare[] = [];
const diffsToServer: SeriesSyncCompare[] = [];
serverSyncedSeries.forEach((serverSeries: SyncedSeries): void => {
const localSeries: SyncedSeries | undefined = localSyncedSeries.find((series: SyncedSeries): boolean => series.id === serverSeries.id);
if (!localSeries) return;
const diff: SeriesSyncCompare | null = compareSeriesSyncs(serverSeries, localSeries);
if (diff) diffsFromServer.push(diff);
});
localSyncedSeries.forEach((localSeries: SyncedSeries): void => {
const serverSeries: SyncedSeries | undefined = serverSyncedSeries.find((series: SyncedSeries): boolean => series.id === localSeries.id);
if (!serverSeries) return;
const diff: SeriesSyncCompare | null = compareSeriesSyncs(localSeries, serverSeries);
if (diff) diffsToServer.push(diff);
});
setSeriesSyncDiffsFromServer(diffsFromServer);
setSeriesSyncDiffsToServer(diffsToServer);
setServerOnlySeries(serverSyncedSeries.filter((s: SyncedSeries): boolean => !localSyncedSeries.find((l: SyncedSeries): boolean => l.id === s.id)));
setLocalOnlySeries(localSyncedSeries.filter((l: SyncedSeries): boolean => !serverSyncedSeries.find((s: SyncedSeries): boolean => s.id === l.id)));
}, [localSyncedSeries, serverSyncedSeries]);
async function getBooks(): Promise<void> {
try {
let localBooksResponse: SyncedBook[] = [];
let serverBooksResponse: SyncedBook[] = [];
if (!isCurrentlyOffline()) {
if (isDesktop && offlineMode.isDatabaseInitialized) {
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
const localTombstones: tauri.TombstoneRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as tauri.TombstoneRecord[];
const serverResponse: { books: SyncedBook[]; tombstones: tauri.TombstoneRecord[] } = await apiPost<{
books: SyncedBook[]; tombstones: tauri.TombstoneRecord[];
}>('books/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
serverBooksResponse = serverResponse.books;
await tauri.applyBookTombstones(serverResponse.tombstones ?? []);
} else {
const serverResponse: { books: SyncedBook[] } = await apiPost<{
books: SyncedBook[]
}>('books/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
if (serverResponse) serverBooksResponse = serverResponse.books;
}
} else {
if (isDesktop && offlineMode.isDatabaseInitialized) {
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
}
}
setServerSyncedBooks(serverBooksResponse);
setLocalSyncedBooks(localBooksResponse);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t('homePage.errors.fetchBooksError'));
}
}
}
async function getSeries(): Promise<void> {
try {
let localSeriesResponse: SyncedSeries[] = [];
let serverSeriesResponse: SyncedSeries[] = [];
if (!isCurrentlyOffline()) {
if (isDesktop && offlineMode.isDatabaseInitialized) {
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
const localTombstones: tauri.TombstoneRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as tauri.TombstoneRecord[];
const serverResponse: { series: SyncedSeries[]; tombstones: tauri.TombstoneRecord[] } = await apiPost<{
series: SyncedSeries[]; tombstones: tauri.TombstoneRecord[];
}>('series/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
serverSeriesResponse = serverResponse.series;
await tauri.applySeriesTombstones(serverResponse.tombstones ?? []);
} else {
const serverResponse: { series: SyncedSeries[] } = await apiPost<{
series: SyncedSeries[]
}>('series/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
if (serverResponse) serverSeriesResponse = serverResponse.series;
}
} else {
if (isDesktop && offlineMode.isDatabaseInitialized) {
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
}
}
setServerSyncedSeries(serverSeriesResponse);
setLocalSyncedSeries(localSeriesResponse);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t('homePage.errors.fetchBooksError'));
}
}
}
if (isLoading) {
return (
<div
className="bg-background text-text-primary h-screen flex flex-col items-center justify-center font-['Lora']">
<div className="flex flex-col items-center space-y-6">
<div className="animate-pulse">
<img src="/logo.png" alt="ERitors Logo" width={400} height={400}/>
</div>
<PulseLoader text={t('homePage.loading')}/>
</div>
</div>
);
}
return (
<SessionContext.Provider value={{session: session, setSession: setSession}}>
<LocalSyncQueueContext.Provider value={{
queue: localSyncQueue,
setQueue: setLocalSyncQueue,
addToQueue: addToLocalSyncQueue,
isProcessing: isQueueProcessing,
}}>
<BooksSyncContext.Provider value={{
serverSyncedBooks,
setServerSyncedBooks,
localSyncedBooks,
setLocalSyncedBooks,
booksToSyncFromServer: bookSyncDiffsFromServer,
setBooksToSyncFromServer: setBookSyncDiffsFromServer,
booksToSyncToServer: bookSyncDiffsToServer,
setBooksToSyncToServer: setBookSyncDiffsToServer,
setServerOnlyBooks,
setLocalOnlyBooks,
serverOnlyBooks,
localOnlyBooks
}}>
<SeriesSyncContext.Provider value={{
serverSyncedSeries,
localSyncedSeries,
seriesToSyncFromServer: seriesSyncDiffsFromServer,
seriesToSyncToServer: seriesSyncDiffsToServer,
setServerSyncedSeries,
setLocalSyncedSeries,
setServerOnlySeries,
setLocalOnlySeries,
setSeriesToSyncFromServer: setSeriesSyncDiffsFromServer,
setSeriesToSyncToServer: setSeriesSyncDiffsToServer,
serverOnlySeries,
localOnlySeries
}}>
{isDesktop && <AutoSyncOnReconnect/>}
<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
}}>
<SettingBookContext.Provider value={{bookSettingId, setBookSettingId}}>
<div
className="bg-tertiary text-text-primary h-screen flex flex-col font-['Lora']">
<EditorContext.Provider value={{editor: editor}}>
<ScribeControllerBar/>
<div className="flex-1 flex overflow-hidden">
<ScribeLeftBar/>
<div
className="flex-1 min-w-0 bg-darkest-background rounded-xl m-1 overflow-hidden">
{children}
</div>
<ComposerRightBar/>
</div>
<ScribeFooterBar/>
</EditorContext.Provider>
</div>
{homeStepsGuide &&
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
onClose={(): void => setHomeStepsGuide(false)}/>
}
{!isCurrentlyOffline() && !isTermsAccepted && <TermsOfUse onAccept={handleTermsAcceptance}/>}
{isDesktop && showPinSetup && (
<OfflinePinSetup
showOnFirstLogin={true}
onClose={(): void => setShowPinSetup(false)}
onSuccess={(): void => setShowPinSetup(false)}
/>
)}
{isDesktop && showPinVerify && (
<OfflinePinVerify
onSuccess={handlePinVerifySuccess}
onCancel={(): void => {}}
/>
)}
</SettingBookContext.Provider>
</AIUsageContext.Provider>
</ChapterContext.Provider>
</BookContext.Provider>
</SeriesSyncContext.Provider>
</BooksSyncContext.Provider>
</LocalSyncQueueContext.Provider>
</SessionContext.Provider>
);
}
export default function ScribeShell({children}: { children: ReactNode }) {
const [locale, setLocale] = useState<SupportedLocale>('fr');
const {theme, setTheme} = useTheme();
useEffect((): void => {
initCrashReporter();
}, []);
useEffect((): void => {
const lang: string | null = getCookie('lang');
if (lang && isSupportedLocale(lang)) {
setLocale(lang);
}
}, []);
useEffect((): void => {
changeLanguage(locale);
}, [locale]);
return (
<ThemeContext.Provider value={{theme, setTheme}}>
<LangContext.Provider value={{lang: locale, setLang: setLocale}}>
<OfflineProvider>
<AlertProvider>
<ScribeContent>{children}</ScribeContent>
</AlertProvider>
</OfflineProvider>
</LangContext.Provider>
</ThemeContext.Provider>
);
}