sync: check local file availability before opening (#262)

This commit is contained in:
Huang Xin
2025-01-28 21:02:19 +01:00
committed by GitHub
parent 171a7ee759
commit c0d715bf77
6 changed files with 84 additions and 51 deletions
@@ -79,8 +79,27 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const isImportingBook = useRef(false);
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(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<BookshelfProps> = ({
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 {
@@ -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]);
@@ -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);
}
});
+13 -4
View File
@@ -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<void> {
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();
}
}
+1 -1
View File
@@ -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;
+6 -1
View File
@@ -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<void>;
clearViewState: (key: string) => void;
getViewState: (key: string) => ViewState | null;
}