diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index ea12a27a..3b077e3b 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -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" ] } diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json index 3a959bc8..e46653ad 100644 --- a/apps/readest-app/src-tauri/tauri.conf.json +++ b/apps/readest-app/src-tauri/tauri.conf.json @@ -18,7 +18,8 @@ "height": 600, "resizable": true, "fullscreen": false, - "maximized": true + "maximized": true, + "decorations": false } ], "security": { diff --git a/apps/readest-app/src/app/globals.css b/apps/readest-app/src/app/globals.css index 6055049f..524e66eb 100644 --- a/apps/readest-app/src/app/globals.css +++ b/apps/readest-app/src/app/globals.css @@ -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; +} \ No newline at end of file diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index e1f7a474..966df30f 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -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 = ({ libraryBooks, onImportBooks }) => { +const Bookshelf: React.FC = ({ libraryBooks, isSelectMode, onImportBooks }) => { const router = useRouter(); + const { envConfig } = useEnv(); + const { deleteBook } = useReaderStore(); + const [selectedBooks, setSelectedBooks] = React.useState([]); + 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 (
- {/* Books Grid */} -
+
{bookshelfItems.map((item, index) => ( -
+
{'format' in item ? ( -
handleBookClick(item.hash)}> +
handleBookClick(item.hash)} + >
= ({ libraryBooks, onImportBooks }) => fill={true} className='object-cover' /> + {selectedBooks.includes(item.hash) && ( +
+ )} + {isSelectMode && ( +
+ {selectedBooks.includes(item.hash) ? ( + + ) : ( + + )} +
+ )}
@@ -103,7 +158,7 @@ const Bookshelf: React.FC = ({ libraryBooks, onImportBooks }) => {bookshelfItems.length > 0 && (
@@ -111,6 +166,26 @@ const Bookshelf: React.FC = ({ libraryBooks, onImportBooks }) =>
)}
+ {selectedBooks.length > 0 && ( +
+ + +
+ )} + {showDeleteAlert && ( + setShowDeleteAlert(false)} + onClickConfirm={confirmDelete} + /> + )}
); }; diff --git a/apps/readest-app/src/app/library/components/Header.tsx b/apps/readest-app/src/app/library/components/Header.tsx new file mode 100644 index 00000000..38eaecd6 --- /dev/null +++ b/apps/readest-app/src/app/library/components/Header.tsx @@ -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 = ({ onImportBooks, onToggleSelectMode }) => { + return ( +
+
+
+ + + + +
+ + +
+ +
+
    +
  • + +
  • +
+
+ +
+
+ +
+
+ ); +}; + +export default LibraryHeader; diff --git a/apps/readest-app/src/app/library/components/SearchBar.tsx b/apps/readest-app/src/app/library/components/SearchBar.tsx deleted file mode 100644 index 4cc8d518..00000000 --- a/apps/readest-app/src/app/library/components/SearchBar.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react'; -import { FaSearch, FaPlus } from 'react-icons/fa'; - -interface SearchBarProps { - onImportBooks: () => void; -} - -const SearchBar: React.FC = ({ onImportBooks }) => { - return ( -
-
- {/* Search Input with Magnifier and Plus Icon */} -
- {/* Magnifier Icon */} - - - - {/* Search Input */} - - {/* Plus Icon */} - - -
    -
  • - -
  • -
-
-
-
-
- ); -}; - -export default SearchBar; diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index fa631c7c..210e35a3 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -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 ( -
- -
-
+
+ +
+
- +
{!loading && libraryBooks.length === 0 && (
diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index 9b3a4a7e..a469dd9a 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -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; prev: () => Promise; }; @@ -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 }); diff --git a/apps/readest-app/src/app/reader/components/FooterBar.tsx b/apps/readest-app/src/app/reader/components/FooterBar.tsx index fb6dc52d..640b280c 100644 --- a/apps/readest-app/src/app/reader/components/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/FooterBar.tsx @@ -41,7 +41,7 @@ const FooterBar: React.FC = ({
= ({
= ({ onMouseLeave={() => setHoveredBookKey('')} >
-

+

{bookTitle}

diff --git a/apps/readest-app/src/app/reader/components/ReaderContent.tsx b/apps/readest-app/src/app/reader/components/ReaderContent.tsx index 058590f3..b4ebc64a 100644 --- a/apps/readest-app/src/app/reader/components/ReaderContent.tsx +++ b/apps/readest-app/src/app/reader/components/ReaderContent.tsx @@ -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} /> + + 1} + isHoveredAnim={false} hoveredBookKey={hoveredBookKey} setHoveredBookKey={setHoveredBookKey} /> - - ;
); })} diff --git a/apps/readest-app/src/app/reader/components/SideBar.tsx b/apps/readest-app/src/app/reader/components/SideBar.tsx index a5b9ca10..49ce3563 100644 --- a/apps/readest-app/src/app/reader/components/SideBar.tsx +++ b/apps/readest-app/src/app/reader/components/SideBar.tsx @@ -82,7 +82,7 @@ const SideBar: React.FC<{ return ( isVisible && (
@@ -119,28 +119,28 @@ const SideBar: React.FC<{ {activeTab === 'bookmarks' &&
Bookmarks
} {activeTab === 'annotations' &&
Annotations
}
-
+
diff --git a/apps/readest-app/src/app/reader/page.tsx b/apps/readest-app/src/app/reader/page.tsx index 60e17e8f..47e3eae7 100644 --- a/apps/readest-app/src/app/reader/page.tsx +++ b/apps/readest-app/src/app/reader/page.tsx @@ -33,7 +33,7 @@ const ReaderPage = () => { return ( settings.globalReadSettings && ( -
+
diff --git a/apps/readest-app/src/components/Alert.tsx b/apps/readest-app/src/components/Alert.tsx new file mode 100644 index 00000000..a5d984d6 --- /dev/null +++ b/apps/readest-app/src/components/Alert.tsx @@ -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 ( +
+
+ + + +
+

{title}

+
{message}
+
+
+
+ + +
+
+ ); +}; + +export default Alert; diff --git a/apps/readest-app/src/components/Spinner.tsx b/apps/readest-app/src/components/Spinner.tsx index 76956883..2475da61 100644 --- a/apps/readest-app/src/components/Spinner.tsx +++ b/apps/readest-app/src/components/Spinner.tsx @@ -1,10 +1,8 @@ import React from 'react'; -interface SpinnerProps { +const Spinner: React.FC<{ loading: boolean; -} - -const Spinner: React.FC = ({ loading }) => { +}> = ({ loading }) => { if (!loading) return null; return ( diff --git a/apps/readest-app/src/components/WindowButtons.tsx b/apps/readest-app/src/components/WindowButtons.tsx new file mode 100644 index 00000000..2c4a108f --- /dev/null +++ b/apps/readest-app/src/components/WindowButtons.tsx @@ -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 ( +
+ + + + + +
+ ); +}; + +export default WindowButtons; diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 84385535..3a26b86c 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -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, diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index 0f269cc5..5e9979b5 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -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((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();