Refactor the Bookshelf more

This commit is contained in:
chrox
2024-10-04 11:49:29 +02:00
parent d0487f83b0
commit b04f7c77cc
6 changed files with 60 additions and 47 deletions
+6 -13
View File
@@ -2,8 +2,9 @@
import * as React from 'react';
import { useState } from 'react';
import { Book, BooksGroup, LibraryItem } from '../types/book';
import { useEnv } from '../context/EnvContext';
import { Book } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import Navbar from '@/components/Navbar';
import Spinner from '@/components/Spinner';
@@ -11,16 +12,10 @@ import Bookshelf from '@/components/Bookshelf';
type AppState = 'Init' | 'Loading' | 'Library' | 'Reader';
const generateLibraryItems = (groups: BooksGroup[]): LibraryItem[] => {
const ungroupedBooks: Book[] = groups.find((group) => group.id === 'ungrouped')?.books || [];
const groupedBooks: BooksGroup[] = groups.filter((group) => group.id !== 'ungrouped');
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.lastUpdated - a.lastUpdated);
};
const LibraryPage = () => {
const { envConfig } = useEnv();
const [appState, setAppState] = useState<AppState>('Init');
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
const [libraryBooks, setLibraryBooks] = useState<Book[]>([]);
const [loading, setLoading] = useState<boolean>(false);
React.useEffect(() => {
@@ -33,9 +28,7 @@ const LibraryPage = () => {
appService
.loadLibraryBooks()
.then((libraryBooks) => {
const libraryItems = generateLibraryItems(libraryBooks);
console.log('Library items:', libraryItems);
setLibraryItems(libraryItems);
setLibraryBooks(libraryBooks);
setAppState('Library');
setLoading(false);
})
@@ -59,7 +52,7 @@ const LibraryPage = () => {
<div className='min-h-screen p-2 pt-16'>
<div className='hero-content'>
<Spinner loading={loading} />
<Bookshelf libraryItems={libraryItems} onImport={handleImport} />
<Bookshelf libraryBooks={libraryBooks} onImport={handleImport} />
</div>
</div>
</div>
+42 -5
View File
@@ -1,19 +1,56 @@
import * as React from 'react';
import Image from 'next/image';
import { Book, BooksGroup, LibraryItem } from '../types/book';
import { Book, BooksGroup } from '../types/book';
import { FaPlus } from 'react-icons/fa';
interface BookshelfProps {
libraryItems: LibraryItem[];
libraryBooks: Book[];
onImport: () => void;
}
const Bookshelf: React.FC<BookshelfProps> = ({ libraryItems, onImport }) => {
type BookshelfItem = Book | BooksGroup;
const UNGROUPED_NAME = 'ungrouped';
const MOCK_BOOKS: Book[] = Array.from({ length: 14 }, (_v, k) => ({
id: `book-${k}`,
format: 'EPUB',
title: `Book ${k}`,
author: `Author ${k}`,
lastUpdated: Date.now() - 1000000 * k,
coverImageUrl: `https://placehold.co/800?text=Book+${k}&font=roboto`,
}));
const generateBookshelfItems = (books: Book[]): BookshelfItem[] => {
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
book.group = book.group || UNGROUPED_NAME;
const groupIndex = acc.findIndex((group) => group.name === book.group);
const booksGroup = acc[acc.findIndex((group) => group.name === book.group)];
if (booksGroup) {
booksGroup.books.push(book);
booksGroup.lastUpdated = Math.max(acc[groupIndex]!.lastUpdated, book.lastUpdated);
} else {
acc.push({
name: book.group,
books: [book],
lastUpdated: book.lastUpdated,
});
}
return acc;
}, []);
const ungroupedBooks: Book[] = groups.find((group) => group.name === UNGROUPED_NAME)?.books || [];
const groupedBooks: BooksGroup[] = groups.filter((group) => group.name !== UNGROUPED_NAME);
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.lastUpdated - a.lastUpdated);
};
const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, onImport }) => {
libraryBooks = [...libraryBooks, ...MOCK_BOOKS];
const bookshelfItems = generateBookshelfItems(libraryBooks);
return (
<div>
{/* Books Grid */}
<div className='grid grid-cols-3 gap-6 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
{libraryItems.map((item, index) => (
{bookshelfItems.map((item, index) => (
<div key={`library-item-${index}`} className=''>
<div className='grid gap-2'>
{'format' in item ? (
@@ -53,7 +90,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryItems, onImport }) => {
</div>
))}
{libraryItems.length > 0 && (
{bookshelfItems.length > 0 && (
<div
className='border-1 flex aspect-[28/41] items-center justify-center bg-white'
role='button'
@@ -0,0 +1,2 @@
export const LOCAL_BOOKS_SUBDIR = 'DigestLibrary/Books';
export const CLOUD_BOOKS_SUBDIR = 'DigestLibrary/Books';
@@ -20,22 +20,13 @@ import {
import { convertFileSrc } from '@tauri-apps/api/core';
import { open, message } from '@tauri-apps/plugin-dialog';
import { Book, BooksGroup } from '../types/book';
import { Book } from '../types/book';
import { SystemSettings } from '../types/settings';
import { AppService, BaseDir, ToastType } from '../types/system';
import { LOCAL_BOOKS_SUBDIR } from './constants';
const BOOKS_SUBDIR = 'DigestLibrary/Books';
let BOOKS_DIR = '';
const MOCK_BOOKS: Book[] = Array.from({ length: 14 }, (_v, k) => ({
id: `book-${k}`,
format: 'EPUB',
title: `Book ${k}`,
author: `Author ${k}`,
lastUpdated: Date.now() - 1000000 * k,
coverImageUrl: `https://placehold.co/800?text=Book+${k}&font=roboto`,
}));
function resolvePath(
fp: string,
base: BaseDir,
@@ -52,7 +43,7 @@ function resolvePath(
case 'Books':
return {
baseDir: BaseDirectory.Document,
fp: `${BOOKS_SUBDIR}/${fp}`,
fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`,
base,
dir: () => new Promise((r) => r(`${BOOKS_DIR}/`)),
};
@@ -130,7 +121,7 @@ export const nativeAppService: AppService = {
const txt = await nativeAppService.fs.readFile(fp, base, 'text');
settings = JSON.parse(txt as string);
} catch {
const INIT_BOOKS_DIR = await join(await documentDir(), BOOKS_SUBDIR);
const INIT_BOOKS_DIR = await join(await documentDir(), LOCAL_BOOKS_SUBDIR);
await nativeAppService.fs.createDir('', 'Books', true);
settings = {
localBooksDir: INIT_BOOKS_DIR,
@@ -182,7 +173,6 @@ export const nativeAppService: AppService = {
await message(msg, { kind, title, okLabel });
},
loadLibraryBooks: async () => {
// TODO: Burrently only ungrouped books are supported
let books: Book[] = [];
try {
const txt = await nativeAppService.fs.readFile('books.json', 'Books', 'text');
@@ -194,16 +184,8 @@ export const nativeAppService: AppService = {
books.forEach((book) => {
book.coverImageUrl = nativeAppService.generateCoverUrl(book);
});
books = [...books, ...MOCK_BOOKS];
const ungroupedBooks: BooksGroup[] = [
{
id: 'ungrouped',
name: 'Ungrouped',
books,
lastUpdated: Date.now(),
},
];
return ungroupedBooks;
return books;
},
generateCoverUrl: (book: Book) => {
return convertFileSrc(`${BOOKS_DIR}/${book.id}/cover.png`);
+2 -3
View File
@@ -5,6 +5,8 @@ export interface Book {
format: BookFormat;
title: string;
author: string;
group?: string;
tags?: string[];
lastUpdated: number;
isRemoved?: boolean;
coverImageUrl?: string | null;
@@ -31,10 +33,7 @@ export interface BookConfig {
}
export interface BooksGroup {
id: string;
name: string;
books: Book[];
lastUpdated: number;
}
export type LibraryItem = Book | BooksGroup;
+2 -2
View File
@@ -1,5 +1,5 @@
import { SystemSettings } from './settings';
import { Book, BooksGroup } from './book';
import { Book } from './book';
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Log' | 'Cache' | 'None';
export type ToastType = 'info' | 'warning' | 'error';
@@ -23,6 +23,6 @@ export interface AppService {
selectFiles(name: string, extensions: string[]): Promise<string[]>;
showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise<void>;
loadLibraryBooks(): Promise<BooksGroup[]>;
loadLibraryBooks(): Promise<Book[]>;
generateCoverUrl(book: Book): string;
}