forked from akai/readest
Refactor the Bookshelf more
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user