From c0d715bf77fa474f4e89c50d8ca3efe47b8454a9 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 28 Jan 2025 21:02:19 +0100 Subject: [PATCH] sync: check local file availability before opening (#262) --- .../src/app/library/components/Bookshelf.tsx | 34 ++++++--- .../src/app/library/hooks/useBooksSync.ts | 71 ++++++++++--------- .../app/reader/components/ReaderContent.tsx | 4 +- apps/readest-app/src/services/appService.ts | 17 +++-- apps/readest-app/src/services/constants.ts | 2 +- apps/readest-app/src/store/readerStore.ts | 7 +- 6 files changed, 84 insertions(+), 51 deletions(-) diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index 5b5eafb2..be6d5f19 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -79,8 +79,27 @@ const Bookshelf: React.FC = ({ const isImportingBook = useRef(false); const [showDetailsBook, setShowDetailsBook] = useState(null); - const showBookDetailsModal = (book: Book) => { - setShowDetailsBook(book); + const makeBookAvailable = async (book: Book) => { + console.log('Make book available:', book); + if (book.uploadedAt && !book.downloadedAt) { + setLoading(true); + let available = false; + try { + await appService?.downloadBook(book); + updateBook(envConfig, book); + available = true; + } finally { + setLoading(false); + return available; + } + } + return true; + }; + + const showBookDetailsModal = async (book: Book) => { + if (await makeBookAvailable(book)) { + setShowDetailsBook(book); + } }; const dismissBookDetailsModal = () => { @@ -115,15 +134,8 @@ const Bookshelf: React.FC = ({ const bookshelfItems = generateBookshelfItems(libraryBooks); const handleBookClick = async (book: Book) => { - if (book.uploadedAt && !book.downloadedAt) { - setLoading(true); - try { - await appService?.downloadBook(book); - updateBook(envConfig, book); - } finally { - setLoading(false); - } - } + if (!(await makeBookAvailable(book))) return; + if (isSelectMode) { toggleSelection(book.hash); } else { diff --git a/apps/readest-app/src/app/library/hooks/useBooksSync.ts b/apps/readest-app/src/app/library/hooks/useBooksSync.ts index 91a8f0b5..33b47f74 100644 --- a/apps/readest-app/src/app/library/hooks/useBooksSync.ts +++ b/apps/readest-app/src/app/library/hooks/useBooksSync.ts @@ -1,9 +1,10 @@ import { useEffect, useRef } from 'react'; import { useAuth } from '@/context/AuthContext'; +import { useEnv } from '@/context/EnvContext'; import { useSync } from '@/hooks/useSync'; import { useLibraryStore } from '@/store/libraryStore'; import { SYNC_BOOKS_INTERVAL_SEC } from '@/services/constants'; -import { useEnv } from '@/context/EnvContext'; +import { Book } from '@/types/book'; export const useBooksSync = () => { const { user } = useAuth(); @@ -51,41 +52,45 @@ export const useBooksSync = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [library]); - useEffect(() => { - const updateLibrary = async () => { - if (syncedBooks?.length) { - const updatedLibrary = library.map((oldBook) => { - const matchingBook = syncedBooks.find((newBook) => newBook.hash === oldBook.hash); - if (matchingBook) { - return { - ...oldBook, - ...matchingBook, - updatedAt: oldBook.updatedAt, - deletedAt: oldBook.deletedAt, - progress: oldBook.progress, - }; + const updateLibrary = async () => { + if (!syncedBooks?.length) return; + + const processOldBook = async (oldBook: Book) => { + const matchingBook = syncedBooks.find((newBook) => newBook.hash === oldBook.hash); + if (matchingBook) { + if (!matchingBook.deletedAt && matchingBook.uploadedAt && !oldBook.downloadedAt) { + await appService?.downloadBook(oldBook, true); + } + return { + ...oldBook, + ...matchingBook, + updatedAt: oldBook.updatedAt, + progress: matchingBook.progress ?? oldBook.progress, + }; + } + return oldBook; + }; + + const updatedLibrary = await Promise.all(library.map(processOldBook)); + const processNewBook = async (newBook: Book) => { + if (!updatedLibrary.some((oldBook) => oldBook.hash === newBook.hash)) { + if (newBook.uploadedAt && !newBook.deletedAt) { + try { + updatedLibrary.push(newBook); + await appService?.downloadBook(newBook, true); + newBook.coverImageUrl = await appService?.generateCoverImageUrl(newBook); + } catch { + console.error('Failed to download book:', newBook); } - return oldBook; - }); - await Promise.all( - syncedBooks.map(async (newBook) => { - if (!updatedLibrary.some((oldBook) => oldBook.hash === newBook.hash)) { - if (newBook.uploadedAt) { - try { - await appService?.downloadBook(newBook, true); - newBook.coverImageUrl = await appService?.generateCoverImageUrl(newBook); - updatedLibrary.push(newBook); - } catch { - console.error('Failed to download book:', newBook); - } - } - } - }), - ); - setLibrary(updatedLibrary); - appService?.saveLibraryBooks(updatedLibrary); + } } }; + await Promise.all(syncedBooks.map(processNewBook)); + setLibrary(updatedLibrary); + appService?.saveLibraryBooks(updatedLibrary); + }; + + useEffect(() => { updateLibrary(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [syncedBooks]); diff --git a/apps/readest-app/src/app/reader/components/ReaderContent.tsx b/apps/readest-app/src/app/reader/components/ReaderContent.tsx index 02592d63..1a2f1715 100644 --- a/apps/readest-app/src/app/reader/components/ReaderContent.tsx +++ b/apps/readest-app/src/app/reader/components/ReaderContent.tsx @@ -59,7 +59,9 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i const isPrimary = !uniqueIds.has(id); uniqueIds.add(id); if (!getViewState(key)) { - initViewState(envConfig, id, key, isPrimary); + initViewState(envConfig, id, key, isPrimary).catch((error) => { + console.log('Error initializing book', key, error); + }); if (index === 0) setSideBarBookKey(key); } }); diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index edaf9e13..8fba9c93 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -224,17 +224,22 @@ export abstract class BaseAppService implements AppService { book.updatedAt = Date.now(); book.uploadedAt = Date.now(); book.downloadedAt = Date.now(); + } else { + throw new Error('Book file not uploaded'); } } async downloadBook(book: Book, onlyCover = false): Promise { - const fps = [getCoverFilename(book)]; + const bookFp = getFilename(book); + const coverFp = getCoverFilename(book); + const fps = [coverFp]; if (!onlyCover) { - fps.push(getFilename(book)); + fps.push(bookFp); } - let downloaded = false; + let bookDownloaded = false; for (const fp of fps) { + let downloaded = false; const existed = await this.fs.exists(fp, 'Books'); if (existed) { downloaded = true; @@ -245,8 +250,12 @@ export abstract class BaseAppService implements AppService { await this.fs.writeFile(fp, 'Books', await fileobj.arrayBuffer()); downloaded = true; } + if (fp === bookFp) { + bookDownloaded = downloaded; + } } - if (!onlyCover && downloaded) { + // some books may not have cover image, so we need to check if the book is downloaded + if (bookDownloaded) { book.downloadedAt = Date.now(); } } diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index 4c9ebffe..407de1aa 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -351,7 +351,7 @@ export const READEST_WEB_BASE_URL = 'https://web.readest.com'; export const SYNC_PROGRESS_INTERVAL_SEC = 60; export const SYNC_NOTES_INTERVAL_SEC = 60; -export const SYNC_BOOKS_INTERVAL_SEC = 60; +export const SYNC_BOOKS_INTERVAL_SEC = 10; export const CHECK_UPDATE_INTERVAL_SEC = 24 * 60 * 60; export const MAX_ZOOM_LEVEL = 140; diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index 07d00abc..e921542f 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -49,7 +49,12 @@ interface ReaderStore { setViewSettings: (key: string, viewSettings: ViewSettings) => void; getViewSettings: (key: string) => ViewSettings | null; - initViewState: (envConfig: EnvConfigType, id: string, key: string, isPrimary?: boolean) => void; + initViewState: ( + envConfig: EnvConfigType, + id: string, + key: string, + isPrimary?: boolean, + ) => Promise; clearViewState: (key: string) => void; getViewState: (key: string) => ViewState | null; }