forked from akai/readest
Refactor the library page to Navbar, Spinner and Bookshelf
This commit is contained in:
@@ -16,10 +16,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta
|
||||
name='viewport'
|
||||
content='width=device-width, height=device-height, initial-scale=1.0'
|
||||
/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1' />
|
||||
</head>
|
||||
<body>
|
||||
<EnvProvider>
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Book, BooksGroup } from '../types/book';
|
||||
import { Book, BooksGroup, LibraryItem } from '../types/book';
|
||||
import { useEnv } from '../context/EnvContext';
|
||||
import { FaSearch, FaPlus } from 'react-icons/fa';
|
||||
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import Bookshelf from '@/components/Bookshelf';
|
||||
|
||||
type AppState = 'Init' | 'Loading' | 'Library' | 'Reader';
|
||||
type LibraryItem = Book | BooksGroup;
|
||||
|
||||
const generateLibraryItems = (groups: BooksGroup[]): LibraryItem[] => {
|
||||
const ungroupedBooks: Book[] = groups.find((group) => group.id === 'ungrouped')?.books || [];
|
||||
@@ -20,12 +21,12 @@ const LibraryPage = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const [appState, setAppState] = useState<AppState>('Init');
|
||||
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
||||
const [loadingInfo, setLoadingInfo] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (appState !== 'Init') return;
|
||||
setAppState('Loading');
|
||||
setLoadingInfo('Loading library books...');
|
||||
setLoading(true);
|
||||
envConfig.appService().then((appService) => {
|
||||
appService.loadSettings().then((settings) => {
|
||||
console.log('Settings', settings);
|
||||
@@ -36,12 +37,12 @@ const LibraryPage = () => {
|
||||
console.log('Library items:', libraryItems);
|
||||
setLibraryItems(libraryItems);
|
||||
setAppState('Library');
|
||||
setLoadingInfo('');
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setLoadingInfo('');
|
||||
appService.showMessage('Failed to load library books', 'error');
|
||||
setLoading(false);
|
||||
appService.showMessage(`Failed to load library books: ${err}`, 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -54,96 +55,11 @@ const LibraryPage = () => {
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-gray-100'>
|
||||
{/* Sticky Top Navbar */}
|
||||
<div className='fixed top-0 z-10 w-full bg-gray-100 p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
{/* Search Input with Magnifier and Plus Icon */}
|
||||
<div className='sm:w relative flex w-full items-center'>
|
||||
{/* Magnifier Icon */}
|
||||
<span className='absolute left-4 text-gray-500'>
|
||||
<FaSearch className='w-3' />
|
||||
</span>
|
||||
{/* Search Input */}
|
||||
<input
|
||||
type='text'
|
||||
placeholder='Search books...'
|
||||
className='input input-sm rounded-badge w-full bg-gray-200 pl-10 pr-10 focus:border-none focus:outline-none' // Padding-right added for FaPlus
|
||||
/>
|
||||
{/* Plus Icon */}
|
||||
<span className='dropdown dropdown-end absolute right-4 cursor-pointer text-gray-500'>
|
||||
<FaPlus tabIndex={0} className='w-3' role='button' />
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow'
|
||||
>
|
||||
<li>
|
||||
<button onClick={handleImport}>From Local File</button>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Navbar onImport={handleImport} />
|
||||
<div className='min-h-screen p-2 pt-16'>
|
||||
<div className='hero-content'>
|
||||
{loadingInfo && (
|
||||
<div className='absolute left-1/2 top-4 -translate-x-1/2 transform pt-16 text-center'>
|
||||
{/* Spacer to offset fixed navbar height */}
|
||||
<span className='loading loading-dots loading-lg'></span>
|
||||
</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) => (
|
||||
<div key={`library-item-${index}`} className=''>
|
||||
<div className='grid gap-2'>
|
||||
{'format' in item ? (
|
||||
<div>
|
||||
<div key={(item as Book).id} className='card bg-base-100 w-full shadow-md'>
|
||||
<Image
|
||||
width={10}
|
||||
height={10}
|
||||
src={(item as Book).coverImageUrl!}
|
||||
alt={(item as Book).title}
|
||||
className='aspect-[28/41] w-full object-cover'
|
||||
/>
|
||||
</div>
|
||||
<div className='card-body p-0 pt-2'>
|
||||
<h3 className='card-title text-sm'>{(item as Book).title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{(item as BooksGroup).books.map((book) => (
|
||||
<div key={book.id} className='card bg-base-100 w-full shadow-md'>
|
||||
<figure>
|
||||
<Image
|
||||
width={10}
|
||||
height={10}
|
||||
src={book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
className='h-48 w-full object-cover'
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
))}
|
||||
<h2 className='mb-2 text-lg font-bold'>{(item as BooksGroup).name}</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{libraryItems.length > 0 && (
|
||||
<div
|
||||
className='border-1 flex aspect-[28/41] items-center justify-center bg-white'
|
||||
role='button'
|
||||
onClick={handleImport}
|
||||
>
|
||||
<FaPlus size='2rem' color='gray' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Spinner loading={loading} />
|
||||
<Bookshelf libraryItems={libraryItems} onImport={handleImport} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Book, BooksGroup, LibraryItem } from '../types/book';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
|
||||
interface BookshelfProps {
|
||||
libraryItems: LibraryItem[];
|
||||
onImport: () => void;
|
||||
}
|
||||
|
||||
const Bookshelf: React.FC<BookshelfProps> = ({ libraryItems, onImport }) => {
|
||||
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) => (
|
||||
<div key={`library-item-${index}`} className=''>
|
||||
<div className='grid gap-2'>
|
||||
{'format' in item ? (
|
||||
<div>
|
||||
<div key={(item as Book).id} className='card bg-base-100 w-full shadow-md'>
|
||||
<Image
|
||||
width={10}
|
||||
height={10}
|
||||
src={(item as Book).coverImageUrl!}
|
||||
alt={(item as Book).title}
|
||||
className='aspect-[28/41] w-full object-cover'
|
||||
/>
|
||||
</div>
|
||||
<div className='card-body p-0 pt-2'>
|
||||
<h3 className='card-title text-sm'>{(item as Book).title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{(item as BooksGroup).books.map((book) => (
|
||||
<div key={book.id} className='card bg-base-100 w-full shadow-md'>
|
||||
<figure>
|
||||
<Image
|
||||
width={10}
|
||||
height={10}
|
||||
src={book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
className='h-48 w-full object-cover'
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
))}
|
||||
<h2 className='mb-2 text-lg font-bold'>{(item as BooksGroup).name}</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{libraryItems.length > 0 && (
|
||||
<div
|
||||
className='border-1 flex aspect-[28/41] items-center justify-center bg-white'
|
||||
role='button'
|
||||
onClick={onImport}
|
||||
>
|
||||
<FaPlus className='size-8' color='gray' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bookshelf;
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { FaSearch, FaPlus } from 'react-icons/fa';
|
||||
|
||||
interface NavbarProps {
|
||||
onImport: () => void;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ onImport }) => {
|
||||
return (
|
||||
<div className='fixed top-0 z-10 w-full bg-gray-100 p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
{/* Search Input with Magnifier and Plus Icon */}
|
||||
<div className='sm:w relative flex w-full items-center'>
|
||||
{/* Magnifier Icon */}
|
||||
<span className='absolute left-4 text-gray-500'>
|
||||
<FaSearch className='w-3' />
|
||||
</span>
|
||||
{/* Search Input */}
|
||||
<input
|
||||
type='text'
|
||||
placeholder='Search books...'
|
||||
className='input input-sm rounded-badge w-full bg-gray-200 pl-10 pr-10 text-base focus:border-none focus:outline-none'
|
||||
/>
|
||||
{/* Plus Icon */}
|
||||
<span className='dropdown dropdown-end absolute right-4 cursor-pointer text-gray-500'>
|
||||
<FaPlus tabIndex={0} className='w-3' role='button' />
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow'
|
||||
>
|
||||
<li>
|
||||
<button onClick={onImport}>From Local File</button>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SpinnerProps {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const Spinner: React.FC<SpinnerProps> = ({ loading }) => {
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute left-1/2 top-4 -translate-x-1/2 transform pt-16 text-center'
|
||||
role='status'
|
||||
>
|
||||
<span className='loading loading-dots loading-lg'></span>
|
||||
<span className='hidden'>Loading...</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
remove,
|
||||
BaseDirectory,
|
||||
} from '@tauri-apps/plugin-fs';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
import {
|
||||
join,
|
||||
appConfigDir,
|
||||
@@ -25,7 +24,6 @@ import { Book, BooksGroup } from '../types/book';
|
||||
import { SystemSettings } from '../types/settings';
|
||||
import { AppService, BaseDir, ToastType } from '../types/system';
|
||||
|
||||
const IS_MOBILE = osType() === 'ios' || osType() === 'android';
|
||||
const BOOKS_SUBDIR = 'DigestLibrary/Books';
|
||||
let BOOKS_DIR = '';
|
||||
|
||||
@@ -53,7 +51,7 @@ function resolvePath(
|
||||
return { baseDir: BaseDirectory.AppLog, fp, base, dir: appLogDir };
|
||||
case 'Books':
|
||||
return {
|
||||
baseDir: IS_MOBILE ? BaseDirectory.AppData : BaseDirectory.Document,
|
||||
baseDir: BaseDirectory.Document,
|
||||
fp: `${BOOKS_SUBDIR}/${fp}`,
|
||||
base,
|
||||
dir: () => new Promise((r) => r(`${BOOKS_DIR}/`)),
|
||||
@@ -132,10 +130,8 @@ 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(
|
||||
IS_MOBILE ? await appDataDir() : await documentDir(),
|
||||
BOOKS_SUBDIR,
|
||||
);
|
||||
const INIT_BOOKS_DIR = await join(await documentDir(), BOOKS_SUBDIR);
|
||||
await nativeAppService.fs.createDir('', 'Books', true);
|
||||
settings = {
|
||||
localBooksDir: INIT_BOOKS_DIR,
|
||||
globalReadSettings: {
|
||||
|
||||
@@ -36,3 +36,5 @@ export interface BooksGroup {
|
||||
books: Book[];
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export type LibraryItem = Book | BooksGroup;
|
||||
|
||||
Reference in New Issue
Block a user