feat(reader): send end-anchor CFI for EPUB highlights

Web highlights stored only epubcfi_start, so devices received
degenerate pos0 == pos1 (zero-length) highlight ranges. The reader
now collapses the selection range to its end point for a second CFI
and stores it as epubcfi_end (PDF rect anchors reuse the JSON anchor
for both ends).
This commit is contained in:
2026-08-18 19:13:51 -04:00
parent 1585aa1073
commit 97e546b2a4
+16
View File
@@ -412,6 +412,7 @@ document.addEventListener("alpine:init", () => {
note: string; note: string;
color: string; color: string;
cfi: string; cfi: string;
cfiEnd: string;
percentage: number; percentage: number;
pdfPage: number; pdfPage: number;
pdfRects: number[][]; pdfRects: number[][];
@@ -443,6 +444,7 @@ document.addEventListener("alpine:init", () => {
y: 0, y: 0,
text: "", text: "",
cfi: "", cfi: "",
cfiEnd: "",
id: "", id: "",
color: "#ffd54f", color: "#ffd54f",
note: "", note: "",
@@ -701,8 +703,15 @@ document.addEventListener("alpine:init", () => {
const text = sel.toString().replace(/\s+/g, " ").trim(); const text = sel.toString().replace(/\s+/g, " ").trim();
if (!text) return; if (!text) return;
let cfi: string; let cfi: string;
let cfiEnd: string;
try { try {
cfi = this.view.getCFI(index, range); cfi = this.view.getCFI(index, range);
// Collapse to the end point for a distinct end anchor —
// KOReader sync renders the highlight box from pos0/pos1, and
// pos1 == pos0 would be a degenerate (zero-length) range.
const endRange = range.cloneRange();
endRange.collapse(false);
cfiEnd = this.view.getCFI(index, endRange);
} catch { } catch {
return; return;
} }
@@ -715,6 +724,7 @@ document.addEventListener("alpine:init", () => {
y: (iframeRect?.top ?? 0) + rect.top, y: (iframeRect?.top ?? 0) + rect.top,
text, text,
cfi, cfi,
cfiEnd,
}); });
}; };
doc.addEventListener( doc.addEventListener(
@@ -827,6 +837,7 @@ document.addEventListener("alpine:init", () => {
y: (iframeRect?.top ?? 0) + rect.top, y: (iframeRect?.top ?? 0) + rect.top,
text: h.text, text: h.text,
cfi: h.cfi, cfi: h.cfi,
cfiEnd: h.cfiEnd,
id: h.id, id: h.id,
color: h.color, color: h.color,
note: h.note, note: h.note,
@@ -1070,6 +1081,7 @@ document.addEventListener("alpine:init", () => {
y: number; y: number;
text: string; text: string;
cfi: string; cfi: string;
cfiEnd?: string;
id?: string; id?: string;
color?: string; color?: string;
note?: string; note?: string;
@@ -1080,6 +1092,7 @@ document.addEventListener("alpine:init", () => {
p.mode = opts.mode; p.mode = opts.mode;
p.text = opts.text; p.text = opts.text;
p.cfi = opts.cfi; p.cfi = opts.cfi;
p.cfiEnd = opts.cfiEnd ?? "";
p.id = opts.id ?? ""; p.id = opts.id ?? "";
p.color = opts.color || "#ffd54f"; p.color = opts.color || "#ffd54f";
p.note = opts.note ?? ""; p.note = opts.note ?? "";
@@ -1147,6 +1160,7 @@ document.addEventListener("alpine:init", () => {
note: r.note_text ?? "", note: r.note_text ?? "",
color: r.color ?? "#ffff00", color: r.color ?? "#ffff00",
cfi, cfi,
cfiEnd: r.epubcfi_end ?? "",
percentage: r.percentage_start ?? 0, percentage: r.percentage_start ?? 0,
pdfPage, pdfPage,
pdfRects, pdfRects,
@@ -1205,6 +1219,7 @@ document.addEventListener("alpine:init", () => {
start_position: "", start_position: "",
end_position: "", end_position: "",
epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi, epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi,
epubcfi_end: p.pdfPage >= 0 ? pdfAnchor : p.cfiEnd,
color, color,
note_text: "", note_text: "",
percentage_start: this.lastRelocateDetail?.fraction ?? 0, percentage_start: this.lastRelocateDetail?.fraction ?? 0,
@@ -1263,6 +1278,7 @@ document.addEventListener("alpine:init", () => {
start_position: "", start_position: "",
end_position: "", end_position: "",
epubcfi_start: anchor, epubcfi_start: anchor,
epubcfi_end: p.pdfPage >= 0 ? anchor : p.cfiEnd,
color: p.color, color: p.color,
note_text: p.note, note_text: p.note,
}), }),