Various enhancements and fixes (#70)

* Fix excerpts cannot be deleted in Chrome browsers

* Show book details in reader sidebar

* Set minimum search term length to 1 for CJK languages

* Use more system fonts as custom fonts, closes #31
This commit is contained in:
Huang Xin
2024-12-31 01:08:27 +01:00
committed by GitHub
parent 1be567ed4a
commit cd67f6ec3f
23 changed files with 298 additions and 34 deletions
@@ -9,21 +9,23 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { Book } from '@/types/book';
import { SystemSettings } from '@/types/settings';
import { parseOpenWithFiles } from '@/helpers/cli';
import { tauriHandleClose, tauriHandleOnCloseWindow } from '@/utils/window';
import { isTauriAppPlatform } from '@/services/environment';
import { uniqueId } from '@/utils/misc';
import { eventDispatcher } from '@/utils/event';
import { navigateToLibrary } from '@/utils/nav';
import { BOOK_IDS_SEPARATOR } from '@/services/constants';
import useBooksManager from '../hooks/useBooksManager';
import useBookShortcuts from '../hooks/useBookShortcuts';
import BookDetailModal from '@/components/BookDetailModal';
import Spinner from '@/components/Spinner';
import SideBar from './sidebar/SideBar';
import Notebook from './notebook/Notebook';
import BooksGrid from './BooksGrid';
import { eventDispatcher } from '@/utils/event';
const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ ids, settings }) => {
const router = useRouter();
@@ -35,6 +37,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
const { getConfig, getBookData, saveConfig } = useBookDataStore();
const { getView, setBookKeys } = useReaderStore();
const { initViewState, getViewState, clearViewState } = useReaderStore();
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
const isInitiating = useRef(false);
const [loading, setLoading] = useState(false);
@@ -59,6 +62,13 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
if (index === 0) setSideBarBookKey(key);
}
});
const handleShowBookDetails = (event: CustomEvent) => {
const book = event.detail as Book;
setShowDetailsBook(book);
return true;
};
eventDispatcher.onSync('show-book-details', handleShowBookDetails);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -139,6 +149,13 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
<Notebook />
{showDetailsBook && (
<BookDetailModal
isOpen={!!showDetailsBook}
book={showDetailsBook}
onClose={() => setShowDetailsBook(null)}
/>
)}
</div>
);
};
@@ -57,7 +57,7 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
className={clsx(
'note-editor textarea textarea-ghost min-h-[1em] resize-none !outline-none',
'inset-0 w-full rounded-none border-0 bg-transparent p-0 leading-normal',
'text-base',
'text-sm',
)}
ref={editorRef}
value={note}
@@ -70,7 +70,7 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
<button
className={clsx(
'btn btn-ghost settings-content hover:bg-transparent',
'flex h-[1.5em] min-h-[1.5em] items-end p-0',
'flex h-[1.3em] min-h-[1.3em] items-end p-0',
editorRef.current && editorRef.current.value ? '' : 'btn-disabled !bg-opacity-0',
)}
onClick={handleSaveNote}
@@ -160,21 +160,15 @@ const Notebook: React.FC = ({}) => {
>
<p className='line-clamp-1'>{item.text || `Excerpt ${index + 1}`}</p>
</div>
<div
className='collapse-content select-text px-3 pb-0 text-xs'
onClick={(e) => e.stopPropagation()}
>
<div className='collapse-content select-text px-3 pb-0 text-xs'>
<p className='hyphens-auto text-justify'>{item.text}</p>
<div className='flex justify-end'>
<button
className={clsx(
'btn btn-ghost settings-content hover:bg-transparent',
'flex h-[1em] min-h-[1em] items-end p-0',
)}
<div
className='cursor-pointer align-bottom text-xs text-red-400'
onClick={handleEditNote.bind(null, item, true)}
>
<div className='align-bottom text-xs text-red-400'>{_('Delete')}</div>
</button>
{_('Delete')}
</div>
</div>
</div>
</div>
@@ -1,11 +1,14 @@
import clsx from 'clsx';
import React from 'react';
import { FiChevronDown } from 'react-icons/fi';
import { FiChevronUp, FiChevronLeft } from 'react-icons/fi';
import { MdCheck } from 'react-icons/md';
import { useTranslation } from '@/hooks/useTranslation';
interface DropdownProps {
family?: string;
selected: string;
options: string[];
moreOptions?: string[];
onSelect: (option: string) => void;
onGetFontFamily: (option: string, family: string) => string;
}
@@ -14,21 +17,23 @@ const FontDropdown: React.FC<DropdownProps> = ({
family,
selected,
options,
moreOptions,
onSelect,
onGetFontFamily,
}) => {
const _ = useTranslation();
return (
<div className='dropdown dropdown-end'>
<div className='dropdown dropdown-top'>
<button
tabIndex={0}
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
>
<span style={{ fontFamily: onGetFontFamily(selected, family ?? '') }}>{selected}</span>
<FiChevronDown className='h-4 w-4' />
<FiChevronUp className='h-4 w-4' />
</button>
<ul
tabIndex={0}
className='dropdown-content dropdown-right menu bg-base-100 rounded-box z-[1] mt-4 w-44 shadow'
className='dropdown-content menu bg-base-100 rounded-box absolute right-0 z-[1] mt-4 w-44 shadow'
>
{options.map((option) => (
<li key={option} onClick={() => onSelect(option)}>
@@ -40,6 +45,36 @@ const FontDropdown: React.FC<DropdownProps> = ({
</div>
</li>
))}
{moreOptions && moreOptions.length > 0 && (
<li className='dropdown dropdown-left dropdown-top'>
<div className='flex items-center px-0'>
<span style={{ minWidth: '20px' }}>
<FiChevronLeft className='h-4 w-4' />
</span>
<span>{_('System Fonts')}</span>
</div>
<ul
tabIndex={0}
className={clsx(
'dropdown-content menu bg-base-100 rounded-box relative z-[1] overflow-y-scroll shadow',
'mb-[-40px] mr-4 max-h-80 w-80',
)}
>
{moreOptions.map((option) => (
<li key={option} onClick={() => onSelect(option)}>
<div className='flex items-center px-0'>
<span style={{ minWidth: '20px' }}>
{selected === option && <MdCheck size={20} className='text-base-content' />}
</span>
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
{option}
</span>
</div>
</li>
))}
</ul>
</li>
)}
</ul>
</div>
);
@@ -3,18 +3,27 @@ import React, { useEffect, useState } from 'react';
import NumberInput from './NumberInput';
import FontDropdown from './FontDropDown';
import { MONOSPACE_FONTS, SANS_SERIF_FONTS, SERIF_FONTS } from '@/services/constants';
import {
LINUX_FONTS,
MACOS_FONTS,
MONOSPACE_FONTS,
SANS_SERIF_FONTS,
SERIF_FONTS,
WINDOWS_FONTS,
} from '@/services/constants';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getOSPlatform } from '@/utils/misc';
interface FontFaceProps {
className?: string;
family: string;
label: string;
options: string[];
moreOptions?: string[];
selected: string;
onSelect: (option: string) => void;
}
@@ -25,12 +34,21 @@ const handleFontFaceFont = (option: string, family: string) => {
return `'${option}', ${family}`;
};
const FontFace = ({ className, family, label, options, selected, onSelect }: FontFaceProps) => (
const FontFace = ({
className,
family,
label,
options,
moreOptions,
selected,
onSelect,
}: FontFaceProps) => (
<div className={clsx('config-item', className)}>
<span className=''>{label}</span>
<FontDropdown
family={family}
options={options}
moreOptions={moreOptions}
selected={selected}
onSelect={onSelect}
onGetFontFamily={handleFontFaceFont}
@@ -46,6 +64,21 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const viewSettings = getViewSettings(bookKey)!;
const { themeCode } = useTheme();
const osPlatform = getOSPlatform();
let moreFonts: string[] = [];
switch (osPlatform) {
case 'macos':
moreFonts = MACOS_FONTS;
break;
case 'windows':
moreFonts = WINDOWS_FONTS;
break;
case 'linux':
moreFonts = LINUX_FONTS;
break;
default:
break;
}
const [defaultFontSize, setDefaultFontSize] = useState(viewSettings.defaultFontSize!);
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
const [overrideFont, setOverrideFont] = useState(viewSettings.overrideFont!);
@@ -206,6 +239,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
family='serif'
label={_('Serif Font')}
options={SERIF_FONTS}
moreOptions={moreFonts}
selected={serifFont}
onSelect={setSerifFont}
/>
@@ -213,6 +247,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
family='sans-serif'
label={_('Sans-Serif Font')}
options={SANS_SERIF_FONTS}
moreOptions={moreFonts}
selected={sansSerifFont}
onSelect={setSansSerifFont}
/>
@@ -221,6 +256,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
family='monospace'
label={_('Monospace Font')}
options={MONOSPACE_FONTS}
moreOptions={moreFonts}
selected={monospaceFont}
onSelect={setMonospaceFont}
/>
@@ -39,7 +39,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
return (
<dialog className='modal modal-open min-w-90 w-full !bg-[rgba(0,0,0,0.2)]'>
<div className='modal-box settings-content flex h-[60%] w-1/2 min-w-[480px] max-w-full flex-col p-0'>
<div className='modal-box settings-content flex h-[65%] w-1/2 min-w-[540px] max-w-full flex-col p-0'>
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center px-4 pt-2'>
<div className='dialog-tabs flex h-10 max-w-[80%] flex-grow items-center justify-around'>
<button
@@ -1,20 +1,22 @@
import React from 'react';
import Image from 'next/image';
import { MdInfoOutline } from 'react-icons/md';
import { Book } from '@/types/book';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
interface BookCardProps {
cover: string;
title: string;
author: string;
}
const BookCard: React.FC<BookCardProps> = ({ cover, title, author }) => {
const BookCard = ({ book }: { book: Book }) => {
const { coverImageUrl, title, author } = book;
const _ = useTranslation();
const showBookDetails = () => {
eventDispatcher.dispatchSync('show-book-details', book);
};
return (
<div className='flex h-20 w-full items-center'>
<Image
src={cover}
src={coverImageUrl!}
alt={_('Book Cover')}
width={56}
height={80}
@@ -31,7 +33,7 @@ const BookCard: React.FC<BookCardProps> = ({ cover, title, author }) => {
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
aria-label={_('More Info')}
>
<MdInfoOutline size={18} className='fill-base-content' />
<MdInfoOutline size={18} className='fill-base-content' onClick={showBookDetails} />
</button>
</div>
);
@@ -10,7 +10,8 @@ import { BookSearchConfig, BookSearchResult } from '@/types/book';
import Dropdown from '@/components/Dropdown';
import SearchOptions from './SearchOptions';
const MINIMUM_SEARCH_TERM_LENGTH = 2;
const MINIMUM_SEARCH_TERM_LENGTH_DEFAULT = 2;
const MINIMUM_SEARCH_TERM_LENGTH_CJK = 1;
interface SearchBarProps {
isVisible: boolean;
@@ -97,8 +98,15 @@ const SearchBar: React.FC<SearchBarProps> = ({
handleSearchTermChange(searchTerm);
};
const exceedMinSearchTermLength = (searchTerm: string) => {
const isCJK = /[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]/.test(searchTerm);
const minLength = isCJK ? MINIMUM_SEARCH_TERM_LENGTH_CJK : MINIMUM_SEARCH_TERM_LENGTH_DEFAULT;
return searchTerm.length >= minLength;
};
const handleSearchTermChange = (term: string) => {
if (term.length >= MINIMUM_SEARCH_TERM_LENGTH) {
if (exceedMinSearchTermLength(term)) {
handleSearch(term);
} else {
resetSearch();
@@ -118,7 +126,10 @@ const SearchBar: React.FC<SearchBarProps> = ({
onSearchResultChange([...results]);
isSearchPending.current = false;
console.log('search done');
if (queuedSearchTerm.current !== term && queuedSearchTerm.current.length > 2) {
if (
queuedSearchTerm.current !== term &&
exceedMinSearchTermLength(queuedSearchTerm.current)
) {
handleSearch(queuedSearchTerm.current);
}
}
@@ -125,7 +125,7 @@ const SideBar: React.FC<{
/>
</div>
<div className='border-base-300/50 border-b px-3'>
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
<BookCard book={book} />
</div>
</div>
{isSearchBarVisible && searchResults ? (