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

@@ -14,6 +14,8 @@ import User from "@/lib/models/User";
import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
export default function BookList() {
const {session, setSession} = useContext(SessionContext);
@@ -23,6 +25,7 @@ export default function BookList() {
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext)
const {booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks} = useContext<BooksSyncContextProps>(BooksSyncContext)
const [searchQuery, setSearchQuery] = useState<string>('');
const [groupedBooks, setGroupedBooks] = useState<Record<string, BookProps[]>>({});
@@ -86,7 +89,7 @@ export default function BookList() {
useEffect((): void => {
getBooks().then()
}, [session.user?.books]);
}, [booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks]);
useEffect((): void => {
if (accessToken) getBooks().then();
@@ -115,11 +118,21 @@ export default function BookList() {
async function getBooks(): Promise<void> {
setIsLoadingBooks(true);
try {
let bookResponse: BookListProps[] = [];
let bookResponse: (BookListProps & { itIsLocal: boolean })[] = [];
if (!isCurrentlyOffline()) {
bookResponse = await System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang);
const [onlineBooks, localBooks]: [BookListProps[], BookListProps[]] = await Promise.all([
System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang),
window.electron.invoke<BookListProps[]>('db:book:books')
]);
const onlineBookIds: Set<string> = new Set(onlineBooks.map((book: BookListProps): string => book.id));
const uniqueLocalBooks: BookListProps[] = localBooks.filter((book: BookListProps): boolean => !onlineBookIds.has(book.id));
bookResponse = [
...onlineBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: false })),
...uniqueLocalBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true }))
];
} else {
bookResponse = await window.electron.invoke<BookListProps[]>('db:book:books');
const localBooks: BookListProps[] = await window.electron.invoke<BookListProps[]>('db:book:books');
bookResponse = localBooks.map((book: BookListProps): BookListProps & { itIsLocal: boolean } => ({ ...book, itIsLocal: true }));
}
if (bookResponse) {
const booksByType: Record<string, BookProps[]> = bookResponse.reduce((groups: Record<string, BookProps[]>, book: BookListProps): Record<string, BookProps[]> => {
@@ -170,6 +183,22 @@ export default function BookList() {
{}
);
function detectBookSyncStatus(bookId: string):SyncType {
if (serverOnlyBooks.find((book: SyncedBook):boolean => book.id === bookId)) {
return 'server-only';
}
if (localOnlyBooks.find((book: SyncedBook):boolean => book.id === bookId)) {
return 'local-only';
}
if (booksToSyncFromServer.find((book: SyncedBook):boolean => book.id === bookId)) {
return 'to-sync-from-server';
}
if (booksToSyncToServer.find((book: SyncedBook):boolean => book.id === bookId)) {
return 'to-sync-to-server';
}
return 'synced';
}
async function getBook(bookId: string): Promise<void> {
try {
let bookResponse: BookListProps|null = null;
@@ -267,8 +296,10 @@ export default function BookList() {
{...(idx === 0 && {'data-guide': 'book-card'})}
className={`w-full sm:w-1/3 md:w-1/4 lg:w-1/5 xl:w-1/6 p-2 box-border ${User.guideTourDone(session.user?.guideTour || [], 'new-first-book') && 'mb-[200px]'}`}>
<BookCard book={book}
syncStatus={detectBookSyncStatus(book.bookId)}
onClickCallback={getBook}
index={idx}/>
index={idx}
/>
</div>
))
}