sync: check local file availability before opening (#262)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user