diff --git a/package-lock.json b/package-lock.json index 454e5cded..a531daab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "timeout-signal": "^2.0.0", "tmp": "^0.2.1", "typed-redux-saga": "^1.5.0", + "use-sync-external-store": "^1.2.0", "uuid": "^9.0.0", "validator": "^13.11.0", "xml-js": "^1.6.11", @@ -94,6 +95,7 @@ "@types/remote-redux-devtools": "^0.5.5", "@types/tmp": "^0.2.3", "@types/urijs": "^1.19.19", + "@types/use-sync-external-store": "^0.0.4", "@types/uuid": "^9.0.2", "@types/validator": "^13.11.1", "@types/xmldom": "^0.1.31", @@ -4465,6 +4467,12 @@ "integrity": "sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.4.tgz", + "integrity": "sha512-DMBc2WDEfaGsWXqH/Sk2oBaUkvlUwqgt/YEygpqX0MaiEjqR7afd1QgE4Pq2zBr/TRz0Mpu92eBBo5UQjtTD5Q==", + "dev": true + }, "node_modules/@types/uuid": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", @@ -26435,6 +26443,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", diff --git a/package.json b/package.json index 5af5b6729..118e5630b 100644 --- a/package.json +++ b/package.json @@ -298,6 +298,7 @@ "timeout-signal": "^2.0.0", "tmp": "^0.2.1", "typed-redux-saga": "^1.5.0", + "use-sync-external-store": "^1.2.0", "uuid": "^9.0.0", "validator": "^13.11.0", "xml-js": "^1.6.11", @@ -335,6 +336,7 @@ "@types/remote-redux-devtools": "^0.5.5", "@types/tmp": "^0.2.3", "@types/urijs": "^1.19.19", + "@types/use-sync-external-store": "^0.0.4", "@types/uuid": "^9.0.2", "@types/validator": "^13.11.1", "@types/xmldom": "^0.1.31", diff --git a/src/common/services/translator.ts b/src/common/services/translator.ts index fbdd2d6cd..9e96f8fbd 100644 --- a/src/common/services/translator.ts +++ b/src/common/services/translator.ts @@ -191,7 +191,23 @@ export type I18nTyped = TFunction; @injectable() export class Translator { public translate = this._translate as I18nTyped; + public subscribe = this._subscribe.bind(this); private locale = "en"; + private listeners: Set<() => void>; + + constructor() { + this.listeners = new Set(); + } + + private _subscribe(fn: () => void) { + if (fn) { + this.listeners.add(fn); + return () => { + this.listeners.delete(fn); + }; + } + return () => {}; + } public getLocale(): string { return this.locale; @@ -215,6 +231,10 @@ export class Translator { } else { resolve(); } + }).finally(() => { + for (const listener of this.listeners) { + listener(); + } }); } diff --git a/src/renderer/common/hooks/useApi.ts b/src/renderer/common/hooks/useApi.ts new file mode 100644 index 000000000..6b8d9cd32 --- /dev/null +++ b/src/renderer/common/hooks/useApi.ts @@ -0,0 +1,36 @@ +// ==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 React from "react"; +import { ReactReduxContext } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; +import { TApiMethod, TApiMethodName } from "readium-desktop/common/api/api.type"; +import { TModuleApi } from "readium-desktop/common/api/moduleApi.type"; +import { TMethodApi } from "readium-desktop/common/api/methodApi.type"; +import { apiActions } from "readium-desktop/common/redux/actions"; +import { ApiResponse } from "readium-desktop/common/redux/states/api"; +import { TReturnPromiseOrGeneratorType } from "readium-desktop/typings/api"; +import { useSyncExternalStore } from "./useSyncExternalStore"; + +export function useApi(_requestId: string, apiPath: T, ...requestData: Parameters): ApiResponse> { + + const requestId = _requestId || React.useMemo(() => uuidv4(), []); + const { store } = React.useContext(ReactReduxContext); + React.useEffect(() => { + const splitPath = apiPath.split("/"); + const moduleId = splitPath[0] as TModuleApi; + const methodId = splitPath[1] as TMethodApi; + store.dispatch(apiActions.request.build(requestId, moduleId, methodId, requestData)); + + return () => { + store.dispatch(apiActions.clean.build(requestId)); + }; + }, []); // componentDidMount + + const apiResult = useSyncExternalStore(store.subscribe, () => store.getState().api[requestId]); + return apiResult; +}; diff --git a/src/renderer/common/hooks/useDispatch.ts b/src/renderer/common/hooks/useDispatch.ts new file mode 100644 index 000000000..cb9c8275b --- /dev/null +++ b/src/renderer/common/hooks/useDispatch.ts @@ -0,0 +1,18 @@ +// ==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 React from "react"; +import { ReactReduxContext} from "react-redux"; +import { Action } from "readium-desktop/common/models/redux"; +import { Dispatch } from "redux"; + +export function useDispatch>(): Dispatch { + + const {store} = React.useContext(ReactReduxContext); + const storeDispatchFn = store.dispatch; + return storeDispatchFn; +} diff --git a/src/renderer/common/hooks/useKeyboardShortcut.ts b/src/renderer/common/hooks/useKeyboardShortcut.ts new file mode 100644 index 000000000..877f261ec --- /dev/null +++ b/src/renderer/common/hooks/useKeyboardShortcut.ts @@ -0,0 +1,23 @@ +// ==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 React from "react"; +import { TKeyboardShortcutReadOnly } from "readium-desktop/common/keyboard"; +import { registerKeyboardListener, unregisterKeyboardListener } from "../keyboard"; +import { useSelector } from "./useSelector"; +import { ICommonRootState } from "readium-desktop/common/redux/states/commonRootState"; + +export function useKeyboardShortcut(ListenForKeyUP: boolean, keyboardShortcut: (s: ICommonRootState["keyboard"]["shortcuts"]) => TKeyboardShortcutReadOnly, callback: () => void) { + + const keyboardShortcutState = useSelector((state: ICommonRootState) => state.keyboard.shortcuts); + React.useEffect(() => { + registerKeyboardListener(ListenForKeyUP, keyboardShortcut(keyboardShortcutState), callback); + return () => unregisterKeyboardListener(callback); + }, [keyboardShortcutState]); + + return ; +} diff --git a/src/renderer/common/hooks/useSelector.ts b/src/renderer/common/hooks/useSelector.ts new file mode 100644 index 000000000..439ad94a7 --- /dev/null +++ b/src/renderer/common/hooks/useSelector.ts @@ -0,0 +1,17 @@ +// ==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 React from "react"; +import { ReactReduxContext, ReactReduxContextValue } from "react-redux"; +import { useSyncExternalStore } from "./useSyncExternalStore"; + +export function useSelector(selector: (state: State) => Selected): Selected { + + const {store} = React.useContext>(ReactReduxContext); + const selected = useSyncExternalStore(store.subscribe, () => selector(store.getState())); + return selected; +} diff --git a/src/renderer/common/hooks/useSyncExternalStore.ts b/src/renderer/common/hooks/useSyncExternalStore.ts new file mode 100644 index 000000000..224cf6e80 --- /dev/null +++ b/src/renderer/common/hooks/useSyncExternalStore.ts @@ -0,0 +1,9 @@ +// ==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 { useSyncExternalStore } from "use-sync-external-store/shim"; +export { useSyncExternalStore }; diff --git a/src/renderer/common/hooks/useTranslator.ts b/src/renderer/common/hooks/useTranslator.ts new file mode 100644 index 000000000..890158381 --- /dev/null +++ b/src/renderer/common/hooks/useTranslator.ts @@ -0,0 +1,26 @@ +// ==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 React from "react"; +import { Translator } from "readium-desktop/common/services/translator"; +import { TranslatorContext } from "readium-desktop/renderer/common/translator.context"; + +export function useTranslator(): [typeof Translator.prototype.translate, Translator] { + + const translator = React.useContext(TranslatorContext); + const { translate: __ } = translator; + + const [, forceUpdate] = React.useReducer(x => x + 1, 0); + React.useEffect(() => { + const handleLocaleChange = () => { + forceUpdate(); + }; + return translator.subscribe(handleLocaleChange); + }, [translator.subscribe]); + + return [__, translator]; +}