Various enhancements (#58)

* Add H and L keyboard shortcuts for back and forward navigation, consistent with Vimium

* Fallback bookmark text to page number if current page has no text, closes #51

* Add delete and edit buttons for highlights and booknotes in the sidebar, closes #57

* Each book now has its own sidebar default tab
This commit is contained in:
Huang Xin
2024-12-27 20:53:30 +01:00
committed by GitHub
parent 3dcab5a1ee
commit 71ffe76e29
15 changed files with 113 additions and 74 deletions
@@ -10,6 +10,7 @@ import { useEnv } from '@/context/EnvContext';
import { BookNote } from '@/types/book';
import { uniqueId } from '@/utils/misc';
import Button from '@/components/Button';
import { getCurrentPage } from '@/utils/book';
interface BookmarkTogglerProps {
bookKey: string;
@@ -19,10 +20,11 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
const { getConfig, saveConfig, getBookData, updateBooknotes } = useBookDataStore();
const { getProgress, setBookmarkRibbonVisibility } = useReaderStore();
const config = getConfig(bookKey)!;
const progress = getProgress(bookKey)!;
const bookData = getBookData(bookKey)!;
const [isBookmarked, setIsBookmarked] = useState(false);
@@ -38,7 +40,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
id: uniqueId(),
type: 'bookmark',
cfi,
text: truncatedText,
text: truncatedText ? truncatedText : `${getCurrentPage(bookData.book!, progress)}`,
note: '',
createdAt: Date.now(),
updatedAt: Date.now(),
@@ -49,6 +51,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
if (existingBookmark) {
existingBookmark.deletedAt = null;
existingBookmark.updatedAt = Date.now();
existingBookmark.text = bookmark.text;
} else {
bookmarks.push(bookmark);
}
@@ -191,12 +191,7 @@ const Notebook: React.FC = ({}) => {
)}
<ul className=''>
{annotationNotes.map((item, index) => (
<BooknoteItem
key={`${index}-${item.cfi}`}
bookKey={sideBarBookKey}
item={item}
editable={true}
/>
<BooknoteItem key={`${index}-${item.cfi}`} bookKey={sideBarBookKey} item={item} />
))}
</ul>
</div>
@@ -13,15 +13,14 @@ import useScrollToItem from '../../hooks/useScrollToItem';
interface BooknoteItemProps {
bookKey: string;
item: BookNote;
editable?: boolean;
}
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = false }) => {
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
const { getProgress, getView } = useReaderStore();
const { getProgress, getView, getViewsById } = useReaderStore();
const { setNotebookEditAnnotation, setNotebookVisible } = useNotebookStore();
const { text, cfi, note } = item;
@@ -44,6 +43,8 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
booknotes.forEach((item) => {
if (item.id === note.id) {
item.deletedAt = Date.now();
const views = getViewsById(bookKey.split('-')[0]!);
views.forEach((view) => view?.addAnnotation(item, true));
}
});
const updatedConfig = updateBooknotes(bookKey, booknotes);
@@ -53,6 +54,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
};
const editNote = (note: BookNote) => {
setNotebookVisible(true);
setNotebookEditAnnotation(note);
};
@@ -60,15 +62,15 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
<li
ref={viewRef}
className={clsx(
'border-base-300 my-2 cursor-pointer rounded-lg p-2 text-sm',
editable && 'collapse-arrow collapse',
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-200 bg-base-100',
'border-base-300 group relative my-2 cursor-pointer rounded-lg p-2 text-sm',
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/55 bg-base-100',
'transition-all duration-300 ease-in-out',
)}
tabIndex={0}
onClick={handleClickItem}
>
<div
className={clsx('collapse-title min-h-4 p-0', editable && 'pr-4')}
className={clsx('min-h-4 p-0 transition-all duration-300 ease-in-out')}
style={
{
'--top-override': '0.7rem',
@@ -98,40 +100,42 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
</div>
</div>
</div>
{editable && (
<div
className={clsx('collapse-content invisible !p-0 text-xs')}
style={
{
'--bottom-override': 0,
} as React.CSSProperties
}
onClick={(e) => e.stopPropagation()}
>
<div className='flex justify-end space-x-3'>
{item.note && (
<button
className={clsx(
'btn btn-ghost settings-content hover:bg-transparent',
'flex h-4 min-h-4 items-end p-0',
)}
onClick={editNote.bind(null, item)}
>
<div className='align-bottom text-blue-400'>{_('Edit')}</div>
</button>
)}
<div
className={clsx(
'max-h-0 overflow-hidden p-0 text-xs',
'transition-[max-height] duration-300 ease-in-out',
'group-hover:max-h-8 group-hover:overflow-visible',
)}
style={
{
'--bottom-override': 0,
} as React.CSSProperties
}
onClick={(e) => e.stopPropagation()}
>
<div className='flex justify-end space-x-3 p-2'>
{item.note && (
<button
className={clsx(
'btn btn-ghost settings-content hover:bg-transparent',
'flex h-4 min-h-4 items-end p-0',
)}
onClick={deleteNote.bind(null, item)}
onClick={editNote.bind(null, item)}
>
<div className='align-bottom text-red-400'>{_('Delete')}</div>
<div className='align-bottom text-blue-400'>{_('Edit')}</div>
</button>
</div>
)}
<button
className={clsx(
'btn btn-ghost settings-content hover:bg-transparent',
'flex h-4 min-h-4 items-end p-0',
)}
onClick={deleteNote.bind(null, item)}
>
<div className='align-bottom text-red-400'>{_('Delete')}</div>
</button>
</div>
)}
</div>
</li>
);
};
@@ -26,7 +26,7 @@ const BooknoteView: React.FC<{
const booknoteGroups: { [href: string]: BooknoteGroup } = {};
for (const booknote of booknotes) {
const tocItem = findTocItemBS(toc, booknote.cfi);
const tocItem = findTocItemBS(toc ?? [], booknote.cfi);
const href = tocItem?.href || '';
const label = tocItem?.label || '';
const id = tocItem?.id || 0;
@@ -1,18 +1,20 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { BookDoc } from '@/libs/document';
import { useBookDataStore } from '@/store/bookDataStore';
import TOCView from './TOCView';
import BooknoteView from './BooknoteView';
import TabNavigation from './TabNavigation';
const SidebarContent: React.FC<{
activeTab: string;
bookDoc: BookDoc;
sideBarBookKey: string;
onTabChange: (tab: string) => void;
}> = ({ activeTab, bookDoc, sideBarBookKey, onTabChange }) => {
}> = ({ bookDoc, sideBarBookKey }) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const { getConfig, setConfig } = useBookDataStore();
const config = getConfig(sideBarBookKey);
const [activeTab, setActiveTab] = useState(config?.viewSettings?.sideBarTab || 'toc');
useEffect(() => {
const container = scrollContainerRef.current;
@@ -39,6 +41,20 @@ const SidebarContent: React.FC<{
};
}, []);
useEffect(() => {
if (!sideBarBookKey) return;
const config = getConfig(sideBarBookKey!)!;
setActiveTab(config.viewSettings!.sideBarTab!);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sideBarBookKey]);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
const config = getConfig(sideBarBookKey!)!;
config.viewSettings!.sideBarTab = tab;
setConfig(sideBarBookKey!, config);
};
return (
<>
<div
@@ -52,15 +68,15 @@ const SidebarContent: React.FC<{
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} />
)}
{activeTab === 'annotations' && (
<BooknoteView type='annotation' toc={bookDoc.toc} bookKey={sideBarBookKey} />
<BooknoteView type='annotation' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
)}
{activeTab === 'bookmarks' && (
<BooknoteView type='bookmark' toc={bookDoc.toc} bookKey={sideBarBookKey} />
<BooknoteView type='bookmark' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
)}
</div>
</div>
<div className='flex-shrink-0'>
<TabNavigation activeTab={activeTab} onTabChange={onTabChange} />
<TabNavigation activeTab={activeTab} onTabChange={handleTabChange} />
</div>
</>
);
@@ -1,7 +1,6 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
@@ -23,15 +22,13 @@ const MAX_SIDEBAR_WIDTH = 0.45;
const SideBar: React.FC<{
onGoToLibrary: () => void;
}> = ({ onGoToLibrary }) => {
const { envConfig } = useEnv();
const { settings, saveSettings } = useSettingsStore();
const { settings } = useSettingsStore();
const { sideBarBookKey } = useSidebarStore();
const { getBookData } = useBookDataStore();
const { getView } = useReaderStore();
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [activeTab, setActiveTab] = useState(settings.globalReadSettings.sideBarTab);
const {
sideBarWidth,
isSideBarPinned,
@@ -70,12 +67,6 @@ const SideBar: React.FC<{
setSideBarVisible(false);
};
const handleTabChange = (tab: string) => {
setActiveTab(tab);
settings.globalReadSettings.sideBarTab = tab;
saveSettings(envConfig, settings);
};
const handleToggleSearchBar = () => {
setIsSearchBarVisible((prev) => !prev);
if (isSearchBarVisible) {
@@ -144,12 +135,7 @@ const SideBar: React.FC<{
onSelectResult={handleSearchResultClick}
/>
) : (
<SidebarContent
activeTab={activeTab}
bookDoc={bookDoc}
sideBarBookKey={sideBarBookKey!}
onTabChange={handleTabChange}
/>
<SidebarContent bookDoc={bookDoc} sideBarBookKey={sideBarBookKey!} />
)}
<div
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
@@ -51,6 +51,14 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
getView(sideBarBookKey)?.next(distance);
};
const goBack = () => {
getView(sideBarBookKey)?.history.back();
};
const goForward = () => {
getView(sideBarBookKey)?.history.forward();
};
const reloadPage = () => {
window.location.reload();
};
@@ -67,6 +75,8 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
onGoRight: goRight,
onGoPrev: goPrev,
onGoNext: goNext,
onGoBack: goBack,
onGoForward: goForward,
},
[sideBarBookKey, bookKeys],
);
@@ -10,6 +10,8 @@ export interface ShortcutConfig {
onGoRight: string[];
onGoNext: string[];
onGoPrev: string[];
onGoBack: string[];
onGoForward: string[];
}
const DEFAULT_SHORTCUTS: ShortcutConfig = {
@@ -24,6 +26,8 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
onGoRight: ['ArrowRight', 'PageDown', 'l'],
onGoNext: ['ArrowDown', 'j'],
onGoPrev: ['ArrowUp', 'k'],
onGoBack: ['shift+ArrowLeft', 'shift+h'],
onGoForward: ['shift+ArrowRight', 'shift+l'],
};
// Load shortcuts from localStorage or fallback to defaults
+2 -2
View File
@@ -57,8 +57,8 @@ export interface BookDoc {
editor?: string;
publisher?: string;
};
toc: Array<TOCItem>;
sections: Array<SectionItem>;
toc?: Array<TOCItem>;
sections?: Array<SectionItem>;
splitTOCHref(href: string): Array<string | number>;
getCover(): Promise<Blob | null>;
}
+4 -1
View File
@@ -21,6 +21,7 @@ import {
DEFAULT_BOOK_LAYOUT,
DEFAULT_BOOK_STYLE,
DEFAULT_BOOK_FONT,
DEFAULT_VIEW_CONFIG,
DEFAULT_READSETTINGS,
SYSTEM_SETTINGS_VERSION,
DEFAULT_BOOK_SEARCH_CONFIG,
@@ -65,6 +66,7 @@ export abstract class BaseAppService implements AppService {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...DEFAULT_VIEW_CONFIG,
...settings.globalViewSettings,
};
} catch {
@@ -80,6 +82,7 @@ export abstract class BaseAppService implements AppService {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...DEFAULT_VIEW_CONFIG,
},
};
@@ -220,7 +223,7 @@ export abstract class BaseAppService implements AppService {
}
async fetchBookDetails(book: Book, settings: SystemSettings) {
const { file } = await this.loadBookContent(book, settings) as BookContent;
const { file } = (await this.loadBookContent(book, settings)) as BookContent;
const bookDoc = (await new DocumentLoader(file).open()).book as BookDoc;
return bookDoc.metadata;
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { BookFont, BookLayout, BookSearchConfig, BookStyle } from '@/types/book';
import { BookFont, BookLayout, BookSearchConfig, BookStyle, ViewConfig } from '@/types/book';
import { ReadSettings } from '@/types/settings';
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
@@ -10,7 +10,6 @@ export const FILE_ACCEPT_FORMATS = SUPPORTED_FILE_EXTS.map((ext) => `.${ext}`).j
export const DEFAULT_READSETTINGS: ReadSettings = {
sideBarWidth: '25%',
isSideBarPinned: true,
sideBarTab: 'toc',
notebookWidth: '25%',
isNotebookPinned: false,
autohideCursor: true,
@@ -57,6 +56,10 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
userStylesheet: '',
};
export const DEFAULT_VIEW_CONFIG: ViewConfig = {
sideBarTab: 'toc',
};
export const DEFAULT_BOOK_SEARCH_CONFIG: BookSearchConfig = {
scope: 'book',
matchCase: false,
+1 -1
View File
@@ -114,7 +114,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
console.log('Loading book', key);
const { book: loadedBookDoc } = await new DocumentLoader(file).open();
const bookDoc = loadedBookDoc as BookDoc;
if (bookDoc.toc?.length > 0 && bookDoc.sections?.length > 0) {
if (bookDoc.toc?.length && bookDoc.sections?.length) {
updateTocID(bookDoc.toc);
const sections = bookDoc.sections.reduce((map: Record<string, SectionItem>, section) => {
map[section.id] = section;
+5 -1
View File
@@ -75,7 +75,11 @@ export interface BookFont {
fontWeight: number;
}
export interface ViewSettings extends BookLayout, BookStyle, BookFont {}
export interface ViewConfig {
sideBarTab: string;
}
export interface ViewSettings extends BookLayout, BookStyle, BookFont, ViewConfig {}
export interface BookProgress {
location: string;
-1
View File
@@ -5,7 +5,6 @@ export type ThemeType = 'light' | 'dark' | 'auto';
export interface ReadSettings {
sideBarWidth: string;
isSideBarPinned: boolean;
sideBarTab: string;
notebookWidth: string;
isNotebookPinned: boolean;
autohideCursor: boolean;
+13 -1
View File
@@ -1,5 +1,5 @@
import { EXTS } from '@/libs/document';
import { Book, BookConfig } from '@/types/book';
import { Book, BookConfig, BookProgress } from '@/types/book';
import { getUserLang, makeSafeFilename } from './misc';
export const getDir = (book: Book) => {
@@ -98,3 +98,15 @@ export const formatSubject = (subject: string | string[] | undefined) => {
if (!subject) return '';
return Array.isArray(subject) ? subject.join(', ') : subject;
};
export const getCurrentPage = (book: Book, progress: BookProgress) => {
const bookFormat = book.format;
const { section, pageinfo } = progress;
return bookFormat === 'PDF'
? section
? section.current + 1
: 0
: pageinfo
? pageinfo.current + 1
: 0;
};