Skip to content

Commit fe5dd0f

Browse files
authored
fix(notes): export notes from library window (PR #2934 Fixes #2924)
1 parent bc4b304 commit fe5dd0f

File tree

23 files changed

+474
-324
lines changed

23 files changed

+474
-324
lines changed

src/common/readium/annotation/converter.ts

+11-109
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77

88
import * as debug_ from "debug";
99

10-
import { ICssSelector, IProgressionSelector, IReadiumAnnotation, IReadiumAnnotationSet, isCssSelector, ISelector, isProgressionSelector, isTextPositionSelector, isTextQuoteSelector, ITextPositionSelector, ITextQuoteSelector } from "./annotationModel.type";
10+
import { ICssSelector, IReadiumAnnotation, IReadiumAnnotationSet, isCssSelector, isProgressionSelector, isTextPositionSelector, isTextQuoteSelector, ITextPositionSelector, ITextQuoteSelector } from "./annotationModel.type";
1111
import { v4 as uuidv4 } from "uuid";
1212
import { _APP_NAME, _APP_VERSION } from "readium-desktop/preprocessor-directives";
1313
import { PublicationView } from "readium-desktop/common/views/publication";
1414
import { rgbToHex } from "readium-desktop/common/rgb";
1515
import { ICacheDocument } from "readium-desktop/common/redux/states/renderer/resourceCache";
1616
import { getDocumentFromICacheDocument } from "readium-desktop/utils/xmlDom";
17-
import { createCssSelectorMatcher, createTextPositionSelectorMatcher, createTextQuoteSelectorMatcher, describeTextPosition, describeTextQuote } from "readium-desktop/third_party/apache-annotator/dom";
17+
import { createCssSelectorMatcher, createTextPositionSelectorMatcher, createTextQuoteSelectorMatcher } from "readium-desktop/third_party/apache-annotator/dom";
1818
import { makeRefinable } from "readium-desktop/third_party/apache-annotator/selector";
19-
import { convertRange, convertRangeInfo, normalizeRange } from "@r2-navigator-js/electron/renderer/webview/selection";
19+
import { convertRange, normalizeRange } from "@r2-navigator-js/electron/renderer/webview/selection";
2020
import { MiniLocatorExtended } from "readium-desktop/common/redux/states/locatorInitialState";
2121
import { uniqueCssSelector } from "@r2-navigator-js/electron/renderer/common/cssselector3";
2222
import { IRangeInfo, ISelectedTextInfo, ISelectionInfo } from "@r2-navigator-js/electron/common/selection";
@@ -235,117 +235,19 @@ export async function convertSelectorTargetToLocatorExtended(target: IReadiumAnn
235235
return locatorExtended;
236236
}
237237

238-
export type INoteStateWithICacheDocument = INoteState & { __cacheDocument?: ICacheDocument | undefined };
238+
// export type INoteStateWithICacheDocument = INoteState & { __cacheDocument?: ICacheDocument | undefined };
239239

240-
const describeCssSelectorWithTextPosition = async (range: Range, document: Document, root: HTMLElement): Promise<ICssSelector<ITextPositionSelector> | undefined> => {
241-
// normalizeRange can fundamentally alter the DOM Range by repositioning / snapping to Text boundaries, this is an internal implementation detail inside navigator when CREATING ranges from user document selections.
242-
// const rangeNormalize = normalizeRange(range); // from r2-nav and not from third-party/apache-annotator
240+
export function convertAnnotationStateToReadiumAnnotation(note: INoteState): IReadiumAnnotation {
243241

244-
const commonAncestorHTMLElement =
245-
(range.commonAncestorContainer && range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE)
246-
? range.commonAncestorContainer as Element
247-
: (range.startContainer.parentNode && range.startContainer.parentNode.nodeType === Node.ELEMENT_NODE)
248-
? range.startContainer.parentNode as Element
249-
: undefined;
250-
if (!commonAncestorHTMLElement) {
251-
return undefined;
252-
}
253-
254-
return {
255-
type: "CssSelector",
256-
value: uniqueCssSelector(commonAncestorHTMLElement, document, { root }),
257-
refinedBy: await describeTextPosition(
258-
range,
259-
commonAncestorHTMLElement,
260-
),
261-
};
262-
};
263-
264-
export async function convertAnnotationStateToSelector(annotationWithCacheDoc: INoteStateWithICacheDocument, isLcp: boolean): Promise<[ISelector[], isABookmark: boolean]> {
265-
266-
const selector: ISelector<any>[] = [];
267-
268-
const {__cacheDocument, ...annotation} = annotationWithCacheDoc;
269-
270-
const xmlDom = getDocumentFromICacheDocument(__cacheDocument);
271-
if (!xmlDom) {
272-
return [[], false];
273-
}
274-
275-
const document = xmlDom;
276-
const root = xmlDom.body;
277-
278-
const { locatorExtended, drawType } = annotation;
279-
const { selectionInfo, locator } = locatorExtended;
280-
const { locations } = locator;
281-
const { progression } = locations;
282-
283-
// the range start/end is guaranteed in document order (internally used in navigator whenever deserialising DOM Ranges from JSON expression) ... but DOM Ranges are always ordered anyway (only the user / document selection object can be reversed)
284-
const rangeInfo = selectionInfo?.rangeInfo || locator.locations.caretInfo?.rangeInfo;
285-
if (!rangeInfo) {
286-
debug("ERROR!! RangeInfo not defined !!!");
287-
debug(rangeInfo);
288-
return [selector, false];
289-
}
290-
const range = convertRangeInfo(xmlDom, rangeInfo);
291-
debug("Dump range memory found:", range);
292-
293-
if (range.collapsed) {
294-
debug("RANGE COLLAPSED??! skipping...");
295-
return [selector, false];
296-
}
297-
298-
// createTextPositionSelectorMatcher()
299-
const selectorCssSelectorWithTextPosition = await describeCssSelectorWithTextPosition(range, document, root);
300-
if (selectorCssSelectorWithTextPosition) {
301-
302-
debug("CssWithTextPositionSelector : ", selectorCssSelectorWithTextPosition);
303-
selector.push(selectorCssSelectorWithTextPosition);
304-
}
305-
306-
// describeTextPosition()
307-
const selectorTextPosition = await describeTextPosition(range, root);
308-
debug("TextPositionSelector : ", selectorTextPosition);
309-
selector.push(selectorTextPosition);
310-
311-
if (!isLcp) {
312-
313-
// describeTextQuote()
314-
const selectorTextQuote = await describeTextQuote(range, root);
315-
debug("TextQuoteSelector : ", selectorTextQuote);
316-
selector.push(selectorTextQuote);
317-
}
318-
319-
const progressionSelector: IProgressionSelector = {
320-
type: "ProgressionSelector",
321-
value: progression || -1,
322-
};
323-
debug("ProgressionSelector : ", progressionSelector);
324-
selector.push(progressionSelector);
325-
326-
// Next TODO: CFI !?!
327-
328-
// this normally occurs at import time, but let's save debugging effort by checking immediately when exporting...
329-
// errors are non-fatal, just hunt for the "IRangeInfo DIFF" console logs
330-
const isABookmark = drawType === EDrawType.bookmark; // rangeInfo.endContainerChildTextNodeIndex === rangeInfo.startContainerChildTextNodeIndex && rangeInfo.endContainerElementCssSelector === rangeInfo.startContainerElementCssSelector && rangeInfo.endOffset - rangeInfo.startOffset === 1;
331-
if (IS_DEV) {
332-
await convertSelectorTargetToLocatorExtended({ source: "", selector }, __cacheDocument, rangeInfo, isABookmark);
333-
}
334-
return [selector, isABookmark];
335-
}
336-
337-
export async function convertAnnotationStateToReadiumAnnotation(annotation: INoteStateWithICacheDocument, isLcp: boolean): Promise<IReadiumAnnotation> {
338-
339-
const { uuid, color, locatorExtended: def, tags, drawType, textualValue, creator, created, modified } = annotation;
242+
const { uuid, color, locatorExtended: def, tags, drawType, textualValue, creator, created, modified, readiumAnnotation } = note;
340243
const { locator, headings, epubPage/*, selectionInfo*/ } = def;
341244
const { href /*text, locations*/ } = locator;
342245
// const { afterRaw, beforeRaw, highlightRaw } = text || {};
343246
// const { rangeInfo: rangeInfoSelection } = selectionInfo || {};
344247
// const { progression } = locations;
345248

346249
const highlight = (drawType === EDrawType.solid_background ? "solid" : EDrawType[drawType]) as IReadiumAnnotation["body"]["highlight"];
347-
348-
const [selector, isABookmark] = await convertAnnotationStateToSelector(annotation, isLcp);
250+
const isABookmark = drawType === EDrawType.bookmark;
349251

350252
return {
351253
"@context": "http://www.w3.org/ns/anno.jsonld",
@@ -374,17 +276,17 @@ export async function convertAnnotationStateToReadiumAnnotation(annotation: INot
374276
headings: (headings || []).map(({ txt, level }) => ({ txt, level })),
375277
page: epubPage || "",
376278
},
377-
selector,
279+
selector: readiumAnnotation?.export?.selector || [],
378280
},
379281
motivation: isABookmark ? "bookmarking" : undefined, // isABookmark = drawType === EDrawType.bookmark
380282
};
381283
}
382284

383-
export async function convertAnnotationStateArrayToReadiumAnnotationSet(locale: keyof typeof availableLanguages, annotationArray: INoteStateWithICacheDocument[], publicationView: PublicationView, label?: string): Promise<IReadiumAnnotationSet> {
285+
export function convertAnnotationStateArrayToReadiumAnnotationSet(locale: keyof typeof availableLanguages, notes: INoteState[], publicationView: PublicationView, label?: string): IReadiumAnnotationSet {
384286

385287
const currentDate = new Date();
386288
const dateString: string = currentDate.toISOString();
387-
const isLcp = !!publicationView.lcp;
289+
// const iLcp = !!publicationView.lcp;
388290

389291
return {
390292
"@context": "http://www.w3.org/ns/anno.jsonld",
@@ -412,6 +314,6 @@ export async function convertAnnotationStateArrayToReadiumAnnotationSet(locale:
412314
}) : [],
413315
"dc:date": publicationView.publishedAt || "",
414316
},
415-
items: await Promise.all((annotationArray || []).map(async (v) => await convertAnnotationStateToReadiumAnnotation(v, isLcp))),
317+
items: notes.map((v) => convertAnnotationStateToReadiumAnnotation(v)),
416318
};
417319
}

