- Added support for creating books locally when the cloud limit is reached. - Enhanced error handling in `AddNewBookForm` with `ApiError` and fallback logic for local book creation. - Implemented `BookTypeLimit` to manage type-specific book limits with visual indicators in `BookList`. - Refactored `TombstoneRecord` to standardize naming conventions for better API compatibility. - Updated `useSyncSeries` and `useSyncBooks` to handle empty tombstones gracefully. - Updated locales with new translations for fallback and error messaging.
261 lines
10 KiB
TypeScript
261 lines
10 KiB
TypeScript
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<void> {
|
|
for (const diff of booksToSyncToServer) {
|
|
await syncToServer(diff.id);
|
|
}
|
|
}
|
|
|
|
async function syncAllFromServer(): Promise<void> {
|
|
for (const diff of booksToSyncFromServer) {
|
|
await syncFromServer(diff.id);
|
|
}
|
|
}
|
|
|
|
async function refreshBooks(): Promise<void> {
|
|
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<SyncedBooksResponse>(
|
|
'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<SyncedBooksResponse>(
|
|
'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
|
|
};
|
|
}
|