From f7b410e1c6ac81db2833d4c9b428078a6fee521c Mon Sep 17 00:00:00 2001 From: Pierre Leroux Date: Wed, 18 Sep 2024 14:24:29 +0200 Subject: [PATCH] feat(annotation): export reader annotations list to the readium annotations json format (PR #2551) --- .../annotation/exportW3CAnnotationSet.ts | 29 ---- src/common/redux/actions/annotation/index.ts | 12 -- src/common/redux/actions/index.ts | 2 - .../w3c/annotation/annotationModel.type.ts | 6 +- .../w3c/annotation/converter.ts | 40 ++--- src/main/redux/sagas/annotation.ts | 88 ----------- src/main/redux/sagas/index.ts | 4 - src/main/w3c/annotation/fs.ts | 25 --- .../publication/menu/CatalogMenu.tsx | 11 +- src/renderer/library/redux/middleware/sync.ts | 2 - src/renderer/reader/components/ReaderMenu.tsx | 28 +++- src/resources/locales/ar.json | 2 + src/resources/locales/bg.json | 2 + src/resources/locales/ca.json | 2 + src/resources/locales/da.json | 8 +- src/resources/locales/de.json | 2 + src/resources/locales/el.json | 2 + src/resources/locales/en.json | 2 + src/resources/locales/es.json | 2 + src/resources/locales/eu.json | 2 + src/resources/locales/fi.json | 2 + src/resources/locales/fr.json | 2 + src/resources/locales/gl.json | 2 + src/resources/locales/hr.json | 2 + src/resources/locales/it.json | 2 + src/resources/locales/ja.json | 2 + src/resources/locales/ka.json | 2 + src/resources/locales/ko.json | 2 + src/resources/locales/lt.json | 2 + src/resources/locales/nl.json | 2 + src/resources/locales/pt-br.json | 2 + src/resources/locales/pt-pt.json | 2 + src/resources/locales/ru.json | 2 + src/resources/locales/sl.json | 2 + src/resources/locales/sv.json | 2 + src/resources/locales/zh-cn.json | 2 + src/resources/locales/zh-tw.json | 2 + src/typings/en.translation.d.ts | 144 +++++++++--------- 38 files changed, 175 insertions(+), 274 deletions(-) delete mode 100644 src/common/redux/actions/annotation/exportW3CAnnotationSet.ts delete mode 100644 src/common/redux/actions/annotation/index.ts rename src/{main => common}/w3c/annotation/annotationModel.type.ts (94%) rename src/{main => common}/w3c/annotation/converter.ts (74%) delete mode 100644 src/main/redux/sagas/annotation.ts delete mode 100644 src/main/w3c/annotation/fs.ts diff --git a/src/common/redux/actions/annotation/exportW3CAnnotationSet.ts b/src/common/redux/actions/annotation/exportW3CAnnotationSet.ts deleted file mode 100644 index 37dcac015..000000000 --- a/src/common/redux/actions/annotation/exportW3CAnnotationSet.ts +++ /dev/null @@ -1,29 +0,0 @@ -// ==LICENSE-BEGIN== -// Copyright 2017 European Digital Reading Lab. All rights reserved. -// Licensed to the Readium Foundation under one or more contributor license agreements. -// Use of this source code is governed by a BSD-style license -// that can be found in the LICENSE file exposed on Github (readium) in the project repository. -// ==LICENSE-END== - -import { Action } from "readium-desktop/common/models/redux"; - -export const ID = "ANNOTATION_EXPORT_W3CANNOTATIONSET"; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Payload { - publicationIdentifier: string; - // filePath: string, -} - -export function build(publicationIdentifier: string/*, filePath: string*/): Action { - - return { - type: ID, - payload: { - publicationIdentifier, - // filePath, - }, - }; -} -build.toString = () => ID; // Redux StringableActionCreator -export type TAction = ReturnType; diff --git a/src/common/redux/actions/annotation/index.ts b/src/common/redux/actions/annotation/index.ts deleted file mode 100644 index cd168a42f..000000000 --- a/src/common/redux/actions/annotation/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -// ==LICENSE-BEGIN== -// Copyright 2017 European Digital Reading Lab. All rights reserved. -// Licensed to the Readium Foundation under one or more contributor license agreements. -// Use of this source code is governed by a BSD-style license -// that can be found in the LICENSE file exposed on Github (readium) in the project repository. -// ==LICENSE-END== - -import * as exportW3CAnnotationSetFromAnnotations from "./exportW3CAnnotationSet"; - -export { - exportW3CAnnotationSetFromAnnotations, -}; diff --git a/src/common/redux/actions/index.ts b/src/common/redux/actions/index.ts index bca2b374c..d9d4d692c 100644 --- a/src/common/redux/actions/index.ts +++ b/src/common/redux/actions/index.ts @@ -24,7 +24,6 @@ import * as publicationActions from "./publication"; import * as themeActions from "./theme"; import * as wizardActions from "./wizard"; import * as versionUpdateActions from "./version-update"; -import * as annotationActions from "./annotation"; export { historyActions, @@ -46,5 +45,4 @@ export { themeActions, wizardActions, versionUpdateActions, - annotationActions, }; diff --git a/src/main/w3c/annotation/annotationModel.type.ts b/src/common/w3c/annotation/annotationModel.type.ts similarity index 94% rename from src/main/w3c/annotation/annotationModel.type.ts rename to src/common/w3c/annotation/annotationModel.type.ts index f6122420c..f52a35595 100644 --- a/src/main/w3c/annotation/annotationModel.type.ts +++ b/src/common/w3c/annotation/annotationModel.type.ts @@ -15,8 +15,10 @@ export interface IW3CAnnotationModel { body: { type: string; value: string; - format: string; - color: string; + tag?: string; + highlight?: "solid" | "underline" | "strikethrough" | "outline"; + format?: string; + color?: string; textDirection?: string; language?: string; }; diff --git a/src/main/w3c/annotation/converter.ts b/src/common/w3c/annotation/converter.ts similarity index 74% rename from src/main/w3c/annotation/converter.ts rename to src/common/w3c/annotation/converter.ts index 96b377ed8..4e8469a3b 100644 --- a/src/main/w3c/annotation/converter.ts +++ b/src/common/w3c/annotation/converter.ts @@ -5,7 +5,7 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { IW3CAnnotationModel, IW3CAnnotationModelSet, IW3CAnnotationSetAboutView } from "./annotationModel.type"; +import { IW3CAnnotationModel, IW3CAnnotationModelSet } from "./annotationModel.type"; import { v4 as uuidv4 } from "uuid"; import { _APP_NAME, _APP_VERSION } from "readium-desktop/preprocessor-directives"; import { PublicationView } from "readium-desktop/common/views/publication"; @@ -16,11 +16,13 @@ export function convertAnnotationToW3CAnnotationModel(annotation: IAnnotationSta const currentDate = new Date(); const dateString: string = currentDate.toISOString(); - const { uuid, color, locatorExtended: def } = annotation; + const { uuid, color, locatorExtended: def, tags, drawType } = annotation; const { selectionInfo, locator, headings, epubPage } = def; const { cleanText, rawText, rawBefore, rawAfter } = selectionInfo; const { href } = locator; + const highlight: IW3CAnnotationModel["body"]["highlight"] = drawType === "solid_background" ? "solid" : drawType; + return { "@context": "http://www.w3.org/ns/anno.jsonld", id: uuid ? "urn:uuid:" + uuid : "", @@ -33,6 +35,8 @@ export function convertAnnotationToW3CAnnotationModel(annotation: IAnnotationSta value: cleanText || "", format: "text/plain", color: rgbToHex(color), + tag: (tags || [])[0] || "", + highlight, // textDirection: "ltr", // language: "fr", }, @@ -72,11 +76,8 @@ export function convertAnnotationToW3CAnnotationModel(annotation: IAnnotationSta }; } -export function convertAnnotationListToW3CAnnotationModelSet(annotationArray: IAnnotationState[], - publicationMetadata: IW3CAnnotationSetAboutView, -): IW3CAnnotationModelSet { +export function convertAnnotationListToW3CAnnotationSet(annotationArray: IAnnotationState[], publicationView: PublicationView): IW3CAnnotationModelSet { - const { identiferArrayString, mimeType, title, publisher, creator, publishedAt, source } = publicationMetadata; const currentDate = new Date(); const dateString: string = currentDate.toISOString(); @@ -93,28 +94,15 @@ export function convertAnnotationListToW3CAnnotationModelSet(annotationArray: IA generated: dateString, label: "Annotations set", about: { - "dc:identifier": identiferArrayString || [], - "dc:format": mimeType || "", - "dc:title": title || "", - "dc:publisher": publisher || [], - "dc:creator": creator || [], - "dc:date": publishedAt || "", - "dc:source": source || "", + "dc:identifier": [publicationView.workIdentifier ? ((publicationView.workIdentifier.startsWith("urn:") ? "" : "urn:isbn:") + publicationView.workIdentifier) : ""], + "dc:format": "application/epub+zip", + "dc:title": publicationView.documentTitle || "", + "dc:publisher": publicationView.publishers || [], + "dc:creator": publicationView.authors || [], + "dc:date": publicationView.publishedAt || "", + "dc:source": "urn:uuid:" + publicationView.identifier, }, total: annotationArray.length, items: (annotationArray || []).map((v) => convertAnnotationToW3CAnnotationModel(v)), }; } - -export function convertPublicationToAnnotationStateAbout(publicationView: PublicationView, publicationIdentifier: string): IW3CAnnotationSetAboutView { - - return { - identiferArrayString: ["urn:isbn:" + publicationView.workIdentifier || ""], - mimeType: "application/epub+zip", - title: publicationView.documentTitle || "", - publisher: publicationView.publishers || [], - creator: publicationView.authors || [], - publishedAt: publicationView.publishedAt || "", - source: "urn:uuid:" + publicationIdentifier, - }; -} diff --git a/src/main/redux/sagas/annotation.ts b/src/main/redux/sagas/annotation.ts deleted file mode 100644 index 628751264..000000000 --- a/src/main/redux/sagas/annotation.ts +++ /dev/null @@ -1,88 +0,0 @@ -// ==LICENSE-BEGIN== -// Copyright 2017 European Digital Reading Lab. All rights reserved. -// Licensed to the Readium Foundation under one or more contributor license agreements. -// Use of this source code is governed by a BSD-style license -// that can be found in the LICENSE file exposed on Github (readium) in the project repository. -// ==LICENSE-END== - -import * as debug_ from "debug"; -// import { LocaleConfigIdentifier, LocaleConfigValueType } from "readium-desktop/common/config"; -import { annotationActions } from "readium-desktop/common/redux/actions"; -import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; -import { error } from "readium-desktop/main/tools/error"; -// eslint-disable-next-line local-rules/typed-redux-saga-use-typed-effects -import { call as callTyped, select as selectTyped } from "typed-redux-saga/macro"; -import { getPublication } from "./api/publication/getPublication"; -import { convertAnnotationListToW3CAnnotationModelSet, convertPublicationToAnnotationStateAbout } from "readium-desktop/main/w3c/annotation/converter"; -import { RootState } from "../states"; -import { saveW3CAnnotationModelSetFromFilePath } from "readium-desktop/main/w3c/annotation/fs"; -import { getLibraryWindowFromDi } from "readium-desktop/main/di"; -import { dialog } from "electron"; -import { promises as fsp } from "fs"; -import * as path from "path"; -import { IAnnotationState } from "readium-desktop/common/redux/states/renderer/annotation"; - -// Logger -const filename_ = "readium-desktop:main:saga:annotation"; -const debug = debug_(filename_); -debug("_"); - -function* exportW3CAnnotationWithFilePath(publicationIdentifier: string, filePath: string) { - - const pub = yield* callTyped(getPublication, publicationIdentifier); - - let annotations: IAnnotationState[] = []; - - const sessionReader = yield* selectTyped((state: RootState) => state.win.session.reader); - const winSessionReaderState = Object.values(sessionReader).find((v) => v.publicationIdentifier === publicationIdentifier); - if (winSessionReaderState) { - annotations = (winSessionReaderState?.reduxState?.annotation || []).map(([,v]) => v); - } else { - const sessionRegistry = yield* selectTyped((state: RootState) => state.win.registry.reader); - if (Object.keys(sessionRegistry).find((v) => v === publicationIdentifier)) { - annotations = (sessionRegistry[publicationIdentifier]?.reduxState?.annotation || []).map(([, v]) => v); - } - } - - const publicationMetadata = yield* callTyped(convertPublicationToAnnotationStateAbout, pub, publicationIdentifier); - const W3CAnnotationSet = yield* callTyped(convertAnnotationListToW3CAnnotationModelSet, annotations, publicationMetadata); - - yield* callTyped(saveW3CAnnotationModelSetFromFilePath, filePath, W3CAnnotationSet); -} - -export function* exportW3CAnnotation(action: annotationActions.exportW3CAnnotationSetFromAnnotations.TAction) { - - const { publicationIdentifier } = action.payload; - - const libraryAppWindow = yield* callTyped(() => getLibraryWindowFromDi()); - - // Open a dialog to select a folder then copy the publication in it - const res = yield* callTyped(() => dialog.showOpenDialog( - libraryAppWindow ? libraryAppWindow : undefined, - { - properties: ["openDirectory"], - }, - )); - - if (!res.canceled) { - if (res.filePaths && res.filePaths.length > 0) { - let destinationPath = res.filePaths[0]; - // If the selected path is a file then choose the directory containing this file - const stat = yield* callTyped(() => fsp.stat(destinationPath)); - if (stat?.isFile()) { - destinationPath = path.join(path.dirname(destinationPath)); - } - destinationPath = path.join(destinationPath, `${publicationIdentifier}.annotation`); - yield* callTyped(exportW3CAnnotationWithFilePath, publicationIdentifier, destinationPath); - } - } -} - -export function saga() { - - return takeSpawnLeading( - annotationActions.exportW3CAnnotationSetFromAnnotations.ID, - exportW3CAnnotation, - (e) => error(filename_, e), - ); -} diff --git a/src/main/redux/sagas/index.ts b/src/main/redux/sagas/index.ts index 1e104aefc..701b34af7 100644 --- a/src/main/redux/sagas/index.ts +++ b/src/main/redux/sagas/index.ts @@ -35,7 +35,6 @@ import * as win from "./win"; import * as telemetry from "./telemetry"; import * as lcp from "./lcp"; import * as catalog from "./catalog"; -import * as annotation from "./annotation"; import { IS_DEV } from "readium-desktop/preprocessor-directives"; // Logger @@ -125,9 +124,6 @@ export function* rootSaga() { // need to track the previous state version before update in initSuccess.build yield call(telemetry.collectSaveAndSend); - // export annotations - yield annotation.saga(); - // app initialized yield put(appActions.initSuccess.build()); diff --git a/src/main/w3c/annotation/fs.ts b/src/main/w3c/annotation/fs.ts deleted file mode 100644 index 88c72f785..000000000 --- a/src/main/w3c/annotation/fs.ts +++ /dev/null @@ -1,25 +0,0 @@ -// ==LICENSE-BEGIN== -// Copyright 2017 European Digital Reading Lab. All rights reserved. -// Licensed to the Readium Foundation under one or more contributor license agreements. -// Use of this source code is governed by a BSD-style license -// that can be found in the LICENSE file exposed on Github (readium) in the project repository. -// ==LICENSE-END== - -import * as debug_ from "debug"; -import { writeFileSync } from "fs"; -import { IW3CAnnotationModelSet } from "./annotationModel.type"; - -// Logger -const filename_ = "readium-desktop:main:w3c:annotation:fs"; -const debug = debug_(filename_); - -export function saveW3CAnnotationModelSetFromFilePath(filePath: string, data: IW3CAnnotationModelSet) { - try { - const jsonData = JSON.stringify(data, null, 2); - writeFileSync(filePath, jsonData, "utf-8"); - - debug(`Data successfully written to ${filePath}`); - } catch (error) { - debug(`Error writing data to ${filePath}: ${error}`); - } -} diff --git a/src/renderer/library/components/publication/menu/CatalogMenu.tsx b/src/renderer/library/components/publication/menu/CatalogMenu.tsx index ca90cd079..34ff0e4f9 100644 --- a/src/renderer/library/components/publication/menu/CatalogMenu.tsx +++ b/src/renderer/library/components/publication/menu/CatalogMenu.tsx @@ -15,10 +15,9 @@ import SVG from "readium-desktop/renderer/common/components/SVG"; import * as InfoIcon from "readium-desktop/renderer/assets/icons/info-icon.svg"; import * as TrashIcon from "readium-desktop/renderer/assets/icons/trash-icon.svg"; import * as DoubleCheckIcon from "readium-desktop/renderer/assets/icons/doubleCheck-icon.svg"; -import { annotationActions, publicationActions } from "readium-desktop/common/redux/actions"; +import { publicationActions } from "readium-desktop/common/redux/actions"; import { useDispatch } from "readium-desktop/renderer/common/hooks/useDispatch"; import { apiDispatch } from "readium-desktop/renderer/common/redux/api/api"; -import * as SaveIcon from "readium-desktop/renderer/assets/icons/SaveAs-icon.svg"; const CatalogMenu: React.FC<{publicationView: PublicationView}> = (props) => { const [__] = useTranslator(); @@ -72,14 +71,6 @@ const CatalogMenu: React.FC<{publicationView: PublicationView}> = (props) => { -
- ); }; diff --git a/src/renderer/library/redux/middleware/sync.ts b/src/renderer/library/redux/middleware/sync.ts index dc1bd313b..19200cb60 100644 --- a/src/renderer/library/redux/middleware/sync.ts +++ b/src/renderer/library/redux/middleware/sync.ts @@ -7,7 +7,6 @@ import { apiActions, authActions, catalogActions, downloadActions, i18nActions, keyboardActions, lcpActions, readerActions, sessionActions, themeActions, publicationActions, wizardActions, - annotationActions, } from "readium-desktop/common/redux/actions"; import { syncFactory } from "readium-desktop/renderer/common/redux/middleware/syncFactory"; @@ -56,7 +55,6 @@ const SYNCHRONIZABLE_ACTIONS: string[] = [ sessionActions.save.ID, - annotationActions.exportW3CAnnotationSetFromAnnotations.ID, ]; export const reduxSyncMiddleware = syncFactory(SYNCHRONIZABLE_ACTIONS); diff --git a/src/renderer/reader/components/ReaderMenu.tsx b/src/renderer/reader/components/ReaderMenu.tsx index 173a67228..4b6902602 100644 --- a/src/renderer/reader/components/ReaderMenu.tsx +++ b/src/renderer/reader/components/ReaderMenu.tsx @@ -67,7 +67,7 @@ import { Locator } from "r2-shared-js/dist/es8-es2017/src/models/locator"; import { TextArea } from "react-aria-components"; import { AnnotationEdit } from "./AnnotationEdit"; import { IAnnotationState, IColor, TAnnotationState, TDrawType } from "readium-desktop/common/redux/states/renderer/annotation"; -import { readerActions } from "readium-desktop/common/redux/actions"; +import { readerActions, toastActions } from "readium-desktop/common/redux/actions"; import { readerLocalActionLocatorHrefChanged, readerLocalActionSetConfig } from "../redux/actions"; import * as stylesGlobal from "readium-desktop/renderer/assets/styles/global.scss"; import * as CheckIcon from "readium-desktop/renderer/assets/icons/singlecheck-icon.svg"; @@ -90,6 +90,8 @@ import { ObjectKeys } from "readium-desktop/utils/object-keys-values"; import type { Selection } from "react-aria-components"; import { rgbToHex } from "readium-desktop/common/rgb"; +import { ToastType } from "readium-desktop/common/models/toast"; +import { convertAnnotationListToW3CAnnotationSet } from "readium-desktop/common/w3c/annotation/converter"; @@ -650,6 +652,7 @@ const AnnotationList: React.FC<{ annotationUUIDFocused: string, resetAnnotationU const [__] = useTranslator(); const annotationsQueue = useSelector((state: IReaderRootState) => state.reader.annotation); + const publicationView = useSelector((state: IReaderRootState) => state.reader.info.publicationView); const [tagArrayFilter, setTagArrayFilter] = React.useState(new Set([])); const [colorArrayFilter, setColorArrayFilter] = React.useState(new Set([])); @@ -814,6 +817,29 @@ const AnnotationList: React.FC<{ annotationUUIDFocused: string, resetAnnotationU +