src/common/redux/states/renderer/note.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { hexToRgb } from "readium-desktop/common/rgb";
1010
import { TTranslatorKeyParameter } from "readium-desktop/typings/en.translation-keys";
1111
import { MiniLocatorExtended } from "../locatorInitialState";
1212
import { INoteCreator } from "../creator";
13-
import { IReadiumAnnotation } from "readium-desktop/common/readium/annotation/annotationModel.type";
13+
import { IReadiumAnnotation, ISelector } from "readium-desktop/common/readium/annotation/annotationModel.type";
1414

1515
// DO NOT REMOVE THIS COMMENT BLOCK (USED FOR TRANSLATOR KEYS DETECTION DURING CODE SCANNING)
1616
// __("reader.notes.colors.red")
@@ -92,6 +92,13 @@ export interface INoteState {
9292
created: number;
9393
creator?: INoteCreator;
9494
group: "bookmark" | "annotation";
95+
readiumAnnotation?: {
96+
export?: {
97+
selector: ISelector[];
98+
}
99+
100+
// TODO: import !?
101+
}
95102
}
96103

97104
export type TDrawView = "annotation" | "margin" | "hide";

src/main/redux/sagas/annotation.ts

+27-22
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,32 @@ const filename_ = "readium-desktop:main:saga:annotationsImporter";
3434
const debug = debug_(filename_);
3535
debug("_");
3636

