Add multiple book open and delete
This commit is contained in:
@@ -42,6 +42,11 @@
|
||||
},
|
||||
"dialog:default",
|
||||
"os:default",
|
||||
"http:default"
|
||||
"http:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"maximized": true
|
||||
"maximized": true,
|
||||
"decorations": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
@@ -37,3 +37,9 @@ foliate-view {
|
||||
transform: scale(1.02);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.window-button {
|
||||
@apply inline-flex h-6 w-6 items-center justify-center rounded-full;
|
||||
@apply transition duration-200 ease-in-out transform hover:scale-105;
|
||||
@apply bg-base-300 hover:bg-gray-300;
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { MdDelete, MdOpenInNew } from 'react-icons/md';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import Alert from '@/components/Alert';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
|
||||
type BookshelfItem = Book | BooksGroup;
|
||||
|
||||
const UNGROUPED_NAME = 'ungrouped';
|
||||
|
||||
// const MOCK_BOOKS: Book[] = Array.from({ length: 14 }, (_v, k) => ({
|
||||
// hash: `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;
|
||||
@@ -42,27 +39,70 @@ const generateBookshelfItems = (books: Book[]): BookshelfItem[] => {
|
||||
|
||||
interface BookshelfProps {
|
||||
libraryBooks: Book[];
|
||||
isSelectMode: boolean;
|
||||
onImportBooks: () => void;
|
||||
}
|
||||
|
||||
const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, onImportBooks }) => {
|
||||
const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImportBooks }) => {
|
||||
const router = useRouter();
|
||||
const { envConfig } = useEnv();
|
||||
const { deleteBook } = useReaderStore();
|
||||
const [selectedBooks, setSelectedBooks] = React.useState<string[]>([]);
|
||||
const [showDeleteAlert, setShowDeleteAlert] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelectedBooks([]);
|
||||
}, [isSelectMode]);
|
||||
|
||||
const bookshelfItems = generateBookshelfItems(libraryBooks);
|
||||
|
||||
const handleBookClick = (id: string) => {
|
||||
router.push(`/reader?id=${id}`);
|
||||
if (isSelectMode) {
|
||||
toggleSelection(id);
|
||||
} else {
|
||||
router.push(`/reader?ids=${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelection = (id: string) => {
|
||||
setSelectedBooks((prev) =>
|
||||
prev.includes(id) ? prev.filter((bookId) => bookId !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
const openSelectedBooks = () => {
|
||||
router.push(`/reader?ids=${selectedBooks.join(',')}`);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
for (const selectedBook of selectedBooks) {
|
||||
const book = libraryBooks.find((b) => b.hash === selectedBook);
|
||||
if (book) {
|
||||
deleteBook(envConfig, book);
|
||||
}
|
||||
}
|
||||
setSelectedBooks([]);
|
||||
setShowDeleteAlert(false);
|
||||
};
|
||||
|
||||
const deleteSelectedBooks = () => {
|
||||
setShowDeleteAlert(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='bookshelf'>
|
||||
{/* Books Grid */}
|
||||
<div className='grid grid-cols-3 gap-6 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
|
||||
<div className='grid grid-cols-3 gap-0 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
|
||||
{bookshelfItems.map((item, index) => (
|
||||
<div key={`library-item-${index}`} className='hover:bg-base-200 flex h-full flex-col'>
|
||||
<div
|
||||
key={`library-item-${index}`}
|
||||
className='flex h-full flex-col rounded-md p-4 hover:bg-gray-200'
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
<div className='bookItem cursor-pointer' onClick={() => handleBookClick(item.hash)}>
|
||||
<div
|
||||
className='book-item cursor-pointer'
|
||||
onClick={() => handleBookClick(item.hash)}
|
||||
>
|
||||
<div key={(item as Book).hash} className='card bg-base-100 shadow-md'>
|
||||
<div className='relative aspect-[28/41]'>
|
||||
<Image
|
||||
@@ -71,6 +111,21 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, onImportBooks }) =>
|
||||
fill={true}
|
||||
className='object-cover'
|
||||
/>
|
||||
{selectedBooks.includes(item.hash) && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
)}
|
||||
{isSelectMode && (
|
||||
<div className='absolute bottom-1 right-1'>
|
||||
{selectedBooks.includes(item.hash) ? (
|
||||
<MdCheckCircle size={20} className='fill-blue-500' />
|
||||
) : (
|
||||
<MdCheckCircleOutline
|
||||
size={20}
|
||||
className='fill-gray-300 drop-shadow-sm'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='card-body p-0 pt-2'>
|
||||
@@ -103,7 +158,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, onImportBooks }) =>
|
||||
|
||||
{bookshelfItems.length > 0 && (
|
||||
<div
|
||||
className='border-1 flex aspect-[28/41] items-center justify-center bg-white'
|
||||
className='border-1 m-4 flex aspect-[28/41] items-center justify-center bg-white hover:bg-gray-200'
|
||||
role='button'
|
||||
onClick={onImportBooks}
|
||||
>
|
||||
@@ -111,6 +166,26 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, onImportBooks }) =>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedBooks.length > 0 && (
|
||||
<div className='fixed bottom-4 left-1/2 flex -translate-x-1/2 transform space-x-4 rounded-lg bg-gray-800 p-4 text-white shadow-lg'>
|
||||
<button onClick={openSelectedBooks} className='flex items-center space-x-2'>
|
||||
<MdOpenInNew />
|
||||
<span>Open</span>
|
||||
</button>
|
||||
<button onClick={deleteSelectedBooks} className='flex items-center space-x-2'>
|
||||
<MdDelete className='fill-red-500' />
|
||||
<span className='text-red-500'>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showDeleteAlert && (
|
||||
<Alert
|
||||
title='Confirm Deletion'
|
||||
message='Are you sure to delete the selected books?'
|
||||
onClickCancel={() => setShowDeleteAlert(false)}
|
||||
onClickConfirm={confirmDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { FaSearch } from 'react-icons/fa';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { MdSelectAll } from 'react-icons/md';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
|
||||
interface LibraryHeaderProps {
|
||||
onImportBooks: () => void;
|
||||
onToggleSelectMode: () => void;
|
||||
}
|
||||
|
||||
const LibraryHeader: React.FC<LibraryHeaderProps> = ({ onImportBooks, onToggleSelectMode }) => {
|
||||
return (
|
||||
<div id='titlebar' className='titlebar fixed top-0 z-10 w-full bg-gray-100 px-8 py-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='sm:w relative flex w-full items-center'>
|
||||
<span className='absolute left-4 text-gray-500'>
|
||||
<FaSearch className='h-4 w-4' />
|
||||
</span>
|
||||
<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'
|
||||
/>
|
||||
<div className='absolute right-4 flex items-center space-x-4 text-gray-500'>
|
||||
<span className='mx-2 h-5 w-[1px] bg-gray-300'></span>
|
||||
<span className='dropdown dropdown-end h-5 cursor-pointer text-gray-500'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip='Add books'>
|
||||
<PiPlus tabIndex={0} className='h-5 w-5' role='button' />
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow'
|
||||
>
|
||||
<li>
|
||||
<button onClick={onImportBooks}>From Local File</button>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<button onClick={onToggleSelectMode} aria-label='Select Multiple Books' className='h-6'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom cursor-pointer' data-tip='Select books'>
|
||||
<MdSelectAll role='button' className='h-6 w-6' />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<WindowButtons />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryHeader;
|
||||
@@ -1,42 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { FaSearch, FaPlus } from 'react-icons/fa';
|
||||
|
||||
interface SearchBarProps {
|
||||
onImportBooks: () => void;
|
||||
}
|
||||
|
||||
const SearchBar: React.FC<SearchBarProps> = ({ onImportBooks }) => {
|
||||
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={onImportBooks}>From Local File</button>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
@@ -7,7 +7,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
import Spinner from '@/components/Spinner';
|
||||
import SearchBar from '@/app/library/components/SearchBar';
|
||||
import LibraryHeader from '@/app/library/components/Header';
|
||||
import Bookshelf from '@/app/library/components/Bookshelf';
|
||||
|
||||
const LibraryPage = () => {
|
||||
@@ -15,6 +15,7 @@ const LibraryPage = () => {
|
||||
const { library: libraryBooks, setLibrary } = useReaderStore();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const isInitiating = useRef(false);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
@@ -67,13 +68,24 @@ const LibraryPage = () => {
|
||||
importBooks(files);
|
||||
};
|
||||
|
||||
const handleToggleSelectMode = () => {
|
||||
setIsSelectMode(!isSelectMode);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-gray-100'>
|
||||
<SearchBar onImportBooks={handleImportBooks} />
|
||||
<div className='min-h-screen p-2 pt-16'>
|
||||
<div className='hero-content'>
|
||||
<div className='min-h-screen select-none bg-gray-100'>
|
||||
<LibraryHeader
|
||||
onImportBooks={handleImportBooks}
|
||||
onToggleSelectMode={handleToggleSelectMode}
|
||||
/>
|
||||
<div className='min-h-screen pt-10'>
|
||||
<div className='hero-content p-4'>
|
||||
<Spinner loading={loading} />
|
||||
<Bookshelf libraryBooks={libraryBooks} onImportBooks={handleImportBooks} />
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
isSelectMode={isSelectMode}
|
||||
onImportBooks={handleImportBooks}
|
||||
/>
|
||||
</div>
|
||||
{!loading && libraryBooks.length === 0 && (
|
||||
<div className='hero min-h-screen'>
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface FoliateView extends HTMLElement {
|
||||
goRight: () => void;
|
||||
renderer: {
|
||||
setStyles: (css: string) => void;
|
||||
setAttribute: (name: string, value: string | number) => void;
|
||||
next: () => Promise<void>;
|
||||
prev: () => Promise<void>;
|
||||
};
|
||||
@@ -84,6 +85,8 @@ const FoliateViewer: React.FC<{
|
||||
if ('setStyles' in view.renderer) {
|
||||
view.renderer.setStyles(getCSS(2.4, true, true));
|
||||
}
|
||||
view.renderer.setAttribute('margin', '35px');
|
||||
view.renderer.setAttribute('gap', '4%');
|
||||
const lastLocation = bookConfig.location;
|
||||
if (lastLocation) {
|
||||
view.init({ lastLocation });
|
||||
|
||||
@@ -41,7 +41,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center px-4',
|
||||
'shadow-xs bg-gray-100 transition-opacity duration-300',
|
||||
'shadow-xs bg-base-100 transition-opacity duration-300',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
`header-bar absolute top-0 z-10 flex h-10 w-full items-center px-4`,
|
||||
`shadow-xs bg-gray-100 transition-opacity duration-300`,
|
||||
`shadow-xs bg-base-100 transition-opacity duration-300`,
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
|
||||
)}
|
||||
@@ -46,7 +46,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<h2 className='line-clamp-1 max-w-[90%] px-2 text-center text-xs font-semibold'>
|
||||
<h2 className='line-clamp-1 max-w-[80%] px-2 text-center text-xs font-semibold'>
|
||||
{bookTitle}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import FooterBar from './FooterBar';
|
||||
const ReaderContent = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const ids = searchParams.getAll('id') || [];
|
||||
const ids = (searchParams.get('ids') || '').split(',');
|
||||
|
||||
const { envConfig } = useEnv();
|
||||
const { books, settings, initBookState, saveConfig, saveSettings } = useReaderStore();
|
||||
@@ -151,15 +151,15 @@ const ReaderContent = () => {
|
||||
setSideBarBookKey={setSideBarBookKey}
|
||||
setHoveredBookKey={setHoveredBookKey}
|
||||
/>
|
||||
<FoliateViewer bookKey={key} bookDoc={bookDoc!} bookConfig={config!} />
|
||||
<PageInfo bookFormat={book.format} section={section} pageinfo={pageinfo} />
|
||||
<FooterBar
|
||||
bookKey={key}
|
||||
progress={progress}
|
||||
isHoveredAnim={ids.length > 1}
|
||||
isHoveredAnim={false}
|
||||
hoveredBookKey={hoveredBookKey}
|
||||
setHoveredBookKey={setHoveredBookKey}
|
||||
/>
|
||||
<FoliateViewer bookKey={key} bookDoc={bookDoc!} bookConfig={config!} />
|
||||
<PageInfo bookFormat={book.format} section={section} pageinfo={pageinfo} />;
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -82,7 +82,7 @@ const SideBar: React.FC<{
|
||||
return (
|
||||
isVisible && (
|
||||
<div
|
||||
className='sidebar-container z-20 h-full bg-gray-200'
|
||||
className='sidebar-container bg-base-200 z-20 h-full select-none'
|
||||
style={{
|
||||
width: `${width}`,
|
||||
minWidth: `${MIN_SIDEBAR_WIDTH * 100}%`,
|
||||
@@ -94,16 +94,16 @@ const SideBar: React.FC<{
|
||||
<div className='flex h-10 items-center justify-between pl-1.5 pr-3'>
|
||||
<div className='flex items-center'>
|
||||
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
|
||||
<GiBookshelf size={20} className='text-gray-600' />
|
||||
<GiBookshelf size={20} className='fill-base-content' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex size-[30%] min-w-20 items-center justify-between'>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<MdSearch size={20} className='text-gray-600' />
|
||||
<MdSearch size={20} className='fill-base-content' />
|
||||
</button>
|
||||
<button
|
||||
onClick={onTogglePin}
|
||||
className={`${isPinned ? 'bg-gray-400' : 'bg-gray-300'} btn btn-ghost btn-circle right-0 h-6 min-h-6 w-6`}
|
||||
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle right-0 h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
@@ -119,28 +119,28 @@ const SideBar: React.FC<{
|
||||
{activeTab === 'bookmarks' && <div>Bookmarks</div>}
|
||||
{activeTab === 'annotations' && <div>Annotations</div>}
|
||||
</div>
|
||||
<div className='bottom-tab absolute bottom-0 flex w-full bg-gray-200'>
|
||||
<div className='bottom-tab absolute bottom-0 flex w-full'>
|
||||
<button
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === 'toc' ? 'bg-gray-300' : ''}`}
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === 'toc' ? 'bg-base-300' : ''}`}
|
||||
onClick={() => setActiveTab('toc')}
|
||||
>
|
||||
<MdToc size={20} className='mx-auto' />
|
||||
</button>
|
||||
<button
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === 'annotations' ? 'bg-gray-300' : ''}`}
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === 'annotations' ? 'bg-base-300' : ''}`}
|
||||
onClick={() => setActiveTab('annotations')}
|
||||
>
|
||||
<MdEditNote size={20} className='mx-auto' />
|
||||
</button>
|
||||
<button
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === 'bookmarks' ? 'bg-gray-300' : ''}`}
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === 'bookmarks' ? 'bg-base-300' : ''}`}
|
||||
onClick={() => setActiveTab('bookmarks')}
|
||||
>
|
||||
<MdBookmarkBorder size={20} className='mx-auto' />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className='drag-bar bg-base-300 absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ const ReaderPage = () => {
|
||||
|
||||
return (
|
||||
settings.globalReadSettings && (
|
||||
<div className='min-h-screen bg-gray-100'>
|
||||
<div className='min-h-screen select-none'>
|
||||
<Suspense>
|
||||
<ReaderContent />
|
||||
</Suspense>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const Alert: React.FC<{
|
||||
title: string;
|
||||
message: string;
|
||||
onClickCancel: () => void;
|
||||
onClickConfirm: () => void;
|
||||
}> = ({ title, message, onClickCancel, onClickConfirm }) => {
|
||||
return (
|
||||
<div
|
||||
role='alert'
|
||||
className={clsx(
|
||||
'alert fixed bottom-4 left-1/2 flex -translate-x-1/2 transform items-center justify-between',
|
||||
'rounded-lg bg-gray-100 p-4 shadow-2xl',
|
||||
'w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] lg:max-w-[40vw] xl:max-w-[40vw]',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
className='stroke-info h-6 w-6 shrink-0'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth='2'
|
||||
d='M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
|
||||
></path>
|
||||
</svg>
|
||||
<div className=''>
|
||||
<h3 className='font-bold'>{title}</h3>
|
||||
<div className='text-xs'>{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<button className='btn btn-sm' onClick={onClickCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className='btn btn-sm btn-warning' onClick={onClickConfirm}>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
@@ -1,10 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SpinnerProps {
|
||||
const Spinner: React.FC<{
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const Spinner: React.FC<SpinnerProps> = ({ loading }) => {
|
||||
}> = ({ loading }) => {
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const WindowButtons: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const handleMouseDown = async (e: MouseEvent) => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
if (e.buttons === 1) {
|
||||
if (e.detail === 2) {
|
||||
getCurrentWindow().toggleMaximize();
|
||||
} else {
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const header = document.getElementById('titlebar');
|
||||
header?.addEventListener('mousedown', handleMouseDown);
|
||||
return () => {
|
||||
header?.removeEventListener('mousedown', handleMouseDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleMinimize = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().minimize();
|
||||
};
|
||||
|
||||
const handleMaximize = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().toggleMaximize();
|
||||
};
|
||||
|
||||
const handleClose = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().close();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='window-buttons ml-6 flex h-8 items-center justify-end space-x-2'>
|
||||
<button
|
||||
onClick={handleMinimize}
|
||||
className='window-button'
|
||||
aria-label='Minimize'
|
||||
id='titlebar-minimize'
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path fill='currentColor' d='M20 14H4v-2h16' />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleMaximize}
|
||||
className='window-button'
|
||||
aria-label='Maximize/Restore'
|
||||
id='titlebar-maximize'
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path fill='currentColor' d='M4 4h16v16H4zm2 4v10h12V8z' />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className='window-button'
|
||||
aria-label='Close'
|
||||
id='titlebar-close'
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WindowButtons;
|
||||
@@ -24,6 +24,19 @@ import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
|
||||
export const isMobile = ['android', 'ios'].includes(osType());
|
||||
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
|
||||
export const initTitlebar = () => {
|
||||
const appWindow = getCurrentWindow();
|
||||
document
|
||||
.getElementById('titlebar-minimize')!
|
||||
.addEventListener('click', () => appWindow.minimize());
|
||||
document
|
||||
.getElementById('titlebar-maximize')!
|
||||
.addEventListener('click', () => appWindow.toggleMaximize());
|
||||
document.getElementById('titlebar-close')!.addEventListener('click', () => appWindow.close());
|
||||
};
|
||||
|
||||
export const resolvePath = (
|
||||
fp: string,
|
||||
base: BaseDir,
|
||||
|
||||
@@ -36,6 +36,7 @@ interface ReaderStore {
|
||||
setFoliateView: (key: string, view: FoliateView) => void;
|
||||
getFoliateView: (key: string) => FoliateView | null;
|
||||
|
||||
deleteBook: (envConfig: EnvConfigType, book: Book) => void;
|
||||
saveConfig: (envConfig: EnvConfigType, book: Book, config: BookConfig) => void;
|
||||
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
|
||||
|
||||
@@ -68,6 +69,16 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
|
||||
getFoliateView: (key: string) => get().foliateViews[key] || null,
|
||||
|
||||
deleteBook: async (envConfig: EnvConfigType, book: Book) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library } = get();
|
||||
const bookIndex = library.findIndex((b) => b.hash === book.hash);
|
||||
if (bookIndex !== -1) {
|
||||
library.splice(bookIndex, 1);
|
||||
}
|
||||
set({ library });
|
||||
appService.saveLibraryBooks(library);
|
||||
},
|
||||
saveConfig: async (envConfig: EnvConfigType, book: Book, config: BookConfig) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library } = get();
|
||||
|
||||
Reference in New Issue
Block a user