Add note taking in notebook
This commit is contained in:
@@ -5,6 +5,7 @@ import * as CFI from 'foliate-js/epubcfi.js';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
|
||||
interface BookmarkTogglerProps {
|
||||
bookKey: string;
|
||||
@@ -28,6 +29,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
const text = range?.startContainer.textContent?.slice(0, 128) || '';
|
||||
const truncatedText = text.length === 128 ? text + '...' : text;
|
||||
const bookmark: BookNote = {
|
||||
id: uniqueId(),
|
||||
type: 'bookmark',
|
||||
cfi,
|
||||
href,
|
||||
@@ -46,7 +48,11 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
const end = CFI.collapse(cfi, true);
|
||||
const updatedConfig = updateBooknotes(
|
||||
bookKey,
|
||||
bookmarks.filter((item) => CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) > 0),
|
||||
bookmarks.filter(
|
||||
(item) =>
|
||||
item.type !== 'bookmark' ||
|
||||
CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) > 0,
|
||||
),
|
||||
);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
|
||||
@@ -7,6 +7,7 @@ import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SidebarToggler from './SidebarToggler';
|
||||
import BookmarkToggler from './BookmarkToggler';
|
||||
import NotebookToggler from './NotebookToggler';
|
||||
import ViewMenu from './ViewMenu';
|
||||
|
||||
interface HeaderBarProps {
|
||||
@@ -57,6 +58,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
</div>
|
||||
|
||||
<div className='flex h-full items-center space-x-2'>
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { TbLayoutSidebarRight, TbLayoutSidebarRightFilled } from 'react-icons/tb';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
interface NotebookTogglerProps {
|
||||
bookKey: string;
|
||||
}
|
||||
|
||||
const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
const { sideBarBookKey, isNotebookVisible, setSideBarBookKey, toggleNotebook } = useReaderStore();
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleNotebook();
|
||||
} else {
|
||||
setSideBarBookKey(bookKey);
|
||||
if (!isNotebookVisible) toggleNotebook();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<button onClick={handleToggleSidebar} className='p-2'>
|
||||
{sideBarBookKey == bookKey && isNotebookVisible ? (
|
||||
<TbLayoutSidebarRightFilled size={20} />
|
||||
) : (
|
||||
<TbLayoutSidebarRight size={20} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotebookToggler;
|
||||
@@ -12,6 +12,7 @@ import SideBar from './sidebar/SideBar';
|
||||
import useBooks from '../hooks/useBooks';
|
||||
import BookGrid from './BookGrid';
|
||||
import useBookShortcuts from '../hooks/useBookShortcuts';
|
||||
import Notebook from './notebook/Notebook';
|
||||
|
||||
const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) => {
|
||||
const router = useRouter();
|
||||
@@ -76,8 +77,11 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
onGoToLibrary={handleCloseBooks}
|
||||
onOpenSplitView={openSplitView}
|
||||
/>
|
||||
|
||||
<BookGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<Notebook
|
||||
width={settings.globalReadSettings.notebookWidth}
|
||||
isPinned={settings.globalReadSettings.isNotebookPinned}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ interface SidebarTogglerProps {
|
||||
}
|
||||
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
const { isSideBarVisible, setSideBarBookKey, sideBarBookKey, toggleSideBar } = useReaderStore();
|
||||
const { sideBarBookKey, isSideBarVisible, setSideBarBookKey, toggleSideBar } = useReaderStore();
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleSideBar();
|
||||
|
||||
@@ -11,22 +11,17 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
|
||||
import { getPopupPosition, getPosition, Position } from '@/utils/sel';
|
||||
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
|
||||
import Toast from '@/components/Toast';
|
||||
import useOutsideClick from '@/hooks/useOutsideClick';
|
||||
import AnnotationPopup from './AnnotationPopup';
|
||||
|
||||
interface TextSelection {
|
||||
annotated?: boolean;
|
||||
text: string;
|
||||
range: Range;
|
||||
index: number;
|
||||
}
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
|
||||
const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, saveConfig, getProgress, updateBooknotes } = useReaderStore();
|
||||
const { getConfig, getView, getViewsById } = useReaderStore();
|
||||
const { notebookNewAnnotation, setNotebookVisible, setNotebookNewAnnotation } = useReaderStore();
|
||||
const globalReadSettings = settings.globalReadSettings;
|
||||
const config = getConfig(bookKey)!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
@@ -55,8 +50,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { doc, index } = detail;
|
||||
const handlePointerup = () => {
|
||||
const sel = doc.getSelection();
|
||||
if (sel && sel.toString().trim().length > 0) {
|
||||
setSelection({ text: sel.toString(), range: sel.getRangeAt(0), index });
|
||||
if (sel && sel.toString().trim().length > 0 && sel.rangeCount > 0) {
|
||||
setSelection({ key: bookKey, text: sel.toString(), range: sel.getRangeAt(0), index });
|
||||
}
|
||||
};
|
||||
detail.doc?.addEventListener('pointerup', handlePointerup);
|
||||
@@ -84,10 +79,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const annotations = booknotes.filter((booknote) => booknote.type === 'annotation');
|
||||
const annotation = annotations.find((annotation) => annotation.cfi === cfi);
|
||||
if (!annotation) return;
|
||||
const selection = { annotated: true, text: annotation.text, range, index };
|
||||
const selection = { key: bookKey, annotated: true, text: annotation.text ?? '', range, index };
|
||||
setSelectedStyle(annotation.style!);
|
||||
setSelectedColor(annotation.color!);
|
||||
setSelection(selection as TextSelection);
|
||||
setSelection(selection);
|
||||
};
|
||||
|
||||
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation }, [config]);
|
||||
@@ -117,9 +112,39 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
}, [toastMessage]);
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowPopup(false);
|
||||
setToastMessage('Copied to clipboard');
|
||||
setToastMessage('Copied to notebook');
|
||||
|
||||
const { booknotes: annotations = [] } = config;
|
||||
if (selection) navigator.clipboard.writeText(selection.text);
|
||||
const { tocHref: href } = progress;
|
||||
const cfi = view?.getCFI(selection.index, selection.range);
|
||||
if (!cfi) return;
|
||||
const annotation: BookNote = {
|
||||
id: uniqueId(),
|
||||
type: 'excerpt',
|
||||
cfi,
|
||||
href,
|
||||
text: selection.text,
|
||||
note: '',
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
const existingIndex = annotations.findIndex(
|
||||
(annotation) => annotation.cfi === cfi && annotation.type === 'excerpt',
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
annotations[existingIndex] = annotation;
|
||||
} else {
|
||||
annotations.push(annotation);
|
||||
}
|
||||
const updatedConfig = updateBooknotes(bookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
setHighlightOptionsVisible(false);
|
||||
setNotebookVisible(true);
|
||||
};
|
||||
|
||||
const handleHighlight = (update = false) => {
|
||||
@@ -131,11 +156,17 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
if (!cfi) return;
|
||||
const style = globalReadSettings.highlightStyle;
|
||||
const color = globalReadSettings.highlightStyles[style];
|
||||
const text = selection.text;
|
||||
const type = 'annotation';
|
||||
const created = Date.now();
|
||||
const note = '';
|
||||
const annotation: BookNote = { type, cfi, href, style, color, text, note, created };
|
||||
const annotation: BookNote = {
|
||||
id: uniqueId(),
|
||||
type: 'annotation',
|
||||
cfi,
|
||||
href,
|
||||
style,
|
||||
color,
|
||||
text: selection.text,
|
||||
note: '',
|
||||
created: Date.now(),
|
||||
};
|
||||
const existingIndex = annotations.findIndex(
|
||||
(annotation) => annotation.cfi === cfi && annotation.type === 'annotation',
|
||||
);
|
||||
@@ -155,15 +186,20 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSelection({ ...selection, annotated: true });
|
||||
}
|
||||
|
||||
const dedupedAnnotations = Array.from(
|
||||
new Map(annotations.map((item) => [`${item.type}-${item.cfi}`, item])).values(),
|
||||
);
|
||||
const updatedConfig = updateBooknotes(bookKey, dedupedAnnotations);
|
||||
const updatedConfig = updateBooknotes(bookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
};
|
||||
const handleAnnotate = () => {};
|
||||
const handleAnnotate = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
const { tocHref: href } = progress;
|
||||
selection.href = href;
|
||||
setHighlightOptionsVisible(false);
|
||||
setNotebookVisible(true);
|
||||
setNotebookNewAnnotation(selection);
|
||||
handleHighlight(true);
|
||||
};
|
||||
const handleSearch = () => {};
|
||||
const handleDictionary = () => {};
|
||||
|
||||
@@ -181,7 +217,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div ref={popupRef}>
|
||||
<div ref={notebookNewAnnotation ? null : popupRef}>
|
||||
{showPopup && trianglePosition && popupPosition && (
|
||||
<AnnotationPopup
|
||||
buttons={buttons}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
|
||||
const NotebookHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
handleTogglePin: () => void;
|
||||
}> = ({ isPinned, handleTogglePin }) => (
|
||||
<div className='notebook-header relative flex h-11 items-center px-3'>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<div className='notebook-title text-sm font-medium'>Notebook</div>
|
||||
</div>
|
||||
<div className='z-10 flex items-center space-x-3'>
|
||||
<button
|
||||
onClick={handleTogglePin}
|
||||
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<FiSearch size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotebookHeader;
|
||||
@@ -0,0 +1,95 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
|
||||
interface NoteEditorProps {
|
||||
onSave: (selction: TextSelection, note: string) => void;
|
||||
onEdit: (annotation: BookNote) => void;
|
||||
}
|
||||
|
||||
const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
const { notebookNewAnnotation, notebookEditAnnotation } = useReaderStore();
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [note, setNote] = React.useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (notebookEditAnnotation) {
|
||||
setNote(notebookEditAnnotation.note);
|
||||
}
|
||||
}, [notebookEditAnnotation]);
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.style.height = 'auto';
|
||||
editorRef.current.style.height = `${editorRef.current.scrollHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
adjustHeight();
|
||||
setNote(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleSaveNote = () => {
|
||||
if (editorRef.current && notebookNewAnnotation) {
|
||||
onSave(notebookNewAnnotation, editorRef.current.value);
|
||||
} else if (editorRef.current && notebookEditAnnotation) {
|
||||
notebookEditAnnotation.note = editorRef.current.value;
|
||||
onEdit(notebookEditAnnotation);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='note-editor-container mt-2 rounded-md bg-white p-2'>
|
||||
<div className='flex w-full justify-between space-x-2'>
|
||||
<div className='settings-content relative w-full'>
|
||||
<textarea
|
||||
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',
|
||||
)}
|
||||
ref={editorRef}
|
||||
value={note}
|
||||
rows={1}
|
||||
spellCheck={false}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleInput}
|
||||
placeholder='Add your notes here...'
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost settings-content hover:bg-transparent',
|
||||
'flex h-[1.5em] min-h-[1.5em] items-end p-0',
|
||||
editorRef.current && editorRef.current.value ? '' : 'btn-disabled !bg-opacity-0',
|
||||
)}
|
||||
onClick={handleSaveNote}
|
||||
>
|
||||
<div className='align-bottom text-blue-400'>Save</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex items-start pt-2'>
|
||||
<div className='mr-2 min-h-full self-stretch border-l-2 border-gray-300'></div>
|
||||
<div className='note-citation settings-content line-clamp-3 text-sm'>
|
||||
{notebookNewAnnotation?.text || notebookEditAnnotation?.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoteEditor;
|
||||
@@ -0,0 +1,206 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
|
||||
const MIN_NOTEBOOK_WIDTH = 0.05;
|
||||
const MAX_NOTEBOOK_WIDTH = 0.45;
|
||||
|
||||
const Notebook: React.FC<{ width: string; isPinned: boolean }> = ({ width, isPinned }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, sideBarBookKey, notebookWidth } = useReaderStore();
|
||||
const { isNotebookVisible, isNotebookPinned } = useReaderStore();
|
||||
const { notebookNewAnnotation, notebookEditAnnotation } = useReaderStore();
|
||||
const { setNotebookWidth, setNotebookPin, toggleNotebookPin } = useReaderStore();
|
||||
const { getConfig, saveConfig, getView, setNotebookVisible, updateBooknotes } = useReaderStore();
|
||||
const { setNotebookNewAnnotation, setNotebookEditAnnotation } = useReaderStore();
|
||||
|
||||
const handleNotebookResize = (newWidth: string) => {
|
||||
setNotebookWidth(newWidth);
|
||||
settings.globalReadSettings.notebookWidth = newWidth;
|
||||
};
|
||||
|
||||
const handleTogglePin = () => {
|
||||
toggleNotebookPin();
|
||||
settings.globalReadSettings.isNotebookPinned = !isNotebookPinned;
|
||||
};
|
||||
|
||||
const handleClickOverlay = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setNotebookVisible(false);
|
||||
};
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = 1 - e.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_NOTEBOOK_WIDTH, Math.min(MAX_NOTEBOOK_WIDTH, widthFraction));
|
||||
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
|
||||
const handleSaveNote = (selection: TextSelection, note: string) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
const config = getConfig(sideBarBookKey)!;
|
||||
|
||||
const cfi = view?.getCFI(selection.index, selection.range);
|
||||
if (!cfi) return;
|
||||
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const annotation: BookNote = {
|
||||
id: uniqueId(),
|
||||
type: 'annotation',
|
||||
cfi,
|
||||
note,
|
||||
href: selection.href || '',
|
||||
text: selection.text,
|
||||
created: Date.now(),
|
||||
};
|
||||
annotations.push(annotation);
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, sideBarBookKey, updatedConfig, settings);
|
||||
}
|
||||
setNotebookNewAnnotation(null);
|
||||
};
|
||||
|
||||
const handleEditNote = (annotation: BookNote) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const config = getConfig(sideBarBookKey)!;
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const existingIndex = annotations.findIndex((item) => item.id === annotation.id);
|
||||
if (existingIndex === -1) return;
|
||||
annotation.modified = Date.now();
|
||||
annotations[existingIndex] = annotation;
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, sideBarBookKey, updatedConfig, settings);
|
||||
}
|
||||
setNotebookEditAnnotation(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNotebookWidth(width);
|
||||
setNotebookPin(isPinned);
|
||||
setNotebookVisible(isPinned);
|
||||
}, []);
|
||||
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
const deleteNote = (note: BookNote) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const config = getConfig(sideBarBookKey);
|
||||
if (!config) return;
|
||||
const { booknotes = [] } = config;
|
||||
const updatedNotes = booknotes.filter((item) => item.id !== note.id);
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, updatedNotes);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, sideBarBookKey, updatedConfig, settings);
|
||||
}
|
||||
};
|
||||
|
||||
if (!sideBarBookKey) return null;
|
||||
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const { booknotes: allNotes = [] } = config || {};
|
||||
const annotationNotes = allNotes
|
||||
.filter((note) => note.type === 'annotation' && note.note)
|
||||
.sort((a, b) => b.created - a.created);
|
||||
const excerptNotes = allNotes
|
||||
.filter((note) => note.type === 'excerpt')
|
||||
.sort((a, b) => a.created - b.created);
|
||||
|
||||
return isNotebookVisible ? (
|
||||
<>
|
||||
{!isPinned && (
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/20' onClick={handleClickOverlay} />
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'notebook-container bg-base-200 right-0 z-20 h-full min-w-60 select-none',
|
||||
'rounded-window-top-right rounded-window-bottom-right',
|
||||
!isPinned && 'shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
width: `${notebookWidth}`,
|
||||
maxWidth: `${MAX_NOTEBOOK_WIDTH * 100}%`,
|
||||
position: isPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<NotebookHeader isPinned={isPinned} handleTogglePin={handleTogglePin} />
|
||||
<div className='max-h-[calc(100vh-44px)] overflow-y-auto px-3'>
|
||||
<div>{excerptNotes.length > 0 && <p className='pt-1 text-sm'>Excerpts</p>}</div>
|
||||
<ul className=''>
|
||||
{excerptNotes.map((item, index) => (
|
||||
<li key={`${index}-${item.id}`} className='my-2'>
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='collapse-arrow border-base-300 collapse border bg-white'
|
||||
>
|
||||
<div
|
||||
className='collapse-title h-9 min-h-9 p-2 pr-8 text-sm font-medium'
|
||||
style={
|
||||
{
|
||||
'--top-override': '1.2rem',
|
||||
'--end-override': '0.7rem',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<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()}
|
||||
>
|
||||
<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',
|
||||
)}
|
||||
onClick={deleteNote.bind(null, item)}
|
||||
>
|
||||
<div className='align-bottom text-xs text-red-400'>Delete</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
{(notebookNewAnnotation || annotationNotes.length > 0) && (
|
||||
<p className='pt-1 text-sm'>Notes</p>
|
||||
)}
|
||||
</div>
|
||||
{(notebookNewAnnotation || notebookEditAnnotation) && (
|
||||
<NoteEditor onSave={handleSaveNote} onEdit={handleEditNote} />
|
||||
)}
|
||||
<ul className=''>
|
||||
{annotationNotes.map((item, index) => (
|
||||
<BooknoteItem
|
||||
key={`${index}-${item.cfi}`}
|
||||
bookKey={sideBarBookKey}
|
||||
item={item}
|
||||
editable={true}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default Notebook;
|
||||
@@ -0,0 +1,130 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import * as CFI from 'foliate-js/epubcfi.js';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
interface BooknoteItemProps {
|
||||
bookKey: string;
|
||||
item: BookNote;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = false }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, getProgress, getView } = useReaderStore();
|
||||
const { getConfig, saveConfig, updateBooknotes, setNotebookEditAnnotation } = useReaderStore();
|
||||
const [isCurrent, setIsCurrent] = useState(false);
|
||||
|
||||
const { text, cfi } = item;
|
||||
const progress = getProgress(bookKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (!progress) return;
|
||||
const { location } = progress;
|
||||
const start = CFI.collapse(location);
|
||||
const end = CFI.collapse(location, true);
|
||||
setIsCurrent(CFI.compare(start, cfi) * CFI.compare(end, cfi) <= 0);
|
||||
}, [progress]);
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
getView(bookKey)?.goTo(cfi);
|
||||
};
|
||||
|
||||
const deleteNote = (note: BookNote) => {
|
||||
if (!bookKey) return;
|
||||
const config = getConfig(bookKey);
|
||||
if (!config) return;
|
||||
const { booknotes = [] } = config;
|
||||
const updatedNotes = booknotes.filter((item) => item.id !== note.id);
|
||||
const updatedConfig = updateBooknotes(bookKey, updatedNotes);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
};
|
||||
|
||||
const editNote = (note: BookNote) => {
|
||||
setNotebookEditAnnotation(note);
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
className={clsx(
|
||||
'border-base-300 my-2 cursor-pointer rounded-lg p-2 text-sm',
|
||||
editable && 'collapse-arrow collapse',
|
||||
isCurrent ? 'bg-base-300 hover:bg-gray-300/70' : 'hover:bg-base-300 bg-white',
|
||||
)}
|
||||
tabIndex={0}
|
||||
onClick={handleClickItem}
|
||||
>
|
||||
<div
|
||||
className={clsx('collapse-title min-h-4 p-0', editable && 'pr-4')}
|
||||
style={
|
||||
{
|
||||
'--top-override': '0.7rem',
|
||||
'--end-override': '0.3rem',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{item.note && <span className='line-clamp-3 font-normal'>{item.note}</span>}
|
||||
<div className='flex items-start'>
|
||||
{item.note && (
|
||||
<div className='my-1 mr-2 min-h-full self-stretch border-l-2 border-gray-300'></div>
|
||||
)}
|
||||
<div className='line-clamp-3'>
|
||||
<span
|
||||
className={clsx(
|
||||
'inline',
|
||||
item.note && 'text-xs text-gray-500',
|
||||
(item.style === 'underline' || item.style === 'squiggly') &&
|
||||
'underline decoration-2',
|
||||
item.style === 'highlight' && `bg-${item.color}-100`,
|
||||
item.style === 'underline' && `decoration-${item.color}-400`,
|
||||
item.style === 'squiggly' && `decoration-wavy decoration-${item.color}-400`,
|
||||
)}
|
||||
>
|
||||
{text || ''}
|
||||
</span>
|
||||
</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'>
|
||||
<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-xs text-blue-400'>Edit</div>
|
||||
</button>
|
||||
<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-xs text-red-400'>Delete</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default BooknoteItem;
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import * as CFI from 'foliate-js/epubcfi.js';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { findParentPath } from '@/utils/toc';
|
||||
import { TOCItem } from '@/libs/document';
|
||||
import { BookNote, BookNoteType } from '@/types/book';
|
||||
import clsx from 'clsx';
|
||||
import BooknoteItem from './BooknoteItem';
|
||||
|
||||
interface BooknoteGroup {
|
||||
id: number;
|
||||
@@ -14,59 +14,6 @@ interface BooknoteGroup {
|
||||
booknotes: BookNote[];
|
||||
}
|
||||
|
||||
interface BooknoteItemProps {
|
||||
bookKey: string;
|
||||
item: BookNote;
|
||||
}
|
||||
|
||||
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
const { getProgress, getView } = useReaderStore();
|
||||
const [isCurrentBookmark, setIsCurrentBookmark] = useState(false);
|
||||
|
||||
const { text, cfi } = item;
|
||||
const progress = getProgress(bookKey)!;
|
||||
|
||||
useEffect(() => {
|
||||
const { location } = progress;
|
||||
const start = CFI.collapse(location);
|
||||
const end = CFI.collapse(location, true);
|
||||
setIsCurrentBookmark(CFI.compare(start, cfi) * CFI.compare(end, cfi) <= 0);
|
||||
}, [progress]);
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
getView(bookKey)?.goTo(cfi);
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
className={clsx(
|
||||
'my-2 cursor-pointer rounded-lg p-2 text-sm',
|
||||
isCurrentBookmark ? 'bg-base-300 hover:bg-gray-300' : 'hover:bg-base-300 bg-white',
|
||||
)}
|
||||
onClick={handleClickItem}
|
||||
>
|
||||
<div className='line-clamp-3 overflow-hidden'>
|
||||
<span
|
||||
className={clsx(
|
||||
'inline',
|
||||
(item.style === 'underline' || item.style === 'squiggly') && 'underline decoration-2',
|
||||
item.type === 'annotation' && item.style === 'highlight' && `bg-${item.color}-100`,
|
||||
item.type === 'annotation' &&
|
||||
item.style === 'underline' &&
|
||||
`decoration-${item.color}-400`,
|
||||
item.type === 'annotation' &&
|
||||
item.style === 'squiggly' &&
|
||||
`decoration-wavy decoration-${item.color}-400`,
|
||||
)}
|
||||
>
|
||||
{text || ''}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const BooknoteView: React.FC<{
|
||||
type: BookNoteType;
|
||||
bookKey: string;
|
||||
|
||||
@@ -10,8 +10,8 @@ const SidebarHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
onGoToLibrary: () => void;
|
||||
onOpenSplitView: () => void;
|
||||
handleSideBarTogglePin: () => void;
|
||||
}> = ({ isPinned, onGoToLibrary, onOpenSplitView, handleSideBarTogglePin }) => (
|
||||
handleTogglePin: () => void;
|
||||
}> = ({ isPinned, onGoToLibrary, onOpenSplitView, handleTogglePin }) => (
|
||||
<div className='sidebar-header flex h-11 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}>
|
||||
@@ -30,7 +30,7 @@ const SidebarHeader: React.FC<{
|
||||
<BookMenu openSplitView={onOpenSplitView} />
|
||||
</Dropdown>
|
||||
<button
|
||||
onClick={handleSideBarTogglePin}
|
||||
onClick={handleTogglePin}
|
||||
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} />}
|
||||
|
||||
@@ -9,7 +9,7 @@ import BookCard from './BookCard';
|
||||
import useSidebar from '../../hooks/useSidebar';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
|
||||
const MIN_SIDEBAR_WIDTH = 0.15;
|
||||
const MIN_SIDEBAR_WIDTH = 0.05;
|
||||
const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
|
||||
const SideBar: React.FC<{
|
||||
@@ -25,11 +25,17 @@ const SideBar: React.FC<{
|
||||
const {
|
||||
sideBarWidth,
|
||||
isSideBarVisible,
|
||||
setSideBarVisible,
|
||||
handleSideBarResize,
|
||||
handleSideBarTogglePin,
|
||||
setSideBarVisibility,
|
||||
} = useSidebar(width, isPinned);
|
||||
const { handleMouseDown } = useDragBar(handleSideBarResize, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = e.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, widthFraction));
|
||||
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideBarBookKey) return;
|
||||
@@ -38,7 +44,7 @@ const SideBar: React.FC<{
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
const handleClickOverlay = () => {
|
||||
setSideBarVisibility(false);
|
||||
setSideBarVisible(false);
|
||||
};
|
||||
|
||||
if (!sideBarBookKey) return null;
|
||||
@@ -67,7 +73,7 @@ const SideBar: React.FC<{
|
||||
isPinned={isPinned}
|
||||
onGoToLibrary={onGoToLibrary}
|
||||
onOpenSplitView={onOpenSplitView}
|
||||
handleSideBarTogglePin={handleSideBarTogglePin}
|
||||
handleTogglePin={handleSideBarTogglePin}
|
||||
/>
|
||||
<div className='border-b px-3'>
|
||||
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
const uniqueId = () => Math.random().toString(36).substring(2, 9);
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
|
||||
const useBooks = () => {
|
||||
const { envConfig } = useEnv();
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useDragBar = (
|
||||
handleSideBarResize: (width: string) => void,
|
||||
minWidth: number,
|
||||
maxWidth: number,
|
||||
) => {
|
||||
const useDragBar = (handleDragMove: (e: MouseEvent) => void) => {
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
const newWidthPx = e.clientX;
|
||||
const width = `${Math.round((newWidthPx / window.innerWidth) * 10000) / 100}%`;
|
||||
const minWidthPx = minWidth * window.innerWidth;
|
||||
const maxWidthPx = maxWidth * window.innerWidth;
|
||||
if (newWidthPx >= minWidthPx && newWidthPx <= maxWidthPx) {
|
||||
handleSideBarResize(width);
|
||||
}
|
||||
handleDragMove(e);
|
||||
},
|
||||
[handleSideBarResize, minWidth, maxWidth],
|
||||
[handleDragMove],
|
||||
);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
};
|
||||
const handleMouseUp = useCallback(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
},
|
||||
[handleMouseMove, handleMouseUp],
|
||||
);
|
||||
|
||||
return { handleMouseDown };
|
||||
};
|
||||
|
||||
@@ -5,19 +5,19 @@ const useSidebar = (initialWidth: string, isPinned: boolean) => {
|
||||
const { settings } = useReaderStore();
|
||||
const {
|
||||
sideBarWidth,
|
||||
setSideBarWidth,
|
||||
isSideBarVisible,
|
||||
setSideBarVisibility,
|
||||
toggleSideBar,
|
||||
isSideBarPinned,
|
||||
setSideBarWidth,
|
||||
setSideBarVisible,
|
||||
setSideBarPin,
|
||||
toggleSideBar,
|
||||
toggleSideBarPin,
|
||||
} = useReaderStore();
|
||||
|
||||
useEffect(() => {
|
||||
setSideBarWidth(initialWidth);
|
||||
setSideBarPin(isPinned);
|
||||
setSideBarVisibility(isPinned);
|
||||
setSideBarVisible(isPinned);
|
||||
}, []);
|
||||
|
||||
const handleSideBarResize = (newWidth: string) => {
|
||||
@@ -28,7 +28,7 @@ const useSidebar = (initialWidth: string, isPinned: boolean) => {
|
||||
const handleSideBarTogglePin = () => {
|
||||
toggleSideBarPin();
|
||||
settings.globalReadSettings.isSideBarPinned = !isSideBarPinned;
|
||||
if (isSideBarPinned && isSideBarVisible) setSideBarVisibility(false);
|
||||
if (isSideBarPinned && isSideBarVisible) setSideBarVisible(false);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -37,7 +37,7 @@ const useSidebar = (initialWidth: string, isPinned: boolean) => {
|
||||
isSideBarVisible,
|
||||
handleSideBarResize,
|
||||
handleSideBarTogglePin,
|
||||
setSideBarVisibility,
|
||||
setSideBarVisible,
|
||||
toggleSideBar,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
|
||||
export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
sideBarWidth: '25%',
|
||||
isSideBarPinned: true,
|
||||
notebookWidth: '25%',
|
||||
isNotebookPinned: false,
|
||||
autohideCursor: true,
|
||||
|
||||
highlightStyle: 'highlight',
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SystemSettings } from '@/types/settings';
|
||||
import { FoliateView } from '@/app/reader/components/FoliateViewer';
|
||||
import { BookDoc, DocumentLoader, TOCItem } from '@/libs/document';
|
||||
import { updateTocID } from '@/utils/toc';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
|
||||
export interface BookData {
|
||||
/* Persistent data shared with different views of the same book */
|
||||
@@ -58,9 +59,22 @@ interface ReaderStore {
|
||||
setSideBarWidth: (width: string) => void;
|
||||
toggleSideBar: () => void;
|
||||
toggleSideBarPin: () => void;
|
||||
setSideBarVisibility: (visible: boolean) => void;
|
||||
setSideBarVisible: (visible: boolean) => void;
|
||||
setSideBarPin: (pinned: boolean) => void;
|
||||
|
||||
notebookWidth: string;
|
||||
isNotebookVisible: boolean;
|
||||
isNotebookPinned: boolean;
|
||||
notebookNewAnnotation: TextSelection | null;
|
||||
notebookEditAnnotation: BookNote | null;
|
||||
setNotebookWidth: (width: string) => void;
|
||||
toggleNotebook: () => void;
|
||||
toggleNotebookPin: () => void;
|
||||
setNotebookVisible: (visible: boolean) => void;
|
||||
setNotebookPin: (pinned: boolean) => void;
|
||||
setNotebookNewAnnotation: (selection: TextSelection | null) => void;
|
||||
setNotebookEditAnnotation: (note: BookNote | null) => void;
|
||||
|
||||
setBookmarkRibbonVisibility: (key: string, visible: boolean) => void;
|
||||
|
||||
isFontLayoutSettingsDialogOpen: boolean;
|
||||
@@ -120,9 +134,23 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
setSideBarWidth: (width: string) => set({ sideBarWidth: width }),
|
||||
toggleSideBar: () => set((state) => ({ isSideBarVisible: !state.isSideBarVisible })),
|
||||
toggleSideBarPin: () => set((state) => ({ isSideBarPinned: !state.isSideBarPinned })),
|
||||
setSideBarVisibility: (visible: boolean) => set({ isSideBarVisible: visible }),
|
||||
setSideBarVisible: (visible: boolean) => set({ isSideBarVisible: visible }),
|
||||
setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }),
|
||||
|
||||
notebookWidth: '',
|
||||
isNotebookVisible: false,
|
||||
isNotebookPinned: false,
|
||||
notebookNewAnnotation: null,
|
||||
notebookEditAnnotation: null,
|
||||
setNotebookWidth: (width: string) => set({ notebookWidth: width }),
|
||||
toggleNotebook: () => set((state) => ({ isNotebookVisible: !state.isNotebookVisible })),
|
||||
toggleNotebookPin: () => set((state) => ({ isNotebookPinned: !state.isNotebookPinned })),
|
||||
setNotebookVisible: (visible: boolean) => set({ isNotebookVisible: visible }),
|
||||
setNotebookPin: (pinned: boolean) => set({ isNotebookPinned: pinned }),
|
||||
setNotebookNewAnnotation: (selection: TextSelection | null) =>
|
||||
set({ notebookNewAnnotation: selection }),
|
||||
setNotebookEditAnnotation: (note: BookNote | null) => set({ notebookEditAnnotation: note }),
|
||||
|
||||
isFontLayoutSettingsDialogOpen: false,
|
||||
isFontLayoutSettingsGlobal: true,
|
||||
setFontLayoutSettingsDialogOpen: (open: boolean) => set({ isFontLayoutSettingsDialogOpen: open }),
|
||||
@@ -389,10 +417,13 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
const id = key.split('-')[0]!;
|
||||
const book = state.booksData[id];
|
||||
if (!book) return state;
|
||||
const dedupedBooknotes = Array.from(
|
||||
new Map(booknotes.map((item) => [`${item.id}-${item.type}-${item.cfi}`, item])).values(),
|
||||
);
|
||||
updatedConfig = {
|
||||
...book.config,
|
||||
lastUpdated: Date.now(),
|
||||
booknotes,
|
||||
booknotes: dedupedBooknotes,
|
||||
};
|
||||
return {
|
||||
booksData: {
|
||||
@@ -402,7 +433,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
config: {
|
||||
...book.config,
|
||||
lastUpdated: Date.now(),
|
||||
booknotes,
|
||||
booknotes: dedupedBooknotes,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -157,3 +157,7 @@ foliate-view {
|
||||
@apply rounded-b-2xl;
|
||||
}
|
||||
|
||||
.collapse-arrow > .collapse-title:after {
|
||||
top: var(--top-override, default-value);
|
||||
inset-inline-end: var(--end-override, default-value);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type BookFormat = 'EPUB' | 'PDF' | 'MOBI' | 'CBZ' | 'FB2' | 'FBZ';
|
||||
export type BookNoteType = 'bookmark' | 'highlight' | 'annotation';
|
||||
export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt';
|
||||
export type HighlightStyle = 'highlight' | 'underline' | 'squiggly';
|
||||
export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet';
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface PageInfo {
|
||||
}
|
||||
|
||||
export interface BookNote {
|
||||
id: string;
|
||||
type: BookNoteType;
|
||||
cfi: string;
|
||||
href: string;
|
||||
|
||||
@@ -5,6 +5,8 @@ export type ThemeType = 'light' | 'dark' | 'auto';
|
||||
export interface ReadSettings {
|
||||
sideBarWidth: string;
|
||||
isSideBarPinned: boolean;
|
||||
notebookWidth: string;
|
||||
isNotebookPinned: boolean;
|
||||
autohideCursor: boolean;
|
||||
|
||||
highlightStyle: HighlightStyle;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const uniqueId = () => Math.random().toString(36).substring(2, 9);
|
||||
@@ -22,6 +22,15 @@ export interface Position {
|
||||
dir?: PositionDir;
|
||||
}
|
||||
|
||||
export interface TextSelection {
|
||||
key: string;
|
||||
text: string;
|
||||
range: Range;
|
||||
index: number;
|
||||
href?: string;
|
||||
annotated?: boolean;
|
||||
}
|
||||
|
||||
const frameRect = (frame: Frame, rect: Rect, sx = 1, sy = 1) => {
|
||||
const left = sx * rect.left + frame.left;
|
||||
const right = sx * rect.right + frame.left;
|
||||
|
||||
Reference in New Issue
Block a user