37+
export function* getAnnotationFromMainWinState(publicationIdentifier: string): SagaGenerator<INoteState[]> {
38+
39+
let annotations: INoteState[] = [];
40+
const sessionReader = yield* selectTyped((state: RootState) => state.win.session.reader);
41+
const winSessionReaderStateArray = Object.values(sessionReader).filter((v) => v.publicationIdentifier === publicationIdentifier);
42+
if (winSessionReaderStateArray.length) {
43+
const winSessionReaderStateFirst = winSessionReaderStateArray[0]; // TODO: get the first only !?!
44+
annotations = winSessionReaderStateFirst?.reduxState?.note || [];
45+
46+
debug("current publication AnnotationsList come from the readerSession (there are one or many readerWin currently open)");
47+
} else {
48+
const sessionRegistry = yield* selectTyped((state: RootState) => state.win.registry.reader);
49+
if (Object.keys(sessionRegistry).find((v) => v === publicationIdentifier)) {
50+
annotations = sessionRegistry[publicationIdentifier]?.reduxState?.note || [];
51+
52+
debug("current publication AnnotationsList come from the readerRegistry (no readerWin currently open)");
53+
}
54+
}
55+
debug("There are", annotations.length, "annotation(s) loaded from the current publicationIdentifier");
56+
if (!annotations.length) {
57+
debug("Be careful, there are no annotation loaded for this publication!");
58+
}
59+
60+
return annotations;
61+
}
62+
3763
function* importAnnotationSet(action: annotationActions.importAnnotationSet.TAction): SagaGenerator<void> {
3864

3965
const { payload: { publicationIdentifier, winId } } = action;
@@ -121,28 +147,7 @@ function* importAnnotationSet(action: annotationActions.importAnnotationSet.TAct
121147
debug("GOOD ! spineItemHref matched : publication identified, let's continue the importation");
122148

123149
// OK publication identified
124-
125-
let annotations: INoteState[] = [];
126-
const sessionReader = yield* selectTyped((state: RootState) => state.win.session.reader);
127-
const winSessionReaderStateArray = Object.values(sessionReader).filter((v) => v.publicationIdentifier === publicationIdentifier);
128-
if (winSessionReaderStateArray.length) {
129-
const winSessionReaderStateFirst = winSessionReaderStateArray[0]; // TODO: get the first only !?!
130-
annotations = winSessionReaderStateFirst?.reduxState?.note || [];
131-
132-
debug("current publication AnnotationsList come from the readerSession (there are one or many readerWin currently open)");
133-
} else {
134-
const sessionRegistry = yield* selectTyped((state: RootState) => state.win.registry.reader);
135-
if (Object.keys(sessionRegistry).find((v) => v === publicationIdentifier)) {
136-
annotations = sessionRegistry[publicationIdentifier]?.reduxState?.note || [];
137-
138-
debug("current publication AnnotationsList come from the readerRegistry (no readerWin currently open)");
139-
}
140-
}
141-
debug("There are", annotations.length, "annotation(s) loaded from the current publicationIdentifier");
142-
if (!annotations.length) {
143-
debug("Be careful, there are no annotation loaded for this publication!");
144-
}
145-
150+
const annotations = yield* callTyped(getAnnotationFromMainWinState, publicationIdentifier);
146151

147152
const annotationsParsedNoConflictArray: INotePreParsingState[] = [];
148153
const annotationsParsedConflictOlderArray: INotePreParsingState[] = [];

src/main/streamer/streamerNoHttp.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ import {
4747
// import { OPDS_MEDIA_SCHEME } from "readium-desktop/main/redux/sagas/getEventChannel";
4848
import { THORIUM_READIUM2_ELECTRON_HTTP_PROTOCOL } from "readium-desktop/common/streamerProtocol";
4949
import { findMimeTypeWithExtension } from "readium-desktop/utils/mimeTypes";
50+
import { diMainGet } from "../di";
51+
import { getAnnotationFromMainWinState } from "../redux/sagas/annotation";
52+
import { INoteState } from "readium-desktop/common/redux/states/renderer/note";
5053

5154
// import { _USE_HTTP_STREAMER } from "readium-desktop/preprocessor-directives";
5255

@@ -215,6 +218,9 @@ const streamProtocolHandler = async (
215218
}
216219
}
217220

221+
const notesFromPublicationPrefix = "/publication-notes/";
222+
const isNotesFromPublicationRequest = uPathname.startsWith(notesFromPublicationPrefix);
223+
218224
const pdfjsAssetsPrefix = "/pdfjs/";
219225
const isPdfjsAssets = uPathname.startsWith(pdfjsAssetsPrefix);
220226

@@ -274,7 +280,27 @@ const streamProtocolHandler = async (
274280
headers["Access-Control-Allow-Headers"] = "Content-Type, Content-Length, Accept-Ranges, Content-Range, Range, Link, Transfer-Encoding, X-Requested-With, Authorization, Accept, Origin, User-Agent, DNT, Cache-Control, Keep-Alive, If-Modified-Since";
275281
headers["Access-Control-Expose-Headers"] = "Content-Type, Content-Length, Accept-Ranges, Content-Range, Range, Link, Transfer-Encoding, X-Requested-With, Authorization, Accept, Origin, User-Agent, DNT, Cache-Control, Keep-Alive, If-Modified-Since";
276282

277-
if (isPdfjsAssets) {
283+
if (isNotesFromPublicationRequest) {
284+
285+
const publicationUUID = uPathname.substr(notesFromPublicationPrefix.length);
286+
287+
const sagaMiddleware = diMainGet("saga-middleware");
288+
const notes = await sagaMiddleware.run(getAnnotationFromMainWinState, publicationUUID).toPromise<INoteState[]>();
289+
const notesSerialized = JSON.stringify(notes);
290+
const notesSerializedBuf = Buffer.from(notesSerialized, "utf-8");
291+
const contentLength = `${notesSerializedBuf.length || 0}`;
292+
headers["Content-Length"] = contentLength;
293+
const contentType = "application/json; charset=utf-8";
294+
headers["Content-Type"] = contentType;
295+
296+
const obj = {
297+
data: bufferToStream(notesSerializedBuf),
298+
headers,
299+
statusCode: 200,
300+
};
301+
callback(obj);
302+
return;
303+
} else if (isPdfjsAssets) {
278304

279305
const pdfjsUrlPathname = uPathname.substr(pdfjsAssetsPrefix.length);
280306
debug("PDFJS request this file:", pdfjsUrlPathname);

0 commit comments

Comments
 (0)