This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface AnnotationNotesProps {
|
||||
bookKey: string;
|
||||
isVertical: boolean;
|
||||
notes: BookNote[];
|
||||
triangleDir: 'up' | 'down' | 'left' | 'right';
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
}
|
||||
|
||||
const AnnotationNotes: React.FC<AnnotationNotesProps> = ({
|
||||
bookKey,
|
||||
isVertical,
|
||||
notes,
|
||||
triangleDir,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
}) => {
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const { setSideBarVisible } = useSidebarStore();
|
||||
const config = getConfig(bookKey);
|
||||
const maxSize = useResponsiveSize(300);
|
||||
|
||||
const handleShowAnnotation = (note: BookNote) => {
|
||||
if (!note.id) return;
|
||||
|
||||
setHoveredBookKey('');
|
||||
setSideBarVisible(true);
|
||||
if (config?.viewSettings) {
|
||||
setConfig(bookKey, {
|
||||
viewSettings: { ...config.viewSettings, sideBarTab: 'annotations' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'annotation-notes absolute flex overflow-y-auto rounded-lg text-white shadow-lg',
|
||||
'flex-col',
|
||||
)}
|
||||
style={{
|
||||
...(isVertical
|
||||
? {
|
||||
right: `${triangleDir === 'left' ? `${popupWidth + 16}px` : undefined}`,
|
||||
left: `${triangleDir === 'right' ? `${popupWidth + 16}px` : undefined}`,
|
||||
height: `${popupHeight}px`,
|
||||
maxWidth: `${maxSize}px`,
|
||||
}
|
||||
: {
|
||||
top: triangleDir === 'up' ? undefined : `${popupHeight + 16}px`,
|
||||
bottom: triangleDir === 'up' ? `${popupHeight + 16}px` : undefined,
|
||||
width: `${popupWidth}px`,
|
||||
maxHeight: `${maxSize}px`,
|
||||
}),
|
||||
scrollbarWidth: 'thin',
|
||||
}}
|
||||
>
|
||||
<div className={clsx('flex gap-4', isVertical ? 'h-full flex-row' : 'flex-col')}>
|
||||
{notes.map((note, index) => (
|
||||
<div
|
||||
role='none'
|
||||
key={note.id || index}
|
||||
onClick={() => handleShowAnnotation?.(note)}
|
||||
className='cursor-pointer rounded-lg bg-gray-600 p-4 shadow-md transition-colors'
|
||||
>
|
||||
{note.note && (
|
||||
<div
|
||||
dir='auto'
|
||||
className={clsx(
|
||||
'hyphens-auto text-justify font-sans text-sm text-white',
|
||||
isVertical && 'writing-vertical-rl',
|
||||
)}
|
||||
style={
|
||||
isVertical
|
||||
? {
|
||||
fontFeatureSettings: "'vrt2' 1, 'vert' 1",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{note.note}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnotationNotes;
|
||||
@@ -1,13 +1,14 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { BookNote, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import Popup from '@/components/Popup';
|
||||
import PopupButton from './PopupButton';
|
||||
import AnnotationNotes from './AnnotationNotes';
|
||||
import HighlightOptions from './HighlightOptions';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface AnnotationPopupProps {
|
||||
bookKey: string;
|
||||
dir: 'ltr' | 'rtl';
|
||||
isVertical: boolean;
|
||||
buttons: Array<{
|
||||
@@ -17,6 +18,7 @@ interface AnnotationPopupProps {
|
||||
disabled?: boolean;
|
||||
visible?: boolean;
|
||||
}>;
|
||||
notes: BookNote[];
|
||||
position: Position;
|
||||
trianglePosition: Position;
|
||||
highlightOptionsVisible: boolean;
|
||||
@@ -28,13 +30,12 @@ interface AnnotationPopupProps {
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT_PIX = 28;
|
||||
const OPTIONS_PADDING_PIX = 16;
|
||||
|
||||
const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
bookKey,
|
||||
dir,
|
||||
isVertical,
|
||||
buttons,
|
||||
notes,
|
||||
position,
|
||||
trianglePosition,
|
||||
highlightOptionsVisible,
|
||||
@@ -45,8 +46,6 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
onHighlight,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const highlightOptionsHeightPx = useResponsiveSize(OPTIONS_HEIGHT_PIX);
|
||||
const highlightOptionsPaddingPx = useResponsiveSize(OPTIONS_PADDING_PIX);
|
||||
return (
|
||||
<div dir={dir}>
|
||||
<Popup
|
||||
@@ -59,57 +58,52 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
triangleClassName='text-gray-600'
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'selection-buttons flex h-full w-full items-center justify-between p-2',
|
||||
isVertical ? 'flex-col overflow-y-auto' : 'flex-row overflow-x-auto',
|
||||
)}
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{buttons.map((button, index) => {
|
||||
if (button.visible === false) return null;
|
||||
return (
|
||||
<PopupButton
|
||||
key={index}
|
||||
showTooltip={!highlightOptionsVisible}
|
||||
tooltipText={button.tooltipText}
|
||||
Icon={button.Icon}
|
||||
onClick={button.onClick}
|
||||
disabled={button.disabled}
|
||||
<div className={clsx('flex h-full gap-4', isVertical ? 'flex-row' : 'flex-col')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'selection-buttons flex h-full w-full items-center justify-between p-2',
|
||||
isVertical ? 'flex-col overflow-y-auto' : 'flex-row overflow-x-auto',
|
||||
)}
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{buttons.map((button, index) => {
|
||||
if (button.visible === false) return null;
|
||||
return (
|
||||
<PopupButton
|
||||
key={index}
|
||||
showTooltip={!highlightOptionsVisible}
|
||||
tooltipText={button.tooltipText}
|
||||
Icon={button.Icon}
|
||||
onClick={button.onClick}
|
||||
disabled={button.disabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{notes.length > 0 ? (
|
||||
<AnnotationNotes
|
||||
bookKey={bookKey}
|
||||
isVertical={isVertical}
|
||||
notes={notes}
|
||||
triangleDir={trianglePosition.dir!}
|
||||
popupWidth={isVertical ? popupHeight : popupWidth}
|
||||
popupHeight={isVertical ? popupWidth : popupHeight}
|
||||
/>
|
||||
) : (
|
||||
highlightOptionsVisible && (
|
||||
<HighlightOptions
|
||||
isVertical={isVertical}
|
||||
triangleDir={trianglePosition.dir!}
|
||||
popupWidth={isVertical ? popupHeight : popupWidth}
|
||||
popupHeight={isVertical ? popupWidth : popupHeight}
|
||||
selectedStyle={selectedStyle}
|
||||
selectedColor={selectedColor}
|
||||
onHandleHighlight={onHighlight}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Popup>
|
||||
{highlightOptionsVisible && (
|
||||
<HighlightOptions
|
||||
isVertical={isVertical}
|
||||
style={{
|
||||
width: `${isVertical ? popupHeight : popupWidth}px`,
|
||||
height: `${isVertical ? popupWidth : popupHeight}px`,
|
||||
...(isVertical
|
||||
? {
|
||||
left: `${
|
||||
position.point.x +
|
||||
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
|
||||
(trianglePosition.dir === 'left' ? -1 : 1)
|
||||
}px`,
|
||||
top: `${position.point.y}px`,
|
||||
}
|
||||
: {
|
||||
left: `${position.point.x}px`,
|
||||
top: `${
|
||||
position.point.y +
|
||||
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
|
||||
(trianglePosition.dir === 'up' ? -1 : 1)
|
||||
}px`,
|
||||
}),
|
||||
}}
|
||||
selectedStyle={selectedStyle}
|
||||
selectedColor={selectedColor}
|
||||
onHandleHighlight={onHighlight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as CFI from 'foliate-js/epubcfi.js';
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote, BooknoteGroup, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { NOTE_PREFIX } from '@/types/view';
|
||||
import { getOSPlatform, uniqueId } from '@/utils/misc';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
@@ -69,6 +70,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [translatorPopupPosition, setTranslatorPopupPosition] = useState<Position>();
|
||||
const [proofreadPopupPosition, setProofreadPopupPosition] = useState<Position>();
|
||||
const [highlightOptionsVisible, setHighlightOptionsVisible] = useState(false);
|
||||
const [showAnnotationNotes, setShowAnnotationNotes] = useState(false);
|
||||
const [annotationNotes, setAnnotationNotes] = useState<BookNote[]>([]);
|
||||
|
||||
const [selectedStyle, setSelectedStyle] = useState<HighlightStyle>(
|
||||
settings.globalReadSettings.highlightStyle,
|
||||
@@ -78,6 +81,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
);
|
||||
|
||||
const popupPadding = useResponsiveSize(10);
|
||||
const trianglePadding = popupPadding * 2 + 6;
|
||||
const maxWidth = window.innerWidth - 2 * popupPadding;
|
||||
const maxHeight = window.innerHeight - 2 * popupPadding;
|
||||
const dictPopupWidth = Math.min(480, maxWidth);
|
||||
@@ -96,7 +100,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
|
||||
if (!gridFrame) return;
|
||||
const rect = gridFrame.getBoundingClientRect();
|
||||
const triangPos = getPosition(selection.range, rect, popupPadding, viewSettings.vertical);
|
||||
const triangPos = getPosition(selection, rect, trianglePadding, viewSettings.vertical);
|
||||
const annotPopupPos = getPopupPosition(
|
||||
triangPos,
|
||||
rect,
|
||||
@@ -135,21 +139,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setTranslatorPopupPosition(transPopupPos);
|
||||
setProofreadPopupPosition(proofreadPopupPos);
|
||||
setTrianglePosition(triangPos);
|
||||
}, [
|
||||
selection,
|
||||
bookKey,
|
||||
osPlatform,
|
||||
popupPadding,
|
||||
viewSettings.vertical,
|
||||
annotPopupHeight,
|
||||
annotPopupWidth,
|
||||
dictPopupWidth,
|
||||
dictPopupHeight,
|
||||
transPopupWidth,
|
||||
transPopupHeight,
|
||||
proofreadPopupWidth,
|
||||
proofreadPopupHeight,
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selection, bookKey, viewSettings.vertical]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedStyle(settings.globalReadSettings.highlightStyle);
|
||||
@@ -275,23 +266,40 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const onShowAnnotation = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { value: cfi, index, range } = detail;
|
||||
const { value, index, range } = detail;
|
||||
const { booknotes = [] } = getConfig(bookKey)!;
|
||||
const isNote = value.startsWith(NOTE_PREFIX);
|
||||
const cfi = isNote ? value.replace(NOTE_PREFIX, '') : value;
|
||||
const annotations = booknotes.filter(
|
||||
(booknote) => booknote.type === 'annotation' && !booknote.deletedAt,
|
||||
(booknote) => booknote.type === 'annotation' && !booknote.deletedAt && booknote.cfi === cfi,
|
||||
);
|
||||
const annotation = annotations.find(
|
||||
(annotation) => (!isNote && annotation.style) || (isNote && annotation.note),
|
||||
);
|
||||
const annotation = annotations.find((annotation) => annotation.cfi === cfi);
|
||||
if (!annotation) return;
|
||||
|
||||
const { style, color, text, note } = annotation;
|
||||
const selection = {
|
||||
key: bookKey,
|
||||
annotated: true,
|
||||
text: annotation.text ?? '',
|
||||
cfi: view?.getCFI(index, range),
|
||||
text: text ?? '',
|
||||
note: note ?? '',
|
||||
rect: isNote ? detail.rect : undefined,
|
||||
cfi,
|
||||
range,
|
||||
index,
|
||||
};
|
||||
setSelectedStyle(annotation.style!);
|
||||
setSelectedColor(annotation.color!);
|
||||
if (isNote) {
|
||||
setShowAnnotationNotes(true);
|
||||
setHighlightOptionsVisible(false);
|
||||
} else {
|
||||
setShowAnnotationNotes(false);
|
||||
setAnnotationNotes([]);
|
||||
if (style && color) {
|
||||
setSelectedStyle(style);
|
||||
setSelectedColor(color);
|
||||
}
|
||||
}
|
||||
setSelection(selection);
|
||||
handleUpToPopup();
|
||||
};
|
||||
@@ -353,7 +361,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
|
||||
if (!gridFrame) return;
|
||||
const rect = gridFrame.getBoundingClientRect();
|
||||
const triangPos = getPosition(selection.range, rect, popupPadding, viewSettings.vertical);
|
||||
const triangPos = getPosition(selection, rect, trianglePadding, viewSettings.vertical);
|
||||
const annotPopupPos = getPopupPosition(
|
||||
triangPos,
|
||||
rect,
|
||||
@@ -411,14 +419,36 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
CFI.compare(item.cfi, start) >= 0 &&
|
||||
CFI.compare(item.cfi, end) <= 0,
|
||||
);
|
||||
const notes = booknotes.filter(
|
||||
(item) =>
|
||||
!item.deletedAt &&
|
||||
item.type === 'annotation' &&
|
||||
item.note &&
|
||||
item.note.trim().length > 0 &&
|
||||
CFI.compare(item.cfi, start) >= 0 &&
|
||||
CFI.compare(item.cfi, end) <= 0,
|
||||
);
|
||||
try {
|
||||
Promise.all(annotations.map((annotation) => view?.addAnnotation(annotation)));
|
||||
Promise.all(
|
||||
notes.map((note) => view?.addAnnotation({ ...note, value: `${NOTE_PREFIX}${note.cfi}` })),
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.booknotes || !selection?.cfi || !showAnnotationNotes) return;
|
||||
const annotations = config.booknotes.filter(
|
||||
(booknote) =>
|
||||
booknote.type === 'annotation' && !booknote.deletedAt && booknote.cfi === selection.cfi,
|
||||
);
|
||||
const notes = annotations.filter((item) => item.note && item.note.trim().length > 0);
|
||||
setAnnotationNotes(notes);
|
||||
}, [selection?.cfi, showAnnotationNotes, config.booknotes]);
|
||||
|
||||
const handleShowAnnotPopup = () => {
|
||||
if (!appService?.isMobile) {
|
||||
containerRef.current?.focus();
|
||||
@@ -500,7 +530,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
};
|
||||
const existingIndex = annotations.findIndex(
|
||||
(annotation) =>
|
||||
annotation.cfi === cfi && annotation.type === 'annotation' && !annotation.deletedAt,
|
||||
annotation.cfi === cfi &&
|
||||
annotation.type === 'annotation' &&
|
||||
annotation.style &&
|
||||
!annotation.deletedAt,
|
||||
);
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
if (existingIndex !== -1) {
|
||||
@@ -779,9 +812,11 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
)}
|
||||
{showAnnotPopup && trianglePosition && annotPopupPosition && (
|
||||
<AnnotationPopup
|
||||
bookKey={bookKey}
|
||||
dir={viewSettings.rtl ? 'rtl' : 'ltr'}
|
||||
isVertical={viewSettings.vertical}
|
||||
buttons={buttons}
|
||||
notes={annotationNotes}
|
||||
position={annotPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
highlightOptionsVisible={highlightOptionsVisible}
|
||||
|
||||
@@ -12,15 +12,22 @@ const colors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
|
||||
|
||||
interface HighlightOptionsProps {
|
||||
isVertical: boolean;
|
||||
style: React.CSSProperties;
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
triangleDir: 'up' | 'down' | 'left' | 'right';
|
||||
selectedStyle: HighlightStyle;
|
||||
selectedColor: HighlightColor;
|
||||
onHandleHighlight: (update: boolean) => void;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT_PIX = 28;
|
||||
const OPTIONS_PADDING_PIX = 16;
|
||||
|
||||
const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
style,
|
||||
isVertical,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
triangleDir,
|
||||
selectedStyle: _selectedStyle,
|
||||
selectedColor: _selectedColor,
|
||||
onHandleHighlight,
|
||||
@@ -34,6 +41,8 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
const size16 = useResponsiveSize(16);
|
||||
const size18 = useResponsiveSize(18);
|
||||
const size28 = useResponsiveSize(28);
|
||||
const highlightOptionsHeightPx = useResponsiveSize(OPTIONS_HEIGHT_PIX);
|
||||
const highlightOptionsPaddingPx = useResponsiveSize(OPTIONS_PADDING_PIX);
|
||||
|
||||
const handleSelectStyle = (style: HighlightStyle) => {
|
||||
const newGlobalReadSettings = { ...globalReadSettings, highlightStyle: style };
|
||||
@@ -58,7 +67,23 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
'highlight-options absolute flex items-center justify-between',
|
||||
isVertical ? 'flex-col' : 'flex-row',
|
||||
)}
|
||||
style={style}
|
||||
style={{
|
||||
width: `${popupWidth}px`,
|
||||
height: `${popupHeight}px`,
|
||||
...(isVertical
|
||||
? {
|
||||
left: `${
|
||||
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
|
||||
(triangleDir === 'left' ? -1 : 1)
|
||||
}px`,
|
||||
}
|
||||
: {
|
||||
top: `${
|
||||
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
|
||||
(triangleDir === 'up' ? -1 : 1)
|
||||
}px`,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx('flex gap-2', isVertical ? 'flex-col' : 'flex-row')}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { eventDispatcher } from '@/utils/event';
|
||||
import { getBookDirFromLanguage } from '@/utils/book';
|
||||
import { Overlay } from '@/components/Overlay';
|
||||
import { saveSysSettings } from '@/helpers/settings';
|
||||
import { NOTE_PREFIX } from '@/types/view';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
@@ -118,6 +119,7 @@ const Notebook: React.FC = ({}) => {
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
view?.addAnnotation({ ...annotation, value: `${NOTE_PREFIX}${annotation.cfi}` });
|
||||
annotations.push(annotation);
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
@@ -128,6 +130,7 @@ const Notebook: React.FC = ({}) => {
|
||||
|
||||
const handleEditNote = (note: BookNote, isDelete: boolean) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
const config = getConfig(sideBarBookKey)!;
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const existingIndex = annotations.findIndex((item) => item.id === note.id);
|
||||
@@ -138,6 +141,7 @@ const Notebook: React.FC = ({}) => {
|
||||
note.updatedAt = Date.now();
|
||||
}
|
||||
annotations[existingIndex] = note;
|
||||
view?.addAnnotation({ ...note, value: `${NOTE_PREFIX}${note.cfi}` }, true);
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, sideBarBookKey, updatedConfig, settings);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { NOTE_PREFIX } from '@/types/view';
|
||||
import useScrollToItem from '../../hooks/useScrollToItem';
|
||||
import TextButton from '@/components/TextButton';
|
||||
import TextEditor, { TextEditorRef } from '@/components/TextEditor';
|
||||
@@ -57,7 +58,9 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
if (item.id === note.id) {
|
||||
item.deletedAt = Date.now();
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
views.forEach((view) => view?.addAnnotation(item, true));
|
||||
views.forEach((view) =>
|
||||
view?.addAnnotation({ ...item, value: `${NOTE_PREFIX}${item.cfi}` }, true),
|
||||
);
|
||||
}
|
||||
});
|
||||
const updatedConfig = updateBooknotes(bookKey, booknotes);
|
||||
|
||||
@@ -105,7 +105,7 @@ const Popup = ({
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={`triangle text-base-300 absolute ${triangleClassName}`}
|
||||
className={`triangle text-base-300 absolute z-50 ${triangleClassName}`}
|
||||
style={{
|
||||
left:
|
||||
trianglePosition?.dir === 'left'
|
||||
|
||||
@@ -3,6 +3,8 @@ import { BookNote, BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import { TTSGranularity } from '@/services/tts';
|
||||
import { TTS } from 'foliate-js/tts.js';
|
||||
|
||||
export const NOTE_PREFIX = 'foliate-note:';
|
||||
|
||||
export interface FoliateView extends HTMLElement {
|
||||
open: (book: BookDoc) => Promise<void>;
|
||||
close: () => void;
|
||||
@@ -15,7 +17,10 @@ export interface FoliateView extends HTMLElement {
|
||||
goRight: () => void;
|
||||
getCFI: (index: number, range: Range) => string;
|
||||
resolveCFI: (cfi: string) => { index: number; anchor: (doc: Document) => Range };
|
||||
addAnnotation: (note: BookNote, remove?: boolean) => { index: number; label: string };
|
||||
addAnnotation: (
|
||||
note: BookNote & { value?: string },
|
||||
remove?: boolean,
|
||||
) => { index: number; label: string };
|
||||
search: (config: BookSearchConfig) => AsyncGenerator<BookSearchResult | string, void, void>;
|
||||
clearSearch: () => void;
|
||||
select: (target: string | number | { fraction: number }) => void;
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface TextSelection {
|
||||
cfi?: string;
|
||||
href?: string;
|
||||
annotated?: boolean;
|
||||
rect?: Rect;
|
||||
}
|
||||
|
||||
const frameRect = (frame: Frame, rect?: Rect, sx = 1, sy = 1) => {
|
||||
@@ -68,17 +69,21 @@ const getIframeElement = (nodeElement: Range | Element): HTMLIFrameElement | nul
|
||||
|
||||
const constrainPointWithinRect = (point: Point, rect: Rect, padding: number) => {
|
||||
return {
|
||||
x: Math.max(padding, Math.min(point.x, rect.right - padding)),
|
||||
y: Math.max(padding, Math.min(point.y, rect.bottom - padding)),
|
||||
x: Math.max(padding, Math.min(point.x, rect.right - rect.left - padding)),
|
||||
y: Math.max(padding, Math.min(point.y, rect.bottom - rect.top - padding)),
|
||||
};
|
||||
};
|
||||
|
||||
export const getPosition = (
|
||||
target: Range | Element,
|
||||
targetElement: Range | Element | TextSelection,
|
||||
rect: Rect,
|
||||
paddingPx: number,
|
||||
isVertical: boolean = false,
|
||||
) => {
|
||||
const { range: target, rect: targetRect } =
|
||||
targetElement && 'range' in targetElement
|
||||
? targetElement
|
||||
: { range: targetElement, rect: undefined };
|
||||
const frameElement = getIframeElement(target);
|
||||
const transform = frameElement ? getComputedStyle(frameElement).transform : '';
|
||||
const match = transform.match(/matrix\((.+)\)/);
|
||||
@@ -103,8 +108,12 @@ export const getPosition = (
|
||||
left: rect.left + padding.left,
|
||||
};
|
||||
});
|
||||
const first = frameRect(frame, rects[0], sx, sy);
|
||||
const last = frameRect(frame, rects.at(-1), sx, sy);
|
||||
const first = targetRect
|
||||
? frameRect(frame, targetRect, sx, sy)
|
||||
: frameRect(frame, rects[0], sx, sy);
|
||||
const last = targetRect
|
||||
? frameRect(frame, targetRect, sx, sy)
|
||||
: frameRect(frame, rects.at(-1), sx, sy);
|
||||
|
||||
if (isVertical) {
|
||||
const leftSpace = first.left - rect.left;
|
||||
@@ -126,11 +135,19 @@ export const getPosition = (
|
||||
}
|
||||
|
||||
const start = {
|
||||
point: { x: (first.left + first.right) / 2 - rect.left, y: first.top - rect.top - 12 },
|
||||
point: constrainPointWithinRect(
|
||||
{ x: (first.left + first.right) / 2 - rect.left, y: first.top - rect.top - 12 },
|
||||
rect,
|
||||
paddingPx,
|
||||
),
|
||||
dir: 'up',
|
||||
} as Position;
|
||||
const end = {
|
||||
point: { x: (last.left + last.right) / 2 - rect.left, y: last.bottom - rect.top + 6 },
|
||||
point: constrainPointWithinRect(
|
||||
{ x: (last.left + last.right) / 2 - rect.left, y: last.bottom - rect.top + 6 },
|
||||
rect,
|
||||
paddingPx,
|
||||
),
|
||||
dir: 'down',
|
||||
} as Position;
|
||||
const startInView = pointIsInView(start.point);
|
||||
|
||||
@@ -386,7 +386,7 @@ const getLayoutStyles = (
|
||||
}
|
||||
img.has-text-siblings {
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
:is(div) > img.has-text-siblings[style*="object-fit"] {
|
||||
display: block;
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: c80ead5f34...873b545933
Reference in New Issue
Block a user