import {useContext} from 'react'; import {apiGet, apiPatch, apiPost} from '@/lib/api/client'; import {SessionContext} from '@/context/SessionContext'; import {LangContext} from '@/context/LangContext'; import {AlertContext} from '@/context/AlertContext'; import OfflineContext from '@/context/OfflineContext'; import {BooksSyncContext} from '@/context/BooksSyncContext'; import {CompleteBook} from '@/lib/types/book-tables'; import {BookSyncCompare, SyncedBook} from '@/lib/types/synced-book'; import {useTranslations} from '@/lib/i18n'; import * as tauri from '@/lib/tauri'; interface RemovedItemRecord { removal_id: string; table_name: string; entity_id: string; book_id: string | null; user_id: string; deleted_at: number; } interface SyncedBooksResponse { books: SyncedBook[]; tombstones: RemovedItemRecord[]; } export default function useSyncBooks() { const t = useTranslations(); const {session} = useContext(SessionContext); const {lang} = useContext(LangContext); const {errorMessage} = useContext(AlertContext); const {isCurrentlyOffline, offlineMode} = useContext(OfflineContext); const { booksToSyncToServer, booksToSyncFromServer, localOnlyBooks, serverOnlyBooks, setLocalOnlyBooks, setServerOnlyBooks, setServerSyncedBooks, setLocalSyncedBooks, setBooksToSyncFromServer, setBooksToSyncToServer } = useContext(BooksSyncContext); async function upload(bookId: string): Promise { if (isCurrentlyOffline()) return false; try { const bookToSync: CompleteBook = await tauri.uploadBookToServer(bookId) as CompleteBook; if (!bookToSync) { errorMessage(t('bookCard.uploadError')); return false; } const response: boolean = await apiPost('book/sync/upload', { book: bookToSync }, session.accessToken, lang); if (!response) { errorMessage(t('bookCard.uploadError')); return false; } const uploadedBook: SyncedBook | undefined = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId); setLocalOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => { return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId); }); if (uploadedBook) { setLocalSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, uploadedBook]); setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, uploadedBook]); } return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('bookCard.uploadError')); } return false; } } async function download(bookId: string): Promise { if (isCurrentlyOffline()) return false; try { const response: CompleteBook = await apiGet('book/sync/download', session.accessToken, lang, {bookId}); if (!response) { errorMessage(t('bookCard.downloadError')); return false; } const syncStatus: boolean = await tauri.syncSaveBook(response); if (!syncStatus) { errorMessage(t('bookCard.downloadError')); return false; } const downloadedBook: SyncedBook | undefined = serverOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId); setServerOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => { return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId); }); if (downloadedBook) { setLocalSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, downloadedBook]); setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, downloadedBook]); } return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('bookCard.downloadError')); } return false; } } async function syncFromServer(bookId: string): Promise { if (isCurrentlyOffline()) return false; try { const bookToFetch: BookSyncCompare | undefined = booksToSyncFromServer.find((book: BookSyncCompare): boolean => book.id === bookId); if (!bookToFetch) { errorMessage(t('bookCard.syncFromServerError')); return false; } const response: CompleteBook = await apiPost('book/sync/server-to-client', { bookToSync: bookToFetch }, session.accessToken, lang); if (!response) { errorMessage(t('bookCard.syncFromServerError')); return false; } const syncStatus: boolean = await tauri.syncBookToClient(response); if (!syncStatus) { errorMessage(t('bookCard.syncFromServerError')); return false; } setBooksToSyncFromServer((prev: BookSyncCompare[]): BookSyncCompare[] => prev.filter((book: BookSyncCompare): boolean => book.id !== bookId) ); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('bookCard.syncFromServerError')); } return false; } } async function syncToServer(bookId: string): Promise { if (isCurrentlyOffline()) return false; try { const bookToFetch: BookSyncCompare | undefined = booksToSyncToServer.find((book: BookSyncCompare): boolean => book.id === bookId); if (!bookToFetch) { errorMessage(t('bookCard.syncToServerError')); return false; } const bookToSync: CompleteBook = await tauri.syncBookToServer(bookToFetch) as CompleteBook; if (!bookToSync) { errorMessage(t('bookCard.syncToServerError')); return false; } const response: boolean = await apiPatch('book/sync/client-to-server', { book: bookToSync }, session.accessToken, lang); if (!response) { errorMessage(t('bookCard.syncToServerError')); return false; } setBooksToSyncToServer((prev: BookSyncCompare[]): BookSyncCompare[] => prev.filter((book: BookSyncCompare): boolean => book.id !== bookId) ); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('bookCard.syncToServerError')); } return false; } } async function syncAllToServer(): Promise { for (const diff of booksToSyncToServer) { await syncToServer(diff.id); } } async function syncAllFromServer(): Promise { for (const diff of booksToSyncFromServer) { await syncFromServer(diff.id); } } async function refreshBooks(): Promise { try { let localBooksResponse: SyncedBook[] = []; let serverBooksResponse: SyncedBook[] = []; if (!isCurrentlyOffline()) { if (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: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[]; const serverResponse: SyncedBooksResponse = await apiPost( 'books/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, lang ); serverBooksResponse = serverResponse.books; await tauri.applyBookTombstones(serverResponse.tombstones as tauri.TombstoneRecord[]); } else { // No local DB but online - just get server books without tombstones const serverResponse: SyncedBooksResponse = await apiPost( 'books/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, lang ); serverBooksResponse = serverResponse.books; } } else { if (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('bookCard.refreshError')); } } } return { upload, download, syncFromServer, syncToServer, syncAllToServer, syncAllFromServer, refreshBooks, localOnlyBooks, serverOnlyBooks, booksToSyncToServer, booksToSyncFromServer }; }