From 50cc65551d69d22137aeff5efa00aceb452d0bfb Mon Sep 17 00:00:00 2001 From: Pierre Leroux Date: Thu, 7 Sep 2023 12:29:00 +0200 Subject: [PATCH] refactor(API): switch catalog/get API to redux/redux-saga logic (Fixes #1993) I improve the catalogView update to not always re-render it when setReduxState from reader is dispatched. It appears that setReduxState is lot of time dispatched across the app to keep the readingLocation synchronized beetween the main process and renderer process --- src/common/api/api.type.ts | 2 - src/common/api/interface/catalog.interface.ts | 22 -- src/common/api/methodApi.type.ts | 2 - src/common/api/moduleApi.type.ts | 2 - .../redux/actions/catalog/getCatalog.ts | 25 ++ src/common/redux/actions/catalog/index.ts | 14 + .../redux/actions/catalog/setCatalog.ts | 28 ++ src/common/redux/actions/index.ts | 2 + src/main/api/catalog.ts | 249 ---------------- src/main/di.ts | 5 - src/main/redux/middleware/sync.ts | 4 +- src/main/redux/sagas/catalog.ts | 273 ++++++++++++++++++ src/main/redux/sagas/index.ts | 4 + .../library/components/catalog/Catalog.tsx | 13 +- src/renderer/library/redux/middleware/sync.ts | 4 +- .../library/redux/reducers/catalog.ts | 27 ++ src/renderer/library/redux/reducers/index.ts | 4 + src/renderer/library/redux/sagas/catalog.ts | 2 - src/renderer/library/redux/sagas/index.ts | 5 +- src/renderer/library/redux/states/index.ts | 4 + 20 files changed, 398 insertions(+), 293 deletions(-) delete mode 100644 src/common/api/interface/catalog.interface.ts create mode 100644 src/common/redux/actions/catalog/getCatalog.ts create mode 100644 src/common/redux/actions/catalog/index.ts create mode 100644 src/common/redux/actions/catalog/setCatalog.ts delete mode 100644 src/main/api/catalog.ts create mode 100644 src/main/redux/sagas/catalog.ts create mode 100644 src/renderer/library/redux/reducers/catalog.ts diff --git a/src/common/api/api.type.ts b/src/common/api/api.type.ts index f58bb656c..3fc151e2a 100644 --- a/src/common/api/api.type.ts +++ b/src/common/api/api.type.ts @@ -5,14 +5,12 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { ICatalogModuleApi } from "./interface/catalog.interface"; import { IOpdsModuleApi } from "./interface/opdsApi.interface"; import { IApiappModuleApi } from "./interface/apiappApi.interface"; import { IHttpBrowserModuleApi } from "./interface/httpBrowser.interface"; import { IPublicationModuleApi } from "./interface/publicationApi.interface"; export type TApiMethod = - ICatalogModuleApi & IOpdsModuleApi & IApiappModuleApi & IHttpBrowserModuleApi & diff --git a/src/common/api/interface/catalog.interface.ts b/src/common/api/interface/catalog.interface.ts deleted file mode 100644 index d53e611c7..000000000 --- a/src/common/api/interface/catalog.interface.ts +++ /dev/null @@ -1,22 +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 { CatalogView } from "readium-desktop/common/views/catalog"; - -export interface ICatalogApi { - get: () => Promise; - // addEntry: (entryView: CatalogEntryView) => Promise; - // getEntries: () => Promise; - // updateEntries: (entryView: CatalogEntryView[]) => Promise; -} - -export interface ICatalogModuleApi { - "catalog/get": ICatalogApi["get"]; - // "catalog/addEntry": ICatalogApi["addEntry"]; - // "catalog/getEntries": ICatalogApi["getEntries"]; - // "catalog/updateEntries": ICatalogApi["updateEntries"]; -} diff --git a/src/common/api/methodApi.type.ts b/src/common/api/methodApi.type.ts index a7bd348c9..83cd523df 100644 --- a/src/common/api/methodApi.type.ts +++ b/src/common/api/methodApi.type.ts @@ -6,13 +6,11 @@ // ==LICENSE-END== import { IHttpBrowserApi } from "./interface/httpBrowser.interface"; -import { ICatalogApi } from "./interface/catalog.interface"; import { IOpdsApi } from "./interface/opdsApi.interface"; import { IPublicationApi } from "./interface/publicationApi.interface"; import { IApiappApi } from "./interface/apiappApi.interface"; export type TMethodApi = - keyof ICatalogApi | keyof IPublicationApi | keyof IOpdsApi | keyof IApiappApi | diff --git a/src/common/api/moduleApi.type.ts b/src/common/api/moduleApi.type.ts index 19004586d..a6376963a 100644 --- a/src/common/api/moduleApi.type.ts +++ b/src/common/api/moduleApi.type.ts @@ -6,13 +6,11 @@ // ==LICENSE-END== // module typing -type TCatalogApi = "catalog"; type TPublicationApi = "publication"; type TOpdsApi = "opds"; type TApiappApi = "apiapp"; type THttpBrowserApi = "httpbrowser"; export type TModuleApi = - TCatalogApi | TPublicationApi | TOpdsApi | TApiappApi | diff --git a/src/common/redux/actions/catalog/getCatalog.ts b/src/common/redux/actions/catalog/getCatalog.ts new file mode 100644 index 000000000..b8e7ff87e --- /dev/null +++ b/src/common/redux/actions/catalog/getCatalog.ts @@ -0,0 +1,25 @@ +// ==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 = "CATALOG_GET"; + +export interface Payload { +} + +export function build(): + Action { + + return { + type: ID, + payload: { + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/common/redux/actions/catalog/index.ts b/src/common/redux/actions/catalog/index.ts new file mode 100644 index 000000000..3357d4f4e --- /dev/null +++ b/src/common/redux/actions/catalog/index.ts @@ -0,0 +1,14 @@ +// ==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 getCatalog from "./getCatalog"; +import * as setCatalog from "./setCatalog"; + +export { + getCatalog, + setCatalog, +}; diff --git a/src/common/redux/actions/catalog/setCatalog.ts b/src/common/redux/actions/catalog/setCatalog.ts new file mode 100644 index 000000000..12d451115 --- /dev/null +++ b/src/common/redux/actions/catalog/setCatalog.ts @@ -0,0 +1,28 @@ +// ==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"; +import { CatalogView } from "readium-desktop/common/views/catalog"; + +export const ID = "CATALOG_SET_VIEW"; + +export interface Payload { + catalogView: CatalogView; +} + +export function build(catalogView: CatalogView): + Action { + + return { + type: ID, + payload: { + catalogView, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/common/redux/actions/index.ts b/src/common/redux/actions/index.ts index c17572f66..0f3510915 100644 --- a/src/common/redux/actions/index.ts +++ b/src/common/redux/actions/index.ts @@ -19,6 +19,7 @@ import * as netActions from "./net/"; import * as readerActions from "./reader/"; import * as toastActions from "./toast/"; import * as sessionActions from "./session/"; +import * as catalogActions from "./catalog"; export { historyActions, @@ -35,4 +36,5 @@ export { keyboardActions, loadActions, sessionActions, + catalogActions, }; diff --git a/src/main/api/catalog.ts b/src/main/api/catalog.ts deleted file mode 100644 index f58a5d0bd..000000000 --- a/src/main/api/catalog.ts +++ /dev/null @@ -1,249 +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 { inject, injectable } from "inversify"; -import { ICatalogApi } from "readium-desktop/common/api/interface/catalog.interface"; -import { isAudiobookFn, isDivinaFn, isPdfFn } from "readium-desktop/common/isManifestType"; -import { ToastType } from "readium-desktop/common/models/toast"; -import { toastActions } from "readium-desktop/common/redux/actions"; -import { Translator } from "readium-desktop/common/services/translator"; -import { CatalogEntryView, CatalogView } from "readium-desktop/common/views/catalog"; -import { PublicationView } from "readium-desktop/common/views/publication"; -import { PublicationViewConverter } from "readium-desktop/main/converter/publication"; -import { diSymbolTable } from "readium-desktop/main/diSymbolTable"; -import { type Store } from "redux"; - -import { TaJsonDeserialize } from "@r2-lcp-js/serializable"; -import { Publication as R2Publication } from "@r2-shared-js/models/publication"; - -import { PublicationDocument } from "../db/document/publication"; -import { PublicationRepository } from "../db/repository/publication"; -import { diMainGet } from "../di"; -import { aboutFiltered } from "../tools/filter"; -import { RootState } from "../redux/states"; - -export const CATALOG_CONFIG_ID = "catalog"; - -const NB_PUB = 5; - -// TODO: this memo-ization is very expensive (memory and CPU-wise) ... -// and TaJsonDeserialize() is called in several other places in the library lifecycle -// (including below via convertDocumentToView()) -// so it would make sense to hoist the cache higher in the application architecture -const viewToR2Pub = (view: PublicationView) => { - // Legacy Base64 data blobs - // const r2PublicationStr = Buffer.from(view.r2PublicationBase64, "base64").toString("utf-8"); - // const r2PublicationJson = JSON.parse(r2PublicationStr); - const r2Publication = TaJsonDeserialize(view.r2PublicationJson, R2Publication); - - return r2Publication; -}; -const _pdfMemo: {[str: string]: boolean} = {}; -const isPdfMemo = (view: PublicationView): boolean => { - if (typeof _pdfMemo[view.identifier] === "undefined") { - const r2Publication = viewToR2Pub(view); - _pdfMemo[view.identifier] = isPdfFn(r2Publication); - } - return _pdfMemo[view.identifier]; -}; - -// Logger -const debug = debug_("readium-desktop:main:api:catalog"); - -@injectable() -export class CatalogApi implements ICatalogApi { - @inject(diSymbolTable["publication-repository"]) - private readonly publicationRepository!: PublicationRepository; - - @inject(diSymbolTable["publication-view-converter"]) - private readonly publicationViewConverter!: PublicationViewConverter; - - @inject(diSymbolTable.translator) - private readonly translator!: Translator; - - @inject(diSymbolTable.store) - private readonly store!: Store; - - public async get(): Promise { - const __ = this.translator.translate.bind(this.translator); - - const { - audio: { - readed: audiobookReaded, - }, - divina: { - readed: divinaReaded, - }, - epub: { - readed: epubReaded, - }, - pdf: { - readed: pdfReaded, - }, - all: { - added: allAdded, - }, - } = await this.getPublicationView(); - - const _allAdded = aboutFiltered(allAdded); - const _epubReaded = aboutFiltered(epubReaded); - - const allAdded_ = _allAdded.slice(0, NB_PUB); - const epubReaded_ = _epubReaded.slice(0, NB_PUB); - const audiobookReaded_ = audiobookReaded.slice(0, NB_PUB); - const divinaReaded_ = divinaReaded.slice(0, NB_PUB); - const pdfReaded_ = pdfReaded.slice(0, NB_PUB); - - // Dynamic entries - const entries: CatalogEntryView[] = [ - { - title: __("catalog.entry.lastAdditions"), - totalCount: allAdded_.length, - publicationViews: allAdded_, - }, - { - title: __("catalog.entry.continueReading"), - totalCount: epubReaded_.length, - publicationViews: epubReaded_, - }, - { - title: __("catalog.entry.continueReadingAudioBooks"), - totalCount: audiobookReaded_.length, - publicationViews: audiobookReaded_, - }, - { - title: __("catalog.entry.continueReadingDivina"), - totalCount: divinaReaded_.length, - publicationViews: divinaReaded_, - }, - { - title: __("catalog.entry.continueReadingPdf"), - totalCount: pdfReaded_.length, - publicationViews: pdfReaded_, - }, - ]; - - return { - entries, - }; - } - - private async getPublicationView() { - - const errorDeletePub = (doc: PublicationDocument | undefined, e: any) => { - debug("Error in convertDocumentToView doc=", doc); - - this.store.dispatch(toastActions.openRequest.build(ToastType.Error, doc?.title || "")); - - debug(`${doc?.identifier} => ${doc?.title} should be removed`); - try { - const str = typeof e.toString === "function" ? e.toString() : (typeof e.message === "string" ? e.message : (typeof e === "string" ? e : JSON.stringify(e))); - - // tslint:disable-next-line: no-floating-promises - // this.publicationService.deletePublication(doc.identifier, str); - const sagaMiddleware = diMainGet("saga-middleware"); - const pubApi = diMainGet("publication-api"); - // tslint:disable-next-line: no-floating-promises - sagaMiddleware.run(pubApi.delete, doc.identifier, str).toPromise(); - } catch { - // ignore - } - }; - - const lastAddedPublicationsDocumentRaw = await this.getLastAddedPublicationDocument(); - const lastReadingPubArray = this.getLastReadingPublicationId(); - - const lastAddedPublicationsDocument = - lastAddedPublicationsDocumentRaw.filter(({ identifier }) => !lastReadingPubArray.includes(identifier)); - const lastReadedPublicationDocument = - lastReadingPubArray - .map( - (identifier) => lastAddedPublicationsDocumentRaw.find((v) => v.identifier === identifier), - ) - .filter((v) => !!v); - - const lastAddedPublicationsView = []; - for (const doc of lastAddedPublicationsDocument) { - try { - lastAddedPublicationsView.push(await this.publicationViewConverter.convertDocumentToView(doc)); - } catch (e) { - debug("lastadded publication view converter", e); - errorDeletePub(doc, e); - } - } - - const lastReadedPublicationsView = []; - for (const doc of lastReadedPublicationDocument) { - try { - lastReadedPublicationsView.push(await this.publicationViewConverter.convertDocumentToView(doc)); - } catch (e) { - debug("lastreaded publication view converter", e); - errorDeletePub(doc, e); - } - } - - const audio = { - readed: lastReadedPublicationsView.filter(isAudiobookFn), - added: lastAddedPublicationsView.filter(isAudiobookFn), - }; - - const divina = { - readed: lastReadedPublicationsView.filter(isDivinaFn), - added: lastAddedPublicationsView.filter(isDivinaFn), - }; - - const pdf = { - readed: lastReadedPublicationsView.filter( - (view: PublicationView) => { - return isPdfMemo(view); - }), - added: lastAddedPublicationsView.filter( - (view: PublicationView) => { - return isPdfMemo(view); - }), - }; - - const epub = { - readed: lastReadedPublicationsView.filter( - (view: PublicationView) => { - return !isAudiobookFn(view) && !isDivinaFn(view) && !isPdfMemo(view); - }), - added: lastAddedPublicationsView.filter( - (view: PublicationView) => { - return !isAudiobookFn(view) && !isDivinaFn(view) && !isPdfMemo(view); - }), - }; - - const all = { - readed: lastReadedPublicationsView, - added: lastAddedPublicationsView, - }; - - return { - audio, - epub, - divina, - pdf, - all, - }; - } - - private async getLastAddedPublicationDocument() { - - const lastAddedPublications = await this.publicationRepository.findAllSortDesc(); - - return lastAddedPublications; - } - - private getLastReadingPublicationId(): string[] { - - const lastReading = this.store.getState().publication.lastReadingQueue; - const pubIdArray = lastReading.map(([, pubId]) => pubId); - return pubIdArray; - } -} diff --git a/src/main/di.ts b/src/main/di.ts index ed5930087..6b52faeb6 100644 --- a/src/main/di.ts +++ b/src/main/di.ts @@ -14,7 +14,6 @@ import { Container } from "inversify"; import * as path from "path"; import { Translator } from "readium-desktop/common/services/translator"; import { ok } from "readium-desktop/common/utils/assert"; -import { CatalogApi } from "readium-desktop/main/api/catalog"; import { LocatorViewConverter } from "readium-desktop/main/converter/locator"; import { OpdsFeedViewConverter } from "readium-desktop/main/converter/opds"; import { PublicationViewConverter } from "readium-desktop/main/converter/publication"; @@ -218,9 +217,6 @@ container.bind(diSymbolTable["lcp-manager"]).to(LcpManager).inSingle container.bind(diSymbolTable["opds-service"]).to(OpdsService).inSingletonScope(); // API -container.bind(diSymbolTable["catalog-api"]).to(CatalogApi).inSingletonScope(); -// container.bind(diSymbolTable["publication-api"]).to(PublicationApi).inSingletonScope(); - container.bind(diSymbolTable["publication-api"]).toConstantValue(publicationApi); container.bind(diSymbolTable["opds-api"]).toConstantValue(opdsApi); container.bind(diSymbolTable["apiapp-api"]).toConstantValue(apiappApi); @@ -271,7 +267,6 @@ interface IGet { // (s: "streamer"): Server; (s: "device-id-manager"): DeviceIdManager; (s: "lcp-manager"): LcpManager; - (s: "catalog-api"): CatalogApi; (s: "publication-api"): typeof publicationApi; (s: "opds-api"): typeof opdsApi; (s: "apiapp-api"): typeof apiappApi; diff --git a/src/main/redux/middleware/sync.ts b/src/main/redux/middleware/sync.ts index f9a9332fa..13ed95e3d 100644 --- a/src/main/redux/middleware/sync.ts +++ b/src/main/redux/middleware/sync.ts @@ -9,7 +9,7 @@ import * as debug_ from "debug"; import { syncIpc } from "readium-desktop/common/ipc"; import { ActionWithSender, SenderType } from "readium-desktop/common/models/sync"; import { - apiActions, authActions, dialogActions, downloadActions, historyActions, i18nActions, keyboardActions, lcpActions, + apiActions, authActions, catalogActions, dialogActions, downloadActions, historyActions, i18nActions, keyboardActions, lcpActions, readerActions, sessionActions, toastActions, } from "readium-desktop/common/redux/actions"; import { ActionSerializer } from "readium-desktop/common/services/serializer"; @@ -56,6 +56,8 @@ const SYNCHRONIZABLE_ACTIONS: string[] = [ sessionActions.enable.ID, lcpActions.unlockPublicationWithPassphrase.ID, + + catalogActions.setCatalog.ID, // send new catalogView to library ]; export const reduxSyncMiddleware: Middleware diff --git a/src/main/redux/sagas/catalog.ts b/src/main/redux/sagas/catalog.ts new file mode 100644 index 000000000..317750c5f --- /dev/null +++ b/src/main/redux/sagas/catalog.ts @@ -0,0 +1,273 @@ +// ==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 { catalogActions, readerActions, toastActions } from "readium-desktop/common/redux/actions"; +import { PublicationRepository } from "readium-desktop/main/db/repository/publication"; +import { error } from "readium-desktop/main/tools/error"; +// eslint-disable-next-line local-rules/typed-redux-saga-use-typed-effects +import { all } from "redux-saga/effects"; +import { call as callTyped, put as putTyped, select as selectTyped, debounce as debounceTyped } from "typed-redux-saga/macro"; +import { RootState } from "../states"; +import { PublicationDocument } from "readium-desktop/main/db/document/publication"; +import { ToastType } from "readium-desktop/common/models/toast"; +import { publicationApi } from "./api"; +import { diMainGet } from "readium-desktop/main/di"; +import { isAudiobookFn, isDivinaFn, isPdfFn } from "readium-desktop/common/isManifestType"; + +import { PublicationView } from "readium-desktop/common/views/publication"; +import { TaJsonDeserialize } from "@r2-lcp-js/serializable"; +import { Publication as R2Publication } from "@r2-shared-js/models/publication"; +import { CatalogEntryView } from "readium-desktop/common/views/catalog"; +import { aboutFiltered } from "readium-desktop/main/tools/filter"; +import { publicationActions } from "../actions"; +import { takeSpawnLatest } from "readium-desktop/common/redux/sagas/takeSpawnLatest"; +import { spawnLeading } from "readium-desktop/common/redux/sagas/spawnLeading"; + +const filename_ = "readium-desktop:main:redux:sagas:catalog"; +const debug = debug_(filename_); + +const NB_PUB = 5; + +// TODO: this memo-ization is very expensive (memory and CPU-wise) ... +// and TaJsonDeserialize() is called in several other places in the library lifecycle +// (including below via convertDocumentToView()) +// so it would make sense to hoist the cache higher in the application architecture +const viewToR2Pub = (view: PublicationView) => { + // Legacy Base64 data blobs + // const r2PublicationStr = Buffer.from(view.r2PublicationBase64, "base64").toString("utf-8"); + // const r2PublicationJson = JSON.parse(r2PublicationStr); + const r2Publication = TaJsonDeserialize(view.r2PublicationJson, R2Publication); + + return r2Publication; +}; +const _pdfMemo: {[str: string]: boolean} = {}; +const isPdfMemo = (view: PublicationView): boolean => { + if (typeof _pdfMemo[view.identifier] === "undefined") { + const r2Publication = viewToR2Pub(view); + _pdfMemo[view.identifier] = isPdfFn(r2Publication); + } + return _pdfMemo[view.identifier]; +}; + + +const getLastAddedPublicationDocument = async (publicationRepository: PublicationRepository) => { + + const lastAddedPublications = await publicationRepository.findAllSortDesc(); + return lastAddedPublications; +}; + +function* getLastReadingPublicationId() { + + const lastReading = yield* selectTyped((state: RootState) => state.publication.lastReadingQueue); + const pubIdArray = lastReading.map(([, pubId]) => pubId); + return pubIdArray; +} + +function* errorDeletePub(doc: PublicationDocument | undefined, e: Error) { + debug("Error in convertDocumentToView doc=", doc); + + yield* putTyped(toastActions.openRequest.build(ToastType.Error, doc?.title || "")); + + debug(`${doc?.identifier} => ${doc?.title} should be removed`); + const str = typeof e.toString === "function" ? e.toString() : (typeof e.message === "string" ? e.message : (typeof e === "string" ? e : JSON.stringify(e))); + try { + yield* callTyped(publicationApi.delete, doc.identifier, str); + } catch (e) { + // ignore + debug("publication not deleted", e); + } +}; + +function* getPublicationView() { + + const publicationRepository = diMainGet("publication-repository"); + const publicationViewConverter = diMainGet("publication-view-converter"); + const lastAddedPublicationsDocumentRaw = yield* callTyped(getLastAddedPublicationDocument, publicationRepository); + const lastReadingPubArray = yield* callTyped(getLastReadingPublicationId); + + const lastAddedPublicationsDocument = + lastAddedPublicationsDocumentRaw.filter(({ identifier }) => !lastReadingPubArray.includes(identifier)); + const lastReadedPublicationDocument = + lastReadingPubArray + .map( + (identifier) => lastAddedPublicationsDocumentRaw.find((v) => v.identifier === identifier), + ) + .filter((v) => !!v); + + const lastAddedPublicationsView = []; + for (const doc of lastAddedPublicationsDocument) { + try { + lastAddedPublicationsView.push(yield* callTyped(() => publicationViewConverter.convertDocumentToView(doc))); + } catch (e) { + debug("lastadded publication view converter", e); + yield* callTyped(errorDeletePub, doc, e); + } + } + + const lastReadedPublicationsView = []; + for (const doc of lastReadedPublicationDocument) { + try { + lastReadedPublicationsView.push(yield* callTyped(() => publicationViewConverter.convertDocumentToView(doc))); + } catch (e) { + debug("lastreaded publication view converter", e); + yield* callTyped(errorDeletePub, doc, e); + } + } + + const audio = { + readed: lastReadedPublicationsView.filter(isAudiobookFn), + added: lastAddedPublicationsView.filter(isAudiobookFn), + }; + + const divina = { + readed: lastReadedPublicationsView.filter(isDivinaFn), + added: lastAddedPublicationsView.filter(isDivinaFn), + }; + + const pdf = { + readed: lastReadedPublicationsView.filter( + (view: PublicationView) => { + return isPdfMemo(view); + }), + added: lastAddedPublicationsView.filter( + (view: PublicationView) => { + return isPdfMemo(view); + }), + }; + + const epub = { + readed: lastReadedPublicationsView.filter( + (view: PublicationView) => { + return !isAudiobookFn(view) && !isDivinaFn(view) && !isPdfMemo(view); + }), + added: lastAddedPublicationsView.filter( + (view: PublicationView) => { + return !isAudiobookFn(view) && !isDivinaFn(view) && !isPdfMemo(view); + }), + }; + + const all = { + readed: lastReadedPublicationsView, + added: lastAddedPublicationsView, + }; + + return { + audio, + epub, + divina, + pdf, + all, + }; +} + +function* getCatalog() { + debug("getCatalog"); + + const translator = diMainGet("translator"); + const __ = translator.translate.bind(translator); + + const { + audio: { + readed: audiobookReaded, + }, + divina: { + readed: divinaReaded, + }, + epub: { + readed: epubReaded, + }, + pdf: { + readed: pdfReaded, + }, + all: { + added: allAdded, + }, + } = yield* callTyped(getPublicationView); + + const _allAdded = aboutFiltered(allAdded); + const _epubReaded = aboutFiltered(epubReaded); + + const allAdded_ = _allAdded.slice(0, NB_PUB); + const epubReaded_ = _epubReaded.slice(0, NB_PUB); + const audiobookReaded_ = audiobookReaded.slice(0, NB_PUB); + const divinaReaded_ = divinaReaded.slice(0, NB_PUB); + const pdfReaded_ = pdfReaded.slice(0, NB_PUB); + + // Dynamic entries + const entries: CatalogEntryView[] = [ + { + title: __("catalog.entry.lastAdditions"), + totalCount: allAdded_.length, + publicationViews: allAdded_, + }, + { + title: __("catalog.entry.continueReading"), + totalCount: epubReaded_.length, + publicationViews: epubReaded_, + }, + { + title: __("catalog.entry.continueReadingAudioBooks"), + totalCount: audiobookReaded_.length, + publicationViews: audiobookReaded_, + }, + { + title: __("catalog.entry.continueReadingDivina"), + totalCount: divinaReaded_.length, + publicationViews: divinaReaded_, + }, + { + title: __("catalog.entry.continueReadingPdf"), + totalCount: pdfReaded_.length, + publicationViews: pdfReaded_, + }, + ]; + + yield* putTyped(catalogActions.setCatalog.build({entries})); +} + +function* updateResumePosition() { + + const eq = (a: string[], b: string[]) => { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; + }; + + let prevState = yield* selectTyped((state: RootState) => state.publication.lastReadingQueue); + yield* debounceTyped(500, readerActions.setReduxState.build, function* worker(){ + const nextState = yield* selectTyped((state: RootState) => state.publication.lastReadingQueue); + + const prevId = prevState.map(([_,v]) => v); + const nextId = nextState.map(([_,v]) => v); + if (!eq(prevId, nextId)) { + yield* callTyped(getCatalog); + } + prevState = yield* selectTyped((state: RootState) => state.publication.lastReadingQueue); + }); +} + +export function saga() { + return all([ + takeSpawnLatest( + [catalogActions.getCatalog.ID, publicationActions.addPublication.ID, publicationActions.deletePublication.ID], + // catalogActions.getCatalog.ID, + getCatalog, + (e) => error(filename_ + ":getCatalog", e), + ), + spawnLeading( + updateResumePosition, + (e) => error(filename_ + ":updateResumePosition", e), + ), + ]); +} diff --git a/src/main/redux/sagas/index.ts b/src/main/redux/sagas/index.ts index 8d244f9a1..c3e6e862e 100644 --- a/src/main/redux/sagas/index.ts +++ b/src/main/redux/sagas/index.ts @@ -26,6 +26,7 @@ import * as streamer from "./streamer"; import * as win from "./win"; import * as telemetry from "./telemetry"; import * as lcp from "./lcp"; +import * as catalog from "./catalog"; // Logger const filename_ = "readium-desktop:main:saga:app"; @@ -99,6 +100,9 @@ export function* rootSaga() { // LCP saga yield lcp.saga(); + // get/set catalog in library win + yield catalog.saga(); + // rehydrate shorcuts in redux yield put(keyboardActions.setShortcuts.build(keyboardShortcuts.getAll(), false)); diff --git a/src/renderer/library/components/catalog/Catalog.tsx b/src/renderer/library/components/catalog/Catalog.tsx index fcd4ceb4e..e0f7247a6 100644 --- a/src/renderer/library/components/catalog/Catalog.tsx +++ b/src/renderer/library/components/catalog/Catalog.tsx @@ -17,7 +17,7 @@ import LibraryLayout from "readium-desktop/renderer/library/components/layout/Li import { ILibraryRootState } from "readium-desktop/renderer/library/redux/states"; import { DisplayType, IRouterLocationState } from "readium-desktop/renderer/library/routing"; import { Dispatch } from "redux"; -import { CATALOG_GET_API_ID_CHANNEL, PUBLICATION_TAGS_API_ID_CHANNEL } from "../../redux/sagas/catalog"; +import { PUBLICATION_TAGS_API_ID_CHANNEL } from "../../redux/sagas/catalog"; import CatalogGridView from "./GridView"; import Header from "./Header"; @@ -41,7 +41,7 @@ class Catalog extends React.Component { const { __, catalog, tags } = this.props; if (this.props.refresh) { - this.props.api(CATALOG_GET_API_ID_CHANNEL)("catalog/get")(); + // this.props.api(CATALOG_GET_API_ID_CHANNEL)("catalog/get")(); this.props.api(PUBLICATION_TAGS_API_ID_CHANNEL)("publication/getAllTags")(); } @@ -54,15 +54,15 @@ class Catalog extends React.Component { secondaryHeader={secondaryHeader} > { - catalog?.data.result + catalog?.entries && ( displayType === DisplayType.Grid ? : ) @@ -73,7 +73,7 @@ class Catalog extends React.Component { } const mapStateToProps = (state: ILibraryRootState) => ({ - catalog: apiState(state)(CATALOG_GET_API_ID_CHANNEL)("catalog/get"), + // catalog: apiState(state)(CATALOG_GET_API_ID_CHANNEL)("catalog/get"), tags: apiState(state)(PUBLICATION_TAGS_API_ID_CHANNEL)("publication/getAllTags"), refresh: apiRefreshToState(state)([ "publication/importFromFs", @@ -85,6 +85,7 @@ const mapStateToProps = (state: ILibraryRootState) => ({ // "reader/setLastReadingLocation", ]), location: state.router.location, + catalog: state.publication.catalog, }); const mapDispatchToProps = (dispatch: Dispatch) => ({ diff --git a/src/renderer/library/redux/middleware/sync.ts b/src/renderer/library/redux/middleware/sync.ts index 081b29ac4..6bf94611b 100644 --- a/src/renderer/library/redux/middleware/sync.ts +++ b/src/renderer/library/redux/middleware/sync.ts @@ -6,7 +6,7 @@ // ==LICENSE-END== import { - apiActions, authActions, downloadActions, i18nActions, keyboardActions, lcpActions, readerActions, sessionActions, + apiActions, authActions, catalogActions, downloadActions, i18nActions, keyboardActions, lcpActions, readerActions, sessionActions, } from "readium-desktop/common/redux/actions"; import { syncFactory } from "readium-desktop/renderer/common/redux/middleware/syncFactory"; @@ -37,6 +37,8 @@ const SYNCHRONIZABLE_ACTIONS: string[] = [ lcpActions.renewPublicationLicense.ID, lcpActions.returnPublication.ID, lcpActions.unlockPublicationWithPassphrase.ID, + + catalogActions.getCatalog.ID, // request to get catalog view ]; export const reduxSyncMiddleware = syncFactory(SYNCHRONIZABLE_ACTIONS); diff --git a/src/renderer/library/redux/reducers/catalog.ts b/src/renderer/library/redux/reducers/catalog.ts new file mode 100644 index 000000000..f00373cba --- /dev/null +++ b/src/renderer/library/redux/reducers/catalog.ts @@ -0,0 +1,27 @@ +// ==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 { CatalogView } from "readium-desktop/common/views/catalog"; +import { catalogActions } from "readium-desktop/common/redux/actions"; + +const initialState: CatalogView = { + entries: [], +}; + +export function catalogViewReducer( + state: CatalogView = initialState, + action: catalogActions.setCatalog.TAction, +): CatalogView { + switch (action.type) { + case catalogActions.setCatalog.ID: + const {catalogView} = action.payload; // comming from RPC, should be already a fresh new object + return catalogView; + + default: + return state; + } +} diff --git a/src/renderer/library/redux/reducers/index.ts b/src/renderer/library/redux/reducers/index.ts index bc39f7e42..d35486a53 100644 --- a/src/renderer/library/redux/reducers/index.ts +++ b/src/renderer/library/redux/reducers/index.ts @@ -24,6 +24,7 @@ import { ILibraryRootState } from "../states"; import { RouterState } from "redux-first-history"; import { sessionReducer } from "readium-desktop/common/redux/reducers/session"; +import { catalogViewReducer } from "./catalog"; export const rootReducer = (routerReducer: Reducer) => { return combineReducers({ @@ -66,5 +67,8 @@ export const rootReducer = (routerReducer: Reducer) => { history: historyReducer, keyboard: keyboardReducer, load: loadReducer, + publication: combineReducers({ + catalog: catalogViewReducer, + }), }); }; diff --git a/src/renderer/library/redux/sagas/catalog.ts b/src/renderer/library/redux/sagas/catalog.ts index 7460c4c30..42282c8b8 100644 --- a/src/renderer/library/redux/sagas/catalog.ts +++ b/src/renderer/library/redux/sagas/catalog.ts @@ -10,12 +10,10 @@ import { apiSaga } from "readium-desktop/renderer/common/redux/sagas/api"; // eslint-disable-next-line local-rules/typed-redux-saga-use-typed-effects import { call, debounce, spawn } from "redux-saga/effects"; -export const CATALOG_GET_API_ID_CHANNEL = "CATALOG_API_ID"; export const PUBLICATION_TAGS_API_ID_CHANNEL = "PUBLICATION_TAGS_API_ID_CHANNEL"; function* update() { - yield apiSaga("catalog/get", CATALOG_GET_API_ID_CHANNEL); yield apiSaga("publication/getAllTags", PUBLICATION_TAGS_API_ID_CHANNEL); } diff --git a/src/renderer/library/redux/sagas/index.ts b/src/renderer/library/redux/sagas/index.ts index 5466686d9..45fbebe7b 100644 --- a/src/renderer/library/redux/sagas/index.ts +++ b/src/renderer/library/redux/sagas/index.ts @@ -6,11 +6,12 @@ // ==LICENSE-END== import * as debug_ from "debug"; -import { i18nActions, keyboardActions } from "readium-desktop/common/redux/actions"; +import { catalogActions, i18nActions, keyboardActions } from "readium-desktop/common/redux/actions"; import { winActions } from "readium-desktop/renderer/common/redux/actions"; import * as publicationInfoSyncTags from "readium-desktop/renderer/common/redux/sagas/dialog/publicationInfosSyncTags"; // eslint-disable-next-line local-rules/typed-redux-saga-use-typed-effects import { all, call, put, take } from "redux-saga/effects"; +import { put as putTyped } from "typed-redux-saga/macro"; import * as publicationInfoOpds from "../../../common/redux/sagas/dialog/publicationInfoOpds"; import * as publicationInfoReaderAndLib from "../../../common/redux/sagas/dialog/publicationInfoReaderAndLib"; @@ -57,8 +58,10 @@ export function* rootSaga() { load.saga(), + // TODO need to update this catalog.saga(), ]); + yield* putTyped(catalogActions.getCatalog.build()); // ask to fill catalog view } diff --git a/src/renderer/library/redux/states/index.ts b/src/renderer/library/redux/states/index.ts index 934eff351..9505ee3ba 100644 --- a/src/renderer/library/redux/states/index.ts +++ b/src/renderer/library/redux/states/index.ts @@ -14,6 +14,7 @@ import { TPQueueState } from "readium-desktop/utils/redux-reducers/pqueue.reduce import { THistoryState } from "./history"; import { IOpdsHeaderState, IOpdsSearchState } from "./opds"; +import { CatalogView } from "readium-desktop/common/views/catalog"; export interface ILibraryRootState extends IRendererCommonRootState { opds: { @@ -27,4 +28,7 @@ export interface ILibraryRootState extends IRendererCommonRootState { download: TPQueueState; history: THistoryState; load: ILoadState; + publication: { + catalog: CatalogView, + } }