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:
natreex
2026-04-05 11:36:12 -04:00
parent b9bc024e91
commit 2b6d4cc48b
6 changed files with 585 additions and 50 deletions

View File

@@ -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">