Files
ERitors-Scribe-Desktop/hooks/useSyncBooks.ts
natreex 209dc6f85a Remove CharacterComponent and CharacterDetail components
- Deleted `CharacterComponent` and `CharacterDetail` files from the project.
- Refactored related logic to improve code maintainability and reduce redundancy.
2026-02-05 14:12:08 -05:00

260 lines
10 KiB
TypeScript

import {useContext} from 'react';
import System from '@/lib/models/System';
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/models/Book';
import {BookSyncCompare, SyncedBook} from '@/lib/models/SyncedBook';
import {useTranslations} from 'next-intl';
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 window.electron.invoke<CompleteBook>('db:book:uploadToServer', bookId);
if (!bookToSync) {
errorMessage(t('bookCard.uploadError'));
return false;
}
const response: boolean = await System.authPostToServer('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 System.authGetQueryToServer('book/sync/download', session.accessToken, lang, {bookId});
if (!response) {
errorMessage(t('bookCard.downloadError'));
return false;
}
const syncStatus: boolean = await window.electron.invoke<boolean>('db:book:syncSave', 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 System.authPostToServer('book/sync/server-to-client', {
bookToSync: bookToFetch
}, session.accessToken, lang);
if (!response) {
errorMessage(t('bookCard.syncFromServerError'));
return false;
}
const syncStatus: boolean = await window.electron.invoke<boolean>('db:book:sync:toClient', 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 window.electron.invoke<CompleteBook>('db:book:sync:toServer', bookToFetch);
if (!bookToSync) {
errorMessage(t('bookCard.syncToServerError'));
return false;
}
const response: boolean = await System.authPatchToServer('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 refreshBooks(): Promise<void> {
try {
let localBooksResponse: SyncedBook[] = [];
let serverBooksResponse: SyncedBook[] = [];
if (!isCurrentlyOffline()) {
if (offlineMode.isDatabaseInitialized) {
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
// Get lastOnlineTimestamp from localStorage (or 0 if not set)
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
// Get local tombstones since lastOnlineTimestamp via IPC
const localTombstones: RemovedItemRecord[] = await window.electron.invoke<RemovedItemRecord[]>(
'db:tombstones:since',
lastOnlineTimestamp
);
// Call server with POST and tombstones
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>(
'books/synced',
{ lastOnlineTimestamp, tombstones: localTombstones },
session.accessToken,
lang
);
serverBooksResponse = serverResponse.books;
// Apply server tombstones locally via IPC
await window.electron.invoke<void>('db:tombstones:apply:books', serverResponse.tombstones);
} else {
// No local DB but online - just get server books without tombstones
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>(
'books/synced',
{ lastOnlineTimestamp: 0, tombstones: [] },
session.accessToken,
lang
);
serverBooksResponse = serverResponse.books;
}
} else {
if (offlineMode.isDatabaseInitialized) {
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
}
}
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,
refreshBooks,
localOnlyBooks,
serverOnlyBooks,
booksToSyncToServer,
booksToSyncFromServer
};
}