A highlight and its note are now a single BookNote. Adding a note attaches it to the highlight at that CFI (or creates one with the current global style) instead of creating a second record, and a unified record renders as both a highlight overlay and a note bubble. - onDrawAnnotation chooses the draw kind from the overlay value prefix (cfi -> highlight, NOTE_PREFIX -> bubble) instead of annotation.note, so a record with both a style and a note draws both. Fixes notes synced from KOReader losing their highlight (#4511). - handleSaveNote updates the existing annotation at the CFI rather than pushing a new record (#3870); re-styling preserves the note. - unifyAnnotations migration (book config schema v1 -> v2, run in deserializeConfig) collapses existing split highlight+note records into one survivor and tombstones the redundant record (deletedAt) so the merge syncs to the cloud and KOReader. - Sidebar: a note's quoted highlight text uses the theme foreground so it stays legible on the highlight background. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,9 @@ import { TransformContext } from '@/services/transformers/types';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import {
|
||||
buildTTSSentenceHighlight,
|
||||
decideAnnotationDraw,
|
||||
getHighlightColorHex,
|
||||
mergeRestyledAnnotation,
|
||||
removeBookNoteOverlays,
|
||||
} from '../../utils/annotatorUtil';
|
||||
import { buildAnnotationIndex, selectLocationAnnotations } from '../../utils/annotationIndex';
|
||||
@@ -495,21 +497,27 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { draw, annotation, doc, range } = detail;
|
||||
const { style, color } = annotation as BookNote;
|
||||
const value = (annotation as BookNote & { value?: string }).value;
|
||||
const hexColor = getHighlightColorHex(settings, color);
|
||||
const einkBgColor = isDarkMode ? '#000000' : '#ffffff';
|
||||
const einkFgColor = isDarkMode ? '#ffffff' : '#000000';
|
||||
if (annotation.note) {
|
||||
// Choose what to draw from the overlay's `value` (cfi vs NOTE_PREFIX+cfi),
|
||||
// not from `annotation.note`: a unified record (style + note) is added as
|
||||
// two overlays and must draw a highlight for the cfi overlay AND a bubble
|
||||
// for the note overlay. Keying off `note` drew only the bubble (#4511).
|
||||
const kind = decideAnnotationDraw(value, style);
|
||||
if (kind === 'bubble') {
|
||||
const { defaultView } = doc;
|
||||
const node = range.startContainer;
|
||||
const el = node.nodeType === 1 ? node : node.parentElement;
|
||||
const { writingMode } = defaultView.getComputedStyle(el);
|
||||
draw(Overlayer.bubble, { writingMode });
|
||||
} else if (style === 'highlight') {
|
||||
} else if (kind === 'highlight') {
|
||||
draw(Overlayer.highlight, {
|
||||
color: isBwEink ? einkBgColor : hexColor,
|
||||
vertical: viewSettings.vertical,
|
||||
});
|
||||
} else if (['underline', 'squiggly'].includes(style as string)) {
|
||||
} else if (kind === 'underline' || kind === 'squiggly') {
|
||||
const { defaultView } = doc;
|
||||
const node = range.startContainer;
|
||||
const el = node.nodeType === 1 ? node : node.parentElement;
|
||||
@@ -522,7 +530,7 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
const padding = viewSettings.vertical
|
||||
? (lineHeightValue - fontSizeValue) / 2 - strokeWidth + verticalCompensation
|
||||
: (lineHeightValue - fontSizeValue) / 2 - strokeWidth + horizontalCompensation;
|
||||
draw(Overlayer[style as keyof typeof Overlayer], {
|
||||
draw(Overlayer[kind], {
|
||||
writingMode,
|
||||
color: isBwEink ? einkFgColor : hexColor,
|
||||
padding,
|
||||
@@ -1089,15 +1097,17 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
views.forEach((view) => removeGlobalAnnotationOverlays(view, existing));
|
||||
}
|
||||
if (update) {
|
||||
annotation.id = existing.id;
|
||||
// Carry the existing `global` flag forward — toggling color/style
|
||||
// shouldn't silently demote a global highlight back to single-range.
|
||||
if (existing.global) annotation.global = true;
|
||||
annotations[existingIndex] = annotation;
|
||||
views.forEach((view) => view?.addAnnotation(annotation));
|
||||
if (annotation.global) {
|
||||
// Preserve the note/text/createdAt and the `global` flag of the existing
|
||||
// record so a restyle (color/style change) of a unified annotation
|
||||
// doesn't wipe its note or silently demote a global highlight. The note
|
||||
// bubble overlay (NOTE_PREFIX) isn't torn down above, so it persists; we
|
||||
// only redraw the highlight overlay (value = cfi).
|
||||
const merged = mergeRestyledAnnotation(existing, annotation);
|
||||
annotations[existingIndex] = merged;
|
||||
views.forEach((view) => view?.addAnnotation(merged));
|
||||
if (merged.global) {
|
||||
views.forEach((view) => {
|
||||
if (view) expandAllRenderedSections(view, annotation);
|
||||
if (view) expandAllRenderedSections(view, merged);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Overlay } from '@/components/Overlay';
|
||||
import { saveSysSettings } from '@/helpers/settings';
|
||||
import { NOTE_PREFIX } from '@/types/view';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import { findAnnotationAtCfi } from '../../utils/annotatorUtil';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import AIAssistant from './AIAssistant';
|
||||
import NotebookHeader from './Header';
|
||||
@@ -154,18 +155,42 @@ const Notebook: React.FC = ({}) => {
|
||||
if (!cfi) return;
|
||||
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const annotation: BookNote = {
|
||||
id: uniqueId(),
|
||||
type: 'annotation',
|
||||
cfi,
|
||||
note,
|
||||
page: selection.page,
|
||||
text: selection.text,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
view?.addAnnotation({ ...annotation, value: `${NOTE_PREFIX}${annotation.cfi}` });
|
||||
annotations.push(annotation);
|
||||
const existingIndex = findAnnotationAtCfi(annotations, cfi);
|
||||
if (existingIndex !== -1) {
|
||||
// Attach the note to the existing highlight at this CFI instead of
|
||||
// creating a second record. The highlight overlay (value = cfi) already
|
||||
// exists; add the note bubble overlay (value = NOTE_PREFIX+cfi).
|
||||
const existing = annotations[existingIndex]!;
|
||||
const updated: BookNote = {
|
||||
...existing,
|
||||
note,
|
||||
text: selection.text || existing.text,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
annotations[existingIndex] = updated;
|
||||
view?.addAnnotation({ ...updated, value: `${NOTE_PREFIX}${updated.cfi}` });
|
||||
} else {
|
||||
// No highlight at this CFI yet (e.g. a note added without first
|
||||
// highlighting): create one unified record with the current global style
|
||||
// so the note still shows an underlying highlight, and draw both overlays.
|
||||
const style = settings.globalReadSettings.highlightStyle;
|
||||
const color = settings.globalReadSettings.highlightStyles[style];
|
||||
const annotation: BookNote = {
|
||||
id: uniqueId(),
|
||||
type: 'annotation',
|
||||
cfi,
|
||||
style,
|
||||
color,
|
||||
note,
|
||||
page: selection.page,
|
||||
text: selection.text,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
view?.addAnnotation(annotation);
|
||||
view?.addAnnotation({ ...annotation, value: `${NOTE_PREFIX}${annotation.cfi}` });
|
||||
annotations.push(annotation);
|
||||
}
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, sideBarBookKey, updatedConfig, settings);
|
||||
|
||||
@@ -199,7 +199,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, isNearest, o
|
||||
<span
|
||||
className={clsx(
|
||||
'booknote-text inline leading-normal',
|
||||
item.note && 'content font-size-xs text-gray-500',
|
||||
item.note && 'content font-size-xs text-base-content',
|
||||
(item.style === 'underline' || item.style === 'squiggly') &&
|
||||
'underline decoration-2',
|
||||
item.style === 'highlight' && 'rounded-[4px] px-[2px] py-[1px]',
|
||||
|
||||
Reference in New Issue
Block a user