Update book handling and improve offline/online sync logic
- Enhanced the `BookProps` struct with updated field mappings for better API compatibility. - Improved offline/online sync workflows in `BookList` by adding `localBook` property handling and new item count methods for segmented tracking of local/online items. - Updated click handlers in `BookList` to fetch data based on connectivity state and prioritize local data when offline. - Refactored the decryption and vault handling logic in Rust to remove obsolete legacy methods and standardize debug behavior. - Introduced `ScribeShell` layout component with foundational logic for book/chapter syncing and offline queue handling. - Added `init_panic_hook` to improve crash reporting during Rust app initialization.
This commit is contained in:
@@ -6,9 +6,10 @@ import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import SearchBook from "./SearchBook";
|
||||
import {useRouter} from "@/lib/navigation";
|
||||
import {Book, ChevronLeft, ChevronRight, Download, Settings, Trash2} from 'lucide-react';
|
||||
import {Book, ChevronLeft, ChevronRight, Cloud, Download, HardDrive, Settings, Trash2} from 'lucide-react';
|
||||
import Badge from "@/components/ui/Badge";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {BookProps, BookTypeLimit} from "@/lib/types/book";
|
||||
import {getBookTypeLabel} from "@/lib/utils/book";
|
||||
import BookCard from "@/components/book/BookCard";
|
||||
@@ -41,6 +42,7 @@ export default function BookList() {
|
||||
serverOnlyBooks, localOnlyBooks
|
||||
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const {isCurrentlyOffline, offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [groupedItems, setGroupedItems] = useState<Record<string, CategoryItem[]>>({});
|
||||
@@ -215,11 +217,11 @@ export default function BookList() {
|
||||
});
|
||||
|
||||
// Transformer les livres avec leur image
|
||||
const transformedBooks: BookProps[] = booksResponse.map((book: BookProps & { bookType?: string }): BookProps => {
|
||||
const transformedBooks: BookProps[] = booksResponse.map((book: BookProps): BookProps => {
|
||||
const imageDataUrl: string = book.coverImage ? 'data:image/jpeg;base64,' + book.coverImage : '';
|
||||
return {
|
||||
bookId: book.bookId,
|
||||
type: book.type || book.bookType || '',
|
||||
type: book.type,
|
||||
title: book.title,
|
||||
subTitle: book.subTitle,
|
||||
summary: book.summary,
|
||||
@@ -228,6 +230,7 @@ export default function BookList() {
|
||||
desiredWordCount: book.desiredWordCount,
|
||||
totalWordCount: 0,
|
||||
coverImage: imageDataUrl,
|
||||
localBook: book.localBook,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -338,13 +341,21 @@ export default function BookList() {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getTotalItemsCount(items: CategoryItem[]): number {
|
||||
function getOnlineItemsCount(items: CategoryItem[]): number {
|
||||
return items.reduce((count: number, item: CategoryItem): number => {
|
||||
if (item.type === 'book') {
|
||||
return count + 1;
|
||||
}
|
||||
if (item.type === 'book' && !item.book?.localBook) return count + 1;
|
||||
if (item.type === 'series' && item.series) {
|
||||
return count + item.series.books.length;
|
||||
return count + item.series.books.filter((b: BookProps): boolean => !b.localBook).length;
|
||||
}
|
||||
return count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getLocalItemsCount(items: CategoryItem[]): number {
|
||||
return items.reduce((count: number, item: CategoryItem): number => {
|
||||
if (item.type === 'book' && item.book?.localBook) return count + 1;
|
||||
if (item.type === 'series' && item.series) {
|
||||
return count + item.series.books.filter((b: BookProps): boolean => !!b.localBook).length;
|
||||
}
|
||||
return count;
|
||||
}, 0);
|
||||
@@ -367,8 +378,60 @@ export default function BookList() {
|
||||
return 'synced';
|
||||
}
|
||||
|
||||
function handleBookClick(bookId: string): void {
|
||||
router.push(`/book/${bookId}`);
|
||||
async function handleBookClick(bookId: string): Promise<void> {
|
||||
try {
|
||||
let localBookOnly: boolean = false;
|
||||
let bookResponse: BookProps | null = null;
|
||||
|
||||
if (isCurrentlyOffline()) {
|
||||
if (!offlineMode.isDatabaseInitialized) {
|
||||
errorMessage(t("bookList.errorBookDetails"));
|
||||
return;
|
||||
}
|
||||
bookResponse = await tauri.getBookBasicInformation(bookId);
|
||||
if (bookResponse) localBookOnly = true;
|
||||
} else {
|
||||
const isOfflineBook = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId);
|
||||
if (isOfflineBook) {
|
||||
bookResponse = await tauri.getBookBasicInformation(bookId);
|
||||
localBookOnly = true;
|
||||
}
|
||||
if (!bookResponse) {
|
||||
bookResponse = await apiGet<BookProps>(
|
||||
'book/basic-information', accessToken, lang, {id: bookId}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bookResponse) {
|
||||
errorMessage(t("bookList.errorBookDetails"));
|
||||
return;
|
||||
}
|
||||
|
||||
setBook({
|
||||
bookId: bookId,
|
||||
type: bookResponse.type,
|
||||
title: bookResponse.title || '',
|
||||
subTitle: bookResponse.subTitle || '',
|
||||
summary: bookResponse.summary || '',
|
||||
serie: bookResponse.serie,
|
||||
seriesId: bookResponse.seriesId,
|
||||
publicationDate: bookResponse.publicationDate || '',
|
||||
desiredWordCount: bookResponse.desiredWordCount || 0,
|
||||
totalWordCount: bookResponse.totalWordCount ?? 0,
|
||||
localBook: localBookOnly,
|
||||
coverImage: bookResponse.coverImage ? 'data:image/jpeg;base64,' + bookResponse.coverImage : '',
|
||||
quillsenseEnabled: bookResponse.quillsenseEnabled,
|
||||
tools: bookResponse.tools,
|
||||
});
|
||||
router.push(`/book/${bookId}`);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("bookList.errorBookDetails"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeriesSettingsClick(seriesId: string): void {
|
||||
@@ -429,7 +492,8 @@ export default function BookList() {
|
||||
</div>
|
||||
|
||||
{Object.entries(filteredItems).map(([category, items]: [string, CategoryItem[]], index: number) => {
|
||||
const itemCount: number = getTotalItemsCount(items);
|
||||
const onlineCount: number = getOnlineItemsCount(items);
|
||||
const localCount: number = getLocalItemsCount(items);
|
||||
const typeLimit: BookTypeLimit | undefined = bookLimits?.[category] ?? undefined;
|
||||
const isLimitReached: boolean = typeLimit !== undefined && typeLimit.current >= typeLimit.max;
|
||||
const categoryLabel: string = t(getBookTypeLabel(category));
|
||||
@@ -442,12 +506,21 @@ export default function BookList() {
|
||||
<span className="w-1 h-8 bg-primary rounded-full"></span>
|
||||
{categoryLabel}
|
||||
</h2>
|
||||
<Badge variant={isLimitReached ? "error" : "muted"} size="md">
|
||||
{typeLimit !== undefined
|
||||
? `${typeLimit.current}/${typeLimit.max}`
|
||||
: itemCount
|
||||
} {t("bookList.works")}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isCurrentlyOffline() && (
|
||||
<Badge variant={isLimitReached ? "error" : "muted"} size="md" icon={Cloud}>
|
||||
{typeLimit !== undefined
|
||||
? `${typeLimit.current}/${typeLimit.max}`
|
||||
: onlineCount
|
||||
}
|
||||
</Badge>
|
||||
)}
|
||||
{isDesktop && localCount > 0 && (
|
||||
<Badge variant="muted" size="md" icon={HardDrive}>
|
||||
{localCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group/carousel relative w-full">
|
||||
|
||||
Reference in New Issue
Block a user