diff --git a/package-lock.json b/package-lock.json index 954335857..bffeb1a2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1187,6 +1187,15 @@ "integrity": "sha512-a9bDi4Z3zCZf4Lv1X/vwnvbbDYSNz59h3i3KdyuYYN+YrLjSeJD0dnphdULDfySvUv6Exy/O0K6wX/kQpnPQ+A==", "dev": true }, + "@types/ramda": { + "version": "0.26.41", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.26.41.tgz", + "integrity": "sha512-EROc+evnzHr4J/8nMBky98eP9eoWhaLtlig8JWJ12f/suvSplMH4jL6L/5X6z35PTjQRWUDf6kqI02x7bX4kHQ==", + "dev": true, + "requires": { + "ts-toolbelt": "^6.1.11" + } + }, "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", @@ -1260,6 +1269,15 @@ "integrity": "sha1-QOr6dXXbNrkSzgBZuF3pjCBbBwg=", "dev": true }, + "@types/remote-redux-devtools": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/remote-redux-devtools/-/remote-redux-devtools-0.5.4.tgz", + "integrity": "sha512-OFbLVB30yemU46Qbx/s3qqSG2nkuSBQlOqf88y1U+hGiO2koz2pRYzZkB9hayTootHJWVdG4NGizlooQg7xFyA==", + "dev": true, + "requires": { + "redux": "^4.0.0" + } + }, "@types/request": { "version": "2.48.4", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.4.tgz", @@ -1600,6 +1618,15 @@ "@xtuc/long": "4.2.2" } }, + "@wry/equality": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.9.tgz", + "integrity": "sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -1846,6 +1873,75 @@ } } }, + "apollo-cache-control": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz", + "integrity": "sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA==", + "dev": true, + "requires": { + "graphql-extensions": "^0.0.x" + } + }, + "apollo-link": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.13.tgz", + "integrity": "sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw==", + "dev": true, + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.20" + } + }, + "apollo-server-core": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-1.4.0.tgz", + "integrity": "sha512-BP1Vh39krgEjkQxbjTdBURUjLHbFq1zeOChDJgaRsMxGtlhzuLWwwC6lLdPatN8jEPbeHq8Tndp9QZ3iQZOKKA==", + "dev": true, + "requires": { + "apollo-cache-control": "^0.1.0", + "apollo-tracing": "^0.1.0", + "graphql-extensions": "^0.0.x" + } + }, + "apollo-server-express": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-1.4.0.tgz", + "integrity": "sha512-zkH00nxhLnJfO0HgnNPBTfZw8qI5ILaPZ5TecMCI9+Y9Ssr2b0bFr9pBRsXy9eudPhI+/O4yqegSUsnLdF/CPw==", + "dev": true, + "requires": { + "apollo-server-core": "^1.4.0", + "apollo-server-module-graphiql": "^1.4.0" + } + }, + "apollo-server-module-graphiql": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz", + "integrity": "sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==", + "dev": true + }, + "apollo-tracing": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.1.4.tgz", + "integrity": "sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ==", + "dev": true, + "requires": { + "graphql-extensions": "~0.0.9" + } + }, + "apollo-utilities": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.3.tgz", + "integrity": "sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw==", + "dev": true, + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + } + }, "app-builder-bin": { "version": "3.5.8", "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-3.5.8.tgz", @@ -1957,6 +2053,48 @@ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1995,6 +2133,12 @@ "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", "dev": true }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, "array-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", @@ -2442,6 +2586,12 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -2858,6 +3008,12 @@ "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", "dev": true }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "dev": true + }, "buffer-equals": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", @@ -3314,6 +3470,12 @@ } } }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, "checkstyle-formatter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/checkstyle-formatter/-/checkstyle-formatter-1.1.0.tgz", @@ -3454,6 +3616,15 @@ } } }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, "cli-truncate": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-1.1.0.tgz", @@ -3497,6 +3668,12 @@ } } }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -4060,6 +4237,12 @@ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -4177,6 +4360,16 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", @@ -4825,11 +5018,23 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, + "deprecated-decorator": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz", + "integrity": "sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=", + "dev": true + }, "deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -4857,6 +5062,12 @@ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, "detect-newline": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", @@ -5236,6 +5447,15 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "eclint": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/eclint/-/eclint-2.8.1.tgz", @@ -6186,6 +6406,12 @@ } } }, + "expirymanager": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/expirymanager/-/expirymanager-0.9.3.tgz", + "integrity": "sha1-5fazugDY12z2MxHCtx19/JvePk8=", + "dev": true + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -6301,6 +6527,28 @@ } } }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -6554,6 +6802,15 @@ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", "dev": true }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "file-js": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/file-js/-/file-js-0.3.0.tgz", @@ -6731,12 +6988,54 @@ "resolve-dir": "^1.0.1" } }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, "flatten": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", "dev": true }, + "fleximap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fleximap/-/fleximap-1.0.0.tgz", + "integrity": "sha512-zg/PthjBzESYKomTw/wivo8Id6B+obVkWriIzDuRfuw4wxEIV2/0D/NIGf+LKcGTTifHRfw73+oAAQozZ9MAhA==", + "dev": true + }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -6821,6 +7120,15 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, "foreach": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", @@ -6919,6 +7227,15 @@ "universalify": "^0.1.0" } }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, "fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -7602,6 +7919,59 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -7613,6 +7983,12 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-params": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz", + "integrity": "sha1-uuDfq6WIoMYNeDTA2Nwv9g7u8v4=", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -7636,6 +8012,12 @@ "assert-plus": "^1.0.0" } }, + "getport": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getport/-/getport-0.1.0.tgz", + "integrity": "sha1-q93z1dHnfdlnzPorA2oKH7Jv1/c=", + "dev": true + }, "git-rev-sync": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-2.0.0.tgz", @@ -7888,24 +8270,73 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "gud": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", - "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + "graphql": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.13.2.tgz", + "integrity": "sha512-QZ5BL8ZO/B20VA8APauGBg3GyEgZ19eduvpLWoq5x7gMmWnHoy8rlQWPLmWgFvo1yNgjSEFMesmS4R6pPr7xog==", + "dev": true, + "requires": { + "iterall": "^1.2.1" + } }, - "gulp-exclude-gitignore": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gulp-exclude-gitignore/-/gulp-exclude-gitignore-1.2.0.tgz", - "integrity": "sha512-J3LCmz9C1UU1pxf5Npx6SNc5o9YQptyc9IHaqLiBlihZmg44jaaTplWUZ0JPQkMdOTae0YgEDvT9TKlUWDSMUA==", + "graphql-extensions": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.0.10.tgz", + "integrity": "sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA==", "dev": true, "requires": { - "gulp-ignore": "^2.0.2" + "core-js": "^2.5.3", + "source-map-support": "^0.5.1" + } + }, + "graphql-server-express": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/graphql-server-express/-/graphql-server-express-1.4.1.tgz", + "integrity": "sha512-7HEIz2USTCXgk4YMKIcOVUdVZQT429nZnPQr4Gqp5pydZ08KJM9Y2sl9+VU+3a91HGKyrtF04eUumuYeS2fDcg==", + "dev": true, + "requires": { + "apollo-server-express": "^1.4.0" + } + }, + "graphql-tools": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.6.tgz", + "integrity": "sha512-jHLQw8x3xmSNRBCsaZqelXXsFfUSUSktSCUP8KYHiX1Z9qEuwcMpAf+FkdBzk8aTAFqOlPdNZ3OI4DKKqGKUqg==", + "dev": true, + "requires": { + "apollo-link": "^1.2.3", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, + "gulp-exclude-gitignore": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gulp-exclude-gitignore/-/gulp-exclude-gitignore-1.2.0.tgz", + "integrity": "sha512-J3LCmz9C1UU1pxf5Npx6SNc5o9YQptyc9IHaqLiBlihZmg44jaaTplWUZ0JPQkMdOTae0YgEDvT9TKlUWDSMUA==", + "dev": true, + "requires": { + "gulp-ignore": "^2.0.2" } }, "gulp-filter": { @@ -8203,6 +8634,12 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -8849,6 +9286,15 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "image-size": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.8.3.tgz", @@ -8990,6 +9436,75 @@ } } }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + } + } + }, "internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -9301,6 +9816,12 @@ "isobject": "^4.0.0" } }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", @@ -9500,6 +10021,12 @@ "html-escaper": "^2.0.0" } }, + "iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", + "dev": true + }, "jake": { "version": "10.6.1", "resolved": "https://registry.npmjs.org/jake/-/jake-10.6.1.tgz", @@ -10342,6 +10869,12 @@ } } }, + "jsan": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.13.tgz", + "integrity": "sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g==", + "dev": true + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -10490,6 +11023,24 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dev": true, + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -10590,6 +11141,27 @@ } } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -10617,6 +11189,100 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "knex": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.15.2.tgz", + "integrity": "sha1-YFm4dIlgX0zIdZmm0qnSZXCek0A=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "bluebird": "^3.5.1", + "chalk": "2.3.2", + "commander": "^2.16.0", + "debug": "3.1.0", + "inherits": "~2.0.3", + "interpret": "^1.1.0", + "liftoff": "2.5.0", + "lodash": "^4.17.10", + "minimist": "1.2.0", + "mkdirp": "^0.5.1", + "pg-connection-string": "2.0.0", + "tarn": "^1.1.4", + "tildify": "1.2.0", + "uuid": "^3.3.2", + "v8flags": "^3.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, "latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -10864,6 +11530,60 @@ } } }, + "liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^2.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "dependencies": { + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "linez": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/linez/-/linez-4.1.4.tgz", @@ -10874,6 +11594,12 @@ "iconv-lite": "^0.4.15" } }, + "linked-list": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", + "integrity": "sha1-eYsP+X0bkqT9CEgPVa6k6dSdN78=", + "dev": true + }, "listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", @@ -10963,18 +11689,54 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=", + "dev": true + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", "dev": true }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11121,6 +11883,15 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -11449,6 +12220,33 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -11682,12 +12480,23 @@ "minimatch": "^3.0.0" } }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, "nan": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "dev": true, - "optional": true + "dev": true + }, + "nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", + "dev": true }, "nanomatch": { "version": "1.2.13", @@ -11720,12 +12529,43 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncom": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ncom/-/ncom-1.0.3.tgz", + "integrity": "sha512-PfA7rjxxMAItsGo2qXrGn2GvKJIwN0bUTa3GehsblrKRVdCCEwB0QG2ymM6/DppQGUt7YqbfxQB7LaMWMiHHWQ==", + "dev": true, + "requires": { + "sc-formatter": "~3.0.1" + } + }, "ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", "dev": true }, + "needle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.2.tgz", + "integrity": "sha512-DUzITvPVDUy6vczKKYTnWc/pBZ0EnjMJnQ3y+Jo5zfKFimJs7S3HFCxCRZYB9FUZcrzUQr3WsmvZgddMEIZv6w==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -11888,6 +12728,35 @@ "which": "^1.3.0" } }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "dev": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "node-stream-zip": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.10.1.tgz", @@ -11947,6 +12816,15 @@ "once": "^1.3.2" } }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, "npm-conf": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", @@ -11958,6 +12836,23 @@ "pify": "^3.0.0" } }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -11967,6 +12862,18 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -12083,6 +12990,32 @@ "object-keys": "^1.0.11" } }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "object.getownpropertydescriptors": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", @@ -12093,6 +13026,16 @@ "es-abstract": "^1.17.0-next.1" } }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -12154,6 +13097,15 @@ "integrity": "sha512-YZSypViXzu3ul5LMu/m6XjJ9ol8qAy9S2VjHl5E6UlhUH1KGKWabyEJifn0Jjpw23bYDzC2ucKMPGiH5kfwSGQ==", "dev": true }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, "opds-feed-parser": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/opds-feed-parser/-/opds-feed-parser-0.0.18.tgz", @@ -12490,6 +13442,17 @@ "safe-buffer": "^5.1.1" } }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -12585,6 +13548,21 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -12622,6 +13600,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "pg-connection-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.0.0.tgz", + "integrity": "sha1-Pu/lmX4G2Ugh5NUC5CtqHHP434I=", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -15175,6 +16159,11 @@ "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" }, + "ramda": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", + "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -15631,12 +16620,114 @@ "symbol-observable": "^1.2.0" } }, + "redux-devtools-cli": { + "version": "0.0.1-1", + "resolved": "https://registry.npmjs.org/redux-devtools-cli/-/redux-devtools-cli-0.0.1-1.tgz", + "integrity": "sha512-6cnQ+INNmUiU0J/m42BtEp6dwbprEc4RNq/17fTHd+8ZfksFoi/PL5l2ti7lpulN4Ow0PAz026qiGyuD+YmzwA==", + "dev": true, + "requires": { + "body-parser": "^1.15.0", + "chalk": "^1.1.3", + "cors": "^2.7.1", + "ejs": "^2.4.1", + "express": "^4.13.3", + "getport": "^0.1.0", + "graphql": "^0.13.0", + "graphql-server-express": "^1.4.0", + "graphql-tools": "^4.0.3", + "knex": "^0.15.2", + "lodash": "^4.15.0", + "minimist": "^1.2.0", + "morgan": "^1.7.0", + "semver": "^5.3.0", + "socketcluster": "^14.3.3", + "sqlite3": "^4.0.4", + "uuid": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "redux-devtools-core": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/redux-devtools-core/-/redux-devtools-core-0.2.1.tgz", + "integrity": "sha512-RAGOxtUFdr/1USAvxrWd+Gq/Euzgw7quCZlO5TgFpDfG7rB5tMhZUrNyBjpzgzL2yMk0eHnPYIGm7NkIfRzHxQ==", + "dev": true, + "requires": { + "get-params": "^0.1.2", + "jsan": "^3.1.13", + "lodash": "^4.17.11", + "nanoid": "^2.0.0", + "remotedev-serialize": "^0.1.8" + } + }, "redux-devtools-extension": { "version": "2.13.8", "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz", "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==", "dev": true }, + "redux-devtools-instrument": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.9.6.tgz", + "integrity": "sha512-MwvY4cLEB2tIfWWBzrUR02UM9qRG2i7daNzywRvabOSVdvAY7s9BxSwMmVRH1Y/7QWjplNtOwgT0apKhHg2Qew==", + "dev": true, + "requires": { + "lodash": "^4.2.0", + "symbol-observable": "^1.0.2" + } + }, "redux-saga": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz", @@ -15699,6 +16790,29 @@ "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", "dev": true }, + "remote-redux-devtools": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/remote-redux-devtools/-/remote-redux-devtools-0.5.16.tgz", + "integrity": "sha512-xZ2D1VRIWzat5nsvcraT6fKEX9Cfi+HbQBCwzNnUAM8Uicm/anOc60XGalcaDPrVmLug7nhDl2nimEa3bL3K9w==", + "dev": true, + "requires": { + "jsan": "^3.1.13", + "querystring": "^0.2.0", + "redux-devtools-core": "^0.2.1", + "redux-devtools-instrument": "^1.9.4", + "rn-host-detect": "^1.1.5", + "socketcluster-client": "^14.2.1" + } + }, + "remotedev-serialize": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/remotedev-serialize/-/remotedev-serialize-0.1.8.tgz", + "integrity": "sha512-3YG/FDcOmiK22bl5oMRM8RRnbGrFEuPGjbcDG+z2xi5aQaNQNZ8lqoRnZTwXVfaZtutXuiAQOgPRrogzQk8edg==", + "dev": true, + "requires": { + "jsan": "^3.1.13" + } + }, "remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -16010,6 +17124,16 @@ "lowercase-keys": "^1.0.0" } }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -16051,6 +17175,12 @@ "inherits": "^2.0.1" } }, + "rn-host-detect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rn-host-detect/-/rn-host-detect-1.2.0.tgz", + "integrity": "sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==", + "dev": true + }, "roarr": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.3.tgz", @@ -16081,6 +17211,15 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -16149,6 +17288,126 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "sc-auth": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/sc-auth/-/sc-auth-5.0.2.tgz", + "integrity": "sha512-Le3YBsFjzv5g6wIH6Y+vD+KFkK0HDXiaWy1Gm4nXtYebMQUyNYSf1cS83MtHrYzVEMlhYElRva1b0bvZ0hBqQw==", + "dev": true, + "requires": { + "jsonwebtoken": "^8.3.0", + "sc-errors": "^1.4.1" + }, + "dependencies": { + "sc-errors": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", + "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", + "dev": true + } + } + }, + "sc-broker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/sc-broker/-/sc-broker-6.0.0.tgz", + "integrity": "sha512-c1mFIllUdPnEXDDFxTiX3obYW+cT0hb56fdNM5k+Xo5DI3+3Q9MYxTc8jD23qBIXOHokt4+d/CHocmZQPlAjAQ==", + "dev": true, + "requires": { + "async": "^2.6.1", + "expirymanager": "^0.9.3", + "fleximap": "^1.0.0", + "ncom": "^1.0.2", + "sc-errors": "^1.4.1", + "uuid": "3.1.0" + }, + "dependencies": { + "sc-errors": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", + "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", + "dev": true + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + } + } + }, + "sc-broker-cluster": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/sc-broker-cluster/-/sc-broker-cluster-7.0.0.tgz", + "integrity": "sha512-DNG8sxiFwmRSMS0sUXA25UvDV8QTwEfYnzrutqbp4HlMU9JP65FBcs6GuNFPhjQN4s9VtwAE8BBaCNK5BjNV0g==", + "dev": true, + "requires": { + "async": "2.0.0", + "sc-broker": "^6.0.0", + "sc-channel": "^1.2.0", + "sc-errors": "^1.4.1", + "sc-hasher": "^1.0.1" + }, + "dependencies": { + "async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.0.0.tgz", + "integrity": "sha1-0JAK04WvE4BFQKEJxCFm4657K50=", + "dev": true, + "requires": { + "lodash": "^4.8.0" + } + }, + "sc-errors": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", + "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", + "dev": true + } + } + }, + "sc-channel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", + "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", + "dev": true, + "requires": { + "component-emitter": "1.2.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + } + } + }, + "sc-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-2.0.1.tgz", + "integrity": "sha512-JoVhq3Ud+3Ujv2SIG7W0XtjRHsrNgl6iXuHHsh0s+Kdt5NwI6N2EGAZD4iteitdDv68ENBkpjtSvN597/wxPSQ==", + "dev": true + }, + "sc-formatter": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.2.tgz", + "integrity": "sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==", + "dev": true + }, + "sc-hasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sc-hasher/-/sc-hasher-1.0.1.tgz", + "integrity": "sha512-whZWw70Gp5ibXXMcz6+Tulmk8xkwWMs42gG70p12hGscdUg8BICBvihS3pX2T3dWTw+yeZuGKiULr3MwL37SOQ==", + "dev": true + }, + "sc-simple-broker": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/sc-simple-broker/-/sc-simple-broker-2.1.3.tgz", + "integrity": "sha512-ldt0ybOS5fVZSMea5Z8qVu7lmDBTy0qO9BD6TseJjRuPx+g+stfSqmPAb0RsCsQUXRH8A1koCbwsuUnI9BOxvw==", + "dev": true, + "requires": { + "sc-channel": "^1.2.0" + } + }, "scheduler": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", @@ -16650,6 +17909,168 @@ } } }, + "socketcluster": { + "version": "14.4.2", + "resolved": "https://registry.npmjs.org/socketcluster/-/socketcluster-14.4.2.tgz", + "integrity": "sha512-Z45tSQ6K/XUEyftrID1hyBXSdaK/gDeq6BMqhNR3XvjnUQ6HkkeTrxZUoXIn/In/J8KLl1WRVtvZAB0Zf9pEjA==", + "dev": true, + "requires": { + "async": "2.3.0", + "fs-extra": "6.0.1", + "inquirer": "5.2.0", + "minimist": "1.2.0", + "sc-auth": "^5.0.2", + "sc-broker-cluster": "^7.0.0", + "sc-errors": "^1.4.1", + "socketcluster-server": "^14.7.0", + "uid-number": "0.0.6", + "uuid": "3.2.1" + }, + "dependencies": { + "async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.3.0.tgz", + "integrity": "sha1-EBPRBRBH3TIP4k5JTVxm7K9hR9k=", + "dev": true, + "requires": { + "lodash": "^4.14.0" + } + }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "sc-errors": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", + "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + } + } + }, + "socketcluster-client": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-14.3.1.tgz", + "integrity": "sha512-Sd/T0K/9UlqTfz+HUuFq90dshA5OBJPQbdkRzGtcKIOm52fkdsBTt0FYpiuzzxv5VrU7PWpRm6KIfNXyPwlLpw==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "clone": "2.1.1", + "component-emitter": "1.2.1", + "linked-list": "0.1.0", + "querystring": "0.2.0", + "sc-channel": "^1.2.0", + "sc-errors": "^2.0.1", + "sc-formatter": "^3.0.1", + "uuid": "3.2.1", + "ws": "7.1.0" + }, + "dependencies": { + "buffer": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", + "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "ws": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz", + "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==", + "dev": true, + "requires": { + "async-limiter": "^1.0.0" + } + } + } + }, + "socketcluster-server": { + "version": "14.7.1", + "resolved": "https://registry.npmjs.org/socketcluster-server/-/socketcluster-server-14.7.1.tgz", + "integrity": "sha512-KhZ1c6BKOtGaUWAA9Jdvvs+qSzMq/rBzB8O1Jpq4EpX4+zbq2B4igH6yxnflZw2EamAcAX06XokX+nre5PY+vA==", + "dev": true, + "requires": { + "async": "^3.1.0", + "base64id": "1.0.0", + "component-emitter": "1.2.1", + "lodash.clonedeep": "4.5.0", + "sc-auth": "^5.0.2", + "sc-errors": "^2.0.1", + "sc-formatter": "^3.0.2", + "sc-simple-broker": "^2.1.3", + "uuid": "3.2.1", + "ws": "7.1.0" + }, + "dependencies": { + "async": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async/-/async-3.1.1.tgz", + "integrity": "sha512-X5Dj8hK1pJNC2Wzo2Rcp9FBVdJMGRR/S7V+lH46s8GVFhtbo5O4Le5GECCF/8PISVdkUA6mMPvgz7qTTD1rf1g==", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "ws": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz", + "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==", + "dev": true, + "requires": { + "async-limiter": "^1.0.0" + } + } + } + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", @@ -16845,6 +18266,17 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "sqlite3": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz", + "integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==", + "dev": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.11.0", + "request": "^2.87.0" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -17677,6 +19109,35 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "tarn": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.5.tgz", + "integrity": "sha512-PMtJ3HCLAZeedWjJPgGnCvcphbCOMbtZpjKgLq3qM5Qq9aQud+XHrL0WlrlgnTyS8U+jrjGbEXprFcQrxPy52g==", + "dev": true + }, "temp-file": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.3.7.tgz", @@ -17949,6 +19410,15 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, "time-stamp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", @@ -18167,6 +19637,15 @@ "utf8-byte-length": "^1.0.1" } }, + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "dev": true, + "requires": { + "tslib": "^1.9.3" + } + }, "ts-jest": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-24.3.0.tgz", @@ -18211,6 +19690,12 @@ } } }, + "ts-toolbelt": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.1.13.tgz", + "integrity": "sha512-xfhXvHNMg9i0+L9aALC5kU+/H1eaLl5yydMe6m+2Danmfw/sIX+ixF7JTP4XSQRaG+Xd1idoTmcCVI0QmqZllQ==", + "dev": true + }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", @@ -18381,6 +19866,12 @@ } } }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "dev": true + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -18865,6 +20356,15 @@ "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", "dev": true }, + "v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -19847,6 +21347,48 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -20059,6 +21601,22 @@ "requires": { "buffer-crc32": "~0.2.3" } + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", + "dev": true + }, + "zen-observable-ts": { + "version": "0.8.20", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz", + "integrity": "sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA==", + "dev": true, + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } } } } diff --git a/package.json b/package.json index 94cd73669..f7f92e51e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "start:dev:main:electron": "cross-env DEBUG=r2:*,readium-desktop:* NODE_ENV=development electron .", "start:dev:main": "npm run build:dev:main && npm run start:dev:main:electron", "start:devex": "cross-env WEBPACK=bundle-external npm run start:dev", - "start:dev": "cross-env DEBUG_COLORS=true concurrently --kill-others \"npm run start:dev:renderer:library\" \"npm run start:dev:renderer:reader\" \"npm run start:dev:main\"", + "start:dev": "cross-env DEBUG_COLORS=true concurrently --kill-others \"npm run start:dev:renderer:library\" \"npm run start:dev:renderer:reader\" \"npm run start:dev:main\" \"npm run redux-devtools\"", "vscode:launch:attach:electron": "electron --enable-logging --remote-debugging-port=9223 --inspect=25575 --nolazy .", "vscode:launch:attach": "cross-env DEBUG=r2:*,readium-desktop:* NODE_ENV=development VSCODE_LAUNCH=true npm run build:dev:main && concurrently --kill-others \"npm run start:dev:renderer:library\" \"npm run start:dev:renderer:reader\" \"npm run vscode:launch:attach:electron\"", "vscode:launch:hot": "cross-env DEBUG=r2:*,readium-desktop:* NODE_ENV=development VSCODE_LAUNCH=true npm run build:dev:main", @@ -56,7 +56,8 @@ "i18n-sort": "node ./scripts/locales-sort.js", "i18n-scan": "node ./scripts/translate-scan.js \"src/resources/locales/temp.json\" && sync-i18n --files 'src/resources/locales/*.json' --primary temp --languages en fr de en nl --space 4 --finalnewline --newkeysempty && rimraf \"src/resources/locales/temp.json\"", "i18n-check": "sync-i18n --files 'src/resources/locales/*.json' --primary en --languages fr de en nl --space 4 --finalnewline --newkeysempty", - "i18n-typed": "node ./scripts/locale-wrap.js \"src/resources/locales/en.json\" \"en.json\" && typed_i18n -i \"en.json\" -o src/typings -l typescript && rimraf \"en.json\"" + "i18n-typed": "node ./scripts/locale-wrap.js \"src/resources/locales/en.json\" \"en.json\" && typed_i18n -i \"en.json\" -o src/typings -l typescript && rimraf \"en.json\"", + "redux-devtools": "redux-devtools --hostname=localhost --port=7770" }, "repository": { "type": "git", @@ -212,6 +213,7 @@ "r2-shared-js": "^1.0.35", "r2-streamer-js": "^1.0.27", "r2-utils-js": "^1.0.20", + "ramda": "^0.27.0", "react": "^16.13.1", "react-beautiful-dnd": "^13.0.0", "react-dom": "^16.13.1", @@ -245,6 +247,7 @@ "@types/jest": "^24.9.1", "@types/node": "^12.12.37", "@types/pouchdb-core": "^7.0.5", + "@types/ramda": "^0.26.41", "@types/react": "^16.9.34", "@types/react-beautiful-dnd": "^12.1.2", "@types/react-dom": "^16.9.7", @@ -252,6 +255,7 @@ "@types/react-router": "^5.1.7", "@types/react-router-dom": "^5.1.5", "@types/redux": "^3.6.31", + "@types/remote-redux-devtools": "^0.5.4", "@types/request": "^2.48.4", "@types/tmp": "^0.2.0", "@types/urijs": "^1.19.8", @@ -290,7 +294,9 @@ "pouchdb-adapter-memory": "^7.2.1", "react-axe": "^3.4.1", "react-svg-loader": "^3.0.3", + "redux-devtools-cli": "0.0.1-1", "redux-devtools-extension": "^2.13.8", + "remote-redux-devtools": "^0.5.16", "rimraf": "^3.0.2", "style-loader": "^1.2.1", "svg-sprite-loader": "^4.3.0", diff --git a/src/common/api/api.type.ts b/src/common/api/api.type.ts index af76f6dfb..b79f0d304 100644 --- a/src/common/api/api.type.ts +++ b/src/common/api/api.type.ts @@ -11,6 +11,7 @@ import { ILcpModuleApi } from "./interface/lcpApi.interface"; import { IOpdsModuleApi } from "./interface/opdsApi.interface"; import { IPublicationModuleApi } from "./interface/publicationApi.interface"; import { IReaderModuleApi } from "./interface/readerApi.interface"; +import { ISessionModuleApi } from "./interface/session.interface"; export type TApiMethod = ICatalogModuleApi & @@ -18,6 +19,7 @@ export type TApiMethod = IOpdsModuleApi & IKeyboardModuleApi & IPublicationModuleApi & - IReaderModuleApi; + IReaderModuleApi & + ISessionModuleApi; export type TApiMethodName = keyof TApiMethod; diff --git a/src/common/api/interface/catalog.interface.ts b/src/common/api/interface/catalog.interface.ts index e9d35abd9..d53e611c7 100644 --- a/src/common/api/interface/catalog.interface.ts +++ b/src/common/api/interface/catalog.interface.ts @@ -5,18 +5,18 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { CatalogEntryView, CatalogView } from "readium-desktop/common/views/catalog"; +import { CatalogView } from "readium-desktop/common/views/catalog"; export interface ICatalogApi { get: () => Promise; - addEntry: (entryView: CatalogEntryView) => Promise; - getEntries: () => Promise; - updateEntries: (entryView: CatalogEntryView[]) => 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"]; + // "catalog/addEntry": ICatalogApi["addEntry"]; + // "catalog/getEntries": ICatalogApi["getEntries"]; + // "catalog/updateEntries": ICatalogApi["updateEntries"]; } diff --git a/src/common/api/interface/opdsApi.interface.ts b/src/common/api/interface/opdsApi.interface.ts index 4313682c8..be9f534d2 100644 --- a/src/common/api/interface/opdsApi.interface.ts +++ b/src/common/api/interface/opdsApi.interface.ts @@ -10,7 +10,8 @@ import { IOpdsFeedView, IOpdsLinkView, THttpGetOpdsResultView, } from "readium-desktop/common/views/opds"; -export type TOpdsLinkSearch = Required>; +// quite useless +export type TOpdsLinkSearch = Required>; export interface IOpdsApi { getFeed: ( diff --git a/src/common/api/interface/readerApi.interface.ts b/src/common/api/interface/readerApi.interface.ts index 380efede8..723094043 100644 --- a/src/common/api/interface/readerApi.interface.ts +++ b/src/common/api/interface/readerApi.interface.ts @@ -5,14 +5,16 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import { ReaderMode } from "readium-desktop/common/models/reader"; import { LocatorView } from "readium-desktop/common/views/locator"; import { IEventPayload_R2_EVENT_CLIPBOARD_COPY } from "@r2-navigator-js/electron/common/events"; +import { LocatorExtended } from "@r2-navigator-js/electron/renderer"; import { Locator as R2Locator } from "@r2-shared-js/models/locator"; export interface IReaderApi { - setLastReadingLocation: (publicationIdentifier: string, locator: R2Locator) => Promise; - getLastReadingLocation: (publicationIdentifier: string) => Promise; + // setLastReadingLocation: (publicationIdentifier: string, locator: R2Locator) => Promise; + getLastReadingLocation: (publicationIdentifier: string) => Promise; findBookmarks: (publicationIdentifier: string) => Promise; updateBookmark: ( identifier: string, @@ -29,14 +31,16 @@ export interface IReaderApi { clipboardCopy: ( publicationIdentifier: string, clipboardData: IEventPayload_R2_EVENT_CLIPBOARD_COPY) => Promise; + getMode: () => Promise; } export interface IReaderModuleApi { - "reader/setLastReadingLocation": IReaderApi["setLastReadingLocation"]; + // "reader/setLastReadingLocation": IReaderApi["setLastReadingLocation"]; "reader/getLastReadingLocation": IReaderApi["getLastReadingLocation"]; "reader/findBookmarks": IReaderApi["findBookmarks"]; "reader/updateBookmark": IReaderApi["updateBookmark"]; "reader/addBookmark": IReaderApi["addBookmark"]; "reader/deleteBookmark": IReaderApi["deleteBookmark"]; "reader/clipboardCopy": IReaderApi["clipboardCopy"]; + "reader/getMode": IReaderApi["getMode"]; } diff --git a/src/common/api/interface/session.interface.ts b/src/common/api/interface/session.interface.ts new file mode 100644 index 000000000..1a77b0a12 --- /dev/null +++ b/src/common/api/interface/session.interface.ts @@ -0,0 +1,16 @@ +// ==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== + +export interface ISessionApi { + enable: (bool: boolean) => Promise; + isEnabled: () => Promise; +} + +export interface ISessionModuleApi { + "session/enable": ISessionApi["enable"]; + "session/isEnabled": ISessionApi["isEnabled"]; +} diff --git a/src/common/api/methodApi.type.ts b/src/common/api/methodApi.type.ts index ee3b5cb22..097014a04 100644 --- a/src/common/api/methodApi.type.ts +++ b/src/common/api/methodApi.type.ts @@ -11,6 +11,7 @@ import { ILcpApi } from "./interface/lcpApi.interface"; import { IOpdsApi } from "./interface/opdsApi.interface"; import { IPublicationApi } from "./interface/publicationApi.interface"; import { IReaderApi } from "./interface/readerApi.interface"; +import { ISessionApi } from "./interface/session.interface"; export type TMethodApi = keyof ICatalogApi | @@ -18,4 +19,5 @@ export type TMethodApi = keyof IOpdsApi | keyof IKeyboardApi | keyof ILcpApi | - keyof IReaderApi; + keyof IReaderApi | + keyof ISessionApi; diff --git a/src/common/api/moduleApi.type.ts b/src/common/api/moduleApi.type.ts index 7c3c3d021..356fe5364 100644 --- a/src/common/api/moduleApi.type.ts +++ b/src/common/api/moduleApi.type.ts @@ -12,10 +12,12 @@ type TOpdsApi = "opds"; type TKeyboardApi = "keyboardShortcuts"; type TLcpApi = "lcp"; type TReaderApi = "reader"; +type TSessionApi = "session"; export type TModuleApi = TCatalogApi | TPublicationApi | TOpdsApi | TKeyboardApi | TLcpApi | - TReaderApi; + TReaderApi | + TSessionApi; diff --git a/src/common/errors.ts b/src/common/codeError.class.ts similarity index 100% rename from src/common/errors.ts rename to src/common/codeError.class.ts diff --git a/src/common/computeReadiumCssJsonMessage.ts b/src/common/computeReadiumCssJsonMessage.ts new file mode 100644 index 000000000..78df1437d --- /dev/null +++ b/src/common/computeReadiumCssJsonMessage.ts @@ -0,0 +1,74 @@ +// ==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 { ReaderConfig } from "readium-desktop/common/models/reader"; + +import { IEventPayload_R2_EVENT_READIUMCSS } from "@r2-navigator-js/electron/common/events"; +import { + colCountEnum, IReadiumCSS, readiumCSSDefaults, textAlignEnum, +} from "@r2-navigator-js/electron/common/readium-css-settings"; + +export const computeReadiumCssJsonMessage = (settings: ReaderConfig): IEventPayload_R2_EVENT_READIUMCSS => { + + const cssJson: IReadiumCSS = { + + a11yNormalize: readiumCSSDefaults.a11yNormalize, + + backgroundColor: readiumCSSDefaults.backgroundColor, + + bodyHyphens: readiumCSSDefaults.bodyHyphens, + + colCount: settings.colCount === "1" ? colCountEnum.one : + (settings.colCount === "2" ? colCountEnum.two : colCountEnum.auto), + + darken: settings.darken, + + font: settings.font, + + fontSize: settings.fontSize, + + invert: settings.invert, + + letterSpacing: settings.letterSpacing, + + ligatures: readiumCSSDefaults.ligatures, + + lineHeight: settings.lineHeight, + + night: settings.night, + + pageMargins: settings.pageMargins, + + paged: settings.paged, + + paraIndent: readiumCSSDefaults.paraIndent, + + paraSpacing: settings.paraSpacing, + + sepia: settings.sepia, + + noFootnotes: settings.noFootnotes, + + textAlign: settings.align === textAlignEnum.left ? textAlignEnum.left : + (settings.align === textAlignEnum.right ? textAlignEnum.right : + (settings.align === textAlignEnum.justify ? textAlignEnum.justify : + (settings.align === textAlignEnum.start ? textAlignEnum.start : undefined))), + + textColor: readiumCSSDefaults.textColor, + + typeScale: readiumCSSDefaults.typeScale, + + wordSpacing: settings.wordSpacing, + + mathJax: settings.enableMathJax, + + reduceMotion: readiumCSSDefaults.reduceMotion, + }; + + const jsonMsg: IEventPayload_R2_EVENT_READIUMCSS = { setCSS: cssJson }; + return jsonMsg; +}; diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index c2d61b37f..1a1c29598 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -5,10 +5,12 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import * as readerIpc from "./reader"; import * as syncIpc from "./sync"; import * as winIpc from "./win"; export { + readerIpc, syncIpc, winIpc, }; diff --git a/src/common/ipc/reader.ts b/src/common/ipc/reader.ts new file mode 100644 index 000000000..24e1c8382 --- /dev/null +++ b/src/common/ipc/reader.ts @@ -0,0 +1,22 @@ +// ==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 { + IReaderRootState, +} from "readium-desktop/common/redux/states/renderer/readerRootState"; + +export enum EventType { + request = "REQUEST", + response = "RESPONSE", +} + +export const CHANNEL = "READER_INIT"; + +export interface EventPayload { + type: EventType; + payload: Partial; +} diff --git a/src/common/ipc/win.ts b/src/common/ipc/win.ts index c82c40dfc..ce79673ec 100644 --- a/src/common/ipc/win.ts +++ b/src/common/ipc/win.ts @@ -15,6 +15,6 @@ export const CHANNEL = "WIN"; export interface EventPayload { type: EventType; payload: { - winId: string; + identifier: string; }; } diff --git a/src/common/models/reader.ts b/src/common/models/reader.ts index b6e0ce116..01c6ced77 100644 --- a/src/common/models/reader.ts +++ b/src/common/models/reader.ts @@ -5,10 +5,6 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { BrowserWindow } from "electron"; - -import { Identifiable } from "./identifiable"; - export enum ReaderMode { Attached = "attached", Detached = "detached", @@ -17,12 +13,10 @@ export enum ReaderMode { /** * A reader */ -export interface Reader extends Identifiable { +export interface ReaderInfo { filesystemPath: string; manifestUrl: string; publicationIdentifier: string; - browserWindow: BrowserWindow; - browserWindowID: number; } /** diff --git a/src/common/models/sync.ts b/src/common/models/sync.ts index fff34d246..a670b546b 100644 --- a/src/common/models/sync.ts +++ b/src/common/models/sync.ts @@ -14,7 +14,7 @@ export enum SenderType { export interface WindowSender { type: SenderType; - winId: string; + identifier: string; } export interface WithSender { diff --git a/src/common/models/win.ts b/src/common/models/win.ts index 8aaf2edff..5cc54f7a5 100644 --- a/src/common/models/win.ts +++ b/src/common/models/win.ts @@ -1,24 +1,24 @@ -// ==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== +// // ==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 { BrowserWindow } from "electron"; +// import { BrowserWindow } from "electron"; -import { IOnWindowMoveResize } from "../rectangle/window"; -import { Identifiable } from "./identifiable"; +// import { IOnWindowMoveResize } from "../rectangle/window"; +// import { Identifiable } from "./identifiable"; -export enum AppWindowType { - Library = "library", - Reader = "reader", -} +// export enum AppWindowType { +// Library = "library", +// Reader = "reader", +// } -export interface AppWindow extends Identifiable { - type: AppWindowType; - browserWindow: BrowserWindow; - browserWindowID: number; - onWindowMoveResize: IOnWindowMoveResize; - registerIndex: number; -} +// export interface AppWindow extends Identifiable { +// type: AppWindowType; +// browserWindow: BrowserWindow; +// browserWindowID: number; +// onWindowMoveResize: IOnWindowMoveResize; +// registerIndex: number; +// } diff --git a/src/common/rectangle/window.ts b/src/common/rectangle/window.ts index 24cf3c83b..4ffff8ed2 100644 --- a/src/common/rectangle/window.ts +++ b/src/common/rectangle/window.ts @@ -5,21 +5,21 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import * as debug_ from "debug"; -import { BrowserWindow, Rectangle, screen } from "electron"; -import { ConfigDocument } from "readium-desktop/main/db/document/config"; -import { ConfigRepository } from "readium-desktop/main/db/repository/config"; -import { diMainGet } from "readium-desktop/main/di"; -import { debounce } from "readium-desktop/utils/debounce"; +// import * as debug_ from "debug"; +import { /*BrowserWindow,*/ Rectangle, screen } from "electron"; +// import { ConfigDocument } from "readium-desktop/main/db/document/config"; +// import { ConfigRepository } from "readium-desktop/main/db/repository/config"; +// import { diMainGet } from "readium-desktop/main/di"; +// import { debounce } from "readium-desktop/utils/debounce"; -import { AppWindowType } from "../models/win"; +// import { AppWindowType } from "../models/win"; -// Logger -const debug = debug_("readium-desktop:common:rectangle:window"); +// // Logger +// const debug = debug_("readium-desktop:common:rectangle:window"); -const WINDOW_RECT_CONFIG_ID = "windowRectangle"; +// const WINDOW_RECT_CONFIG_ID = "windowRectangle"; -const defaultRectangle = (): Rectangle => ( +export const defaultRectangle = (): Rectangle => ( { height: 600, width: 800, @@ -27,83 +27,83 @@ const defaultRectangle = (): Rectangle => ( y: Math.round(screen.getPrimaryDisplay().workAreaSize.height / 3), }); -export type t_savedWindowsRectangle = typeof savedWindowsRectangle; -export const savedWindowsRectangle = async (rectangle: Rectangle) => { - try { - const configRepository: ConfigRepository = diMainGet("config-repository"); - await configRepository.save({ - identifier: WINDOW_RECT_CONFIG_ID, - value: rectangle, - }); - debug("new window rectangle position :", rectangle); - } catch (e) { - debug("save error", e); - } - return rectangle; -}; -const debounceSavedWindowsRectangle = debounce(savedWindowsRectangle, 500); - -export const getWindowBounds = async (winType?: AppWindowType): Promise => { - - try { - const winRegistry = diMainGet("win-registry"); - const readerWindows = winRegistry.getReaderWindows(); - - const displayArea = screen.getPrimaryDisplay().workAreaSize; - - if (winType !== AppWindowType.Library && // is reader window - readerWindows.length > 0) { // there are already reader windows - - // readerWindows is ordered by creation/registration time - // so we take the latest reader window and offset the new one - const rectangle = readerWindows[readerWindows.length - 1].browserWindow.getBounds(); - rectangle.x += 100; - rectangle.x %= displayArea.width - rectangle.width; - rectangle.y += 100; - rectangle.y %= displayArea.height - rectangle.height; - return rectangle; - - } else { // winType === AppWindowType.Library || readerWindows.length == 0 - - const configRepository: ConfigRepository = diMainGet("config-repository"); - let rectangle: ConfigDocument | undefined; - try { - rectangle = await configRepository.get(WINDOW_RECT_CONFIG_ID); - } catch (err) { - // ignore - } - if (rectangle && rectangle.value) { - debug("get window rectangle position from db :", rectangle.value); - return rectangle.value; - } - } - } catch (e) { - debug("get error", e); - } - - debug("default window rectangle"); - return defaultRectangle(); -}; - -export interface IOnWindowMoveResize { - attach: () => void; - detach: () => void; -} - -// handler to attach and detach move/resize event to win -export const onWindowMoveResize = (win: BrowserWindow): IOnWindowMoveResize => { - const handler = () => { - debounceSavedWindowsRectangle(win.getBounds()); - }; - - return { - attach: () => { - win.on("move", handler); - win.on("resize", handler); - }, - detach: () => { - win.removeListener("move", handler); - win.removeListener("resize", handler); - }, - }; -}; +// export type t_savedWindowsRectangle = typeof savedWindowsRectangle; +// export const savedWindowsRectangle = async (rectangle: Rectangle) => { +// try { +// const configRepository: ConfigRepository = diMainGet("config-repository"); +// await configRepository.save({ +// identifier: WINDOW_RECT_CONFIG_ID, +// value: rectangle, +// }); +// debug("new window rectangle position :", rectangle); +// } catch (e) { +// debug("save error", e); +// } +// return rectangle; +// }; +// const debounceSavedWindowsRectangle = debounce(savedWindowsRectangle, 500); + +// export const getWindowBounds = async (winType?: AppWindowType): Promise => { + +// try { +// const winRegistry = diMainGet("win-registry"); +// const readerWindows = winRegistry.getReaderWindows(); + +// const displayArea = screen.getPrimaryDisplay().workAreaSize; + +// if (winType !== AppWindowType.Library && // is reader window +// readerWindows.length > 0) { // there are already reader windows + +// // readerWindows is ordered by creation/registration time +// // so we take the latest reader window and offset the new one +// const rectangle = readerWindows[readerWindows.length - 1].browserWindow.getBounds(); +// rectangle.x += 100; +// rectangle.x %= displayArea.width - rectangle.width; +// rectangle.y += 100; +// rectangle.y %= displayArea.height - rectangle.height; +// return rectangle; + +// } else { // winType === AppWindowType.Library || readerWindows.length == 0 + +// const configRepository: ConfigRepository = diMainGet("config-repository"); +// let rectangle: ConfigDocument | undefined; +// try { +// rectangle = await configRepository.get(WINDOW_RECT_CONFIG_ID); +// } catch (err) { +// // ignore +// } +// if (rectangle && rectangle.value) { +// debug("get window rectangle position from db :", rectangle.value); +// return rectangle.value; +// } +// } +// } catch (e) { +// debug("get error", e); +// } + +// debug("default window rectangle"); +// return defaultRectangle(); +// }; + +// export interface IOnWindowMoveResize { +// attach: () => void; +// detach: () => void; +// } + +// // handler to attach and detach move/resize event to win +// export const onWindowMoveResize = (win: BrowserWindow): IOnWindowMoveResize => { +// const handler = () => { +// debounceSavedWindowsRectangle(win.getBounds()); +// }; + +// return { +// attach: () => { +// win.on("move", handler); +// win.on("resize", handler); +// }, +// detach: () => { +// win.removeListener("move", handler); +// win.removeListener("resize", handler); +// }, +// }; +// }; diff --git a/src/common/redux/actions/api/result.ts b/src/common/redux/actions/api/result.ts index 0059cc42d..29ebb05bb 100644 --- a/src/common/redux/actions/api/result.ts +++ b/src/common/redux/actions/api/result.ts @@ -5,7 +5,7 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { CodeError } from "readium-desktop/common/errors"; +import { CodeError } from "readium-desktop/common/codeError.class"; import { Action } from "readium-desktop/common/models/redux"; import { Meta, MetaApi } from "./types"; @@ -29,4 +29,4 @@ export function build(api: MetaApi, payload: any): }; } build.toString = () => ID; // Redux StringableActionCreator -export type TAction = ReturnType; +export type TAction

= Action; diff --git a/src/common/redux/actions/index.ts b/src/common/redux/actions/index.ts index ac9e61e4f..96b3c3d60 100644 --- a/src/common/redux/actions/index.ts +++ b/src/common/redux/actions/index.ts @@ -12,6 +12,7 @@ import * as i18nActions from "./i18n/"; import * as importActions from "./import/"; import * as keyboardActions from "./keyboard/"; import * as lcpActions from "./lcp/"; +import * as loadActions from "./load"; import * as netActions from "./net/"; import * as readerActions from "./reader/"; import * as toastActions from "./toast/"; @@ -30,4 +31,5 @@ export { toastActions, downloadActions, keyboardActions, + loadActions, }; diff --git a/src/common/redux/actions/reader/configSetError.ts b/src/common/redux/actions/load/busy.ts similarity index 73% rename from src/common/redux/actions/reader/configSetError.ts rename to src/common/redux/actions/load/busy.ts index 4aca63851..87115bf3b 100644 --- a/src/common/redux/actions/reader/configSetError.ts +++ b/src/common/redux/actions/load/busy.ts @@ -7,15 +7,18 @@ import { Action } from "readium-desktop/common/models/redux"; -export const ID = "READER_CONFIG_SET_ERROR"; +export const ID = "MAIN_PROCESS_BUSY"; -export function build(error: any): - Action { +// tslint:disable-next-line: no-empty-interface +export interface Payload { +} + +export function build(): Action { return { type: ID, - payload: error, - error: true, + payload: { + }, }; } build.toString = () => ID; // Redux StringableActionCreator diff --git a/src/common/redux/actions/load/idle.ts b/src/common/redux/actions/load/idle.ts new file mode 100644 index 000000000..b8b8b598d --- /dev/null +++ b/src/common/redux/actions/load/idle.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 = "MAIN_PROCESS_IDLE"; + +// tslint:disable-next-line: no-empty-interface +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/load/index.ts b/src/common/redux/actions/load/index.ts new file mode 100644 index 000000000..30209743c --- /dev/null +++ b/src/common/redux/actions/load/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 busy from "./busy"; +import * as idle from "./idle"; + +export { + busy, + idle, +}; diff --git a/src/common/redux/actions/reader/configSetSuccess.ts b/src/common/redux/actions/reader/attachModeRequest.ts similarity index 76% rename from src/common/redux/actions/reader/configSetSuccess.ts rename to src/common/redux/actions/reader/attachModeRequest.ts index 91ae1cc69..a6faef815 100644 --- a/src/common/redux/actions/reader/configSetSuccess.ts +++ b/src/common/redux/actions/reader/attachModeRequest.ts @@ -5,22 +5,20 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { ReaderConfig } from "readium-desktop/common/models/reader"; import { Action } from "readium-desktop/common/models/redux"; -export const ID = "READER_CONFIG_SET_SUCCESS"; +export const ID = "READER_MODE_ATTACH_REQUEST"; +// tslint:disable-next-line: no-empty-interface export interface Payload { - config: ReaderConfig; } -export function build(config: ReaderConfig): +export function build(): Action { return { type: ID, payload: { - config, }, }; } diff --git a/src/common/redux/actions/reader/closeError.ts b/src/common/redux/actions/reader/closeError.ts index 1bbbe0038..88fcd0993 100644 --- a/src/common/redux/actions/reader/closeError.ts +++ b/src/common/redux/actions/reader/closeError.ts @@ -5,22 +5,21 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { Reader } from "readium-desktop/common/models/reader"; import { Action } from "readium-desktop/common/models/redux"; export const ID = "READER_CLOSE_ERROR"; export interface Payload { - reader: Reader; + identifier: string; } -export function build(reader: Reader): +export function build(identifier: string): Action { return { type: ID, payload: { - reader, + identifier, }, error: true, }; diff --git a/src/common/redux/actions/reader/closeRequest.ts b/src/common/redux/actions/reader/closeRequest.ts index 814257ee1..dbc5e508c 100644 --- a/src/common/redux/actions/reader/closeRequest.ts +++ b/src/common/redux/actions/reader/closeRequest.ts @@ -5,24 +5,20 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { Reader } from "readium-desktop/common/models/reader"; -import { Action } from "readium-desktop/common/models/redux"; +import { ActionWithSender, WithSender } from "readium-desktop/common/models/sync"; export const ID = "READER_CLOSE_REQUEST"; +// tslint:disable-next-line: no-empty-interface export interface Payload { - reader: Reader; - gotoLibrary: boolean; } -export function build(reader: Reader, gotoLibrary: boolean = false): - Action { +export function build(): + Omit, keyof WithSender> & Partial { return { type: ID, payload: { - reader, - gotoLibrary, }, }; } diff --git a/src/common/redux/actions/reader/closeSuccess.ts b/src/common/redux/actions/reader/closeSuccess.ts index 767a5ea7e..852df8962 100644 --- a/src/common/redux/actions/reader/closeSuccess.ts +++ b/src/common/redux/actions/reader/closeSuccess.ts @@ -5,22 +5,21 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { Reader } from "readium-desktop/common/models/reader"; import { Action } from "readium-desktop/common/models/redux"; export const ID = "READER_CLOSE_SUCCESS"; export interface Payload { - reader: Reader; + identifier: string; } -export function build(reader: Reader): +export function build(identifier: string): Action { return { type: ID, payload: { - reader, + identifier, }, }; } diff --git a/src/common/redux/actions/reader/configSetRequest.ts b/src/common/redux/actions/reader/configSetDefault.ts similarity index 80% rename from src/common/redux/actions/reader/configSetRequest.ts rename to src/common/redux/actions/reader/configSetDefault.ts index 932c69828..6d42bc801 100644 --- a/src/common/redux/actions/reader/configSetRequest.ts +++ b/src/common/redux/actions/reader/configSetDefault.ts @@ -7,14 +7,15 @@ import { ReaderConfig } from "readium-desktop/common/models/reader"; import { Action } from "readium-desktop/common/models/redux"; +import { readerConfigInitialState } from "../../states/reader"; -export const ID = "READER_CONFIG_SET_REQUEST"; +export const ID = "READER_DEFAULT_CONFIG_SET_REQUEST"; export interface Payload { config: ReaderConfig; } -export function build(config: ReaderConfig): +export function build(config: ReaderConfig = readerConfigInitialState): Action { return { diff --git a/src/common/redux/actions/reader/detachModeRequest.ts b/src/common/redux/actions/reader/detachModeRequest.ts index b84809261..89024154c 100644 --- a/src/common/redux/actions/reader/detachModeRequest.ts +++ b/src/common/redux/actions/reader/detachModeRequest.ts @@ -5,24 +5,20 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { Reader, ReaderMode } from "readium-desktop/common/models/reader"; -import { Action } from "readium-desktop/common/models/redux"; +import { ActionWithSender, WithSender } from "readium-desktop/common/models/sync"; -export const ID = "READER_MODE_SET_REQUEST"; +export const ID = "READER_MODE_DETACH_REQUEST"; +// tslint:disable-next-line: no-empty-interface export interface Payload { - reader: Reader; - mode: ReaderMode; } -export function build(reader: Reader): - Action { +export function build(): + Omit, keyof WithSender> & Partial { return { type: ID, payload: { - reader, - mode: ReaderMode.Detached, }, }; } diff --git a/src/common/redux/actions/reader/detachModeSuccess.ts b/src/common/redux/actions/reader/detachModeSuccess.ts index ff542162a..04a1dd87d 100644 --- a/src/common/redux/actions/reader/detachModeSuccess.ts +++ b/src/common/redux/actions/reader/detachModeSuccess.ts @@ -5,22 +5,20 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { ReaderMode } from "readium-desktop/common/models/reader"; import { Action } from "readium-desktop/common/models/redux"; export const ID = "READER_MODE_SET_SUCCESS"; +// tslint:disable-next-line: no-empty-interface export interface Payload { - mode: ReaderMode; } -export function build(mode: ReaderMode): +export function build(): Action { return { type: ID, payload: { - mode, }, }; } diff --git a/src/common/redux/actions/reader/index.ts b/src/common/redux/actions/reader/index.ts index 3eea959a5..16d22ac4b 100644 --- a/src/common/redux/actions/reader/index.ts +++ b/src/common/redux/actions/reader/index.ts @@ -5,35 +5,36 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import * as attachModeRequest from "./attachModeRequest"; import * as closeError from "./closeError"; import * as closeRequest from "./closeRequest"; import * as closeRequestFromPublication from "./closeRequestFromPublication"; import * as closeSuccess from "./closeSuccess"; -import * as configSetError from "./configSetError"; -import * as configSetRequest from "./configSetRequest"; -import * as configSetSuccess from "./configSetSuccess"; +import * as configSetDefault from "./configSetDefault"; import * as detachModeRequest from "./detachModeRequest"; import * as detachModeSuccess from "./detachModeSuccess"; import * as fullScreenRequest from "./fullScreenRequest"; import * as openError from "./openError"; import * as openRequest from "./openRequest"; -import * as openSuccess from "./openSuccess"; +// import * as openSuccess from "./openSuccess"; +import * as setReduxState from "./setReduxState"; + // import * as saveBookmarkError from "./saveBookmarkError"; // import * as saveBookmarkRequest from "./saveBookmarkRequest"; // import * as saveBookmarkSuccess from "./saveBookmarkSuccess"; export { openRequest, - openSuccess, + // openSuccess, openError, closeRequest, closeSuccess, closeError, + attachModeRequest, detachModeRequest, detachModeSuccess, - configSetRequest, - configSetSuccess, - configSetError, + configSetDefault, + setReduxState, // saveBookmarkRequest, // saveBookmarkSuccess, // saveBookmarkError, diff --git a/src/common/redux/actions/reader/openSuccess.ts b/src/common/redux/actions/reader/openSuccess.ts index 8e48299f9..85d9d0b1b 100644 --- a/src/common/redux/actions/reader/openSuccess.ts +++ b/src/common/redux/actions/reader/openSuccess.ts @@ -1,28 +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== +// // ==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 { Reader } from "readium-desktop/common/models/reader"; -import { Action } from "readium-desktop/common/models/redux"; +// import { Reader } from "readium-desktop/common/models/reader"; +// import { Action } from "readium-desktop/common/models/redux"; -export const ID = "READER_OPEN_SUCCESS"; +// export const ID = "READER_OPEN_SUCCESS"; -export interface Payload { - reader: Reader; -} +// export interface Payload { +// reader: Reader; +// } -export function build(reader: Reader): - Action { +// export function build(reader: Reader): +// Action { - return { - type: ID, - payload: { - reader, - }, - }; -} -build.toString = () => ID; // Redux StringableActionCreator -export type TAction = ReturnType; +// return { +// type: ID, +// payload: { +// reader, +// }, +// }; +// } +// build.toString = () => ID; // Redux StringableActionCreator +// export type TAction = ReturnType; diff --git a/src/common/redux/actions/reader/setReduxState.ts b/src/common/redux/actions/reader/setReduxState.ts new file mode 100644 index 000000000..9b3d0d7e3 --- /dev/null +++ b/src/common/redux/actions/reader/setReduxState.ts @@ -0,0 +1,31 @@ + +// ==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 { IReaderStateReader } from "readium-desktop/common/redux/states/renderer/readerRootState"; + +export const ID = "READER_SET_REDUXSTATE"; + +export interface Payload { + reduxState: IReaderStateReader; + identifier: string; +} + +export function build(id: string, reduxState: IReaderStateReader): + Action { + + return { + type: ID, + payload: { + reduxState, + identifier: id, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/common/redux/sagas/spawnLeading.ts b/src/common/redux/sagas/spawnLeading.ts new file mode 100644 index 000000000..3690d0219 --- /dev/null +++ b/src/common/redux/sagas/spawnLeading.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 { call, ForkEffect, spawn } from "redux-saga/effects"; + +// tslint:disable-next-line: no-empty +const noop = () => { }; + +export function spawnLeading( + worker: () => any, + cbErr: (e: any) => void = noop, +): ForkEffect { + return spawn(function*() { + while (true) { + try { + yield call(worker); + } catch (e) { + cbErr(e); + } + } + }); +} diff --git a/src/common/redux/sagas/takeSpawnEvery.ts b/src/common/redux/sagas/takeSpawnEvery.ts new file mode 100644 index 000000000..794eaa2a0 --- /dev/null +++ b/src/common/redux/sagas/takeSpawnEvery.ts @@ -0,0 +1,50 @@ +// ==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 { TakeableChannel } from "redux-saga"; +import { ActionPattern, call, fork, ForkEffect, spawn, take } from "redux-saga/effects"; + +// tslint:disable-next-line: no-empty +const noop = () => { }; + +export function takeSpawnEvery( + pattern: ActionPattern, + worker: (...args: any[]) => any, + cbErr: (e: any) => void = noop, +): ForkEffect { + return spawn(function*() { + while (true) { + const action = yield take(pattern); + yield fork(function*() { + try { + yield call(worker, action); + } catch (e) { + cbErr(e); + } + }); + } + }); +} + +export function takeSpawnEveryChannel( + pattern: TakeableChannel, + worker: (...args: any[]) => any, + cbErr: (e: any) => void = noop, +): ForkEffect { + return spawn(function*() { + while (true) { + const action = yield take(pattern); + yield fork(function*() { + try { + yield call(worker, action); + } catch (e) { + cbErr(e); + } + }); + } + }); +} diff --git a/src/common/redux/sagas/takeSpawnLatest.ts b/src/common/redux/sagas/takeSpawnLatest.ts new file mode 100644 index 000000000..cae725f1b --- /dev/null +++ b/src/common/redux/sagas/takeSpawnLatest.ts @@ -0,0 +1,35 @@ +// ==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 { Task } from "redux-saga"; +import { ActionPattern, call, cancel, fork, ForkEffect, spawn, take } from "redux-saga/effects"; + +// tslint:disable-next-line: no-empty +const noop = () => { }; + +export function takeSpawnLatest( + pattern: ActionPattern, + worker: (...args: any[]) => any, + cbErr: (e: any) => void = noop, +): ForkEffect { + return spawn(function*() { + let lastTask: Task; + while (true) { + const action = yield take(pattern); + if (lastTask) { + yield cancel(lastTask); + } + lastTask = yield fork(function*() { + try { + yield call(worker, action); + } catch (e) { + cbErr(e); + } + }); + } + }); +} diff --git a/src/common/redux/sagas/takeSpawnLeading.ts b/src/common/redux/sagas/takeSpawnLeading.ts new file mode 100644 index 000000000..1cf6e11af --- /dev/null +++ b/src/common/redux/sagas/takeSpawnLeading.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 { ActionPattern, call, ForkEffect, spawn, take } from "redux-saga/effects"; + +// tslint:disable-next-line: no-empty +const noop = () => { }; + +export function takeSpawnLeading

( + pattern: P, + worker: (...args: any[]) => any, + cbErr: (e: any) => void = noop, +): ForkEffect { + return spawn(function*() { + while (true) { + const action = yield take(pattern); + try { + yield call(worker, action); + } catch (e) { + cbErr(e); + } + } + }); +} diff --git a/src/common/redux/typed-saga.ts b/src/common/redux/sagas/typed-saga.ts similarity index 100% rename from src/common/redux/typed-saga.ts rename to src/common/redux/sagas/typed-saga.ts diff --git a/src/common/redux/states/locatorInitialState.ts b/src/common/redux/states/locatorInitialState.ts new file mode 100644 index 000000000..89a35d0fb --- /dev/null +++ b/src/common/redux/states/locatorInitialState.ts @@ -0,0 +1,24 @@ +// ==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 { LocatorExtended } from "@r2-navigator-js/electron/renderer"; +import { Locator as R2Locator } from "@r2-shared-js/models/locator"; + +export const LocatorExtendedWithLocatorOnly = (locator: R2Locator): LocatorExtended => ({ + audioPlaybackInfo: undefined, + paginationInfo: undefined, + selectionInfo: undefined, + selectionIsNew: undefined, + docInfo: undefined, + epubPage: undefined, + locator, +}); + +export const locatorInitialState: LocatorExtended = LocatorExtendedWithLocatorOnly({ + href: undefined, + locations: {}, +}); diff --git a/src/common/redux/states/reader.ts b/src/common/redux/states/reader.ts new file mode 100644 index 000000000..d8c68b8ff --- /dev/null +++ b/src/common/redux/states/reader.ts @@ -0,0 +1,29 @@ +// ==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 { ReaderConfig } from "readium-desktop/common/models/reader"; + +export const readerConfigInitialState: ReaderConfig = { + align: "auto", + colCount: "auto", + dark: false, + font: "DEFAULT", + fontSize: "100%", + invert: false, + lineHeight: "1.5", + night: false, + paged: false, + readiumcss: true, + sepia: false, + enableMathJax: false, + pageMargins: "0.5", + wordSpacing: "0", + letterSpacing: "0", + paraSpacing: "0", + noFootnotes: undefined, + darken: undefined, +}; diff --git a/src/renderer/common/redux/states/index.ts b/src/common/redux/states/renderer/commonRootState.ts similarity index 92% rename from src/renderer/common/redux/states/index.ts rename to src/common/redux/states/renderer/commonRootState.ts index 685903fc4..33462b873 100644 --- a/src/renderer/common/redux/states/index.ts +++ b/src/common/redux/states/renderer/commonRootState.ts @@ -11,7 +11,7 @@ import { KeyboardState } from "readium-desktop/common/redux/states/keyboard"; import { ToastState } from "readium-desktop/common/redux/states/toast"; import { ApiState } from "readium-desktop/renderer/common/redux/states/api"; -import { WinState } from "./win"; +import { WinState } from "../../../../renderer/common/redux/states/win"; export interface ICommonRootState { api: ApiState; diff --git a/src/common/redux/states/renderer/readerRootState.ts b/src/common/redux/states/renderer/readerRootState.ts new file mode 100644 index 000000000..d201bbe10 --- /dev/null +++ b/src/common/redux/states/renderer/readerRootState.ts @@ -0,0 +1,20 @@ +// ==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 { LocatorExtended } from "@r2-navigator-js/electron/renderer"; +import { ReaderConfig, ReaderInfo } from "readium-desktop/common/models/reader"; +import { ICommonRootState } from "readium-desktop/common/redux/states/renderer/commonRootState"; + +export interface IReaderRootState extends ICommonRootState { + reader: IReaderStateReader; +} + +export interface IReaderStateReader { + config: ReaderConfig; + info: ReaderInfo; + locator: LocatorExtended; +} diff --git a/src/common/services/serializer.ts b/src/common/services/serializer.ts index bfd3daf13..2061a8650 100644 --- a/src/common/services/serializer.ts +++ b/src/common/services/serializer.ts @@ -8,12 +8,12 @@ import "reflect-metadata"; import { injectable} from "inversify"; -import { CodeError } from "readium-desktop/common/errors"; +import { CodeError } from "readium-desktop/common/codeError.class"; import { Action } from "../models/redux"; @injectable() export class ActionSerializer { - public serialize(action: Action): Action { + public static serialize(action: Action): Action { if (action.error && action.payload instanceof CodeError) { return Object.assign( {}, @@ -27,7 +27,7 @@ export class ActionSerializer { } } - public deserialize(json: Action): Action { + public static deserialize(json: Action): Action { if (json.error && json.payload && json.payload.class && diff --git a/src/common/utils/time.ts b/src/common/utils/time.ts index 2f394343e..d03680576 100644 --- a/src/common/utils/time.ts +++ b/src/common/utils/time.ts @@ -6,23 +6,20 @@ // ==LICENSE-END== export function formatTime(seconds: number): string { + + seconds = Math.round(seconds); + const secondsPerMinute = 60; const minutesPerHours = 60; const secondsPerHour = minutesPerHours * secondsPerMinute; - let remainingSeconds = seconds; - const nHours = Math.floor(remainingSeconds / secondsPerHour); - remainingSeconds -= (nHours * secondsPerHour); - if (remainingSeconds < 0) { - remainingSeconds = 0; - } - const nMinutes = Math.floor(remainingSeconds / secondsPerMinute); - remainingSeconds -= (nMinutes * secondsPerMinute); - if (remainingSeconds < 0) { - remainingSeconds = 0; - } - remainingSeconds = Math.floor(remainingSeconds); - return formatTime_(nHours, nMinutes, remainingSeconds); + const hours = Math.floor(seconds / secondsPerHour); + seconds %= secondsPerHour; + + const minutes = Math.floor(seconds / secondsPerMinute); + seconds %= secondsPerMinute; + + return formatTime_(hours, minutes, seconds); } export function formatTime_(nHours: number, nMinutes: number, nSeconds: number): string { diff --git a/src/common/views/publication.ts b/src/common/views/publication.ts index 3be74cca7..89e6bbc6e 100644 --- a/src/common/views/publication.ts +++ b/src/common/views/publication.ts @@ -19,12 +19,6 @@ export interface CustomCoverView { bottomColor: string; } -export interface ITimeDuration { - hours: number; - seconds: number; - minutes: number; -} - export interface PublicationView extends Identifiable { title: string; authors: string[]; @@ -38,7 +32,7 @@ export interface PublicationView extends Identifiable { customCover?: CustomCoverView; RDFType?: string; - duration?: ITimeDuration; + duration?: number; nbOfTracks?: number; lcp?: LcpInfo; diff --git a/src/main.ts b/src/main.ts index 7c8653650..13f486819 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,17 +6,12 @@ // ==LICENSE-END== import * as debug_ from "debug"; -import { app, ipcMain } from "electron"; +import { app, dialog } from "electron"; import * as path from "path"; -import { syncIpc } from "readium-desktop/common/ipc"; -import { ActionWithSender } from "readium-desktop/common/models/sync"; import { cli } from "readium-desktop/main/cli/process"; -import { createWindow } from "readium-desktop/main/createWindow"; -import { diMainGet } from "readium-desktop/main/di"; -import { initApp, registerProtocol } from "readium-desktop/main/init"; -import { - _PACKAGING, _VSCODE_LAUNCH, -} from "readium-desktop/preprocessor-directives"; +import { createStoreFromDi } from "readium-desktop/main/di"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { _PACKAGING, _VSCODE_LAUNCH } from "readium-desktop/preprocessor-directives"; import { setLcpNativePluginPath } from "@r2-lcp-js/parser/epub/lcp"; import { initSessions } from "@r2-navigator-js/electron/main/sessions"; @@ -25,6 +20,8 @@ import { initGlobalConverters_GENERIC, initGlobalConverters_SHARED, } from "@r2-shared-js/init-globals"; +import { appActions } from "./main/redux/actions"; + if (_PACKAGING !== "0") { // Disable debug in packaged app delete process.env.DEBUG; @@ -36,7 +33,7 @@ if (_PACKAGING !== "0") { /* console.log = (_message?: any, ..._optionalParams: any[]) => { return; }; console.warn = (_message?: any, ..._optionalParams: any[]) => { return; }; - console.error = (_message?: any, ..._optionalParams: any[]) => { return; }; + console.error = (_message?: IArrayWinRegistryReaderState,any, ..._optionalParams: any[]) => { return; }; console.info = (_message?: any, ..._optionalParams: any[]) => { return; }; */ } @@ -61,60 +58,51 @@ setLcpNativePluginPath(lcpNativePluginPath); // process.exit(); // }); -if (_VSCODE_LAUNCH === "true") { - main(); -} else { - cli(main); -} -debug(process.versions); +const main = async (flushSession: boolean = false) => { -function main() { + // protocol.registerSchemesAsPrivileged should be called before app is ready at initSessions initSessions(); app.allowRendererProcessReuse = true; - // Quit when all windows are closed. - app.on("window-all-closed", () => { - // At the moment, there are no menu items to revive / re-open windows, - // so let's terminate the app on MacOS too. - // if (process.platform !== "darwin") { - // app.quit(); - // } - app.quit(); - }); - - // Call 'createWindow()' on startup. - app.on("ready", async () => { - debug("ready"); - initApp(); - - // launch library window - await createWindow(); - registerProtocol(); - }); - - // Listen to renderer action - ipcMain.on(syncIpc.CHANNEL, (_0: any, data: syncIpc.EventPayload) => { - const store = diMainGet("store"); - const actionSerializer = diMainGet("action-serializer"); - - switch (data.type) { - case syncIpc.EventType.RendererAction: - // Dispatch renderer action to main reducers - store.dispatch(Object.assign( - {}, - actionSerializer.deserialize(data.payload.action), - { sender: data.sender } as ActionWithSender, - )); - break; + try { + const store = await createStoreFromDi(); + + if (flushSession) { + + const readers = store.getState().win.session.reader; + for (const key in readers) { + if (readers[key]) { + + const reader = readers[key]; + store.dispatch(winActions.session.unregisterReader.build(reader.identifier)); + store.dispatch(winActions.registry.registerReaderPublication.build( + reader.publicationIdentifier, + reader.windowBound, + reader.reduxState, + )); + } + } } - }); - - app.on("accessibility-support-changed", (_ev, accessibilitySupportEnabled) => { - debug(`accessibilitySupportEnabled: ${accessibilitySupportEnabled}`); - }); - // setInterval(() => { - // const a11y = app.isAccessibilitySupportEnabled(); - // debug(`isAccessibilitySupportEnabled: ${a11y}`); - // }, 500); + + store.dispatch(appActions.initRequest.build()); + debug("STORE MOUNTED -> MOUNTING THE APP NOW"); + + } catch (err) { + const message = `REDUX STATE MANAGER CAN'T BE INITIALIZED, ERROR: ${JSON.stringify(err)} \n\nYou should remove your 'AppData' folder\nThorium Exit code 1`; + process.stderr.write(message); + + dialog.showErrorBox("THORIUM ERROR", message); + + app.exit(1); + } +}; + +if (_VSCODE_LAUNCH === "true") { + // tslint:disable-next-line: no-floating-promises + main(); +} else { + cli(main); } + +debug("Process version:", process.versions); diff --git a/src/main/api/catalog.ts b/src/main/api/catalog.ts index cdb19f0a9..1d9e867a1 100644 --- a/src/main/api/catalog.ts +++ b/src/main/api/catalog.ts @@ -5,35 +5,46 @@ // 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 * as Ramda from "ramda"; import { ICatalogApi } from "readium-desktop/common/api/interface/catalog.interface"; -import { LocatorType } from "readium-desktop/common/models/locator"; +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 { - CatalogConfig, CatalogEntry, ConfigDocument, -} from "readium-desktop/main/db/document/config"; -import { ConfigRepository } from "readium-desktop/main/db/repository/config"; -import { LocatorRepository } from "readium-desktop/main/db/repository/locator"; -import { PublicationRepository } from "readium-desktop/main/db/repository/publication"; +// import { +// CatalogConfig, CatalogEntry, ConfigDocument, +// } from "readium-desktop/main/db/document/config"; +// import { ConfigRepository } from "readium-desktop/main/db/repository/config"; import { diSymbolTable } from "readium-desktop/main/diSymbolTable"; +import { Store } from "redux"; -import { LocatorDocument } from "../db/document/locator"; +import { PublicationRepository } from "../db/repository/publication"; +import { publicationActions } from "../redux/actions"; +import { RootState } from "../redux/states"; +import { PublicationService } from "../services/publication"; export const CATALOG_CONFIG_ID = "catalog"; +// 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["config-repository"]) - private readonly configRepository!: ConfigRepository; + @inject(diSymbolTable["publication-service"]) + private readonly publicationService!: PublicationService; + + // @inject(diSymbolTable["config-repository"]) + // private readonly configRepository!: ConfigRepository; - @inject(diSymbolTable["locator-repository"]) - private readonly locatorRepository!: LocatorRepository; + // @inject(diSymbolTable["locator-repository"]) + // private readonly locatorRepository!: LocatorRepository; @inject(diSymbolTable["publication-view-converter"]) private readonly publicationViewConverter!: PublicationViewConverter; @@ -41,47 +52,102 @@ export class CatalogApi implements ICatalogApi { @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); - // Last read publicatons - const lastLocators = await this.locatorRepository.find( - { - selector: { locatorType: LocatorType.LastReadingLocation }, - limit: 10, - sort: [ { updatedAt: "desc" } ], - }, - ); - const lastLocatorPublicationIdentifiers = lastLocators.map( - (locator: LocatorDocument) => locator.publicationIdentifier, - ); - const lastReadPublicationViews = []; - - for (const pubIdentifier of lastLocatorPublicationIdentifiers) { - let pubDoc = null; - - try { - pubDoc = await this.publicationRepository.get(pubIdentifier); - } catch (error) { - // Document not found - continue; + // // Last read publicatons + // const lastLocators = await this.locatorRepository.find( + // { + // selector: { locatorType: LocatorType.LastReadingLocation }, + // limit: 10, + // sort: [ { updatedAt: "desc" } ], + // }, + // ); + // const lastLocatorPublicationIdentifiers = lastLocators.map( + // (locator: LocatorDocument) => locator.publicationIdentifier, + // ); + + // for (const pubIdentifier of lastLocatorPublicationIdentifiers) { + // let pubDoc = null; + + // try { + // pubDoc = await this.publicationRepository.get(pubIdentifier); + // } catch (error) { + // // Document not found + // continue; + // } + + // lastReadPublicationViews.push( + // this.publicationViewConverter.convertDocumentToView(pubDoc), + // ); + // } + + const lastReadPublicationViews: PublicationView[] = []; + + const lastReading = this.store.getState().publication.lastReadingQueue; + + const pushPublication = async (i: number) => { + + if (i < lastReading.length) { + const [, pubId] = lastReading[i]; + + try { + const pub = await this.publicationService.getPublication(pubId); + lastReadPublicationViews.push(pub); + } catch (e) { + // ignore + + // TODO toastInfo? + + // dispatch action to update publication/lastReadingQueue reducer + this.store.dispatch(publicationActions.deletePublication.build(pubId)); + } } + }; - lastReadPublicationViews.push( - this.publicationViewConverter.convertDocumentToView(pubDoc), - ); - } + await Promise.all(Ramda.times(pushPublication, 10)); // Last added pubs not already on last read list const lastAddedPublications = await this.publicationRepository.find({ - limit: 10, - sort: [ { createdAt: "desc" } ], + sort: [{ createdAt: "desc" }], selector: {}, }); + const lastAddedPublicationViews: PublicationView[] = []; - for (const doc of lastAddedPublications) { - if (!lastReadPublicationViews.find((lastDoc) => lastDoc.identifier === doc.identifier)) { - lastAddedPublicationViews.push(this.publicationViewConverter.convertDocumentToView(doc)); + + { + let i = 0; + while ( + lastAddedPublicationViews.length < 10 + && i < lastAddedPublications.length + ) { + + const doc = lastAddedPublications[i]; + const notInReading = lastReadPublicationViews. + findIndex((lastDoc) => lastDoc.identifier === doc.identifier) < 0; + + if (notInReading) { + try { + const pub = this.publicationViewConverter.convertDocumentToView(doc); + lastAddedPublicationViews.push(pub); + } catch (e) { + 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 { + // tslint:disable-next-line: no-floating-promises + this.publicationService.deletePublication(doc.identifier); + } catch { + // ignore + } + } + } + + ++i; } } @@ -94,7 +160,7 @@ export class CatalogApi implements ICatalogApi { const lastReadAudiobooks = lastReadPublicationViews.filter(isAudiobook); // Dynamic entries - let entries: CatalogEntryView[] = [ + const entries: CatalogEntryView[] = [ { title: __("catalog.entry.continueReading"), totalCount: lastReadPublication.length, @@ -117,84 +183,84 @@ export class CatalogApi implements ICatalogApi { }, ]; - // Concat user entries - const userEntries = await this.getEntries(); - entries = entries.concat(userEntries); + // // Concat user entries + // const userEntries = await this.getEntries(); + // entries = entries.concat(userEntries); return { entries, }; } - public async addEntry(entryView: CatalogEntryView): Promise { - let entries: CatalogEntry[] = []; + // public async addEntry(entryView: CatalogEntryView): Promise { + // let entries: CatalogEntry[] = []; - try { - const config = await this.configRepository.get(CATALOG_CONFIG_ID); - const catalog = config.value; - entries = catalog.entries; - } catch (error) { - // New configuration - } + // try { + // const config = await this.configRepository.get(CATALOG_CONFIG_ID); + // const catalog = config.value; + // entries = catalog.entries; + // } catch (error) { + // // New configuration + // } - entries.push({ - title: entryView.title, - tag: entryView.tag, - }); + // entries.push({ + // title: entryView.title, + // tag: entryView.tag, + // }); - await this.configRepository.save({ - identifier: CATALOG_CONFIG_ID, - value: { entries }, - }); - return this.getEntries(); - } + // await this.configRepository.save({ + // identifier: CATALOG_CONFIG_ID, + // value: { entries }, + // }); + // return this.getEntries(); + // } - /** - * Returns entries without pubs - */ - public async getEntries(): Promise { - let config: ConfigDocument; - try { - config = await this.configRepository.get(CATALOG_CONFIG_ID); - } catch (error) { - return []; - } + // /** + // * Returns entries without pubs + // */ + // public async getEntries(): Promise { + // let config: ConfigDocument; + // try { + // config = await this.configRepository.get(CATALOG_CONFIG_ID); + // } catch (error) { + // return []; + // } - const catalog = config.value; - const entryViews: CatalogEntryView[] = []; - - for (const entry of catalog.entries) { - const publicationDocuments = await this.publicationRepository.findByTag(entry.tag); - const publicationViews = publicationDocuments.map((doc) => { - return this.publicationViewConverter.convertDocumentToView(doc); - }); - entryViews.push( - { - title: entry.title, - tag: entry.tag, - publicationViews, - totalCount: publicationViews.length, - }, - ); - } + // const catalog = config.value; + // const entryViews: CatalogEntryView[] = []; - return entryViews; - } + // for (const entry of catalog.entries) { + // const publicationDocuments = await this.publicationRepository.findByTag(entry.tag); + // const publicationViews = publicationDocuments.map((doc) => { + // return this.publicationViewConverter.convertDocumentToView(doc); + // }); + // entryViews.push( + // { + // title: entry.title, + // tag: entry.tag, + // publicationViews, + // totalCount: publicationViews.length, + // }, + // ); + // } - public async updateEntries(entryViews: CatalogEntryView[]): Promise { - const entries = entryViews.map((view) => { - return { - title: view.title, - tag: view.tag, - }; - }); - const catalogConfig: CatalogConfig = { - entries, - }; - await this.configRepository.save({ - identifier: CATALOG_CONFIG_ID, - value: catalogConfig, - }); - return this.getEntries(); - } + // return entryViews; + // } + + // public async updateEntries(entryViews: CatalogEntryView[]): Promise { + // const entries = entryViews.map((view) => { + // return { + // title: view.title, + // tag: view.tag, + // }; + // }); + // const catalogConfig: CatalogConfig = { + // entries, + // }; + // await this.configRepository.save({ + // identifier: CATALOG_CONFIG_ID, + // value: catalogConfig, + // }); + // return this.getEntries(); + // } } diff --git a/src/main/api/publication.ts b/src/main/api/publication.ts index 1f1d47350..c8e4b09b9 100644 --- a/src/main/api/publication.ts +++ b/src/main/api/publication.ts @@ -5,6 +5,7 @@ // 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 { IPublicationApi } from "readium-desktop/common/api/interface/publicationApi.interface"; import { PromiseAllSettled, PromiseFulfilled } from "readium-desktop/common/utils/promise"; @@ -14,16 +15,16 @@ import { PublicationViewConverter } from "readium-desktop/main/converter/publica import { PublicationDocument } from "readium-desktop/main/db/document/publication"; import { PublicationRepository } from "readium-desktop/main/db/repository/publication"; import { diSymbolTable } from "readium-desktop/main/diSymbolTable"; -import { LcpManager } from "readium-desktop/main/services/lcp"; import { PublicationService } from "readium-desktop/main/services/publication"; import { isArray } from "util"; -// import * as debug_ from "debug"; // Logger -// const debug = debug_("readium-desktop:main#services/catalog"); +const debug = debug_("readium-desktop:main#services/catalog"); +debug("_"); @injectable() export class PublicationApi implements IPublicationApi { + @inject(diSymbolTable["publication-repository"]) private readonly publicationRepository!: PublicationRepository; @@ -33,16 +34,12 @@ export class PublicationApi implements IPublicationApi { @inject(diSymbolTable["publication-service"]) private readonly publicationService!: PublicationService; - @inject(diSymbolTable["lcp-manager"]) - private readonly lcpManager!: LcpManager; + // @inject(diSymbolTable.store) + // private readonly store!: Store; // called for publication info dialog modal box public async get(identifier: string, checkLcpLsd: boolean): Promise { - let doc = await this.publicationRepository.get(identifier); - if (checkLcpLsd && doc.lcp) { - doc = await this.lcpManager.checkPublicationLicenseUpdate(doc); - } - return this.publicationViewConverter.convertDocumentToView(doc); + return await this.publicationService.getPublication(identifier, checkLcpLsd); } public async delete(identifier: string): Promise { diff --git a/src/main/api/reader.ts b/src/main/api/reader.ts index 5ea42b0f1..5acde3242 100644 --- a/src/main/api/reader.ts +++ b/src/main/api/reader.ts @@ -22,6 +22,7 @@ import { RootState } from "readium-desktop/main/redux/states"; import { Store } from "redux"; import { IEventPayload_R2_EVENT_CLIPBOARD_COPY } from "@r2-navigator-js/electron/common/events"; +import { LocatorExtended } from "@r2-navigator-js/electron/renderer"; import { Locator as R2Locator } from "@r2-shared-js/models/locator"; @injectable() @@ -41,47 +42,52 @@ export class ReaderApi implements IReaderApi { @inject(diSymbolTable.translator) private readonly translator!: Translator; - public async setLastReadingLocation(publicationIdentifier: string, locator: R2Locator): Promise { - const docs = await this.locatorRepository.findByPublicationIdentifierAndLocatorType( - publicationIdentifier, - LocatorType.LastReadingLocation, - ); - - let newDoc = null; - - if (docs.length === 0) { - // Create new locator - newDoc = { - publicationIdentifier, - locatorType: LocatorType.LastReadingLocation, - locator: Object.assign({}, locator), - }; - } else { - // Update locator - newDoc = Object.assign( - {}, - docs[0], - { - locator: Object.assign({}, locator), - }, - ); - } - - const savedDoc = await this.locatorRepository.save(newDoc); - return this.locatorViewConverter.convertDocumentToView(savedDoc); - } - - public async getLastReadingLocation(publicationIdentifier: string): Promise { - const docs = await this.locatorRepository.findByPublicationIdentifierAndLocatorType( - publicationIdentifier, - LocatorType.LastReadingLocation, - ); - - if (docs.length === 0) { - return null; - } - - return this.locatorViewConverter.convertDocumentToView(docs[0]); + // public async setLastReadingLocation(publicationIdentifier: string, locator: R2Locator): Promise { + // const docs = await this.locatorRepository.findByPublicationIdentifierAndLocatorType( + // publicationIdentifier, + // LocatorType.LastReadingLocation, + // ); + + // let newDoc = null; + + // if (docs.length === 0) { + // // Create new locator + // newDoc = { + // publicationIdentifier, + // locatorType: LocatorType.LastReadingLocation, + // locator: Object.assign({}, locator), + // }; + // } else { + // // Update locator + // newDoc = Object.assign( + // {}, + // docs[0], + // { + // locator: Object.assign({}, locator), + // }, + // ); + // } + + // const savedDoc = await this.locatorRepository.save(newDoc); + // return this.locatorViewConverter.convertDocumentToView(savedDoc); + // } + + public async getLastReadingLocation(publicationIdentifier: string): Promise { + // const docs = await this.locatorRepository.findByPublicationIdentifierAndLocatorType( + // publicationIdentifier, + // LocatorType.LastReadingLocation, + // ); + + // if (docs.length === 0) { + // return null; + // } + + // return this.locatorViewConverter.convertDocumentToView(docs[0]); + + const state = this.store.getState(); + const locator = state.win.registry.reader[publicationIdentifier]?.reduxState.locator; + + return locator; } public async findBookmarks(publicationIdentifier: string): Promise { @@ -141,6 +147,8 @@ export class ReaderApi implements IReaderApi { } } + // TODO + // clipboard can be an action catched in saga, nothing to return public async clipboardCopy( publicationIdentifier: string, clipboardData: IEventPayload_R2_EVENT_CLIPBOARD_COPY): Promise { @@ -194,4 +202,10 @@ export class ReaderApi implements IReaderApi { return true; } + + // TODO + // may be removed and replaced with a action dispatched to every reader + public async getMode() { + return this.store.getState().mode; + } } diff --git a/src/main/api/session.ts b/src/main/api/session.ts new file mode 100644 index 000000000..c1c155cd9 --- /dev/null +++ b/src/main/api/session.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 { inject, injectable } from "inversify"; +import { ISessionApi } from "readium-desktop/common/api/interface/session.interface"; +import { Store } from "redux"; + +import { diSymbolTable } from "../diSymbolTable"; +import { sessionActions } from "../redux/actions"; +import { RootState } from "../redux/states"; + +// import * as debug_ from "debug"; +// Logger +// const debug = debug_("readium-desktop:src/main/api/opds"); + +@injectable() +export class SessionApi implements ISessionApi { + + @inject(diSymbolTable.store) + private readonly store!: Store; + + public async isEnabled(): Promise { + const value = this.store.getState().session.state; + return Promise.resolve(value); + } + + public async enable(value: boolean): Promise { + this.store.dispatch(sessionActions.enable.build(value)); + + return Promise.resolve(); + } +} diff --git a/src/main/cli/commandLine.ts b/src/main/cli/commandLine.ts index 1f1fce1f3..53e3792e6 100644 --- a/src/main/cli/commandLine.ts +++ b/src/main/cli/commandLine.ts @@ -17,6 +17,11 @@ function openReader(publicationView: PublicationView | PublicationView[]) { } if (publicationView) { const store = diMainGet("store"); + // TODO + // FIXME + // Can't call readerActions.openRequest before appInit + // check the flow to throw appInit and openReader consecutively + // and need to exec main here before to call openReader store.dispatch(readerActions.openRequest.build(publicationView.identifier)); return true; } diff --git a/src/main/cli/process.ts b/src/main/cli/process.ts index 4d3af33bb..475f205f5 100644 --- a/src/main/cli/process.ts +++ b/src/main/cli/process.ts @@ -32,7 +32,8 @@ const gotTheLock = lockInstance(); // as it has already been executed by the "second instance" itself (see Yargs handlers). // main Fucntion variable -let mainFct: () => void = () => ({}); +// tslint:disable-next-line: no-empty +let mainFct: (flushSession: boolean) => (void | Promise) = async () => {}; // yargs configuration yargs @@ -120,25 +121,32 @@ yargs type: "string", }) , - (argv) => { + async (argv) => { // if it's the main instance if (gotTheLock) { - mainFct(); - app.whenReady().then(async () => { + + // flush session because user ask to read one publication + await Promise.all([mainFct(true)]); + + try { + + await app.whenReady(); + try { if (!await openTitleFromCli(argv.title)) { const errorMessage = `There is no publication title match for \"${argv.title}\"`; throw new Error(errorMessage); } } catch (e) { - debug("read error :", e); + debug("$0 error :", e); const errorTitle = "No publication to read"; dialog.showErrorBox(errorTitle, e.toString()); process.stderr.write(e.toString() + EOL); } - }).catch(() => { + + } catch (_err) { // ignore - }); + } } else { app.exit(0); } @@ -154,12 +162,26 @@ yargs }) .completion() , - (argv) => { + async (argv) => { // if it's the main instance if (gotTheLock) { - mainFct(); + + // flush session if user want to read his book + await Promise.all( + [ + mainFct( + argv.path + ? true + : false, + ), + ], + ); + if (argv.path) { - app.whenReady().then(async () => { + + try { + await app.whenReady(); + try { if (!await openFileFromCli(argv.path)) { const errorMessage = `Import failed for the publication path : ${argv.path}`; @@ -171,11 +193,13 @@ yargs dialog.showErrorBox(errorTitle, e.toString()); process.stderr.write(e.toString() + EOL); } - }).catch(() => { + + } catch (_err) { // ignore - }); + } } } else { + app.exit(0); } }, @@ -193,12 +217,15 @@ yargs * @param main main function to exec * @param processArgv process.argv */ -export function cli(main: () => void, processArgv = process.argv) { +export function cli(main: (flushSession: boolean) => void | Promise, processArgv = process.argv) { mainFct = main; + const argFormated = processArgv .filter((arg) => knownOption(arg) || !arg.startsWith("-")) .slice((_PACKAGING === "0") ? 2 : 1); + debug("processArgv", processArgv, "arg", argFormated); + yargs.parse(argFormated); } diff --git a/src/main/converter/publication.ts b/src/main/converter/publication.ts index 9293cebc7..04b3ff5d2 100644 --- a/src/main/converter/publication.ts +++ b/src/main/converter/publication.ts @@ -7,9 +7,7 @@ import { injectable } from "inversify"; import * as moment from "moment"; -import { - CoverView, ITimeDuration, PublicationView, -} from "readium-desktop/common/views/publication"; +import { CoverView, PublicationView } from "readium-desktop/common/views/publication"; import { convertContributorArrayToStringArray, } from "readium-desktop/main/converter/tools/localisation"; @@ -64,9 +62,7 @@ export class PublicationViewConverter { lcpRightsCopies: document.lcpRightsCopies, RDFType: r2Publication.Metadata.RDFType, - duration: r2Publication.Metadata.Duration - ? this.convertSecondToHMS(r2Publication.Metadata.Duration) - : undefined, + duration: r2Publication.Metadata.Duration, nbOfTracks: r2Publication.Metadata.AdditionalJSON?.tracks as number | undefined, // doc: r2Publiction.Metadata, @@ -74,23 +70,4 @@ export class PublicationViewConverter { r2PublicationBase64, }; } - - private convertSecondToHMS(seconds: number): ITimeDuration { - - const secondsPerMinute = 60; - const minutesPerHours = 60; - const secondsPerHour = minutesPerHours * secondsPerMinute; - - const hours = Math.floor(seconds / secondsPerHour); - seconds %= secondsPerHour; - - const minutes = Math.floor(seconds / secondsPerMinute); - seconds %= secondsPerMinute; - - return { - hours, - minutes, - seconds, - }; - } } diff --git a/src/main/di.ts b/src/main/di.ts index 30bf9efb2..fc2fcc8e3 100644 --- a/src/main/di.ts +++ b/src/main/di.ts @@ -7,12 +7,11 @@ import "reflect-metadata"; -import { app } from "electron"; +import { app, BrowserWindow } from "electron"; import * as fs from "fs"; import { Container } from "inversify"; import * as path from "path"; import * as PouchDBCore from "pouchdb-core"; -import { ActionSerializer } from "readium-desktop/common/services/serializer"; import { Translator } from "readium-desktop/common/services/translator"; import { CatalogApi } from "readium-desktop/main/api/catalog"; import { LcpApi } from "readium-desktop/main/api/lcp"; @@ -36,8 +35,8 @@ import { initStore } from "readium-desktop/main/redux/store/memory"; import { DeviceIdManager } from "readium-desktop/main/services/device"; import { Downloader } from "readium-desktop/main/services/downloader"; import { LcpManager } from "readium-desktop/main/services/lcp"; +// import { WinRegistry } from "readium-desktop/main/services/win-registry"; import { PublicationService } from "readium-desktop/main/services/publication"; -import { WinRegistry } from "readium-desktop/main/services/win-registry"; import { PublicationStorage } from "readium-desktop/main/storage/publication-storage"; import { streamer } from "readium-desktop/main/streamer"; import { @@ -49,9 +48,11 @@ import { Server } from "@r2-streamer-js/http/server"; import { KeyboardApi } from "./api/keyboard"; import { ReaderApi } from "./api/reader"; +import { SessionApi } from "./api/session"; import { RootState } from "./redux/states"; import { OpdsService } from "./services/opds"; +export const CONFIGREPOSITORY_REDUX_PERSISTENCE = "CONFIGREPOSITORY_REDUX_PERSISTENCE"; const capitalizedAppName = _APP_NAME.charAt(0).toUpperCase() + _APP_NAME.substring(1); declare const __POUCHDB_ADAPTER_PACKAGE__: string; @@ -161,20 +162,24 @@ if (!fs.existsSync(publicationRepositoryPath)) { // Create container used for dependency injection const container = new Container(); -// Create store -const store = initStore(); -container.bind>(diSymbolTable.store).toConstantValue(store); +const createStoreFromDi = async () => { + const store = await initStore(configRepository); + + container.bind>(diSymbolTable.store).toConstantValue(store); + + // Create downloader + const downloader = new Downloader(null, configRepository, store); + container.bind(diSymbolTable.downloader).toConstantValue(downloader); + + return store; +}; // Create window registry -container.bind(diSymbolTable["win-registry"]).to(WinRegistry).inSingletonScope(); +// container.bind(diSymbolTable["win-registry"]).to(WinRegistry).inSingletonScope(); // Create translator container.bind(diSymbolTable.translator).to(Translator).inSingletonScope(); -// Create downloader -const downloader = new Downloader(null, configRepository, store); -container.bind(diSymbolTable.downloader).toConstantValue(downloader); - // Create repositories container.bind(diSymbolTable["publication-repository"]).toConstantValue( publicationRepository, @@ -226,22 +231,32 @@ container.bind(diSymbolTable["opds-api"]).to(OpdsApi).inSingletonScope( container.bind(diSymbolTable["keyboard-api"]).to(KeyboardApi).inSingletonScope(); container.bind(diSymbolTable["lcp-api"]).to(LcpApi).inSingletonScope(); container.bind(diSymbolTable["reader-api"]).to(ReaderApi).inSingletonScope(); +container.bind(diSymbolTable["session-api"]).to(SessionApi).inSingletonScope(); -// Create action serializer -container.bind(diSymbolTable["action-serializer"]).to(ActionSerializer).inSingletonScope(); +const saveLibraryWindowInDi = + (libWin: BrowserWindow) => + container.bind("WIN_REGISTRY_LIBRARY").toConstantValue(libWin); -// -// end of create Depedency Injection Container -// +const getLibraryWindowFromDi = + () => + container.get("WIN_REGISTRY_LIBRARY"); -// -// Overload container.get with our own type return -// +const saveReaderWindowInDi = + (readerWin: BrowserWindow, id: string) => + container.bind("WIN_REGISTRY_READER").toConstantValue(readerWin).whenTargetNamed(id); + +const getReaderWindowFromDi = + (id: string) => + container.getNamed("WIN_REGISTRY_READER", id); + +const getAllReaderWindowFromDi = + () => + container.getAll("WIN_REGISTRY_READER"); // local interface to force type return interface IGet { (s: "store"): Store; - (s: "win-registry"): WinRegistry; + // (s: "win-registry"): WinRegistry; (s: "translator"): Translator; (s: "downloader"): Downloader; (s: "publication-repository"): PublicationRepository; @@ -263,7 +278,6 @@ interface IGet { (s: "keyboard-api"): KeyboardApi; (s: "lcp-api"): LcpApi; (s: "reader-api"): ReaderApi; - (s: "action-serializer"): ActionSerializer; // minor overload type used in api.ts/LN32 (s: keyof typeof diSymbolTable): any; } @@ -274,4 +288,10 @@ const diGet: IGet = (symbol: keyof typeof diSymbolTable) => container.get(d export { diGet as diMainGet, + getLibraryWindowFromDi, + getReaderWindowFromDi, + saveLibraryWindowInDi, + saveReaderWindowInDi, + getAllReaderWindowFromDi, + createStoreFromDi, }; diff --git a/src/main/diSymbolTable.ts b/src/main/diSymbolTable.ts index 12d858a74..0286be5fd 100644 --- a/src/main/diSymbolTable.ts +++ b/src/main/diSymbolTable.ts @@ -2,7 +2,7 @@ // create Dependency Injection Symbol Table export const diSymbolTable = { "store": Symbol("store"), - "win-registry": Symbol("win-registry"), + // "win-registry": Symbol("win-registry"), "translator": Symbol("translator"), "downloader": Symbol("downloader"), "publication-repository": Symbol("publication-repository"), @@ -25,5 +25,5 @@ export const diSymbolTable = { "keyboard-api": Symbol("keyboard-api"), "lcp-api": Symbol("lcp-api"), "reader-api": Symbol("reader-api"), - "action-serializer": Symbol("action-serializer"), + "session-api": Symbol("session-api"), }; diff --git a/src/main/error.ts b/src/main/error.ts new file mode 100644 index 000000000..db00d7dc8 --- /dev/null +++ b/src/main/error.ts @@ -0,0 +1,45 @@ +// ==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 { dialog } from "electron"; +import { diMainGet } from "readium-desktop/main/di"; +import { _APP_NAME } from "readium-desktop/preprocessor-directives"; +import { types } from "util"; + +// Logger +const filename_ = "readium-desktop:main:error"; +const debug = debug_(filename_); +debug("_"); + +export function error(filename: string, err: any) { + + debug(err); + debug(err.stack); + + let errorMessage: string; + if (types.isNativeError(err)) { + + // disable "Error: " + err.name = ""; + errorMessage = err.toString(); + } else { + errorMessage = JSON.stringify(err); + } + + const translator = diMainGet("translator"); + + dialog.showErrorBox( + translator.translate("error.errorBox.title", { appName: _APP_NAME }), + ` + ${translator.translate("error.errorBox.message", { filename })} + + ${translator.translate("error.errorBox.error")} + ${errorMessage} + `, + ); +} diff --git a/src/main/init.ts b/src/main/init.ts index 245bfae3d..6b846a9d3 100644 --- a/src/main/init.ts +++ b/src/main/init.ts @@ -1,217 +1,217 @@ -// ==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 { app, protocol } from "electron"; -import * as path from "path"; -import { LocaleConfigIdentifier, LocaleConfigValueType } from "readium-desktop/common/config"; -import { syncIpc, winIpc } from "readium-desktop/common/ipc"; -import { ReaderMode } from "readium-desktop/common/models/reader"; -import { AppWindow, AppWindowType } from "readium-desktop/common/models/win"; -import { i18nActions, keyboardActions, readerActions } from "readium-desktop/common/redux/actions"; -// import { NetStatus } from "readium-desktop/common/redux/states/net"; -import { AvailableLanguages } from "readium-desktop/common/services/translator"; -import { ConfigRepository } from "readium-desktop/main/db/repository/config"; -import { diMainGet } from "readium-desktop/main/di"; -import { appActions, streamerActions } from "readium-desktop/main/redux/actions/"; -import { ObjectKeys } from "readium-desktop/utils/object-keys-values"; - -import { keyboardShortcuts } from "./keyboard"; - -// Logger -const debug = debug_("readium-desktop:main"); - -// Callback called when a window is opened -const winOpenCallback = (appWindow: AppWindow) => { - // Send information to the new window - - const store = diMainGet("store"); - const state = store.getState(); - const webContents = appWindow.browserWindow.webContents; - - // Send the id to the new window - webContents.send(winIpc.CHANNEL, { - type: winIpc.EventType.IdResponse, - payload: { - winId: appWindow.identifier, - }, - } as winIpc.EventPayload); - - // // Init network on window - // let actionNet = null; - - // switch (state.net.status) { - // case NetStatus.Online: - // actionNet = netActions.online.build(); - // break; - // case NetStatus.Offline: - // default: - // actionNet = netActions.offline.build(); - // break; - // } - - // // Send network status - // webContents.send(syncIpc.CHANNEL, { - // type: syncIpc.EventType.MainAction, - // payload: { - // action: actionNet, - // }, - // } as syncIpc.EventPayload); - - // Send reader information - // even for library view , just it's undefined - const readerActionsOpenSuccess = readerActions.openSuccess.build(state.reader.readers[appWindow.identifier]); - if (readerActionsOpenSuccess?.payload?.reader?.browserWindow) { - // IPC cannot serialize Javascript objects (breaking change in Electron 9+) - delete readerActionsOpenSuccess.payload.reader.browserWindow; - } - webContents.send(syncIpc.CHANNEL, { - type: syncIpc.EventType.MainAction, - payload: { - action: readerActionsOpenSuccess, - }, - } as syncIpc.EventPayload); - - // Send reader config - webContents.send(syncIpc.CHANNEL, { - type: syncIpc.EventType.MainAction, - payload: { - action: readerActions.configSetSuccess.build(state.reader.config), - }, - } as syncIpc.EventPayload); - - // Send reader mode - webContents.send(syncIpc.CHANNEL, { - type: syncIpc.EventType.MainAction, - payload: { - action: readerActions.detachModeSuccess.build(state.reader.mode), - }, - } as syncIpc.EventPayload); - - // Send locale - webContents.send(syncIpc.CHANNEL, { - type: syncIpc.EventType.MainAction, - payload: { - action: i18nActions.setLocale.build(state.i18n.locale), - }, - } as syncIpc.EventPayload); - - // Send keyboard shortcuts - webContents.send(syncIpc.CHANNEL, { - type: syncIpc.EventType.MainAction, - payload: { - action: keyboardActions.setShortcuts.build(state.keyboard.shortcuts, false), - }, - } as syncIpc.EventPayload); - - // // Send update info - // webContents.send(syncIpc.CHANNEL, { - // type: syncIpc.EventType.MainAction, - // payload: { - // action: { - // type: updateActions.latestVersion.ID, - // payload: updateActions.latestVersion.build( - // state.update.status, - // state.update.latestVersion, - // state.update.latestVersionUrl), - // }, - // }, - // } as syncIpc.EventPayload); -}; - -// Callback called when a window is closed -const winCloseCallback = (appWindow: AppWindow) => { - const store = diMainGet("store"); - - const winRegistry = diMainGet("win-registry"); - const readerWindows = winRegistry.getReaderWindows(); - - // library window was closed and unregistered - // => all reader windows must now be closed too (effectively exiting the app) - if (appWindow.type === AppWindowType.Library) { - readerWindows.forEach((w) => w.browserWindow.close()); - return; - } - - // else: appWindow.type === AppWindowType.Reader - const state = store.getState(); - if (state.reader?.readers) { - const readers = Object.values(state.reader.readers); - const reader = readers.find((r) => { - return r.browserWindowID === appWindow.browserWindowID; - }); - if (reader) { - store.dispatch(streamerActions.publicationCloseRequest.build(reader.publicationIdentifier)); - } - } - - // if there is at least one remaining reader, then leave it/them alone - // (it is / they are in detached mode, the library view is visible) - if (readerWindows.length > 0) { - return; - } - - // else, there is one window left, it is the library - // we ensure return to "attached" mode - store.dispatch(readerActions.detachModeSuccess.build(ReaderMode.Attached)); - - // if the library is in fact no visible - // (i.e. the sole closed reader was in attached mode) - // then we close the app - const libraryWindow = winRegistry.getLibraryWindow(); - if (libraryWindow) { - if (libraryWindow.browserWindow.isMinimized()) { - libraryWindow.browserWindow.restore(); - } else if (!libraryWindow.browserWindow.isVisible()) { - libraryWindow.browserWindow.close(); - return; - } - libraryWindow.browserWindow.show(); // focuses as well - } -}; - -// Initialize application -export function initApp() { - - const store = diMainGet("store"); - store.dispatch(appActions.initRequest.build()); - - keyboardShortcuts.init(); - store.dispatch(keyboardActions.setShortcuts.build(keyboardShortcuts.getAll(), false)); - - const configRepository: ConfigRepository = diMainGet("config-repository"); - const config = configRepository.get(LocaleConfigIdentifier); - config.then((i18nLocale) => { - if (i18nLocale && i18nLocale.value && i18nLocale.value.locale) { - store.dispatch(i18nActions.setLocale.build(i18nLocale.value.locale)); - debug(`set the locale ${i18nLocale.value.locale}`); - } else { - debug(`error on configRepository.get("i18n")): ${i18nLocale}`); - } - }).catch(async () => { - const loc = app.getLocale().split("-")[0]; - const langCodes = ObjectKeys(AvailableLanguages); - const lang = langCodes.find((l) => l === loc) || "en"; - store.dispatch(i18nActions.setLocale.build(lang)); - debug(`create i18n key in configRepository with ${lang} locale`); - }); - - const winRegistry = diMainGet("win-registry"); - winRegistry.registerOpenCallback(winOpenCallback); - winRegistry.registerCloseCallback(winCloseCallback); - app.setAppUserModelId("io.github.edrlab.thorium"); -} - -export function registerProtocol() { - protocol.registerFileProtocol("store", (request, callback) => { - // Extract publication item relative url - const relativeUrl = request.url.substr(6); - const pubStorage = diMainGet("publication-storage"); - const filePath: string = path.join(pubStorage.getRootPath(), relativeUrl); - callback(filePath); - }); -} +// // ==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 { app, protocol } from "electron"; +// import * as path from "path"; +// import { LocaleConfigIdentifier, LocaleConfigValueType } from "readium-desktop/common/config"; +// import { syncIpc, winIpc } from "readium-desktop/common/ipc"; +// import { ReaderMode } from "readium-desktop/common/models/reader"; +// import { AppWindow, AppWindowType } from "readium-desktop/common/models/win"; +// import { i18nActions, keyboardActions, readerActions } from "readium-desktop/common/redux/actions"; +// // import { NetStatus } from "readium-desktop/common/redux/states/net"; +// import { AvailableLanguages } from "readium-desktop/common/services/translator"; +// import { ConfigRepository } from "readium-desktop/main/db/repository/config"; +// import { diMainGet } from "readium-desktop/main/di"; +// import { appActions, streamerActions } from "readium-desktop/main/redux/actions/"; +// import { ObjectKeys } from "readium-desktop/utils/object-keys-values"; + +// import { keyboardShortcuts } from "./keyboard"; + +// // Logger +// const debug = debug_("readium-desktop:main"); + +// // Callback called when a window is opened +// const winOpenCallback = (appWindow: AppWindow) => { +// // Send information to the new window + +// const store = diMainGet("store"); +// const state = store.getState(); +// const webContents = appWindow.browserWindow.webContents; + +// // Send the id to the new window +// webContents.send(winIpc.CHANNEL, { +// type: winIpc.EventType.IdResponse, +// payload: { +// winId: appWindow.identifier, +// }, +// } as winIpc.EventPayload); + +// // // Init network on window +// // let actionNet = null; + +// // switch (state.net.status) { +// // case NetStatus.Online: +// // actionNet = netActions.online.build(); +// // break; +// // case NetStatus.Offline: +// // default: +// // actionNet = netActions.offline.build(); +// // break; +// // } + +// // // Send network status +// // webContents.send(syncIpc.CHANNEL, { +// // type: syncIpc.EventType.MainAction, +// // payload: { +// // action: actionNet, +// // }, +// // } as syncIpc.EventPayload); + +// // Send reader information +// // even for library view , just it's undefined +// const readerActionsOpenSuccess = readerActions.openSuccess.build(state.reader.readers[appWindow.identifier]); +// if (readerActionsOpenSuccess?.payload?.reader?.browserWindow) { +// // IPC cannot serialize Javascript objects (breaking change in Electron 9+) +// delete readerActionsOpenSuccess.payload.reader.browserWindow; +// } +// webContents.send(syncIpc.CHANNEL, { +// type: syncIpc.EventType.MainAction, +// payload: { +// action: readerActionsOpenSuccess, +// }, +// } as syncIpc.EventPayload); + +// // Send reader config +// webContents.send(syncIpc.CHANNEL, { +// type: syncIpc.EventType.MainAction, +// payload: { +// action: readerActions.configSetSuccess.build(state.reader.config), +// }, +// } as syncIpc.EventPayload); + +// // Send reader mode +// webContents.send(syncIpc.CHANNEL, { +// type: syncIpc.EventType.MainAction, +// payload: { +// action: readerActions.detachModeSuccess.build(state.reader.mode), +// }, +// } as syncIpc.EventPayload); + +// // Send locale +// webContents.send(syncIpc.CHANNEL, { +// type: syncIpc.EventType.MainAction, +// payload: { +// action: i18nActions.setLocale.build(state.i18n.locale), +// }, +// } as syncIpc.EventPayload); + +// // Send keyboard shortcuts +// webContents.send(syncIpc.CHANNEL, { +// type: syncIpc.EventType.MainAction, +// payload: { +// action: keyboardActions.setShortcuts.build(state.keyboard.shortcuts, false), +// }, +// } as syncIpc.EventPayload); + +// // // Send update info +// // webContents.send(syncIpc.CHANNEL, { +// // type: syncIpc.EventType.MainAction, +// // payload: { +// // action: { +// // type: updateActions.latestVersion.ID, +// // payload: updateActions.latestVersion.build( +// // state.update.status, +// // state.update.latestVersion, +// // state.update.latestVersionUrl), +// // }, +// // }, +// // } as syncIpc.EventPayload); +// }; + +// // Callback called when a window is closed +// const winCloseCallback = (appWindow: AppWindow) => { +// const store = diMainGet("store"); + +// const winRegistry = diMainGet("win-registry"); +// const readerWindows = winRegistry.getReaderWindows(); + +// // library window was closed and unregistered +// // => all reader windows must now be closed too (effectively exiting the app) +// if (appWindow.type === AppWindowType.Library) { +// readerWindows.forEach((w) => w.browserWindow.close()); +// return; +// } + +// // else: appWindow.type === AppWindowType.Reader +// const state = store.getState(); +// if (state.reader?.readers) { +// const readers = Object.values(state.reader.readers); +// const reader = readers.find((r) => { +// return r.browserWindowID === appWindow.browserWindowID; +// }); +// if (reader) { +// store.dispatch(streamerActions.publicationCloseRequest.build(reader.publicationIdentifier)); +// } +// } + +// // if there is at least one remaining reader, then leave it/them alone +// // (it is / they are in detached mode, the library view is visible) +// if (readerWindows.length > 0) { +// return; +// } + +// // else, there is one window left, it is the library +// // we ensure return to "attached" mode +// store.dispatch(readerActions.detachModeSuccess.build(ReaderMode.Attached)); + +// // if the library is in fact no visible +// // (i.e. the sole closed reader was in attached mode) +// // then we close the app +// const libraryWindow = winRegistry.getLibraryWindow(); +// if (libraryWindow) { +// if (libraryWindow.browserWindow.isMinimized()) { +// libraryWindow.browserWindow.restore(); +// } else if (!libraryWindow.browserWindow.isVisible()) { +// libraryWindow.browserWindow.close(); +// return; +// } +// libraryWindow.browserWindow.show(); // focuses as well +// } +// }; + +// // Initialize application +// export function initApp() { + +// const store = diMainGet("store"); +// store.dispatch(appActions.initRequest.build()); + +// keyboardShortcuts.init(); +// store.dispatch(keyboardActions.setShortcuts.build(keyboardShortcuts.getAll(), false)); + +// const configRepository: ConfigRepository = diMainGet("config-repository"); +// const config = configRepository.get(LocaleConfigIdentifier); +// config.then((i18nLocale) => { +// if (i18nLocale && i18nLocale.value && i18nLocale.value.locale) { +// store.dispatch(i18nActions.setLocale.build(i18nLocale.value.locale)); +// debug(`set the locale ${i18nLocale.value.locale}`); +// } else { +// debug(`error on configRepository.get("i18n")): ${i18nLocale}`); +// } +// }).catch(async () => { +// const loc = app.getLocale().split("-")[0]; +// const langCodes = ObjectKeys(AvailableLanguages); +// const lang = langCodes.find((l) => l === loc) || "en"; +// store.dispatch(i18nActions.setLocale.build(lang)); +// debug(`create i18n key in configRepository with ${lang} locale`); +// }); + +// const winRegistry = diMainGet("win-registry"); +// winRegistry.registerOpenCallback(winOpenCallback); +// winRegistry.registerCloseCallback(winCloseCallback); +// app.setAppUserModelId("io.github.edrlab.thorium"); +// } + +// export function registerProtocol() { +// protocol.registerFileProtocol("store", (request, callback) => { +// // Extract publication item relative url +// const relativeUrl = request.url.substr(6); +// const pubStorage = diMainGet("publication-storage"); +// const filePath: string = path.join(pubStorage.getRootPath(), relativeUrl); +// callback(filePath); +// }); +// } diff --git a/src/main/lock.ts b/src/main/lock.ts index 0ab9f4d68..a211eaa26 100644 --- a/src/main/lock.ts +++ b/src/main/lock.ts @@ -7,7 +7,7 @@ import * as debug_ from "debug"; import { app } from "electron"; -import { diMainGet } from "readium-desktop/main/di"; +import { getLibraryWindowFromDi } from "readium-desktop/main/di"; import { openFileFromCli } from "./cli/commandLine"; import { cli } from "./cli/process"; @@ -40,13 +40,12 @@ export function lockInstance() { // Someone tried to run a second instance, we should focus our window. debug("comandLine", argv, _workingDir); - const winRegistry = diMainGet("win-registry"); - const libraryAppWindow = winRegistry.getLibraryWindow(); + const libraryAppWindow = getLibraryWindowFromDi(); if (libraryAppWindow) { - if (libraryAppWindow.browserWindow.isMinimized()) { - libraryAppWindow.browserWindow.restore(); + if (libraryAppWindow.isMinimized()) { + libraryAppWindow.restore(); } - libraryAppWindow.browserWindow.show(); // focuses as well + libraryAppWindow.show(); // focuses as well } // execute command line from second instance @@ -54,7 +53,8 @@ export function lockInstance() { // when the command has needed to open win electron: execute with below cli function // the mainFct is disallow to avoid to generate new mainWindow // remove --version and --help because isn't handle in ready state app - cli(() => ({}), argv.filter((arg) => !arg.startsWith("--"))); + // tslint:disable-next-line: no-empty + cli(() => {}, argv.filter((arg) => !arg.startsWith("--"))); }); } return gotTheLock; diff --git a/src/main/redux/actions/index.ts b/src/main/redux/actions/index.ts index abb2ad7a8..18a6255e5 100644 --- a/src/main/redux/actions/index.ts +++ b/src/main/redux/actions/index.ts @@ -9,11 +9,17 @@ import { netActions } from "readium-desktop/common/redux/actions"; import * as appActions from "./app/"; import * as lcpActions from "./lcp"; +import * as publicationActions from "./publication"; +import * as sessionActions from "./session"; import * as streamerActions from "./streamer/"; +import * as winActions from "./win"; export { appActions, lcpActions, netActions, streamerActions, + winActions, + publicationActions, + sessionActions, }; diff --git a/src/main/redux/actions/publication/deletePublication.ts b/src/main/redux/actions/publication/deletePublication.ts new file mode 100644 index 000000000..ed2751b54 --- /dev/null +++ b/src/main/redux/actions/publication/deletePublication.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 { Action } from "readium-desktop/common/models/redux"; + +export const ID = "PUBLICATION_DELETE"; + +interface Payload { + publicationIdentifier: string; +} + +export function build(publicationIdentifier: string): + Action { + + return { + type: ID, + payload: { + publicationIdentifier, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/publication/index.ts b/src/main/redux/actions/publication/index.ts new file mode 100644 index 000000000..f2676ddfc --- /dev/null +++ b/src/main/redux/actions/publication/index.ts @@ -0,0 +1,12 @@ +// ==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 deletePublication from "./deletePublication"; + +export { + deletePublication, +}; diff --git a/src/main/redux/actions/session/enable.ts b/src/main/redux/actions/session/enable.ts new file mode 100644 index 000000000..d4298058e --- /dev/null +++ b/src/main/redux/actions/session/enable.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 { Action } from "readium-desktop/common/models/redux"; + +export const ID = "SESSION_ENABLE"; + +export interface Payload { + value: boolean; +} + +export function build(value: boolean): Action { + + return { + type: ID, + payload: { + value, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/session/index.ts b/src/main/redux/actions/session/index.ts new file mode 100644 index 000000000..da7afd159 --- /dev/null +++ b/src/main/redux/actions/session/index.ts @@ -0,0 +1,12 @@ +// ==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 enable from "./enable"; + +export { + enable, +}; diff --git a/src/main/redux/actions/streamer/index.ts b/src/main/redux/actions/streamer/index.ts index b0aa4d7b3..7e1c68783 100644 --- a/src/main/redux/actions/streamer/index.ts +++ b/src/main/redux/actions/streamer/index.ts @@ -8,8 +8,8 @@ import * as publicationCloseError from "./publicationCloseError"; import * as publicationCloseRequest from "./publicationCloseRequest"; import * as publicationCloseSuccess from "./publicationCloseSuccess"; -import * as publicationOpenError from "./publicationOpenError"; -import * as publicationOpenRequest from "./publicationOpenRequest"; +// import * as publicationOpenError from "./publicationOpenError"; +// import * as publicationOpenRequest from "./publicationOpenRequest"; import * as publicationOpenSuccess from "./publicationOpenSuccess"; import * as startError from "./startError"; import * as startRequest from "./startRequest"; @@ -28,7 +28,7 @@ export { publicationCloseRequest, publicationCloseSuccess, publicationCloseError, - publicationOpenRequest, + // publicationOpenRequest, publicationOpenSuccess, - publicationOpenError, + // publicationOpenError, }; diff --git a/src/main/redux/actions/streamer/publicationCloseSuccess.ts b/src/main/redux/actions/streamer/publicationCloseSuccess.ts index d7db51bdf..752aa69ea 100644 --- a/src/main/redux/actions/streamer/publicationCloseSuccess.ts +++ b/src/main/redux/actions/streamer/publicationCloseSuccess.ts @@ -6,20 +6,19 @@ // ==LICENSE-END== import { Action } from "readium-desktop/common/models/redux"; -import { PublicationDocument } from "readium-desktop/main/db/document/publication"; export const ID = "STREAMER_PUBLICATION_CLOSE_SUCCESS"; export interface Payload { - publicationDocument: PublicationDocument; + publicationIdentifier: string; } -export function build(publicationDocument: PublicationDocument): +export function build(publicationIdentifier: string): Action { return { type: ID, payload: { - publicationDocument, + publicationIdentifier, }, }; } diff --git a/src/main/redux/actions/streamer/publicationOpenError.ts b/src/main/redux/actions/streamer/publicationOpenError.ts index d3dc9c6a6..f31a41b57 100644 --- a/src/main/redux/actions/streamer/publicationOpenError.ts +++ b/src/main/redux/actions/streamer/publicationOpenError.ts @@ -5,26 +5,26 @@ // 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 { PublicationDocument } from "readium-desktop/main/db/document/publication"; +// import { Action } from "readium-desktop/common/models/redux"; +// import { PublicationDocument } from "readium-desktop/main/db/document/publication"; -export const ID = "STREAMER_PUBLICATION_OPEN_ERROR"; +// export const ID = "STREAMER_PUBLICATION_OPEN_ERROR"; -export interface Meta { - publicationDocument: PublicationDocument; -} +// export interface Meta { +// publicationDocument: PublicationDocument; +// } -export function build(error: any, publicationDocument: PublicationDocument | undefined): - Action { +// export function build(error: any, publicationDocument: PublicationDocument | undefined): +// Action { - return { - type: ID, - payload: error, - error: true, - meta: { - publicationDocument, - }, - }; -} -build.toString = () => ID; // Redux StringableActionCreator -export type TAction = ReturnType; +// return { +// type: ID, +// payload: error, +// error: true, +// meta: { +// publicationDocument, +// }, +// }; +// } +// build.toString = () => ID; // Redux StringableActionCreator +// export type TAction = ReturnType; diff --git a/src/main/redux/actions/streamer/publicationOpenRequest.ts b/src/main/redux/actions/streamer/publicationOpenRequest.ts index 2d2885354..28b9dd8d6 100644 --- a/src/main/redux/actions/streamer/publicationOpenRequest.ts +++ b/src/main/redux/actions/streamer/publicationOpenRequest.ts @@ -5,23 +5,23 @@ // 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 { Action } from "readium-desktop/common/models/redux"; -export const ID = "STREAMER_PUBLICATION_OPEN_REQUEST"; +// export const ID = "STREAMER_PUBLICATION_OPEN_REQUEST"; -export interface Payload { - publicationIdentifier: string; -} +// export interface Payload { +// publicationIdentifier: string; +// } -export function build(publicationIdentifier: string): - Action { +// export function build(publicationIdentifier: string): +// Action { - return { - type: ID, - payload: { - publicationIdentifier, - }, - }; -} -build.toString = () => ID; // Redux StringableActionCreator -export type TAction = ReturnType; +// return { +// type: ID, +// payload: { +// publicationIdentifier, +// }, +// }; +// } +// build.toString = () => ID; // Redux StringableActionCreator +// export type TAction = ReturnType; diff --git a/src/main/redux/actions/streamer/publicationOpenSuccess.ts b/src/main/redux/actions/streamer/publicationOpenSuccess.ts index f0f31fe9b..741b3191f 100644 --- a/src/main/redux/actions/streamer/publicationOpenSuccess.ts +++ b/src/main/redux/actions/streamer/publicationOpenSuccess.ts @@ -6,21 +6,20 @@ // ==LICENSE-END== import { Action } from "readium-desktop/common/models/redux"; -import { PublicationDocument } from "readium-desktop/main/db/document/publication"; export const ID = "STREAMER_PUBLICATION_OPEN_SUCCESS"; export interface Payload { - publicationDocument: PublicationDocument; + publicationIdentifier: string; manifestUrl: string; } -export function build(publicationDocument: PublicationDocument, manifestUrl: string): +export function build(publicationIdentifier: string, manifestUrl: string): Action { return { type: ID, payload: { - publicationDocument, + publicationIdentifier, manifestUrl, }, }; diff --git a/src/main/redux/actions/win/index.ts b/src/main/redux/actions/win/index.ts new file mode 100644 index 000000000..7112909e9 --- /dev/null +++ b/src/main/redux/actions/win/index.ts @@ -0,0 +1,20 @@ +// ==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 library from "./library"; +import * as persistRequest from "./persistRequest"; +import * as reader from "./reader"; +import * as registry from "./registry"; +import * as session from "./session"; + +export { + session, + library, + registry, + reader, + persistRequest, +}; diff --git a/src/main/redux/actions/win/library/closed.ts b/src/main/redux/actions/win/library/closed.ts new file mode 100644 index 000000000..9f0c4b248 --- /dev/null +++ b/src/main/redux/actions/win/library/closed.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 { Action } from "readium-desktop/common/models/redux"; + +export const ID = "LIBRARY_CLOSE_REQUEST"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { +} + +export function build(): + Action { + + return { + type: ID, + payload: { + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/library/index.ts b/src/main/redux/actions/win/library/index.ts new file mode 100644 index 000000000..5748bf826 --- /dev/null +++ b/src/main/redux/actions/win/library/index.ts @@ -0,0 +1,16 @@ +// ==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 closed from "./closed"; +import * as openRequest from "./openRequest"; +import * as openSucess from "./openSucess"; + +export { + closed, + openSucess, + openRequest, +}; diff --git a/src/main/redux/actions/win/library/openRequest.ts b/src/main/redux/actions/win/library/openRequest.ts new file mode 100644 index 000000000..fea7ed59b --- /dev/null +++ b/src/main/redux/actions/win/library/openRequest.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 { Action } from "readium-desktop/common/models/redux"; + +export const ID = "LIBRARY_OPEN_REQUEST"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { +} + +export function build(): + Action { + + return { + type: ID, + payload: { + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/library/openSucess.ts b/src/main/redux/actions/win/library/openSucess.ts new file mode 100644 index 000000000..1bf5476d8 --- /dev/null +++ b/src/main/redux/actions/win/library/openSucess.ts @@ -0,0 +1,31 @@ + +// ==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 = "LIBRARY_OPEN_SUCCESS"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { + win: Electron.BrowserWindow; + identifier: string; +} + +export function build(win: Electron.BrowserWindow, identifier: string): + Action { + + return { + type: ID, + payload: { + win, + identifier, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/persistRequest.ts b/src/main/redux/actions/win/persistRequest.ts new file mode 100644 index 000000000..bd26b18a4 --- /dev/null +++ b/src/main/redux/actions/win/persistRequest.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 { Action } from "readium-desktop/common/models/redux"; + +export const ID = "WIN_PERSIST_REQUEST"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { +} + +export function build(): + Action { + + return { + type: ID, + payload: { + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/reader/closed.ts b/src/main/redux/actions/win/reader/closed.ts new file mode 100644 index 000000000..523728471 --- /dev/null +++ b/src/main/redux/actions/win/reader/closed.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"; + +export const ID = "MAIN_WIN_READER_CLOSE_REQUEST"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { + identifier: string; +} + +export function build(identifier: string): + Action { + + return { + type: ID, + payload: { + identifier, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/reader/index.ts b/src/main/redux/actions/win/reader/index.ts new file mode 100644 index 000000000..5748bf826 --- /dev/null +++ b/src/main/redux/actions/win/reader/index.ts @@ -0,0 +1,16 @@ +// ==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 closed from "./closed"; +import * as openRequest from "./openRequest"; +import * as openSucess from "./openSucess"; + +export { + closed, + openSucess, + openRequest, +}; diff --git a/src/main/redux/actions/win/reader/openRequest.ts b/src/main/redux/actions/win/reader/openRequest.ts new file mode 100644 index 000000000..f9546f0f8 --- /dev/null +++ b/src/main/redux/actions/win/reader/openRequest.ts @@ -0,0 +1,44 @@ +// ==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 { Rectangle } from "electron"; +import { Action } from "readium-desktop/common/models/redux"; +import { IReaderStateReader } from "readium-desktop/common/redux/states/renderer/readerRootState"; + +export const ID = "MAIN_WIN_READER_OPEN_REQUEST"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { + publicationIdentifier: string; + identifier?: string; + winBound: Rectangle; + manifestUrl: string; + reduxState: IReaderStateReader; +} + +export function build( + publicationIdentifier: string, + manifestUrl: string, + winBound: Rectangle | undefined, + reduxState: IReaderStateReader | undefined, + identifier?: string, +): + Action { + + return { + type: ID, + payload: { + publicationIdentifier, + winBound, + manifestUrl, + identifier, + reduxState, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/reader/openSucess.ts b/src/main/redux/actions/win/reader/openSucess.ts new file mode 100644 index 000000000..a8b22ee63 --- /dev/null +++ b/src/main/redux/actions/win/reader/openSucess.ts @@ -0,0 +1,31 @@ + +// ==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 = "MAIN_WIN_READER_OPEN_SUCCESS"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { + win: Electron.BrowserWindow; + identifier: string; +} + +export function build(win: Electron.BrowserWindow, identifier: string): + Action { + + return { + type: ID, + payload: { + win, + identifier, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/registry/index.ts b/src/main/redux/actions/win/registry/index.ts new file mode 100644 index 000000000..90530be1a --- /dev/null +++ b/src/main/redux/actions/win/registry/index.ts @@ -0,0 +1,12 @@ +// ==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 registerReaderPublication from "./registerReaderPublication"; + +export { + registerReaderPublication, +}; diff --git a/src/main/redux/actions/win/registry/registerReaderPublication.ts b/src/main/redux/actions/win/registry/registerReaderPublication.ts new file mode 100644 index 000000000..38cfcc627 --- /dev/null +++ b/src/main/redux/actions/win/registry/registerReaderPublication.ts @@ -0,0 +1,33 @@ +// ==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 { Rectangle } from "electron"; +import { Action } from "readium-desktop/common/models/redux"; +import { IReaderStateReader } from "readium-desktop/common/redux/states/renderer/readerRootState"; + +export const ID = "WIN_REGISTRY_REGISTER_READER_PUBLICATION"; + +export interface Payload { + publicationIdentifier: string; + bound: Rectangle; + reduxStateReader: IReaderStateReader; +} + +export function build(publicationIdentifier: string, bound: Rectangle, reduxStateReader: IReaderStateReader): + Action { + + return { + type: ID, + payload: { + bound, + publicationIdentifier, + reduxStateReader, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/session/index.ts b/src/main/redux/actions/win/session/index.ts new file mode 100644 index 000000000..dcbad203e --- /dev/null +++ b/src/main/redux/actions/win/session/index.ts @@ -0,0 +1,22 @@ +// ==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 registerLibrary from "./registerLibrary"; +import * as registerReader from "./registerReader"; +import * as setBound from "./setBound"; +import * as setReduxState from "./setReduxState"; +import * as unregisterLibrary from "./unregisterLibrary"; +import * as unregisterReader from "./unregisterReader"; + +export { + registerLibrary, + registerReader, + unregisterReader, + unregisterLibrary, + setBound, + setReduxState, +}; diff --git a/src/main/redux/actions/win/session/registerLibrary.ts b/src/main/redux/actions/win/session/registerLibrary.ts new file mode 100644 index 000000000..8c7436425 --- /dev/null +++ b/src/main/redux/actions/win/session/registerLibrary.ts @@ -0,0 +1,35 @@ + +// ==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 { BrowserWindow } from "electron"; +import { Action } from "readium-desktop/common/models/redux"; + +import * as uuid from "uuid"; + +export const ID = "WIN_SESSION_REGISTER_LIBRARY"; + +export interface Payload { + win: BrowserWindow; + identifier: string; + winBound: Electron.Rectangle; +} + +export function build(win: BrowserWindow, winBound: Electron.Rectangle): + Action { + + return { + type: ID, + payload: { + win, + winBound, + identifier: uuid.v4(), + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/session/registerReader.ts b/src/main/redux/actions/win/session/registerReader.ts new file mode 100644 index 000000000..4c5f96c76 --- /dev/null +++ b/src/main/redux/actions/win/session/registerReader.ts @@ -0,0 +1,70 @@ +// ==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 { BrowserWindow, Rectangle } from "electron"; +import { Action } from "readium-desktop/common/models/redux"; +import { locatorInitialState } from "readium-desktop/common/redux/states/locatorInitialState"; +import { IReaderStateReader } from "readium-desktop/common/redux/states/renderer/readerRootState"; +import { diMainGet } from "readium-desktop/main/di"; +import * as uuid from "uuid"; + +export const ID = "WIN_SESSION_REGISTER_READER"; + +export interface Payload { + win: BrowserWindow; + publicationIdentifier: string; + identifier: string; + winBound: Rectangle; + filesystemPath: string; + manifestUrl: string; + reduxStateReader: IReaderStateReader; +} + +export function build( + win: BrowserWindow, + publicationIdentifier: string, + manifestUrl: string, + filesystemPath: string, + winBound: Rectangle, + reduxStateReader?: IReaderStateReader, + identifier: string = uuid.v4()): + Action { + + // we lose purity !! + const store = diMainGet("store"); + const readerConfigDefault = store.getState().reader.defaultConfig; + + reduxStateReader = { + ...{ + config: readerConfigDefault, + locator: locatorInitialState, + }, + ...reduxStateReader, + ...{ + info: { + filesystemPath, + manifestUrl, + publicationIdentifier, + }, + }, + }; + + return { + type: ID, + payload: { + win, + publicationIdentifier, + manifestUrl, + filesystemPath, + winBound, + identifier, + reduxStateReader, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/session/setBound.ts b/src/main/redux/actions/win/session/setBound.ts new file mode 100644 index 000000000..cbe2fa6aa --- /dev/null +++ b/src/main/redux/actions/win/session/setBound.ts @@ -0,0 +1,30 @@ +// ==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 { Rectangle } from "electron"; +import { Action } from "readium-desktop/common/models/redux"; + +export const ID = "WIN_SESSION_SET_BOUND"; + +export interface Payload { + identifier: string; + bound: Rectangle; +} + +export function build(id: string, bound: Rectangle): + Action { + + return { + type: ID, + payload: { + identifier: id, + bound, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/session/setReduxState.ts b/src/main/redux/actions/win/session/setReduxState.ts new file mode 100644 index 000000000..64f833337 --- /dev/null +++ b/src/main/redux/actions/win/session/setReduxState.ts @@ -0,0 +1,31 @@ + +// ==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 { IReaderStateReader } from "readium-desktop/common/redux/states/renderer/readerRootState"; + +export const ID = "WIN_SESSION_SET_REDUXSTATE"; + +export interface Payload { + reduxState: IReaderStateReader; + identifier: string; +} + +export function build(id: string, reduxState: IReaderStateReader): + Action { + + return { + type: ID, + payload: { + reduxState, + identifier: id, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/session/unregisterLibrary.ts b/src/main/redux/actions/win/session/unregisterLibrary.ts new file mode 100644 index 000000000..c38a9aeb2 --- /dev/null +++ b/src/main/redux/actions/win/session/unregisterLibrary.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 { Action } from "readium-desktop/common/models/redux"; + +export const ID = "WIN_SESSION_UNREGISTER_LIBRARY"; + +// tslint:disable-next-line: no-empty-interface +export interface Payload { +} + +export function build(): + Action { + + return { + type: ID, + payload: { + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/actions/win/session/unregisterReader.ts b/src/main/redux/actions/win/session/unregisterReader.ts new file mode 100644 index 000000000..38de843ce --- /dev/null +++ b/src/main/redux/actions/win/session/unregisterReader.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 { Action } from "readium-desktop/common/models/redux"; + +export const ID = "WIN_SESSION_UNREGISTER_READER"; + +export interface Payload { + identifier: string; +} + +export function build(identifier: string): + Action { + + return { + type: ID, + payload: { + identifier, + }, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/main/redux/middleware/persistence.ts b/src/main/redux/middleware/persistence.ts new file mode 100644 index 000000000..6cd0000a8 --- /dev/null +++ b/src/main/redux/middleware/persistence.ts @@ -0,0 +1,38 @@ +// ==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 ramda from "ramda"; +import { ActionWithSender } from "readium-desktop/common/models/sync"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from "redux"; + +import { RootState } from "../states"; + +export const reduxPersistMiddleware: Middleware + = (store: MiddlewareAPI, RootState>) => + (next: Dispatch) => + (action: ActionWithSender) => { + + const prevState = store.getState(); + + const returnValue = next(action); + + const nextState = store.getState(); + + if ( + !ramda.equals(prevState.win, nextState.win) + || !ramda.equals(prevState.publication, nextState.publication) + || !ramda.equals(prevState.reader, nextState.reader) + || !ramda.equals(prevState.session, nextState.session) + ) { + + // dispatch a new round in middleware + store.dispatch(winActions.persistRequest.build()); + } + + return returnValue; + }; diff --git a/src/main/redux/middleware/sync.ts b/src/main/redux/middleware/sync.ts index 07dc6b29d..36402ce16 100644 --- a/src/main/redux/middleware/sync.ts +++ b/src/main/redux/middleware/sync.ts @@ -12,9 +12,12 @@ import { apiActions, dialogActions, downloadActions, i18nActions, keyboardActions, lcpActions, readerActions, toastActions, } from "readium-desktop/common/redux/actions"; -import { diMainGet } from "readium-desktop/main/di"; +import { ActionSerializer } from "readium-desktop/common/services/serializer"; +import { getLibraryWindowFromDi, getReaderWindowFromDi } from "readium-desktop/main/di"; import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from "redux"; +import { RootState } from "../states"; + const debug = debug_("readium-desktop:sync"); // Actions that can be synchronized @@ -26,14 +29,14 @@ const SYNCHRONIZABLE_ACTIONS: string[] = [ dialogActions.openRequest.ID, - readerActions.openError.ID, - readerActions.closeError.ID, - readerActions.closeSuccess.ID, + // readerActions.openError.ID, + // readerActions.closeError.ID, + // readerActions.closeSuccess.ID, readerActions.detachModeSuccess.ID, - readerActions.configSetError.ID, - readerActions.configSetSuccess.ID, + readerActions.configSetDefault.ID, + readerActions.setReduxState.ID, // used only to update the catalog when dispatched from reader // readerActions.saveBookmarkError.ID, // readerActions.saveBookmarkSuccess.ID, @@ -60,51 +63,104 @@ const SYNCHRONIZABLE_ACTIONS: string[] = [ ]; export const reduxSyncMiddleware: Middleware - = (_store: MiddlewareAPI>) => - (next: Dispatch) => - ((action: ActionWithSender) => { - - debug("### action type", action.type); - - // Test if the action must be sent to the rendeder processes - if (SYNCHRONIZABLE_ACTIONS.indexOf(action.type) === -1) { - // Do not send - return next(action); - } - - // Send this action to all the registered renderer processes - const winRegistry = diMainGet("win-registry"); - const appWindows = winRegistry.getAllWindows(); - - // Get action serializer - const actionSerializer = diMainGet("action-serializer"); - - for (const appWindow of appWindows) { - // Notifies renderer process - const winId = appWindow.identifier; - - if (action.sender && - action.sender.type === SenderType.Renderer && - action.sender.winId === winId - ) { - // Do not send in loop an action already sent by this renderer process - continue; - } - - try { - appWindow.browserWindow.webContents.send(syncIpc.CHANNEL, { - type: syncIpc.EventType.MainAction, - payload: { - action: actionSerializer.serialize(action), - }, - sender: { - type: SenderType.Main, - }, - } as syncIpc.EventPayload); - } catch (error) { - console.error("Windows does not exist", winId); - } - } - - return next(action); -}); + = (store: MiddlewareAPI, RootState>) => + (next: Dispatch) => + ((action: ActionWithSender) => { + + debug("### action type", action.type); + + // Test if the action must be sent to the rendeder processes + if (SYNCHRONIZABLE_ACTIONS.indexOf(action.type) === -1) { + // Do not send + return next(action); + } + + // Send this action to all the registered renderer processes + + // actually when a renderer process send an api action this middleware broadcast to all renderer + // It should rather keep the action and don't broadcast an api request between front and back + // this bug become a feature with a hack in publicationInfo in reader + // thanks to this broadcast we can listen on publication tag and make a live refresh + + const browserWin: Map = new Map(); + + const libId = store.getState().win.session.library.identifier; + try { + const libWin = getLibraryWindowFromDi(); + browserWin.set(libId, libWin); + } catch (_err) { + // ignore + // library window may be not initialized in first + } + + const readers = store.getState().win.session.reader; + for (const key in readers) { + if (readers[key]) { + try { + const readerWin = getReaderWindowFromDi(readers[key].identifier); + browserWin.set(readers[key].identifier, readerWin); + } catch (err) { + // ignore + debug("ERROR: Can't found ther reader win from di: ", readers[key].identifier); + } + } + } + + browserWin.forEach( + (win, id) => { + + if ( + !( + action.sender?.type === SenderType.Renderer + && action.sender?.identifier === id + ) + ) { + + debug("send to", id); + try { + win.webContents.send(syncIpc.CHANNEL, { + type: syncIpc.EventType.MainAction, + payload: { + action: ActionSerializer.serialize(action), + }, + sender: { + type: SenderType.Main, + }, + } as syncIpc.EventPayload); + + } catch (error) { + debug("ERROR in SYNC ACTION", error); + } + } + }); + + // for (const readerWindow of readerWindows) { + // // Notifies renderer process + // const winId = readerWindow.id; + + // if (action.sender && + // action.sender.type === SenderType.Renderer && + // action.sender.identifier === identifier + // ) { + // // Do not send in loop an action already sent by this renderer process + // continue; + // } + + // try { + // appWindow.browserWindow.webContents.send(syncIpc.CHANNEL, { + // type: syncIpc.EventType.MainAction, + // payload: { + // action: actionSerializer.serialize(action), + // }, + // sender: { + // type: SenderType.Main, + // }, + // } as syncIpc.EventPayload); + // } catch (error) { + // console.error("Windows does not exist", winId); + // } + // } + + return next(action); + + }); diff --git a/src/main/redux/reducers/index.ts b/src/main/redux/reducers/index.ts index 355e11211..1bd807d7d 100644 --- a/src/main/redux/reducers/index.ts +++ b/src/main/redux/reducers/index.ts @@ -5,25 +5,66 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import { readerActions } from "readium-desktop/common/redux/actions"; import { i18nReducer } from "readium-desktop/common/redux/reducers/i18n"; import { keyboardReducer } from "readium-desktop/common/redux/reducers/keyboard"; // import { netReducer } from "readium-desktop/common/redux/reducers/net"; // import { updateReducer } from "readium-desktop/common/redux/reducers/update"; import { appReducer } from "readium-desktop/main/redux/reducers/app"; -import { readerReducer } from "readium-desktop/main/redux/reducers/reader"; import { streamerReducer } from "readium-desktop/main/redux/reducers/streamer"; import { RootState } from "readium-desktop/main/redux/states"; +import { priorityQueueReducer } from "readium-desktop/utils/redux-reducers/pqueue.reducer"; import { combineReducers } from "redux"; +import { publicationActions } from "../actions"; import { lcpReducer } from "./lcp"; +import { readerDefaultConfigReducer } from "./reader/defaultConfig"; +import { sessionReducer } from "./session"; +import { winRegistryReaderReducer } from "./win/registry/reader"; +import { winSessionLibraryReducer } from "./win/session/library"; +import { winSessionReaderReducer } from "./win/session/reader"; +import { winModeReducer } from "./win/winModeReducer"; export const rootReducer = combineReducers({ + session: sessionReducer, streamer: streamerReducer, i18n: i18nReducer, - reader: readerReducer, + reader: combineReducers({ + defaultConfig: readerDefaultConfigReducer, + }), // net: netReducer, // update: updateReducer, app: appReducer, + win: combineReducers({ + session: combineReducers({ + library: winSessionLibraryReducer, + reader: winSessionReaderReducer, + }), + registry: combineReducers({ + reader: winRegistryReaderReducer, + }), + }), + mode: winModeReducer, lcp: lcpReducer, + publication: combineReducers({ + lastReadingQueue: priorityQueueReducer + < + readerActions.setReduxState.TAction, + publicationActions.deletePublication.TAction + >( + { + push: { + type: readerActions.setReduxState.ID, + selector: (action) => + [(new Date()).getTime(), action.payload.reduxState?.info?.publicationIdentifier], + }, + pop: { + type: publicationActions.deletePublication.ID, + selector: (action) => [undefined, action.payload.publicationIdentifier], + }, + sortFct: (a, b) => b[0] - a[0], + }, + ), + }), keyboard: keyboardReducer, }); diff --git a/src/main/redux/reducers/reader.ts b/src/main/redux/reducers/reader.ts deleted file mode 100644 index abcb3c32e..000000000 --- a/src/main/redux/reducers/reader.ts +++ /dev/null @@ -1,65 +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 { ReaderMode } from "readium-desktop/common/models/reader"; -import { readerActions } from "readium-desktop/common/redux/actions"; -import { ReaderState } from "readium-desktop/main/redux/states/reader"; - -// TODO: centralize this code, currently duplicated -// see src/renderer/redux/reducers/reader.ts -const initialState: ReaderState = { - readers: {}, - mode: ReaderMode.Attached, - // See optionsValues (AdjustableSettingsStrings) - config: { - align: "auto", - colCount: "auto", - dark: false, - font: "DEFAULT", - fontSize: "100%", - invert: false, - lineHeight: "1.5", - night: false, - paged: false, - readiumcss: true, - sepia: false, - enableMathJax: false, - pageMargins: "0.5", - wordSpacing: "0", - letterSpacing: "0", - paraSpacing: "0", - noFootnotes: undefined, - darken: undefined, - }, -}; - -export function readerReducer( - state: ReaderState = initialState, - action: readerActions.closeSuccess.TAction | - readerActions.openSuccess.TAction | - readerActions.detachModeSuccess.TAction | - readerActions.configSetSuccess.TAction, -): ReaderState { - const newState = Object.assign({}, state); - - switch (action.type) { - case readerActions.openSuccess.ID: - newState.readers[action.payload.reader.identifier] = action.payload.reader; - return newState; - case readerActions.closeSuccess.ID: - delete newState.readers[action.payload.reader.identifier]; - return newState; - case readerActions.detachModeSuccess.ID: - newState.mode = action.payload.mode; - return newState; - case readerActions.configSetSuccess.ID: - newState.config = action.payload.config; - return newState; - default: - return state; - } -} diff --git a/src/main/redux/reducers/reader/defaultConfig.ts b/src/main/redux/reducers/reader/defaultConfig.ts new file mode 100644 index 000000000..1d16ece80 --- /dev/null +++ b/src/main/redux/reducers/reader/defaultConfig.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 { ReaderConfig } from "readium-desktop/common/models/reader"; +import { readerActions } from "readium-desktop/common/redux/actions"; +import { readerConfigInitialState } from "readium-desktop/common/redux/states/reader"; + +export function readerDefaultConfigReducer( + state: ReaderConfig = readerConfigInitialState, + action: readerActions.configSetDefault.TAction, +): ReaderConfig { + + switch (action.type) { + case readerActions.configSetDefault.ID: + return { + ...state, + ...action.payload.config, + }; + + default: + return state; + } +} diff --git a/src/main/redux/reducers/session.ts b/src/main/redux/reducers/session.ts new file mode 100644 index 000000000..1d919a6fe --- /dev/null +++ b/src/main/redux/reducers/session.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 { sessionActions } from "../actions"; +import { ISessionState } from "../states/session"; + +const initialState: ISessionState = { + state: false, +}; + +export function sessionReducer( + state = initialState, + action: sessionActions.enable.TAction, +): ISessionState { + switch (action.type) { + case sessionActions.enable.ID: + return { + state: action.payload.value, + }; + default: + return state; + } +} diff --git a/src/main/redux/reducers/streamer.ts b/src/main/redux/reducers/streamer.ts index 4824ff859..90e518bfa 100644 --- a/src/main/redux/reducers/streamer.ts +++ b/src/main/redux/reducers/streamer.ts @@ -30,42 +30,82 @@ export function streamerReducer( streamerActions.publicationCloseSuccess.TAction | streamerActions.stopSuccess.TAction, ): StreamerState { - let pubId = null; - const newState = Object.assign({}, state); switch (action.type) { case streamerActions.startSuccess.ID: - newState.status = StreamerStatus.Running; - newState.baseUrl = action.payload.streamerUrl; - newState.openPublicationCounter = {}; - newState.publicationManifestUrl = {}; - return newState; + return { + ...{ + status: StreamerStatus.Running, + baseUrl: action.payload.streamerUrl, + openPublicationCounter: {}, + publicationManifestUrl: {}, + }, + }; + case streamerActions.stopSuccess.ID: - newState.baseUrl = null; - newState.status = StreamerStatus.Stopped; - newState.openPublicationCounter = {}; - newState.publicationManifestUrl = {}; - return newState; - case streamerActions.publicationOpenSuccess.ID: - pubId = action.payload.publicationDocument.identifier; + return { + ...{ + status: StreamerStatus.Stopped, + baseUrl: undefined, + openPublicationCounter: {}, + publicationManifestUrl: {}, + }, + }; - if (!newState.openPublicationCounter.hasOwnProperty(pubId)) { - newState.openPublicationCounter[pubId] = 1; - newState.publicationManifestUrl[pubId] = action.payload.manifestUrl; - } else { - // Increment the number of pubs opened with the streamer - newState.openPublicationCounter[pubId] = state.openPublicationCounter[pubId] + 1; - } - return newState; - case streamerActions.publicationCloseSuccess.ID: - pubId = action.payload.publicationDocument.identifier; - newState.openPublicationCounter[pubId] = newState.openPublicationCounter[pubId] - 1; + case streamerActions.publicationOpenSuccess.ID: { + const pubId = action.payload.publicationIdentifier; + + return { + ...state, + ...{ + openPublicationCounter: { + ...state.openPublicationCounter, + ...{ + [pubId]: state.openPublicationCounter[pubId] + 1 || 1, + }, + }, + publicationManifestUrl: { + ...state.publicationManifestUrl, + ...{ + [pubId]: action.payload.manifestUrl, + }, + }, + }, + }; + } - if (newState.openPublicationCounter[pubId] <= 0) { - delete newState.openPublicationCounter[pubId]; - delete newState.publicationManifestUrl[pubId]; + case streamerActions.publicationCloseSuccess.ID: { + const pubId = action.payload.publicationIdentifier; + + const ret = { + ...state, + ...{ + openPublicationCounter: { + ...state.openPublicationCounter, + ...{ + [pubId]: state.openPublicationCounter[pubId] - 1 < 1 + ? undefined + : state.openPublicationCounter[pubId] - 1, + }, + }, + publicationManifestUrl: { + ...state.publicationManifestUrl, + ...{ + [pubId]: state.openPublicationCounter[pubId] - 1 < 1 + ? undefined + : state.publicationManifestUrl[pubId], + }, + }, + }, + }; + + if (!ret.openPublicationCounter[pubId]) { + delete ret.openPublicationCounter[pubId]; + delete ret.publicationManifestUrl[pubId]; } - return newState; + + return ret; + } default: return state; } diff --git a/src/main/redux/reducers/win/registry/reader.ts b/src/main/redux/reducers/win/registry/reader.ts new file mode 100644 index 000000000..5685d6430 --- /dev/null +++ b/src/main/redux/reducers/win/registry/reader.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 { winActions } from "readium-desktop/main/redux/actions"; +import { IDictWinRegistryReaderState } from "readium-desktop/main/redux/states/win/registry/reader"; + +const initialState: IDictWinRegistryReaderState = {}; + +export function winRegistryReaderReducer( + state: IDictWinRegistryReaderState = initialState, + action: winActions.registry.registerReaderPublication.TAction, +): IDictWinRegistryReaderState { + switch (action.type) { + + case winActions.registry.registerReaderPublication.ID: + return { + ...state, + ...{ + [action.payload.publicationIdentifier]: { + ...state[action.payload.publicationIdentifier], + ...{ + windowBound: action.payload.bound, + reduxState: action.payload.reduxStateReader, + }, + }, + }, + }; + + default: + return state; + } +} diff --git a/src/main/redux/reducers/win/session/library.ts b/src/main/redux/reducers/win/session/library.ts new file mode 100644 index 000000000..e4ed091c1 --- /dev/null +++ b/src/main/redux/reducers/win/session/library.ts @@ -0,0 +1,55 @@ +// ==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 { winActions } from "readium-desktop/main/redux/actions"; +import { IWinSessionLibraryState } from "readium-desktop/main/redux/states/win/session/library"; + +const initialState: IWinSessionLibraryState = { + browserWindowId: undefined, + windowBound: undefined, + identifier: undefined, +}; + +export function winSessionLibraryReducer( + state: IWinSessionLibraryState = initialState, + action: winActions.session.registerLibrary.TAction | + winActions.session.unregisterLibrary.TAction | + winActions.session.setBound.TAction, +): IWinSessionLibraryState { + switch (action.type) { + + case winActions.session.registerLibrary.ID: + return { + ...state, + ...{ + browserWindowId: action.payload.win.id, + identifier: action.payload.identifier, + windowBound: action.payload.winBound, + }, + }; + + case winActions.session.unregisterLibrary.ID: + return { + ...state, + ...{ + browserWindowId: undefined, + identifier: undefined, + }, + }; + + case winActions.session.setBound.ID: + if (state.identifier === action.payload.identifier) { + return { + ...state, + ...{ + windowBound: action.payload.bound, + }, + }; + } + } + return state; +} diff --git a/src/main/redux/reducers/win/session/reader.ts b/src/main/redux/reducers/win/session/reader.ts new file mode 100644 index 000000000..ba384a5dc --- /dev/null +++ b/src/main/redux/reducers/win/session/reader.ts @@ -0,0 +1,103 @@ +// ==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 { winActions } from "readium-desktop/main/redux/actions"; +import { + IDictWinSessionReaderState, +} from "readium-desktop/main/redux/states/win/session/reader"; + +const initialState: IDictWinSessionReaderState = {}; + +export function winSessionReaderReducer( + state: IDictWinSessionReaderState = initialState, + action: winActions.session.registerReader.TAction | + winActions.session.unregisterReader.TAction | + winActions.session.setBound.TAction | + winActions.session.setReduxState.TAction, +): IDictWinSessionReaderState { + switch (action.type) { + + case winActions.session.registerReader.ID: { + + const id = action.payload.identifier; + return { + ...state, + ...{ + [id]: { + ...{ + windowBound: action.payload.winBound, + reduxState: action.payload.reduxStateReader, + }, + ...state[id], + ...{ + browserWindowId: action.payload.win.id, + publicationIdentifier: action.payload.publicationIdentifier, + manifestUrl: action.payload.manifestUrl, + fileSystemPath: action.payload.filesystemPath, + identifier: id, + }, + }, + }, + }; + } + + case winActions.session.unregisterReader.ID: { + + const id = action.payload.identifier; + + if (state[id]) { + const ret = { + ...state, + }; + delete ret[id]; + return ret; + } + break; + } + + case winActions.session.setBound.ID: { + + const id = action.payload.identifier; + + if (state[id]) { + return { + ...state, + ...{ + [id]: { + ...state[id], + ...{ + windowBound: action.payload.bound, + }, + }, + }, + }; + } + break; + } + + case winActions.session.setReduxState.ID: { + + const id = action.payload.identifier; + + if (state[id]) { + return { + ...state, + ...{ + [id]: { + ...state[id], + ...{ + reduxState: action.payload.reduxState, + }, + }, + }, + }; + } + } + } + + return state; +} diff --git a/src/main/redux/reducers/win/winModeReducer.ts b/src/main/redux/reducers/win/winModeReducer.ts new file mode 100644 index 000000000..ebda92b91 --- /dev/null +++ b/src/main/redux/reducers/win/winModeReducer.ts @@ -0,0 +1,29 @@ +// ==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 { ReaderMode } from "readium-desktop/common/models/reader"; +import { readerActions } from "readium-desktop/common/redux/actions"; + +const initialState: ReaderMode = ReaderMode.Attached; + +export function winModeReducer( + state: ReaderMode = initialState, + action: readerActions.detachModeRequest.TAction | + readerActions.attachModeRequest.TAction, +): ReaderMode { + switch (action.type) { + + case readerActions.detachModeRequest.ID: + return ReaderMode.Detached; + + case readerActions.attachModeRequest.ID: + return ReaderMode.Attached; + + default: + return state; + } +} diff --git a/src/main/redux/sagas/api.ts b/src/main/redux/sagas/api.ts index 1ff21a999..d9af26346 100644 --- a/src/main/redux/sagas/api.ts +++ b/src/main/redux/sagas/api.ts @@ -6,17 +6,18 @@ // ==LICENSE-END== import * as debug_ from "debug"; -import { CodeError } from "readium-desktop/common/errors"; +import { CodeError } from "readium-desktop/common/codeError.class"; import { apiActions } from "readium-desktop/common/redux/actions"; -import { takeTyped } from "readium-desktop/common/redux/typed-saga"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; import { diMainGet } from "readium-desktop/main/di"; import { diSymbolTable } from "readium-desktop/main/diSymbolTable"; +import { error } from "readium-desktop/main/error"; import { ObjectKeys } from "readium-desktop/utils/object-keys-values"; -import { SagaIterator } from "redux-saga"; -import { all, call, fork, put } from "redux-saga/effects"; +import { call, put } from "redux-saga/effects"; // Logger -const debug = debug_("readium-desktop:main#redux/sagas/api"); +const filename_ = "readium-desktop:main:saga:api"; +const debug = debug_(filename_); const getSymbolName = (apiName: string) => { const keys = ObjectKeys(diSymbolTable); @@ -27,11 +28,11 @@ const getSymbolName = (apiName: string) => { throw new Error("Wrong API name called " + apiName); }; -export function* processRequest(requestAction: apiActions.request.TAction): SagaIterator { +function* processRequest(requestAction: apiActions.request.TAction) { const { api } = requestAction.meta; try { - const apiModule = diMainGet(getSymbolName(api.moduleId)); + const apiModule = yield call(() => diMainGet(getSymbolName(api.moduleId))); const apiMethod = apiModule[api.methodId].bind(apiModule); debug(api.moduleId, api.methodId, requestAction.payload); @@ -43,20 +44,16 @@ export function* processRequest(requestAction: apiActions.request.TAction): Saga yield put(apiActions.result.build(api, result)); } catch (error) { - debug(error); + debug("API-ERROR", error, "requestAction: ", requestAction); yield put(apiActions.result.build(api, new CodeError("API-ERROR", error.message))); } } -export function* requestWatcher() { - while (true) { - const action = yield* takeTyped(apiActions.request.build); - yield fork(processRequest, action); - } -} +export function saga() { -export function* watchers() { - yield all([ - call(requestWatcher), - ]); + return takeSpawnEvery( + apiActions.request.ID, + processRequest, + (e) => error(filename_, e), + ); } diff --git a/src/main/redux/sagas/app.ts b/src/main/redux/sagas/app.ts index 16e60d061..370e2fefb 100644 --- a/src/main/redux/sagas/app.ts +++ b/src/main/redux/sagas/app.ts @@ -5,14 +5,49 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { SagaIterator } from "redux-saga"; -import { put, take } from "redux-saga/effects"; +import * as debug_ from "debug"; +import { app, protocol } from "electron"; +import * as path from "path"; +import { diMainGet } from "readium-desktop/main/di"; +import { call } from "redux-saga/effects"; -import { appActions } from "readium-desktop/main/redux/actions"; +// Logger +const filename_ = "readium-desktop:main:saga:app"; +const debug = debug_(filename_); + +export function* init() { + + app.setAppUserModelId("io.github.edrlab.thorium"); + + // moved to saga/persist.ts + // app.on("window-all-closed", async () => { + // // At the moment, there are no menu items to revive / re-open windows, + // // so let's terminate the app on MacOS too. + // // if (process.platform !== "darwin") { + // // app.quit(); + // // } + + // setTimeout(() => app.exit(0), 2000); + // }); + + app.on("accessibility-support-changed", (_ev, accessibilitySupportEnabled) => { + debug(`accessibilitySupportEnabled: ${accessibilitySupportEnabled}`); + }); + + yield call(() => app.whenReady()); + + debug("Main app ready"); + + // register file protocol to link locale file to renderer + protocol.registerFileProtocol("store", + (request, callback) => { + + // Extract publication item relative url + const relativeUrl = request.url.substr(6); + const pubStorage = diMainGet("publication-storage"); + const filePath: string = path.join(pubStorage.getRootPath(), relativeUrl); + callback(filePath); + }, + ); -export function* appInitWatcher(): SagaIterator { - while (true) { - yield take(appActions.initRequest.ID); - yield put(appActions.initSuccess.build()); - } } diff --git a/src/main/redux/sagas/i18n.ts b/src/main/redux/sagas/i18n.ts index d94edb274..ba8fa3abb 100644 --- a/src/main/redux/sagas/i18n.ts +++ b/src/main/redux/sagas/i18n.ts @@ -5,25 +5,38 @@ // 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 { i18nActions } from "readium-desktop/common/redux/actions"; +import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { callTyped } from "readium-desktop/common/redux/sagas/typed-saga"; import { ConfigRepository } from "readium-desktop/main/db/repository/config"; import { diMainGet } from "readium-desktop/main/di"; -import { all, call, takeEvery } from "redux-saga/effects"; +import { error } from "readium-desktop/main/error"; +import { all, call } from "redux-saga/effects"; + +// Logger +const filename_ = "readium-desktop:main:saga:i18n"; +const debug = debug_(filename_); +debug("_"); function* setLocale(action: i18nActions.setLocale.TAction) { - const translator = diMainGet("translator"); - const configRepository: ConfigRepository = diMainGet("config-repository"); - const configRepositorySave = async () => - await configRepository.save({ + const translator = yield* callTyped(() => diMainGet("translator")); + const configRepository: ConfigRepository = yield call( + () => diMainGet("config-repository"), + ); + + const configRepositorySave = () => + configRepository.save({ identifier: LocaleConfigIdentifier, value: { locale: action.payload.locale, }, }); - const translatorSetLocale = async () => - await translator.setLocale(action.payload.locale); + + const translatorSetLocale = () => + translator.setLocale(action.payload.locale); yield all([ call(configRepositorySave), @@ -31,12 +44,11 @@ function* setLocale(action: i18nActions.setLocale.TAction) { ]); } -function* localeWatcher() { - yield takeEvery(i18nActions.setLocale.build, setLocale); -} +export function saga() { -export function* watchers() { - yield all([ - call(localeWatcher), - ]); + return takeSpawnLeading( + i18nActions.setLocale.ID, + setLocale, + (e) => error(filename_, e), + ); } diff --git a/src/main/redux/sagas/index.ts b/src/main/redux/sagas/index.ts index ed013c1d2..8a3f72477 100644 --- a/src/main/redux/sagas/index.ts +++ b/src/main/redux/sagas/index.ts @@ -5,40 +5,96 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { all, call } from "redux-saga/effects"; +import * as debug_ from "debug"; +import { app as appElectron, dialog } from "electron"; +import { keyboardActions } from "readium-desktop/common/redux/actions"; +import { keyboardShortcuts } from "readium-desktop/main/keyboard"; +import { all, call, put, take } from "redux-saga/effects"; +import { appActions, winActions } from "../actions"; import * as api from "./api"; -import { appInitWatcher } from "./app"; +import * as app from "./app"; import * as i18n from "./i18n"; +import * as ipc from "./ipc"; import * as keyboard from "./keyboard"; -// import { netStatusWatcher } from "./net"; +import * as persist from "./persist"; import * as reader from "./reader"; import * as streamer from "./streamer"; +import * as win from "./win"; +// import { netStatusWatcher } from "./net"; // import { updateStatusWatcher } from "./update"; +// Logger +const filename_ = "readium-desktop:main:saga:app"; +const debug = debug_(filename_); + export function* rootSaga() { - yield all([ - call(api.watchers), - // I18N - call(i18n.watchers), + // main entry point + yield take(appActions.initRequest.ID); + + yield i18n.saga(); + // yield spawnLeading(i18n.watchers, (e) => error("main:rootSaga:i18n", e)); + + try { + yield all([ + call(app.init), + call(keyboardShortcuts.init), + ]); + + } catch (e) { + const code = 1; + try { + dialog.showErrorBox("Application init Error", `main rootSaga: ${e}\n\nEXIT CODE ${code}`); + } catch { + // ignore + } + + debug("CRITICAL ERROR => EXIT"); + debug(e); + + // see main/redux/saga/persist.ts#L92 + appElectron.exit(code); + } + + // send initSucess first + yield put(appActions.initSuccess.build()); + + yield api.saga(); + // yield spawnLeading(api.watchers, (e) => error("main:rootSaga:api", e)); + + yield streamer.saga(); + // yield spawnLeading(streamer.watchers, (e) => error("main:rootSaga:streamer", e)); + + yield keyboard.saga(); + // yield spawnLeading(keyboard.watchers, (e) => error("main:rootSaga:keyboard", e)); + + yield win.reader.saga(); + // yield spawnLeading(win.reader.watchers, (e) => error("main:rootSaga:win:reader", e)); + + yield win.library.saga(); + // yield spawnLeading(win.library.watchers, (e) => error("main:rootSaga:win:library", e)); + + yield win.session.reader.saga(); + // yield spawnLeading(win.session.library.watchers, (e) => error("main:rootSaga:win:session:library", e)); - // App - call(appInitWatcher), + yield win.session.library.saga(); + // yield spawnLeading(win.session.reader.watchers, (e) => error("main:rootSaga:win:session:reader", e)); - // Net - // call(netStatusWatcher), + yield ipc.saga(); + // yield spawnLeading(ipc.watchers, (e) => error("main:rootSaga:ipc", e)); - // Reader - call(reader.watchers), + yield reader.saga(); + // yield spawnLeading(reader.watchers, (e) => error("main:rootSaga:reader", e)); - // Streamer - call(streamer.watchers), + // halt entry point + yield persist.saga(); + // yield spawnLeading(persist.watchers, (e) => error("main:rootSaga:persist", e)); - call(keyboard.watchers), + // dispatch at the end + yield put(keyboardActions.setShortcuts.build(keyboardShortcuts.getAll(), false)); - // Update checker - // call(updateStatusWatcher), - ]); + // enjoy the app ! + yield put(winActions.library.openRequest.build()); } diff --git a/src/main/redux/sagas/ipc.ts b/src/main/redux/sagas/ipc.ts new file mode 100644 index 000000000..e59bd93c9 --- /dev/null +++ b/src/main/redux/sagas/ipc.ts @@ -0,0 +1,64 @@ +// ==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 { ipcMain } from "electron"; +import { syncIpc } from "readium-desktop/common/ipc"; +import { ActionWithSender } from "readium-desktop/common/models/sync"; +import { takeSpawnEveryChannel } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { ActionSerializer } from "readium-desktop/common/services/serializer"; +import { eventChannel } from "redux-saga"; +import { put } from "redux-saga/effects"; + +// Logger +const filename_ = "readium-desktop:main:saga:ipc"; +const debug = debug_(filename_); +debug("_"); + +function getIpcChannel() { + + const channel = eventChannel( + (emit) => { + + const handler = (_0: any, data: syncIpc.EventPayload) => { + + if (data.type === syncIpc.EventType.RendererAction) { + emit(data); + } + }; + + ipcMain.on(syncIpc.CHANNEL, handler); + + return () => { + ipcMain.removeListener(syncIpc.CHANNEL, handler); + }; + }, + ); + + return channel; +} + +function* ipcSyncChannel(ipcData: syncIpc.EventPayload) { + + yield put({ + ...ActionSerializer.deserialize(ipcData.payload.action), + ...{ + sender: ipcData.sender, + }, + } as ActionWithSender); +} + +export function saga() { + + const ipcChannel = getIpcChannel(); + + return takeSpawnEveryChannel( + ipcChannel, + ipcSyncChannel, + (e) => debug("redux IPC sync channel error", e), + ); +} diff --git a/src/main/redux/sagas/keyboard.ts b/src/main/redux/sagas/keyboard.ts index 528ae8238..8fbde9a4b 100644 --- a/src/main/redux/sagas/keyboard.ts +++ b/src/main/redux/sagas/keyboard.ts @@ -6,107 +6,81 @@ // ==LICENSE-END== import * as debug_ from "debug"; -import { DEBUG_KEYBOARD } from "readium-desktop/common/keyboard"; import { ToastType } from "readium-desktop/common/models/toast"; import { keyboardActions, toastActions } from "readium-desktop/common/redux/actions"; -import { takeTyped } from "readium-desktop/common/redux/typed-saga"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { callTyped } from "readium-desktop/common/redux/sagas/typed-saga"; import { diMainGet } from "readium-desktop/main/di"; +import { error } from "readium-desktop/main/error"; import { keyboardShortcuts } from "readium-desktop/main/keyboard"; -import { all, call, takeEvery } from "redux-saga/effects"; +import { all } from "redux-saga/effects"; import { put } from "typed-redux-saga"; -import { sortObject } from "@r2-utils-js/_utils/JsonUtils"; +const filename_ = "readium-desktop:main:redux:sagas:keyboard"; +const debug = debug_(filename_); -const debug = debug_("readium-desktop:main:redux:sagas:keyboard"); - -function* showShortcuts(action: keyboardActions.showShortcuts.TAction) { - const showShortcutsFolder = async () => { - if (action.payload.focusFile) { - keyboardShortcuts.showUserFile(); - } else { - keyboardShortcuts.showFolder(); - } - }; - - yield all([ - call(showShortcutsFolder), - ]); +function showShortcuts(action: keyboardActions.showShortcuts.TAction) { + if (action.payload.focusFile) { + keyboardShortcuts.showUserFile(); + } else { + keyboardShortcuts.showFolder(); + } } -function* setShortcuts(action: keyboardActions.setShortcuts.TAction) { - const saveShortcuts = async () => { - if (action.payload.save) { - debug("Keyboard shortcuts saving:", action.payload.shortcuts); - keyboardShortcuts.saveUser(action.payload.shortcuts); - } else { - debug("Keyboard shortcuts NOT saving (defaults):", action.payload.shortcuts); - } - }; - yield all([ - call(saveShortcuts), - ]); +function setShortcuts(action: keyboardActions.setShortcuts.TAction) { + if (action.payload.save) { + debug("Keyboard shortcuts saving:", action.payload.shortcuts); + keyboardShortcuts.saveUser(action.payload.shortcuts); + } else { + debug("Keyboard shortcuts NOT saving (defaults):", action.payload.shortcuts); + } + } -// function* reloadShortcuts(action: keyboardActions.reloadShortcuts.TAction) { -// const loadShortcutsFromJson = async () => { -// if (action.payload.defaults) { -// keyboardShortcuts.loadDefaults(); -// } -// const okay = action.payload.defaults ? true : keyboardShortcuts.loadUser(); -// debug(`Keyboard shortcuts reload JSON (defaults: ${action.payload.defaults}) => ${okay}`); -// if (okay) { -// yield put(keyboardActions.setShortcuts.build(keyboardShortcuts.getAll())); -// } -// }; +function* keyboardReload(action: keyboardActions.reloadShortcuts.TAction) { + if (action.payload.defaults) { + keyboardShortcuts.loadDefaults(); + } + const okay = action.payload.defaults || keyboardShortcuts.loadUser(); -// yield all([ -// call(loadShortcutsFromJson), -// ]); -// } + debug(`Keyboard shortcuts reload JSON (defaults: ${action.payload.defaults}) => ${okay}`); -function* keyboardSetWatcher() { - yield takeEvery(keyboardActions.setShortcuts.build, setShortcuts); -} -function* keyboardShowWatcher() { - yield takeEvery(keyboardActions.showShortcuts.build, showShortcuts); -} -// function* keyboardReloadWatcher() { -// yield takeEvery(keyboardActions.reloadShortcuts.build, reloadShortcuts); -// } -function* keyboardReloadWatcher() { - while (true) { - const action = yield* takeTyped(keyboardActions.reloadShortcuts.build); - if (action.payload.defaults) { - keyboardShortcuts.loadDefaults(); - } - const okay = action.payload.defaults ? true : keyboardShortcuts.loadUser(); + // if (DEBUG_KEYBOARD) { + // const jsonDiff = require("json-diff"); - debug(`Keyboard shortcuts reload JSON (defaults: ${action.payload.defaults}) => ${okay}`); + // const defaultKeyboardShortcuts = keyboardShortcuts.getAllDefaults(); + // const json1 = sortObject(JSON.parse(JSON.stringify(defaultKeyboardShortcuts))); + // const json2 = sortObject(JSON.parse(JSON.stringify(currentKeyboardShortcuts))); + // debug(jsonDiff.diffString(json1, json2) + "\n"); + // } + if (okay) { const currentKeyboardShortcuts = keyboardShortcuts.getAll(); - if (DEBUG_KEYBOARD) { - const jsonDiff = require("json-diff"); - - const defaultKeyboardShortcuts = keyboardShortcuts.getAllDefaults(); - const json1 = sortObject(JSON.parse(JSON.stringify(defaultKeyboardShortcuts))); - const json2 = sortObject(JSON.parse(JSON.stringify(currentKeyboardShortcuts))); - debug(jsonDiff.diffString(json1, json2) + "\n"); - } - - if (okay) { - yield put(keyboardActions.setShortcuts.build(currentKeyboardShortcuts, true)); // !action.payload.defaults + yield put(keyboardActions.setShortcuts.build(currentKeyboardShortcuts, true)); // !action.payload.defaults - const translator = diMainGet("translator"); - yield put(toastActions.openRequest.build(ToastType.Success, - `${translator.translate("settings.keyboard.keyboardShortcuts")}`)); - } + const translator = yield* callTyped(() => diMainGet("translator")); + yield put(toastActions.openRequest.build(ToastType.Success, + `${translator.translate("settings.keyboard.keyboardShortcuts")}`)); } } -export function* watchers() { - yield all([ - call(keyboardSetWatcher), - call(keyboardShowWatcher), - call(keyboardReloadWatcher), +export function saga() { + return all([ + takeSpawnEvery( + keyboardActions.setShortcuts.ID, + setShortcuts, + (e) => error(filename_ + ":setShortcuts", e), + ), + takeSpawnEvery( + keyboardActions.showShortcuts.ID, + showShortcuts, + (e) => error(filename_ + ":showShortcuts", e), + ), + takeSpawnLeading( + keyboardActions.reloadShortcuts.ID, + keyboardReload, + (e) => error(filename_ + ":reloadKeyboard", e), + ), ]); } diff --git a/src/main/redux/sagas/persist.ts b/src/main/redux/sagas/persist.ts new file mode 100644 index 000000000..73ea3f69f --- /dev/null +++ b/src/main/redux/sagas/persist.ts @@ -0,0 +1,98 @@ +// ==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 { app } from "electron"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { ConfigRepository } from "readium-desktop/main/db/repository/config"; +import { CONFIGREPOSITORY_REDUX_PERSISTENCE, diMainGet } from "readium-desktop/main/di"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { RootState } from "readium-desktop/main/redux/states"; +import { eventChannel, Task } from "redux-saga"; +import { call, cancel, debounce, spawn, take } from "redux-saga/effects"; + +const DEBOUNCE_TIME = 1000; + +// Logger +const filename_ = "readium-desktop:main:saga:persist"; +const debug = debug_(filename_); +debug("_"); + +const persistStateToFs = async (nextState: RootState) => { + + // currently saved with pouchDb in one json file. + // may be consuming a lot of I/O + // rather need to save by chunck of data in many json file + + debug("start of persist reduxState in disk"); + const configRepository: ConfigRepository> = diMainGet("config-repository"); + await configRepository.save({ + identifier: CONFIGREPOSITORY_REDUX_PERSISTENCE, + value: { + win: nextState.win, + publication: nextState.publication, + reader: nextState.reader, + session: nextState.session, + }, + }); + debug("end of persist reduxState in disk"); +}; + +function getWindowAllClosedEventChannel() { + + const channel = eventChannel( + (emit) => { + + const handler = () => emit(true); + + app.on("window-all-closed", handler); + + return () => { + app.removeListener("window-all-closed", handler); + }; + }, + ); + + return channel; +} + +function* needToPersistState() { + + try { + + const nextState = yield* selectTyped((store: RootState) => store); + yield call(() => persistStateToFs(nextState)); + } catch (e) { + debug("error persist state in user filesystem", e); + } +} + +function* windowaAllClosedEventManager() { + + const allClosedEventChannel = yield* callTyped(getWindowAllClosedEventChannel); + + const debounceTask: Task = yield debounce( + DEBOUNCE_TIME, + winActions.persistRequest.ID, + needToPersistState, + ); + + // wait untill all windows are closed to continue + yield take(allClosedEventChannel); + + // cancel persistence + yield cancel(debounceTask); + + // persist the winState now and exit 0 the app + yield call(needToPersistState); + + yield call(() => app.exit(0)); +} + +export function saga() { + return spawn(windowaAllClosedEventManager); +} diff --git a/src/main/redux/sagas/publication/openPublication.ts b/src/main/redux/sagas/publication/openPublication.ts new file mode 100644 index 000000000..f466517e4 --- /dev/null +++ b/src/main/redux/sagas/publication/openPublication.ts @@ -0,0 +1,209 @@ +// ==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 { StreamerStatus } from "readium-desktop/common/models/streamer"; +import { lcpActions } from "readium-desktop/common/redux/actions/"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { PublicationDocument } from "readium-desktop/main/db/document/publication"; +import { diMainGet } from "readium-desktop/main/di"; +import { streamerActions } from "readium-desktop/main/redux/actions"; +import { RootState } from "readium-desktop/main/redux/states"; +import { put, take } from "redux-saga/effects"; + +import { StatusEnum } from "@r2-lcp-js/parser/epub/lsd"; +import { Publication as R2Publication } from "@r2-shared-js/models/publication"; + +// Logger +const filename_ = "readium-desktop:main:redux:sagas:publication:open"; +const debug = debug_(filename_); + +export function* streamerOpenPublicationAndReturnManifestUrl(pubId: string) { + + const publicationRepository = yield* callTyped( + () => diMainGet("publication-repository")); + const translator = yield* callTyped( + () => diMainGet("translator")); + + // Get publication + let publicationDocument: PublicationDocument = null; + publicationDocument = yield* callTyped( + () => publicationRepository.get(pubId)); + + const publicationFileLocks = yield* selectTyped( + (s: RootState) => s.lcp.publicationFileLocks); + + if (publicationFileLocks[pubId]) { + throw new Error("lcp publication locked"); + } + // no need to lock here, because once the streamer server accesses the ZIP file and streams resources, + // it's like a giant no-go to inject LCP license (which is why readers are closed before LSD updates) + // also, checkPublicationLicenseUpdate() places a lock or simply skips LSD checks (see below) + // yield put(appActions.publicationFileLock.build({ [publicationDocument.identifier]: true })); + // try { + // } finally { + // yield put(appActions.publicationFileLock.build({ [publicationDocument.identifier]: false })); + // } + + const lcpManager = yield* callTyped( + () => diMainGet("lcp-manager")); + const publicationViewConverter = yield* callTyped( + () => diMainGet("publication-view-converter")); + + if (publicationDocument.lcp) { + try { + publicationDocument = yield* callTyped( + () => lcpManager.checkPublicationLicenseUpdate(publicationDocument), + ); + } catch (error) { + debug("ERROR on call lcpManager.checkPublicationLicenseUpdate", error); + } + + if ( + publicationDocument.lcp && publicationDocument.lcp.lsd && publicationDocument.lcp.lsd.lsdStatus && + publicationDocument.lcp.lsd.lsdStatus.status && + publicationDocument.lcp.lsd.lsdStatus.status !== StatusEnum.Ready && + publicationDocument.lcp.lsd.lsdStatus.status !== StatusEnum.Active + ) { + + const msg = publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Expired ? + translator.translate("publication.expiredLcp") : ( + publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Revoked ? + translator.translate("publication.revokedLcp") : ( + publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Cancelled ? + translator.translate("publication.cancelledLcp") : ( + publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Returned ? + translator.translate("publication.returnedLcp") : + translator.translate("publication.expiredLcp") + ))); + + throw new Error(msg); + } + + // we first unlockPublication() for the transient in-memory R2Publication, + // then we have to unlockPublication() again for the streamer-hosted pub instance (see below) + try { + // TODO: improve this horrible returned union type! + const unlockPublicationRes: string | number | null | undefined = + yield* callTyped(() => lcpManager.unlockPublication(publicationDocument, undefined)); + + if (typeof unlockPublicationRes !== "undefined") { + const message = unlockPublicationRes === 11 + ? translator.translate("publication.expiredLcp") + : lcpManager.convertUnlockPublicationResultToString(unlockPublicationRes); + + try { + const publicationView = publicationViewConverter.convertDocumentToView(publicationDocument); + + // will call API.unlockPublicationWithPassphrase() + yield put(lcpActions.userKeyCheckRequest.build( + publicationView, + publicationView.lcp.textHint, + message, + )); + + throw new Error(message); + } catch (error) { + + throw error; + } + } + } catch (error) { + + throw error; + } + } + + const pubStorage = yield* callTyped(() => diMainGet("publication-storage")); + const epubPath = pubStorage.getPublicationEpubPath(publicationDocument.identifier); + // const epubPath = path.join( + // pubStorage.getRootPath(), + // publicationDocument.files[0].url.substr(6), + // ); + debug("Open publication %s", epubPath); + + // Start streamer if it's not already started + const status = yield* selectTyped((s: RootState) => s.streamer.status); + const streamer = yield* callTyped(() => diMainGet("streamer")); + + if (status === StreamerStatus.Stopped) { + // Streamer is stopped, start it + yield put(streamerActions.startRequest.build()); + + // Wait for streamer + const streamerStartAction = yield take([ + streamerActions.startSuccess.ID, + streamerActions.startError.ID, + ]); + const typedAction = streamerStartAction.error ? + streamerStartAction as streamerActions.startSuccess.TAction : + streamerStartAction as streamerActions.startError.TAction; + + if (typedAction.error) { + const err = "Unable to start server"; + + throw new Error(err); + } + } + + const manifestPaths = streamer.addPublications([epubPath]); + + let r2Publication: R2Publication; + try { + r2Publication = yield* callTyped( + () => streamer.loadOrGetCachedPublication(epubPath), + ); + } catch (error) { + + throw error; + } + + // we unlockPublication() again because previously only done on transient in-memory R2Publication, + // (see above), has not been done yet on streamer-hosted publication instance. + // Consequently, unlockPublicationRes should always be undefined (valid passphrase already obtained) + if (r2Publication.LCP) { + try { + // TODO: improve this horrible returned union type! + const unlockPublicationRes: string | number | null | undefined = + yield* callTyped(() => lcpManager.unlockPublication(publicationDocument, undefined)); + + if (typeof unlockPublicationRes !== "undefined") { + const message = unlockPublicationRes === 11 ? + translator.translate("publication.expiredLcp") : + lcpManager.convertUnlockPublicationResultToString(unlockPublicationRes); + debug(message); + + try { + const publicationView = publicationViewConverter.convertDocumentToView(publicationDocument); + + // will call API.unlockPublicationWithPassphrase() + yield put(lcpActions.userKeyCheckRequest.build( + publicationView, + r2Publication.LCP.Encryption.UserKey.TextHint, + message, + )); + + throw new Error(message); + } catch (error) { + + throw error; + } + } + } catch (error) { + + throw error; + } + } + + const manifestUrl = streamer.serverUrl() + manifestPaths[0]; + debug(pubId, " streamed on ", manifestUrl); + + // add in reducer + yield put(streamerActions.publicationOpenSuccess.build(pubId, manifestUrl)); + + return manifestUrl; +} diff --git a/src/main/redux/sagas/reader.ts b/src/main/redux/sagas/reader.ts index cf2ccab43..7d6961620 100644 --- a/src/main/redux/sagas/reader.ts +++ b/src/main/redux/sagas/reader.ts @@ -6,455 +6,389 @@ // ==LICENSE-END== import * as debug_ from "debug"; -import { BrowserWindow, Menu } from "electron"; -import * as path from "path"; -import { LocatorType } from "readium-desktop/common/models/locator"; -import { Reader, ReaderConfig, ReaderMode } from "readium-desktop/common/models/reader"; -import { Timestampable } from "readium-desktop/common/models/timestampable"; -import { AppWindowType } from "readium-desktop/common/models/win"; -import { getWindowBounds } from "readium-desktop/common/rectangle/window"; -import { readerActions } from "readium-desktop/common/redux/actions"; -import { callTyped, selectTyped, takeTyped } from "readium-desktop/common/redux/typed-saga"; -import { ConfigDocument } from "readium-desktop/main/db/document/config"; -import { ConfigRepository } from "readium-desktop/main/db/repository/config"; -import { diMainGet } from "readium-desktop/main/di"; -import { setMenu } from "readium-desktop/main/menu"; -import { appActions, streamerActions } from "readium-desktop/main/redux/actions"; +import { screen } from "electron"; +import * as ramda from "ramda"; +import { ReaderMode } from "readium-desktop/common/models/reader"; +import { SenderType } from "readium-desktop/common/models/sync"; +import { ToastType } from "readium-desktop/common/models/toast"; +import { readerActions, toastActions } from "readium-desktop/common/redux/actions"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { IReaderStateReader } from "readium-desktop/common/redux/states/renderer/readerRootState"; +import { diMainGet, getLibraryWindowFromDi, getReaderWindowFromDi } from "readium-desktop/main/di"; +import { error } from "readium-desktop/main/error"; +import { streamerActions, winActions } from "readium-desktop/main/redux/actions"; import { RootState } from "readium-desktop/main/redux/states"; import { - _NODE_MODULE_RELATIVE_URL, _PACKAGING, _RENDERER_READER_BASE_URL, _VSCODE_LAUNCH, IS_DEV, + _NODE_MODULE_RELATIVE_URL, _PACKAGING, _RENDERER_READER_BASE_URL, _VSCODE_LAUNCH, } from "readium-desktop/preprocessor-directives"; -import { SagaIterator } from "redux-saga"; +import { ObjectValues } from "readium-desktop/utils/object-keys-values"; import { all, call, put, take } from "redux-saga/effects"; +import { types } from "util"; -import { convertHttpUrlToCustomScheme } from "@r2-navigator-js/electron/common/sessions"; -import { trackBrowserWindow } from "@r2-navigator-js/electron/main/browser-window-tracker"; -import { encodeURIComponent_RFC3986 } from "@r2-utils-js/_utils/http/UrlUtils"; +import { streamerOpenPublicationAndReturnManifestUrl } from "./publication/openPublication"; // Logger -const debug = debug_("readium-desktop:main:redux:sagas:reader"); - -async function openReader(publicationIdentifier: string, manifestUrl: string) { - debug("create readerWindow"); - // Create reader window - const readerWindow = new BrowserWindow({ - ...(await getWindowBounds()), - minWidth: 800, - minHeight: 600, - webPreferences: { - allowRunningInsecureContent: false, - backgroundThrottling: false, - contextIsolation: false, - devTools: IS_DEV, - nodeIntegration: true, - nodeIntegrationInWorker: false, - sandbox: false, - webSecurity: true, - webviewTag: true, - }, - icon: path.join(__dirname, "assets/icons/icon.png"), - }); - - if (IS_DEV) { - const wc = readerWindow.webContents; - wc.on("context-menu", (_ev, params) => { - const { x, y } = params; - const openDevToolsAndInspect = () => { - const devToolsOpened = () => { - wc.off("devtools-opened", devToolsOpened); - wc.inspectElement(x, y); - - setTimeout(() => { - if (wc.isDevToolsOpened() && wc.devToolsWebContents) { - wc.devToolsWebContents.focus(); - } - }, 500); - }; - wc.on("devtools-opened", devToolsOpened); - wc.openDevTools({ activate: true, mode: "detach" }); - }; - Menu.buildFromTemplate([{ - click: () => { - const wasOpened = wc.isDevToolsOpened(); - if (!wasOpened) { - openDevToolsAndInspect(); - } else { - if (!wc.isDevToolsFocused()) { - // wc.toggleDevTools(); - wc.closeDevTools(); - - setImmediate(() => { - openDevToolsAndInspect(); - }); - } else { - // this should never happen, - // as the right-click context menu occurs with focus - // in BrowserWindow / WebView's WebContents - wc.inspectElement(x, y); - } - } - }, - label: "Inspect element", - }]).popup({window: readerWindow}); - }); - - // Already done for primary library BrowserWindow - // readerWindow.webContents.on("did-finish-load", () => { - // const { - // default: installExtension, - // REACT_DEVELOPER_TOOLS, - // REDUX_DEVTOOLS, - // } = require("electron-devtools-installer"); - - // [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS].forEach((extension) => { - // installExtension(extension) - // .then((name: string) => debug("Added Extension: ", name)) - // .catch((err: Error) => debug("An error occurred: ", err)); - // }); - // }); - - if (_VSCODE_LAUNCH !== "true") { - setTimeout(() => { - if (!readerWindow.isDestroyed()) { - readerWindow.webContents.openDevTools({ activate: true, mode: "detach" }); - } - }, 2000); - } - } +const filename_ = "readium-desktop:main:saga:reader"; +const debug = debug_(filename_); +debug("_"); - const winRegistry = diMainGet("win-registry"); - const appWindows = winRegistry.getAllWindows(); +// const READER_CONFIG_ID = "reader"; - const thereIsOnlyTheLibraryWindow = appWindows.length === 1; +// export function* readerConfigSetRequestWatcher(): SagaIterator { +// while (true) { +// // Wait for save request +// const action = yield* takeTyped(readerActions.configSetRequest.build); - // Hide the only window (the library), - // as the new reader window will now take over - // (in other words: "detach" mode is disabled by default for new reader windows) - if (thereIsOnlyTheLibraryWindow) { - // Same as: appWindows[0] - const libraryAppWindow = winRegistry.getLibraryWindow(); - if (libraryAppWindow) { - libraryAppWindow.browserWindow.hide(); - } - } +// const configValue = action.payload.config; +// const config: Omit, keyof Timestampable> = { +// identifier: READER_CONFIG_ID, +// value: configValue, +// }; - const readerAppWindow = winRegistry.registerWindow( - readerWindow, - AppWindowType.Reader, - ); +// // Get reader settings +// const configRepository: ConfigRepository = diMainGet("config-repository"); - if (thereIsOnlyTheLibraryWindow) { - // onWindowMoveResize.detach() is called for reader windows that become ReaderMode.Detached - // (in which case the library window is shown again, and then its position takes precedence) - readerAppWindow.onWindowMoveResize.attach(); - } +// try { +// yield call(() => configRepository.save(config)); +// yield put(readerActions.configSetSuccess.build(configValue)); +// } catch (error) { +// yield put(readerActions.configSetError.build(error)); +// } +// } +// } - // Track it - trackBrowserWindow(readerWindow); +// export function* readerConfigInitWatcher(): SagaIterator { +// // Wait for app initialization +// yield take(appActions.initSuccess.ID); - const pathBase64 = manifestUrl.replace(/.*\/pub\/(.*)\/manifest.json/, "$1"); - const pathDecoded = Buffer.from(decodeURIComponent(pathBase64), "base64").toString("utf8"); +// const configRepository: ConfigRepository = diMainGet("config-repository"); - // Create reader object - const reader: Reader = { - identifier: readerAppWindow.identifier, - publicationIdentifier, - manifestUrl, - filesystemPath: pathDecoded, - browserWindow: readerWindow, - browserWindowID: readerWindow.id, - }; +// try { +// const readerConfigDoc = yield* callTyped(() => configRepository.get(READER_CONFIG_ID)); - // This triggers the origin-sandbox for localStorage, etc. - manifestUrl = convertHttpUrlToCustomScheme(manifestUrl); +// // Returns the first reader configuration available in database +// yield put(readerActions.configSetSuccess.build(readerConfigDoc.value)); +// } catch (error) { +// yield put(readerActions.configSetError.build(error)); +// } +// } - // Load publication in reader window - const encodedManifestUrl = encodeURIComponent_RFC3986(manifestUrl); +// export function* readerBookmarkSaveRequestWatcher(): SagaIterator { +// while (true) { +// // Wait for app initialization +// // tslint:disable-next-line: max-line-length +// const action = yield* takeTyped(readerActions.saveBookmarkRequest.build); - let readerUrl = _RENDERER_READER_BASE_URL; +// const bookmark = action.payload.bookmark; - const htmlPath = "index_reader.html"; +// // Get bookmark manager +// const locatorRepository = diMainGet("locator-repository"); - if (readerUrl === "file://") { - // dist/prod mode (without WebPack HMR Hot Module Reload HTTP server) - readerUrl += path.normalize(path.join(__dirname, htmlPath)); - } else { - // dev/debug mode (with WebPack HMR Hot Module Reload HTTP server) - readerUrl += htmlPath; - } +// try { +// const locator: ExcludeTimestampableWithPartialIdentifiable = { +// // name: "", +// locator: { +// href: bookmark.docHref, +// locations: { +// cssSelector: bookmark.docSelector, +// }, +// }, +// publicationIdentifier: bookmark.publicationIdentifier, +// locatorType: LocatorType.LastReadingLocation, +// }; +// yield call(() => locatorRepository.save(locator)); +// yield put(readerActions.saveBookmarkSuccess.build(bookmark)); +// } catch (error) { +// yield put(readerActions.saveBookmarkError.build(error)); +// } +// } +// } - readerUrl = readerUrl.replace(/\\/g, "/"); - readerUrl += `?pub=${encodedManifestUrl}&pubId=${publicationIdentifier}`; +function* readerFullscreenRequest(action: readerActions.fullScreenRequest.TAction) { - // Get publication last reading location - const locatorRepository = diMainGet("locator-repository"); - const locators = await locatorRepository - .findByPublicationIdentifierAndLocatorType( - publicationIdentifier, - LocatorType.LastReadingLocation, - ); + const sender = action.sender; + + if (sender.identifier && sender.type === SenderType.Renderer) { - if (locators.length > 0) { - const locator = locators[0]; - const docHref = encodeURIComponent_RFC3986(Buffer.from(locator.locator.href).toString("base64")); - const docSelector = locator.locator.locations.cssSelector - ? encodeURIComponent_RFC3986(Buffer.from(locator.locator.locations.cssSelector).toString("base64")) - : undefined; - const docProgression = locator.locator.locations.progression - // tslint:disable-next-line: max-line-length - ? encodeURIComponent_RFC3986(Buffer.from(locator.locator.locations.progression.toString()).toString("base64")) - : undefined; - readerUrl += `&docHref=${docHref}&docSelector=${docSelector ? docSelector : ""}&docProgression=${docProgression ? docProgression : ""}`; + const readerWin = yield* callTyped(() => getReaderWindowFromDi(sender.identifier)); + if (readerWin) { + readerWin.setFullScreen(action.payload.full); + } } +} - setMenu(readerWindow, true); +function* readerDetachRequest(action: readerActions.detachModeRequest.TAction) { - process.nextTick(async () => { - await readerWindow.webContents.loadURL(readerUrl, { extraHeaders: "pragma: no-cache\n" }); - }); + const libWin = yield* callTyped(() => getLibraryWindowFromDi()); - return reader; -} + if (libWin) { -export function* readerOpenRequestWatcher(): SagaIterator { - while (true) { - const action = yield* takeTyped(readerActions.openRequest.build); - const publicationIdentifier = action.payload.publicationIdentifier; - - // Notify the streamer to create a manifest for this publication - yield put(streamerActions.publicationOpenRequest.build(publicationIdentifier)); - - // Wait for the publication to be opened - const streamerAction = yield take([ - streamerActions.publicationOpenSuccess.ID, - streamerActions.publicationOpenError.ID, - ]); - const typedAction = streamerAction.error ? - streamerAction as streamerActions.publicationOpenError.TAction : - streamerAction as streamerActions.publicationOpenSuccess.TAction; - - if (typedAction.error) { - // Failed to open publication - // FIXME: Put publication in meta to be FSA compliant - yield put(readerActions.openError.build(publicationIdentifier)); - continue; + // try-catch to do not trigger an error message when the winbound is not handle by the os + let libBound: Electron.Rectangle; + try { + // get an bound with offset + libBound = yield* callTyped(getWinBound, undefined); + if (libBound) { + libWin.setBounds(libBound); + } + } catch (e) { + + debug("cannot set libBound", libBound, e); } - const manifestUrl = typedAction.payload.manifestUrl; - const reader = yield* callTyped( - () => openReader(publicationIdentifier, manifestUrl), - ); + // this should never occur, but let's do it for certainty + if (libWin.isMinimized()) { + libWin.restore(); + } + libWin.show(); // focuses as well + } + + const readerWinId = action.sender?.identifier; + if (readerWinId && action.sender?.type === SenderType.Renderer) { + + const readerWin = getReaderWindowFromDi(readerWinId); - // Publication is opened in a new reader - yield put(readerActions.openSuccess.build(reader)); + if (readerWin) { + + // this should never occur, but let's do it for certainty + if (readerWin.isMinimized()) { + readerWin.restore(); + } + readerWin.show(); // focuses as well + } } + + yield put(readerActions.detachModeSuccess.build()); } -export function* readerCloseRequestWatcher(): SagaIterator { - while (true) { - const action = yield* takeTyped(readerActions.closeRequest.build); +function* getWinBound(publicationIdentifier: string) { - const reader = action.payload.reader; - const gotoLibrary = action.payload.gotoLibrary; + const readers = yield* selectTyped((state: RootState) => state.win.session.reader); + const library = yield* selectTyped((state: RootState) => state.win.session.library); + const readerArray = ObjectValues(readers); - yield call(() => closeReader(reader, gotoLibrary)); + if (readerArray.length === 0) { + return library.windowBound; } -} -export function* closeReaderFromPublicationWatcher(): SagaIterator { - while (true) { - // tslint:disable-next-line: max-line-length - const action = yield* takeTyped(readerActions.closeRequestFromPublication.build); + let winBound = yield* selectTyped( + (state: RootState) => state.win.registry.reader[publicationIdentifier]?.windowBound, + ); - const publicationIdentifier = action.payload.publicationIdentifier; + const winBoundArray = []; + winBoundArray.push(library.windowBound); + readerArray.forEach( + (reader) => reader && winBoundArray.push(reader.windowBound)); + const winBoundAlreadyTaken = !!winBoundArray.find((bound) => ramda.equals(winBound, bound)); - const readers = yield* selectTyped((s: RootState) => s.reader.readers); + if ( + !winBound + || winBoundAlreadyTaken + ) { - for (const reader of Object.values(readers)) { - if (reader.publicationIdentifier === publicationIdentifier) { - yield call(() => closeReader(reader, false)); - } + if (readerArray.length) { + + const displayArea = yield* callTyped(() => screen.getPrimaryDisplay().workAreaSize); + + const winBoundWithOffset = winBoundArray.map( + (reader) => { + reader.x += 100; + reader.x %= displayArea.width - reader.width; + reader.y += 100; + reader.y %= displayArea.height - reader.height; + + return reader; + }, + ); + + winBound = ramda.uniq(winBoundWithOffset)[0]; + + } else { + winBound = library.windowBound; } } + + return winBound; } -function* closeReader(reader: Reader, gotoLibrary: boolean) { - const publicationIdentifier = reader.publicationIdentifier; +function* readerOpenRequest(action: readerActions.openRequest.TAction) { - // Notify the streamer that a publication has been closed - yield put(streamerActions.publicationCloseRequest.build(publicationIdentifier)); + debug(`readerOpenRequest:action:${JSON.stringify(action)}`); - // Wait for the publication to be closed - const streamerAction = yield take([ - streamerActions.publicationCloseSuccess.ID, - streamerActions.publicationCloseError.ID, - ]); - const typedAction = streamerAction.error ? - streamerAction as streamerActions.publicationCloseError.TAction : - streamerAction as streamerActions.publicationCloseSuccess.TAction; + const publicationIdentifier = action.payload.publicationIdentifier; - if (typedAction.error) { - // Failed to close publication - yield put(readerActions.closeError.build(reader)); - return; - } + let manifestUrl: string; + try { + manifestUrl = yield* callTyped(streamerOpenPublicationAndReturnManifestUrl, publicationIdentifier); - const winRegistry = diMainGet("win-registry"); + } catch (e) { - if (gotoLibrary) { - const libraryAppWindow = winRegistry.getLibraryWindow(); - if (libraryAppWindow) { - yield call(async () => { - libraryAppWindow.browserWindow.setBounds(await getWindowBounds(AppWindowType.Library)); - }); - if (libraryAppWindow.browserWindow.isMinimized()) { - libraryAppWindow.browserWindow.restore(); - } - libraryAppWindow.browserWindow.show(); // focuses as well + const translator = yield* callTyped( + () => diMainGet("translator")); + + if (types.isNativeError(e)) { + // disable "Error: " + e.name = ""; } - } - const readerWindow = winRegistry.getWindowByIdentifier(reader.identifier); - if (readerWindow) { - readerWindow.browserWindow.close(); + yield put( + toastActions.openRequest.build( + ToastType.Error, + translator.translate("message.open.error", {err: e.toString()}), + ), + ); } - yield put(readerActions.closeSuccess.build(reader)); -} + if (manifestUrl) { -const READER_CONFIG_ID = "reader"; + const reduxState = yield* selectTyped( + (state: RootState) => + state.win.registry.reader[publicationIdentifier]?.reduxState || {} as IReaderStateReader, + ); -export function* readerConfigSetRequestWatcher(): SagaIterator { - while (true) { - // Wait for save request - const action = yield* takeTyped(readerActions.configSetRequest.build); + const sessionIsEnabled = yield* selectTyped( + (state: RootState) => state.session.state, + ); + if (!sessionIsEnabled) { + const reduxDefaultConfig = yield* selectTyped( + (state: RootState) => state.reader.defaultConfig, + ); + reduxState.config = reduxDefaultConfig; + } - const configValue = action.payload.config; - const config: Omit, keyof Timestampable> = { - identifier: READER_CONFIG_ID, - value: configValue, - }; + const winBound = yield* callTyped(getWinBound, publicationIdentifier); - // Get reader settings - const configRepository: ConfigRepository = diMainGet("config-repository"); + // const readers = yield* selectTyped( + // (state: RootState) => state.win.session.reader, + // ); + // const readersArray = ObjectValues(readers); - try { - yield call(() => configRepository.save(config)); - yield put(readerActions.configSetSuccess.build(configValue)); - } catch (error) { - yield put(readerActions.configSetError.build(error)); + const mode = yield* selectTyped((state: RootState) => state.mode); + if (mode === ReaderMode.Attached) { + try { + getLibraryWindowFromDi().hide(); + } catch (_err) { + debug("library can't be loaded from di"); + } } + + yield put(winActions.reader.openRequest.build( + publicationIdentifier, + manifestUrl, + winBound, + reduxState, + )); + } } -export function* readerConfigInitWatcher(): SagaIterator { - // Wait for app initialization - yield take(appActions.initSuccess.ID); +function* readerCloseRequestFromPublication(action: readerActions.closeRequestFromPublication.TAction) { - const configRepository: ConfigRepository = diMainGet("config-repository"); + const readers = yield* selectTyped((state: RootState) => state.win.session.reader); - try { - const readerConfigDoc = yield* callTyped(() => configRepository.get(READER_CONFIG_ID)); - - // Returns the first reader configuration available in database - yield put(readerActions.configSetSuccess.build(readerConfigDoc.value)); - } catch (error) { - yield put(readerActions.configSetError.build(error)); + for (const key in readers) { + if (readers[key]?.publicationIdentifier === action.payload.publicationIdentifier) { + yield call(readerCloseRequest, readers[key].identifier); + } } } -// export function* readerBookmarkSaveRequestWatcher(): SagaIterator { -// while (true) { -// // Wait for app initialization -// // tslint:disable-next-line: max-line-length -// const action = yield* takeTyped(readerActions.saveBookmarkRequest.build); +function* readerCLoseRequestFromIdentifier(action: readerActions.closeRequest.TAction) { -// const bookmark = action.payload.bookmark; + yield call(readerCloseRequest, action.sender.identifier); -// // Get bookmark manager -// const locatorRepository = diMainGet("locator-repository"); + const libWin = getLibraryWindowFromDi(); + if (libWin) { -// try { -// const locator: ExcludeTimestampableWithPartialIdentifiable = { -// // name: "", -// locator: { -// href: bookmark.docHref, -// locations: { -// cssSelector: bookmark.docSelector, -// }, -// }, -// publicationIdentifier: bookmark.publicationIdentifier, -// locatorType: LocatorType.LastReadingLocation, -// }; -// yield call(() => locatorRepository.save(locator)); -// yield put(readerActions.saveBookmarkSuccess.build(bookmark)); -// } catch (error) { -// yield put(readerActions.saveBookmarkError.build(error)); -// } -// } -// } - -export function* readerFullscreenRequestWatcher(): SagaIterator { - while (true) { - // Wait for app initialization - const action = yield* takeTyped(readerActions.fullScreenRequest.build); - - // Get browser window - const sender = action.sender; + const winBound = yield* selectTyped( + (state: RootState) => state.win.session.library.windowBound, + ); + libWin.setBounds(winBound); - const winRegistry = diMainGet("win-registry"); - const appWindow = winRegistry.getWindowByIdentifier(sender.winId); - if (appWindow) { - appWindow.browserWindow.setFullScreen(action.payload.full); + if (libWin.isMinimized()) { + libWin.restore(); } + + libWin.show(); // focuses as well + } else { + debug("no library windows found in readerCLoseRequestFromIdentifier function !"); } } -export function* readerDetachRequestWatcher(): SagaIterator { - while (true) { - // Wait for a change mode request - const action = yield* takeTyped(readerActions.detachModeRequest.build); - - const readerMode = action.payload.mode; - const reader = action.payload.reader; +function* readerCloseRequest(identifier?: string) { - if (readerMode === ReaderMode.Detached) { - const winRegistry = diMainGet("win-registry"); + const readers = yield* selectTyped((state: RootState) => state.win.session.reader); - const libraryAppWindow = winRegistry.getLibraryWindow(); - if (libraryAppWindow) { - // this should never occur, but let's do it for certainty - if (libraryAppWindow.browserWindow.isMinimized()) { - libraryAppWindow.browserWindow.restore(); - } - libraryAppWindow.browserWindow.show(); // focuses as well - } + for (const key in readers) { + if (identifier && readers[key]?.identifier === identifier) { + // Notify the streamer that a publication has been closed + yield put( + streamerActions.publicationCloseRequest.build( + readers[key].publicationIdentifier, + )); + } + } - const readerWindow = winRegistry.getWindowByIdentifier(reader.identifier); - if (readerWindow) { - readerWindow.onWindowMoveResize.detach(); + const streamerAction = yield take([ + streamerActions.publicationCloseSuccess.ID, + streamerActions.publicationCloseError.ID, + ]); + const typedAction = streamerAction.error ? + streamerAction as streamerActions.publicationCloseError.TAction : + streamerAction as streamerActions.publicationCloseSuccess.TAction; - // this should never occur, but let's do it for certainty - if (readerWindow.browserWindow.isMinimized()) { - readerWindow.browserWindow.restore(); - } - readerWindow.browserWindow.show(); // focuses as well - } - } + if (typedAction.error) { + // Failed to close publication + yield put(readerActions.closeError.build(identifier)); + return; + } - yield put(readerActions.detachModeSuccess.build(readerMode)); + const readerWindow = getReaderWindowFromDi(identifier); + if (readerWindow) { + readerWindow.close(); } + +} + +function* readerSetReduxState(action: readerActions.setReduxState.TAction) { + + const { identifier, reduxState } = action.payload; + yield put(winActions.session.setReduxState.build(identifier, reduxState)); } -export function* watchers() { - yield all([ - // call(readerBookmarkSaveRequestWatcher), - call(readerCloseRequestWatcher), - call(readerConfigInitWatcher), - call(readerConfigSetRequestWatcher), - call(readerOpenRequestWatcher), - call(readerFullscreenRequestWatcher), - call(readerDetachRequestWatcher), - call(closeReaderFromPublicationWatcher), +export function saga() { + return all([ + takeSpawnEvery( + readerActions.closeRequestFromPublication.ID, + readerCloseRequestFromPublication, + (e) => error(filename_ + ":readerCloseRequestFromPublication", e), + ), + takeSpawnEvery( + readerActions.closeRequest.ID, + readerCLoseRequestFromIdentifier, + (e) => error(filename_ + ":readerCLoseRequestFromIdentifier", e), + ), + takeSpawnEvery( + readerActions.openRequest.ID, + readerOpenRequest, + (e) => error(filename_ + ":readerOpenRequest", e), + ), + takeSpawnLeading( + readerActions.detachModeRequest.ID, + readerDetachRequest, + (e) => error(filename_ + ":readerDetachRequest", e), + ), + takeSpawnLeading( + readerActions.fullScreenRequest.ID, + readerFullscreenRequest, + (e) => error(filename_ + ":readerFullscreenRequest", e), + ), + takeSpawnEvery( + readerActions.setReduxState.ID, + readerSetReduxState, + (e) => error(filename_ + ":readerSetReduxState", e), + ), ]); } diff --git a/src/main/redux/sagas/streamer.ts b/src/main/redux/sagas/streamer.ts index c9133f8cc..b73e45d35 100644 --- a/src/main/redux/sagas/streamer.ts +++ b/src/main/redux/sagas/streamer.ts @@ -7,329 +7,117 @@ import * as debug_ from "debug"; import * as portfinder from "portfinder"; -import { StreamerStatus } from "readium-desktop/common/models/streamer"; -import { ToastType } from "readium-desktop/common/models/toast"; -import { lcpActions, toastActions } from "readium-desktop/common/redux/actions/"; -import { callTyped, selectTyped, takeTyped } from "readium-desktop/common/redux/typed-saga"; -import { PublicationDocument } from "readium-desktop/main/db/document/publication"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; import { diMainGet } from "readium-desktop/main/di"; +import { error } from "readium-desktop/main/error"; import { streamerActions } from "readium-desktop/main/redux/actions"; import { RootState } from "readium-desktop/main/redux/states"; import { SagaIterator } from "redux-saga"; -import { all, call, put, take } from "redux-saga/effects"; +import { all, call, put } from "redux-saga/effects"; -import { StatusEnum } from "@r2-lcp-js/parser/epub/lsd"; -import { Publication as R2Publication } from "@r2-shared-js/models/publication"; import { Server } from "@r2-streamer-js/http/server"; // Logger -const debug = debug_("readium-desktop:main:redux:sagas:streamer"); +const filename_ = "readium-desktop:main:redux:sagas:streamer"; +const debug = debug_(filename_); async function startStreamer(streamer: Server): Promise { // Find a free port on your local machine - return portfinder.getPortPromise() - .then(async (port) => { - // HTTPS, see secureSessions() - await streamer.start(port, true); + const port = await portfinder.getPortPromise(); + // HTTPS, see secureSessions() + await streamer.start(port, true); - const streamerUrl = streamer.serverUrl(); - debug("Streamer started on %s", streamerUrl); + const streamerUrl = streamer.serverUrl(); + debug("Streamer started on %s", streamerUrl); - return streamerUrl; - }); + return streamerUrl; } -function stopStreamer(streamer: Server) { - // Stop server - debug("Stop streamer"); - streamer.stop(); -} +function* startRequest(): SagaIterator { + const streamer = yield* callTyped(() => diMainGet("streamer")); -export function* startRequestWatcher(): SagaIterator { - while (true) { - yield take(streamerActions.startRequest.ID); - const streamer = diMainGet("streamer"); + try { - try { - const streamerUrl = yield* callTyped(() => startStreamer(streamer)); - yield put(streamerActions.startSuccess.build(streamerUrl)); - } catch (error) { - debug("Unable to start streamer"); - yield put(streamerActions.startError.build(error)); - } - } -} + const streamerUrl = yield* callTyped(() => startStreamer(streamer)); + yield put(streamerActions.startSuccess.build(streamerUrl)); + } catch (error) { -export function* stopRequestWatcher(): SagaIterator { - while (true) { - yield take(streamerActions.stopRequest.ID); - const streamer = diMainGet("streamer"); - - try { - yield call(() => stopStreamer(streamer)); - yield put(streamerActions.stopSuccess.build()); - } catch (error) { - debug("Unable to stop streamer"); - yield put(streamerActions.stopError.build(error)); - } + debug("Unable to start streamer", error); + yield put(streamerActions.startError.build(error)); } } -export function* publicationOpenRequestWatcher(): SagaIterator { - while (true) { - // tslint:disable-next-line: max-line-length - const action = yield* takeTyped(streamerActions.publicationOpenRequest.build); - - const publicationRepository = diMainGet("publication-repository"); - - // Get publication - let publicationDocument: PublicationDocument = null; - try { - // tslint:disable-next-line: max-line-length - publicationDocument = yield* callTyped(() => publicationRepository.get(action.payload.publicationIdentifier)); - } catch (error) { - yield put(streamerActions.publicationOpenError.build(error, publicationDocument)); - continue; - } - - const publicationFileLocks = yield* selectTyped((s: RootState) => s.lcp.publicationFileLocks); - - if (publicationFileLocks[action.payload.publicationIdentifier]) { - yield put(streamerActions.publicationOpenError.build(new Error(""), publicationDocument)); - continue; - } - // no need to lock here, because once the streamer server accesses the ZIP file and streams resources, - // it's like a giant no-go to inject LCP license (which is why readers are closed before LSD updates) - // also, checkPublicationLicenseUpdate() places a lock or simply skips LSD checks (see below) - // yield put(appActions.publicationFileLock.build({ [publicationDocument.identifier]: true })); - // try { - // } finally { - // yield put(appActions.publicationFileLock.build({ [publicationDocument.identifier]: false })); - // } - - const translator = diMainGet("translator"); - const lcpManager = diMainGet("lcp-manager"); - const publicationViewConverter = diMainGet("publication-view-converter"); - - if (publicationDocument.lcp) { - try { - publicationDocument = yield* callTyped( - () => lcpManager.checkPublicationLicenseUpdate(publicationDocument), - ); - } catch (error) { - debug(error); - } +function* stopRequest(): SagaIterator { - if (publicationDocument.lcp && publicationDocument.lcp.lsd && publicationDocument.lcp.lsd.lsdStatus && - publicationDocument.lcp.lsd.lsdStatus.status && - publicationDocument.lcp.lsd.lsdStatus.status !== StatusEnum.Ready && - publicationDocument.lcp.lsd.lsdStatus.status !== StatusEnum.Active) { + const streamer = yield* callTyped(() => diMainGet("streamer")); - const msg = publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Expired ? - translator.translate("publication.expiredLcp") : ( - publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Revoked ? - translator.translate("publication.revokedLcp") : ( - publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Cancelled ? - translator.translate("publication.cancelledLcp") : ( - publicationDocument.lcp.lsd.lsdStatus.status === StatusEnum.Returned ? - translator.translate("publication.returnedLcp") : - translator.translate("publication.expiredLcp") - ))); + try { - yield put(toastActions.openRequest.build(ToastType.Error, msg)); + yield call(() => streamer.stop); + yield put(streamerActions.stopSuccess.build()); + } catch (error) { - yield put(streamerActions.publicationOpenError.build(msg, publicationDocument)); - continue; - } - - // we first unlockPublication() for the transient in-memory R2Publication, - // then we have to unlockPublication() again for the streamer-hosted pub instance (see below) - try { - // TODO: improve this horrible returned union type! - const unlockPublicationRes: string | number | null | undefined = - yield* callTyped(() => lcpManager.unlockPublication(publicationDocument, undefined)); - - if (typeof unlockPublicationRes !== "undefined") { - const message = unlockPublicationRes === 11 ? - translator.translate("publication.expiredLcp") : - lcpManager.convertUnlockPublicationResultToString(unlockPublicationRes); - debug(message); - - try { - const publicationView = publicationViewConverter.convertDocumentToView(publicationDocument); - - // will call API.unlockPublicationWithPassphrase() - yield put(lcpActions.userKeyCheckRequest.build( - publicationView, - publicationView.lcp.textHint, - message, - )); + debug("Unable to stop streamer", error); + yield put(streamerActions.stopError.build(error)); + } +} - yield put(streamerActions.publicationOpenError.build(message, publicationDocument)); - continue; - } catch (error) { - debug(error); +function* publicationCloseRequest(action: streamerActions.publicationCloseRequest.TAction) { - yield put(streamerActions.publicationOpenError.build(error, publicationDocument)); - continue; - } - } - } catch (error) { - debug(error); + const pubId = action.payload.publicationIdentifier; + // will decrement on streamerActions.publicationCloseSuccess.build (see below) + const counter = yield* selectTyped((s: RootState) => s.streamer.openPublicationCounter); + const streamer = yield* callTyped(() => diMainGet("streamer")); + const pubStorage = yield* callTyped(() => diMainGet("publication-storage")); - yield put(streamerActions.publicationOpenError.build(error, publicationDocument)); - continue; - } - } + let wasKilled = false; + if (!counter.hasOwnProperty(pubId) || counter[pubId] <= 1) { + wasKilled = true; - const pubStorage = diMainGet("publication-storage"); - const epubPath = pubStorage.getPublicationEpubPath(publicationDocument.identifier); + // Remove publication from streamer because there is no more readers + // open for this publication + // Get epub file from publication + const epubPath = pubStorage.getPublicationEpubPath(pubId); // const epubPath = path.join( // pubStorage.getRootPath(), // publicationDocument.files[0].url.substr(6), // ); - debug("Open publication %s", epubPath); - - // Start streamer if it's not already started - const status = yield* selectTyped((s: RootState) => s.streamer.status); - const streamer = diMainGet("streamer"); - - if (status === StreamerStatus.Stopped) { - // Streamer is stopped, start it - yield put(streamerActions.startRequest.build()); - - // Wait for streamer - const streamerStartAction = yield take([ - streamerActions.startSuccess.ID, - streamerActions.startError.ID, - ]); - const typedAction = streamerStartAction.error ? - streamerStartAction as streamerActions.startSuccess.TAction : - streamerStartAction as streamerActions.startError.TAction; - - if (typedAction.error) { - // Unable to start server - yield put(streamerActions.publicationOpenError.build(typedAction.payload, publicationDocument)); - continue; - } - } - - const manifestPaths = streamer.addPublications([epubPath]); - - let r2Publication: R2Publication; - try { - r2Publication = yield* callTyped( - () => streamer.loadOrGetCachedPublication(epubPath), - ); - } catch (error) { - yield put(streamerActions.publicationOpenError.build(error, publicationDocument)); - continue; - } - - // we unlockPublication() again because previously only done on transient in-memory R2Publication, - // (see above), has not been done yet on streamer-hosted publication instance. - // Consequently, unlockPublicationRes should always be undefined (valid passphrase already obtained) - if (r2Publication.LCP) { - try { - // TODO: improve this horrible returned union type! - const unlockPublicationRes: string | number | null | undefined = - yield* callTyped(() => lcpManager.unlockPublication(publicationDocument, undefined)); - - if (typeof unlockPublicationRes !== "undefined") { - const message = unlockPublicationRes === 11 ? - translator.translate("publication.expiredLcp") : - lcpManager.convertUnlockPublicationResultToString(unlockPublicationRes); - debug(message); - - try { - const publicationView = publicationViewConverter.convertDocumentToView(publicationDocument); - - // will call API.unlockPublicationWithPassphrase() - yield put(lcpActions.userKeyCheckRequest.build( - publicationView, - r2Publication.LCP.Encryption.UserKey.TextHint, - message, - )); - - yield put(streamerActions.publicationOpenError.build(message, publicationDocument)); - continue; - } catch (error) { - debug(error); - - yield put(streamerActions.publicationOpenError.build(error, publicationDocument)); - continue; - } - } - } catch (error) { - debug(error); - - yield put(streamerActions.publicationOpenError.build(error, publicationDocument)); - continue; - } - } - - const manifestUrl = streamer.serverUrl() + manifestPaths[0]; - debug(manifestUrl); - yield put(streamerActions.publicationOpenSuccess.build(publicationDocument, manifestUrl)); + debug(`EPUB ZIP CLEANUP: ${epubPath}`); + streamer.removePublications([epubPath]); } -} - -export function* publicationCloseRequestWatcher(): SagaIterator { - while (true) { - // tslint:disable-next-line: max-line-length - const action = yield* takeTyped(streamerActions.publicationCloseRequest.build); - - const publicationRepository = diMainGet("publication-repository"); - - let publicationDocument: PublicationDocument = null; - try { - publicationDocument = - yield* callTyped(() => publicationRepository.get(action.payload.publicationIdentifier)); - } catch (error) { - continue; - } - - // will decrement on streamerActions.publicationCloseSuccess.build (see below) - const counter = yield* selectTyped((s: RootState) => s.streamer.openPublicationCounter); - const streamer = diMainGet("streamer"); - const pubStorage = diMainGet("publication-storage"); - let wasKilled = false; - if (!counter.hasOwnProperty(publicationDocument.identifier) || counter[publicationDocument.identifier] <= 1) { - wasKilled = true; - - // Remove publication from streamer because there is no more readers - // open for this publication - // Get epub file from publication - const epubPath = pubStorage.getPublicationEpubPath(publicationDocument.identifier); - // const epubPath = path.join( - // pubStorage.getRootPath(), - // publicationDocument.files[0].url.substr(6), - // ); - debug(`EPUB ZIP CLEANUP: ${epubPath}`); - streamer.removePublications([epubPath]); - } - - const pubIds = Object.keys(counter); - let l = pubIds.length; - if (wasKilled && pubIds.includes(publicationDocument.identifier)) { - l--; - } - if (l <= 0) { - debug("STREAMER SHUTDOWN"); - yield put(streamerActions.stopRequest.build()); - } - - // will decrement state.streamer.openPublicationCounter - yield put(streamerActions.publicationCloseSuccess.build(publicationDocument)); + const pubIds = Object.keys(counter); + let l = pubIds.length; + if (wasKilled && pubIds.includes(pubId)) { + l--; } + if (l <= 0) { + debug("STREAMER SHUTDOWN"); + yield put(streamerActions.stopRequest.build()); + } + + // will decrement state.streamer.openPublicationCounter + yield put(streamerActions.publicationCloseSuccess.build(pubId)); } -export function* watchers() { - yield all([ - call(stopRequestWatcher), - call(startRequestWatcher), - call(publicationOpenRequestWatcher), - call(publicationCloseRequestWatcher), +export function saga() { + return all([ + takeSpawnEvery( + streamerActions.publicationCloseRequest.ID, + publicationCloseRequest, + (e) => error(`${filename_}:publicationCloseRequest`, e), + ), + takeSpawnLeading( + streamerActions.startRequest.ID, + startRequest, + (e) => error(`${filename_}:startRequest`, e), + ), + takeSpawnLeading( + streamerActions.startRequest.ID, + stopRequest, + (e) => error(`${filename_}:stopRequest`, e), + ), ]); } diff --git a/src/main/createWindow.ts b/src/main/redux/sagas/win/browserWindow/createLibraryWindow.ts similarity index 60% rename from src/main/createWindow.ts rename to src/main/redux/sagas/win/browserWindow/createLibraryWindow.ts index ed820b4d3..6da05b492 100644 --- a/src/main/createWindow.ts +++ b/src/main/redux/sagas/win/browserWindow/createLibraryWindow.ts @@ -6,28 +6,39 @@ // ==LICENSE-END= import * as debug_ from "debug"; -import { app, BrowserWindow, Event, Menu, shell } from "electron"; +import { BrowserWindow, Event, Menu, shell } from "electron"; import * as path from "path"; -import { AppWindowType } from "readium-desktop/common/models/win"; -import { getWindowBounds } from "readium-desktop/common/rectangle/window"; -import { diMainGet } from "readium-desktop/main/di"; +import { defaultRectangle } from "readium-desktop/common/rectangle/window"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { diMainGet, saveLibraryWindowInDi } from "readium-desktop/main/di"; +import { setMenu } from "readium-desktop/main/menu"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { RootState } from "readium-desktop/main/redux/states"; import { _PACKAGING, _RENDERER_LIBRARY_BASE_URL, _VSCODE_LAUNCH, IS_DEV, } from "readium-desktop/preprocessor-directives"; - -import { setMenu } from "./menu"; +import { ObjectValues } from "readium-desktop/utils/object-keys-values"; +import { put } from "redux-saga/effects"; // Logger -const debug = debug_("readium-desktop:createWindow"); +const debug = debug_("readium-desktop:createLibraryWindow"); // Global reference to the main window, // so the garbage collector doesn't close it. -let mainWindow: BrowserWindow = null; +let libWindow: BrowserWindow = null; // Opens the main window, with a native menu bar. -export async function createWindow() { - mainWindow = new BrowserWindow({ - ...(await getWindowBounds()), +export function* createLibraryWindow(_action: winActions.library.openRequest.TAction) { + + // initial state apply in reducers + let windowBound = yield* selectTyped( + (state: RootState) => state.win.session.library.windowBound); + if (!windowBound) { + windowBound = defaultRectangle(); + } + + libWindow = new BrowserWindow({ + ...windowBound, minWidth: 800, minHeight: 600, webPreferences: { @@ -41,7 +52,7 @@ export async function createWindow() { }); if (IS_DEV) { - const wc = mainWindow.webContents; + const wc = libWindow.webContents; wc.on("context-menu", (_ev, params) => { const { x, y } = params; const openDevToolsAndInspect = () => { @@ -81,10 +92,10 @@ export async function createWindow() { } }, label: "Inspect element", - }]).popup({window: mainWindow}); + }]).popup({window: libWindow}); }); - mainWindow.webContents.on("did-finish-load", () => { + libWindow.webContents.on("did-finish-load", () => { const { default: installExtension, REACT_DEVELOPER_TOOLS, @@ -96,25 +107,37 @@ export async function createWindow() { .then((name: string) => debug("Added Extension: ", name)) .catch((err: Error) => debug("An error occurred: ", err)); }); + + // the dispatching of 'openSuccess' action must be in the 'did-finish-load' event + // because webpack-dev-server automaticaly refresh the window. + const store = diMainGet("store"); + const identifier = store.getState().win.session.library.identifier; + store.dispatch(winActions.library.openSucess.build(libWindow, identifier)); + }); if (_VSCODE_LAUNCH !== "true") { setTimeout(() => { - if (!mainWindow.isDestroyed()) { - mainWindow.webContents.openDevTools({ activate: true, mode: "detach" }); + if (!libWindow.isDestroyed()) { + libWindow.webContents.openDevTools({ activate: true, mode: "detach" }); } }, 2000); } } - const winRegistry = diMainGet("win-registry"); - const appWindow = winRegistry.registerWindow(mainWindow, AppWindowType.Library); + yield put(winActions.session.registerLibrary.build(libWindow, windowBound)); - // watch to record window rectangle position in the db - appWindow.onWindowMoveResize.attach(); + yield* callTyped(() => saveLibraryWindowInDi(libWindow)); - let rendererBaseUrl = _RENDERER_LIBRARY_BASE_URL; + const readers = yield* selectTyped( + (state: RootState) => state.win.session.reader, + ); + const readersArray = ObjectValues(readers); + if (readersArray.length === 1) { + libWindow.hide(); + } + let rendererBaseUrl = _RENDERER_LIBRARY_BASE_URL; if (rendererBaseUrl === "file://") { // dist/prod mode (without WebPack HMR Hot Module Reload HTTP server) rendererBaseUrl += path.normalize(path.join(__dirname, "index_library.html")); @@ -122,14 +145,23 @@ export async function createWindow() { // dev/debug mode (with WebPack HMR Hot Module Reload HTTP server) rendererBaseUrl += "index_library.html"; } - rendererBaseUrl = rendererBaseUrl.replace(/\\/g, "/"); - setMenu(mainWindow, false); + yield* callTyped(() => libWindow.loadURL(rendererBaseUrl)); + // the promise will resolve when the page has finished loading (see did-finish-load) + // and rejects if the page fails to load (see did-fail-load). + + if (!IS_DEV) { + // see 'did-finish-load' otherwise + const identifier = yield* selectTyped((state: RootState) => state.win.session.library.identifier); + yield put(winActions.library.openSucess.build(libWindow, identifier)); + } + + setMenu(libWindow, false); // Redirect link to an external browser const handleRedirect = async (event: Event, url: string) => { - if (url === mainWindow.webContents.getURL()) { + if (url === libWindow.webContents.getURL()) { return; } @@ -137,28 +169,11 @@ export async function createWindow() { await shell.openExternal(url); }; - mainWindow.webContents.on("will-navigate", handleRedirect); - mainWindow.webContents.on("new-window", handleRedirect); + libWindow.webContents.on("will-navigate", handleRedirect); + libWindow.webContents.on("new-window", handleRedirect); // Clear all cache to prevent weird behaviours // Fully handled in r2-navigator-js initSessions(); // (including exit cleanup) - // mainWindow.webContents.session.clearStorageData(); - - mainWindow.on("closed", () => { - // note that winRegistry still contains a reference to mainWindow, so won't necessarily be garbage-collected - mainWindow = null; - }); - - process.nextTick(async () => { - await mainWindow.loadURL(rendererBaseUrl); - }); + // libWindow.webContents.session.clearStorageData(); } - -// On OS X it's common to re-create a window in the app when the dock icon is clicked and there are no other -// windows open. -app.on("activate", async () => { - if (mainWindow === null) { - await createWindow(); - } -}); diff --git a/src/main/redux/sagas/win/browserWindow/createReaderWindow.ts b/src/main/redux/sagas/win/browserWindow/createReaderWindow.ts new file mode 100644 index 000000000..117b579ee --- /dev/null +++ b/src/main/redux/sagas/win/browserWindow/createReaderWindow.ts @@ -0,0 +1,226 @@ +// ==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 { BrowserWindow, Menu } from "electron"; +import * as path from "path"; +import { callTyped, putTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { diMainGet, saveReaderWindowInDi } from "readium-desktop/main/di"; +import { setMenu } from "readium-desktop/main/menu"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { + _RENDERER_READER_BASE_URL, _VSCODE_LAUNCH, IS_DEV, +} from "readium-desktop/preprocessor-directives"; + +import { trackBrowserWindow } from "@r2-navigator-js/electron/main/browser-window-tracker"; + +// Logger +const debug = debug_("readium-desktop:createReaderWindow"); +debug("_"); + +export function* createReaderWindow(action: winActions.reader.openRequest.TAction) { + + const { winBound, publicationIdentifier, manifestUrl, identifier, reduxState } = action.payload; + + const readerWindow = new BrowserWindow({ + ...winBound, + minWidth: 800, + minHeight: 600, + webPreferences: { + allowRunningInsecureContent: false, + backgroundThrottling: false, + contextIsolation: false, + devTools: IS_DEV, + nodeIntegration: true, + nodeIntegrationInWorker: false, + sandbox: false, + webSecurity: true, + webviewTag: true, + }, + icon: path.join(__dirname, "assets/icons/icon.png"), + }); + + if (IS_DEV) { + const wc = readerWindow.webContents; + wc.on("context-menu", (_ev, params) => { + const { x, y } = params; + const openDevToolsAndInspect = () => { + const devToolsOpened = () => { + wc.off("devtools-opened", devToolsOpened); + wc.inspectElement(x, y); + + setTimeout(() => { + if (wc.isDevToolsOpened() && wc.devToolsWebContents) { + wc.devToolsWebContents.focus(); + } + }, 500); + }; + wc.on("devtools-opened", devToolsOpened); + wc.openDevTools({ activate: true, mode: "detach" }); + }; + Menu.buildFromTemplate([{ + click: () => { + const wasOpened = wc.isDevToolsOpened(); + if (!wasOpened) { + openDevToolsAndInspect(); + } else { + if (!wc.isDevToolsFocused()) { + // wc.toggleDevTools(); + wc.closeDevTools(); + + setImmediate(() => { + openDevToolsAndInspect(); + }); + } else { + // this should never happen, + // as the right-click context menu occurs with focus + // in BrowserWindow / WebView's WebContents + wc.inspectElement(x, y); + } + } + }, + label: "Inspect element", + }]).popup({window: readerWindow}); + }); + + } + + const pathBase64 = manifestUrl.replace(/.*\/pub\/(.*)\/manifest.json/, "$1"); + const pathDecoded = Buffer.from(decodeURIComponent(pathBase64), "base64").toString("utf8"); + + const registerReaderAction = yield* putTyped(winActions.session.registerReader.build( + readerWindow, + publicationIdentifier, + manifestUrl, + pathDecoded, + winBound, + reduxState, + identifier, + )); + + yield* callTyped(() => saveReaderWindowInDi(readerWindow, registerReaderAction.payload.identifier)); + + /* + // should be handled in library saga + const thereIsOnlyTheLibraryWindow = appWindows.length === 1; + + // Hide the only window (the library), + // as the new reader window will now take over + // (in other words: "detach" mode is disabled by default for new reader windows) + if (thereIsOnlyTheLibraryWindow) { + // Same as: appWindows[0] + const libraryAppWindow = winRegistry.getLibraryWindow(); + if (libraryAppWindow) { + libraryAppWindow.browserWindow.hide(); + } + } + + if (thereIsOnlyTheLibraryWindow) { + // onWindowMoveResize.detach() is called for reader windows that become ReaderMode.Detached + // (in which case the library window is shown again, and then its position takes precedence) + readerAppWindow.onWindowMoveResize.attach(); + } + */ + + // Track it + trackBrowserWindow(readerWindow); + + // + // TODO: + // remove query url -> sync by redux saga initialisation + // + + // FIXME : It's always required to convert the manifestUrl ? + // otherwise should be disabled in Reader.tsx + // This triggers the origin-sandbox for localStorage, etc. + // manifestUrl = convertHttpUrlToCustomScheme(manifestUrl); + + // Load publication in reader window + // const encodedManifestUrl = encodeURIComponent_RFC3986(manifestUrl); + + let readerUrl = _RENDERER_READER_BASE_URL; + + const htmlPath = "index_reader.html"; + + if (readerUrl === "file://") { + // dist/prod mode (without WebPack HMR Hot Module Reload HTTP server) + readerUrl += path.normalize(path.join(__dirname, htmlPath)); + } else { + // dev/debug mode (with WebPack HMR Hot Module Reload HTTP server) + readerUrl += htmlPath; + } + + readerUrl = readerUrl.replace(/\\/g, "/"); + /* + readerUrl += `?pub=${encodedManifestUrl}&pubId=${publicationIdentifier}`; + */ + /* + should be removed replaced with redux preloaded state + no query url be needed only redux state + + // Get publication last reading location + const locatorRepository = diMainGet("locator-repository"); + const locators = await locatorRepository + .findByPublicationIdentifierAndLocatorType( + publicationIdentifier, + LocatorType.LastReadingLocation, + ); + + if (locators.length > 0) { + const locator = locators[0]; + const docHref = encodeURIComponent_RFC3986(Buffer.from(locator.locator.href).toString("base64")); + const docSelector = + encodeURIComponent_RFC3986(Buffer.from(locator.locator.locations.cssSelector).toString("base64")); + readerUrl += `&docHref=${docHref}&docSelector=${docSelector}`; + } + */ + + yield* callTyped(() => readerWindow.webContents.loadURL(readerUrl, { extraHeaders: "pragma: no-cache\n" })); + + yield* putTyped(winActions.reader.openSucess.build(readerWindow, registerReaderAction.payload.identifier)); + if (IS_DEV) { + + readerWindow.webContents.on("did-finish-load", () => { + + // the dispatching of 'openSuccess' action must be in the 'did-finish-load' event + // because webpack-dev-server automaticaly refresh the window. + const store = diMainGet("store"); + + store.dispatch(winActions.reader.openSucess.build(readerWindow, registerReaderAction.payload.identifier)); + + }); + } + + // Already done for primary library BrowserWindow + // readerWindow.webContents.on("did-finish-load", () => { + // const { + // default: installExtension, + // REACT_DEVELOPER_TOOLS, + // REDUX_DEVTOOLS, + // } = require("electron-devtools-installer"); + + // [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS].forEach((extension) => { + // installExtension(extension) + // .then((name: string) => debug("Added Extension: ", name)) + // .catch((err: Error) => debug("An error occurred: ", err)); + // }); + // }); + + if (_VSCODE_LAUNCH !== "true") { + setTimeout(() => { + + // tslint:disable-next-line: max-line-length + // https://github.com/readium/readium-desktop/commit/c38cbd4860c84334f182d5059fb93107cd8ed709#diff-b35e0b23967fd130d41571c2e35859ff + if (!readerWindow.isDestroyed()) { + readerWindow.webContents.openDevTools({ activate: true, mode: "detach" }); + } + }, 2000); + } + + setMenu(readerWindow, true); + +} diff --git a/src/main/redux/sagas/win/index.ts b/src/main/redux/sagas/win/index.ts new file mode 100644 index 000000000..dd016533f --- /dev/null +++ b/src/main/redux/sagas/win/index.ts @@ -0,0 +1,16 @@ +// ==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 library from "./library"; +import * as reader from "./reader"; +import * as session from "./session"; + +export { + library, + reader, + session, +}; diff --git a/src/main/redux/sagas/win/library.ts b/src/main/redux/sagas/win/library.ts new file mode 100644 index 000000000..32a929a00 --- /dev/null +++ b/src/main/redux/sagas/win/library.ts @@ -0,0 +1,251 @@ +// ==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 { app, dialog } from "electron"; +import { syncIpc, winIpc } from "readium-desktop/common/ipc"; +import { i18nActions, keyboardActions } from "readium-desktop/common/redux/actions"; +import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { diMainGet, getLibraryWindowFromDi, getReaderWindowFromDi } from "readium-desktop/main/di"; +import { error } from "readium-desktop/main/error"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { RootState } from "readium-desktop/main/redux/states"; +import { ObjectValues } from "readium-desktop/utils/object-keys-values"; +import { eventChannel } from "redux-saga"; +import { all, call, delay, put, spawn, take } from "redux-saga/effects"; + +import { IWinSessionReaderState } from "../../states/win/session/reader"; +import { createLibraryWindow } from "./browserWindow/createLibraryWindow"; +import { checkReaderWindowInSession } from "./session/checkReaderWindowInSession"; + +// Logger +const filename_ = "readium-desktop:main:redux:sagas:win:library"; +const debug = debug_(filename_); +debug("_"); + +// On OS X it's common to re-create a window in the app when the dock icon is clicked and there are no other +// windows open. +function* appActivate() { + + const appActivateChannel = eventChannel( + (emit) => { + + const handler = () => emit(true); + app.on("activate", handler); + + return () => { + app.removeListener("activate", handler); + }; + }, + ); + + yield take(appActivateChannel); + + yield put(winActions.library.openRequest.build()); +} + +function* winOpen(action: winActions.library.openSucess.TAction) { + + const identifier = action.payload.identifier; + debug(`library ${identifier} -> winOpen`); + + const libWindow = action.payload.win; + const webContents = libWindow.webContents; + const state = yield* selectTyped((_state: RootState) => _state); + + // Send the id to the new window + webContents.send(winIpc.CHANNEL, { + type: winIpc.EventType.IdResponse, + payload: { + identifier, + }, + } as winIpc.EventPayload); + + // send on redux library + // TODO + // will be replaced with preloaded state injection in Redux createStore. + + // Send locale + webContents.send(syncIpc.CHANNEL, { + type: syncIpc.EventType.MainAction, + payload: { + action: i18nActions.setLocale.build(state.i18n.locale), + }, + } as syncIpc.EventPayload); + + // Send keyboard shortcuts + webContents.send(syncIpc.CHANNEL, { + type: syncIpc.EventType.MainAction, + payload: { + action: keyboardActions.setShortcuts.build(state.keyboard.shortcuts, false), + }, + } as syncIpc.EventPayload); + + // // Init network on window + // let actionNet = null; + + // switch (state.net.status) { + // case NetStatus.Online: + // actionNet = netActions.online.build(); + // break; + // case NetStatus.Offline: + // default: + // actionNet = netActions.offline.build(); + // break; + // } + + // // Send network status + // webContents.send(syncIpc.CHANNEL, { + // type: syncIpc.EventType.MainAction, + // payload: { + // action: actionNet, + // }, + // } as syncIpc.EventPayload); + + // // Send update info + // webContents.send(syncIpc.CHANNEL, { + // type: syncIpc.EventType.MainAction, + // payload: { + // action: { + // type: updateActions.latestVersion.ID, + // payload: updateActions.latestVersion.build( + // state.update.status, + // state.update.latestVersion, + // state.update.latestVersionUrl), + // }, + // }, + // } as syncIpc.EventPayload); + +} + +function* winClose(_action: winActions.library.closed.TAction) { + + debug(`library -> winClose`); + + const library = getLibraryWindowFromDi(); + let value = 0; // window.close() // not saved session by default + + { + + const readers = yield* selectTyped((state: RootState) => state.win.session.reader); + const readersArray = ObjectValues(readers); + debug("reader:", readersArray); + + if (readersArray.length) { + + const sessionIsEnabled = yield* selectTyped((state: RootState) => state.session.state); + debug(sessionIsEnabled ? "session enabled destroy reader" : "session not enabled close reader"); + if (sessionIsEnabled) { + + const messageValue = yield* callTyped( + async () => { + + const translator = diMainGet("translator"); + + return dialog.showMessageBox( + library, + { + type: "question", + buttons: [ + translator.translate("app.session.exit.askBox.button.no"), + translator.translate("app.session.exit.askBox.button.yes"), + ], + defaultId: 1, + title: translator.translate("app.session.exit.askBox.title"), + message: translator.translate("app.session.exit.askBox.message"), + }, + ); + }, + ); + debug("result:", messageValue.response); + value = messageValue.response; + } + + yield all( + readersArray.map( + (reader, index) => { + return call(function*() { + + if (!reader) { + return; + } + try { + const readerWin = yield* callTyped(() => getReaderWindowFromDi(reader.identifier)); + + if (value === 1) { + // force quit the reader windows to keep session in next startup + debug("destroy reader", index); + readerWin.destroy(); + } else { + debug("close reader", index); + readerWin.close(); + + } + } catch (_err) { + // ignore + } + + }); + }, + ), + ); + + } + } + + if (value === 1) { + + // closed the library and thorium + library.destroy(); + } else { + + yield spawn(function*() { + + let readersArray: IWinSessionReaderState[]; + + do { + yield delay(50); + const readers = yield* selectTyped((state: RootState) => state.win.session.reader); + readersArray = ObjectValues(readers); + + } while (readersArray.length); + + library.destroy(); + }); + } +} + +export function saga() { + return all([ + takeSpawnLeading( + winActions.library.openRequest.ID, + createLibraryWindow, + (e) => error(filename_ + ":createLibraryWindow", e), + ), + takeSpawnLeading( + winActions.library.openRequest.ID, + checkReaderWindowInSession, + (e) => error(filename_ + ":checkReaderWindowInSession", e), + ), + takeSpawnLeading( + winActions.library.openSucess.ID, + winOpen, + (e) => error(filename_ + ":winOpen", e), + ), + takeSpawnLeading( + winActions.library.closed.ID, + appActivate, + (e) => error(filename_ + ":appActivate", e), + ), + takeSpawnLeading( + winActions.library.closed.ID, + winClose, + (e) => error(filename_ + ":winClose", e), + ), + ]); +} diff --git a/src/main/redux/sagas/win/reader.ts b/src/main/redux/sagas/win/reader.ts new file mode 100644 index 000000000..b01a1ca7b --- /dev/null +++ b/src/main/redux/sagas/win/reader.ts @@ -0,0 +1,178 @@ +// ==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 { readerIpc } from "readium-desktop/common/ipc"; +import { ReaderMode } from "readium-desktop/common/models/reader"; +import { readerActions } from "readium-desktop/common/redux/actions"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { getLibraryWindowFromDi, getReaderWindowFromDi } from "readium-desktop/main/di"; +import { error } from "readium-desktop/main/error"; +import { streamerActions, winActions } from "readium-desktop/main/redux/actions"; +import { RootState } from "readium-desktop/main/redux/states"; +import { + _NODE_MODULE_RELATIVE_URL, _PACKAGING, _RENDERER_READER_BASE_URL, _VSCODE_LAUNCH, +} from "readium-desktop/preprocessor-directives"; +import { ObjectValues } from "readium-desktop/utils/object-keys-values"; +import { all, put } from "redux-saga/effects"; + +import { createReaderWindow } from "./browserWindow/createReaderWindow"; + +// Logger +const filename_ = "readium-desktop:main:redux:sagas:win:reader"; +const debug = debug_(filename_); +debug("_"); + +function* winOpen(action: winActions.reader.openSucess.TAction) { + + const identifier = action.payload.identifier; + debug(`reader ${identifier} -> winOpen`); + + const readerWin = action.payload.win; + const webContents = readerWin.webContents; + const locale = yield* selectTyped((_state: RootState) => _state.i18n.locale); + const reader = yield* selectTyped((_state: RootState) => _state.win.session.reader[identifier]); + const keyboard = yield* selectTyped((_state: RootState) => _state.keyboard); + + webContents.send(readerIpc.CHANNEL, { + type: readerIpc.EventType.request, + payload: { + i18n: { + locale, + }, + win: { + identifier, + }, + reader: reader?.reduxState, + keyboard, + }, + } as readerIpc.EventPayload); + + // webContents.send(syncIpc.CHANNEL, { + // type: syncIpc.EventType.MainAction, + // payload: { + // action: readerActions.openSuccess.build(state.reader.readers[winId]), + // }, + // } as syncIpc.EventPayload); + + // // Send reader config + // webContents.send(syncIpc.CHANNEL, { + // type: syncIpc.EventType.MainAction, + // payload: { + // action: readerActions.configSetSuccess.build(state.reader.config), + // }, + // } as syncIpc.EventPayload); + + // Send reader mode + // webContents.send(syncIpc.CHANNEL, { + // type: syncIpc.EventType.MainAction, + // payload: { + // action: readerActions.detachModeSuccess.build(state.reader.mode), + // }, + // } as syncIpc.EventPayload); + // send with an API Request now + // should be removed + + // webContents.send(syncIpc.CHANNEL, { + // type: syncIpc.EventType.MainAction, + // payload: { + // action: i18nActions.setLocale.build(state.i18n.locale), + // }, + // } as syncIpc.EventPayload); + +} + +function* winClose(action: winActions.reader.closed.TAction) { + + const identifier = action.payload.identifier; + debug(`reader ${identifier} -> winClose`); + + { + const readers = yield* selectTyped((state: RootState) => state.win.session.reader); + const reader = readers[identifier]; + + if (reader) { + + yield put(winActions.session.unregisterReader.build(identifier)); + + yield put(winActions.registry.registerReaderPublication.build( + reader.publicationIdentifier, + reader.windowBound, + reader.reduxState), + ); + + yield put(streamerActions.publicationCloseRequest.build(reader.publicationIdentifier)); + + // not yet used + // yield put(readerActions.closeSuccess.build(identifier)); + } + } + + { + // readers in session updated + const readers = yield* selectTyped((state: RootState) => state.win.session.reader); + const readersArray = ObjectValues(readers); + + try { + const libraryWindow = yield* callTyped(() => getLibraryWindowFromDi()); + + debug("Nb of readers:", readersArray.length); + debug("readers: ", readersArray); + if (readersArray.length < 1) { + + const mode = yield* selectTyped((state: RootState) => state.mode); + if (mode === ReaderMode.Detached) { + yield put(readerActions.attachModeRequest.build()); + + } else { + try { + const readerWin = getReaderWindowFromDi(identifier); + libraryWindow.setBounds(readerWin.getBounds()); + + } catch (_err) { + debug("can't load readerWin from di :", identifier); + } + } + } + + if (libraryWindow) { + if (libraryWindow.isMinimized()) { + libraryWindow.restore(); + } else if (!libraryWindow.isVisible()) { + libraryWindow.close(); + return; + } + libraryWindow.show(); // focuses as well + } + + } catch (_err) { + debug("can't load libraryWin from di"); + } + } + +} + +export function saga() { + return all([ + takeSpawnEvery( + winActions.reader.openRequest.ID, + createReaderWindow, + (e) => error(filename_ + ":createReaderWindow", e), + ), + takeSpawnEvery( + winActions.reader.openSucess.ID, + winOpen, + (e) => error(filename_ + ":winOpen", e), + ), + takeSpawnEvery( + winActions.reader.closed.ID, + winClose, + (e) => error(filename_ + ":winClose", e), + ), + ]); +} diff --git a/src/renderer/common/actionSerializer.ts b/src/main/redux/sagas/win/registry/reader.ts similarity index 66% rename from src/renderer/common/actionSerializer.ts rename to src/main/redux/sagas/win/registry/reader.ts index 992b42675..ab2c78090 100644 --- a/src/renderer/common/actionSerializer.ts +++ b/src/main/redux/sagas/win/registry/reader.ts @@ -1,8 +1,8 @@ +// ==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 { ActionSerializer } from "readium-desktop/common/services/serializer"; - -export const actionSerializer = new ActionSerializer(); +// nothing diff --git a/src/main/redux/sagas/win/session/checkReaderWindowInSession.ts b/src/main/redux/sagas/win/session/checkReaderWindowInSession.ts new file mode 100644 index 000000000..d1a6fb21d --- /dev/null +++ b/src/main/redux/sagas/win/session/checkReaderWindowInSession.ts @@ -0,0 +1,94 @@ +// ==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 { readerActions } from "readium-desktop/common/redux/actions"; +import { callTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { RootState } from "readium-desktop/main/redux/states"; +import { IWinSessionReaderState } from "readium-desktop/main/redux/states/win/session/reader"; +import { ObjectValues } from "readium-desktop/utils/object-keys-values"; +import { fork, put } from "redux-saga/effects"; + +import { streamerOpenPublicationAndReturnManifestUrl } from "../../publication/openPublication"; + +// Logger +const debug = debug_("readium-desktop:main:saga:checkReaderWindowSession"); + +function* openReaderFromPreviousSession(reader: IWinSessionReaderState, nbOfReaderInSession: number) { + + const publicationIdentifier = reader.publicationIdentifier; + let manifestUrl: string; + try { + manifestUrl = yield* callTyped(streamerOpenPublicationAndReturnManifestUrl, publicationIdentifier); + } catch (err) { + debug("ERROR to open a publication from previous session:", reader.identifier, err); + + yield put(winActions.session.unregisterReader.build(reader.identifier)); + yield put(winActions.registry.registerReaderPublication.build( + reader.publicationIdentifier, + reader.windowBound, + reader.reduxState), + ); + } + + if (manifestUrl) { + + let bound = reader.windowBound; + + if (nbOfReaderInSession > 1) { + yield put(readerActions.detachModeRequest.build()); + } else { + bound = yield* selectTyped((state: RootState) => state.win.session.library.windowBound); + } + + yield put(winActions.reader.openRequest.build( + publicationIdentifier, + manifestUrl, + bound, + reader.reduxState, + reader.identifier, + )); + } +} + +/** + * On library open request action, dispatch the opening of all readers in session + */ +export function* checkReaderWindowInSession(_action: winActions.library.openRequest.TAction) { + + // should be readers identifier from session and not open a new reader with this publicationIdentifier + // ex: if 2 readers with the same pubId was previously opened, + // at the next starting they need to deshydrate data from session and not from pubId registry + + const readers = yield* selectTyped( + (state: RootState) => state.win.session.reader, + ); + const readersArray = ObjectValues(readers); + + debug("checkReaderWindowInSession:", "nb of readers in session:", readersArray.length); + + if (readersArray.length > 10) { + for (const reader of readersArray) { + yield put(winActions.session.unregisterReader.build(reader.identifier)); + yield put(winActions.registry.registerReaderPublication.build( + reader.publicationIdentifier, + reader.windowBound, + reader.reduxState), + ); + } + } else { + + for (const reader of readersArray) { + yield fork(openReaderFromPreviousSession, reader, readersArray.length); + } + } + + // TODO + // Display an infoBox too many reader opened previously + +} diff --git a/src/main/redux/sagas/win/session/index.ts b/src/main/redux/sagas/win/session/index.ts new file mode 100644 index 000000000..6b05bfa3f --- /dev/null +++ b/src/main/redux/sagas/win/session/index.ts @@ -0,0 +1,16 @@ +// ==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 { checkReaderWindowInSession } from "./checkReaderWindowInSession"; +import * as library from "./library"; +import * as reader from "./reader"; + +export { + library, + reader, + checkReaderWindowInSession, +}; diff --git a/src/main/redux/sagas/win/session/library.ts b/src/main/redux/sagas/win/session/library.ts new file mode 100644 index 000000000..691166ce8 --- /dev/null +++ b/src/main/redux/sagas/win/session/library.ts @@ -0,0 +1,89 @@ +// ==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 { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { error } from "readium-desktop/main/error"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { eventChannel, Task } from "redux-saga"; +import { cancel, debounce, fork, put, take } from "redux-saga/effects"; + +// Logger +const filename_ = "readium-desktop:main:redux:sagas:win:session:library"; +const debug = debug_(filename_); +debug("_"); + +function* libraryClosureManagement(action: winActions.session.registerLibrary.TAction) { + + const moveOrResizeTask: Task = yield fork(libraryMoveOrResizeObserver, action); + + const library = action.payload.win; + const channel = eventChannel( + (emit) => { + + const handler = (event: Electron.Event) => { + event.preventDefault(); + emit(true); + }; + library.on("close", handler); + + return () => { + library.removeListener("close", handler); + }; + }, + ); + + // waiting for library window to close + yield take(channel); + + // cancel moveAndResizeObserver + yield cancel(moveOrResizeTask); + + // dispatch closure action + yield put(winActions.session.unregisterLibrary.build()); + yield put(winActions.library.closed.build()); +} + +function* libraryMoveOrResizeObserver(action: winActions.session.registerLibrary.TAction) { + + const library = action.payload.win; + const id = action.payload.identifier; + const DEBOUNCE_TIME = 500; + + const channel = eventChannel( + (emit) => { + + const handler = () => emit(true); + + library.on("move", handler); + library.on("resize", handler); + + return () => { + library.removeListener("move", handler); + library.removeListener("resize", handler); + }; + }, + ); + + yield debounce(DEBOUNCE_TIME, channel, function*() { + + try { + const winBound = library.getBounds(); + yield put(winActions.session.setBound.build(id, winBound)); + } catch (e) { + debug("set library bound error", e); + } + }); +} + +export function saga() { + return takeSpawnLeading( + winActions.session.registerLibrary.ID, + libraryClosureManagement, + (e) => error(filename_ + ":libraryClosureManagement", e), + ); +} diff --git a/src/main/redux/sagas/win/session/reader.ts b/src/main/redux/sagas/win/session/reader.ts new file mode 100644 index 000000000..fe436714b --- /dev/null +++ b/src/main/redux/sagas/win/session/reader.ts @@ -0,0 +1,85 @@ +// ==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 { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { error } from "readium-desktop/main/error"; +import { winActions } from "readium-desktop/main/redux/actions"; +import { eventChannel, Task } from "redux-saga"; +import { cancel, debounce, fork, put, take } from "redux-saga/effects"; + +// Logger +const filename_ = "readium-desktop:main:redux:sagas:win:session:reader"; +const debug = debug_(filename_); + +function* readerClosureManagement(action: winActions.session.registerReader.TAction) { + + const moveOrResizeTask: Task = yield fork(readerMoveOrResizeObserver, action); + + const readerWindow = action.payload.win; + const identifier = action.payload.identifier; + const channel = eventChannel( + (emit) => { + + const handler = () => emit(true); + readerWindow.on("close", handler); + + return () => { + readerWindow.removeListener("close", handler); + }; + }, + ); + + // waiting for reader window to close + yield take(channel); + + // cancel moveAndResizeObserver + yield cancel(moveOrResizeTask); + + debug("event close requested -> emit unregisterReader and closed"); + yield put(winActions.reader.closed.build(identifier)); +} + +function* readerMoveOrResizeObserver(action: winActions.session.registerReader.TAction) { + + const reader = action.payload.win; + const id = action.payload.identifier; + const DEBOUNCE_TIME = 500; + + const channel = eventChannel( + (emit) => { + + const handler = () => emit(true); + + reader.on("move", handler); + reader.on("resize", handler); + + return () => { + reader.removeListener("move", handler); + reader.removeListener("resize", handler); + }; + }, + ); + + yield debounce(DEBOUNCE_TIME, channel, function*() { + + try { + const winBound = reader.getBounds(); + yield put(winActions.session.setBound.build(id, winBound)); + } catch (e) { + debug("set reader bound error", id, e); + } + }); +} + +export function saga() { + return takeSpawnEvery( + winActions.session.registerReader.ID, + readerClosureManagement, + (e) => error(filename_ + ":readerClosureManagement", e), + ); +} diff --git a/src/main/redux/states/index.ts b/src/main/redux/states/index.ts index 744b1d37b..72c49575b 100644 --- a/src/main/redux/states/index.ts +++ b/src/main/redux/states/index.ts @@ -5,23 +5,44 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import { ReaderConfig, ReaderMode } from "readium-desktop/common/models/reader"; import { I18NState } from "readium-desktop/common/redux/states/i18n"; import { KeyboardState } from "readium-desktop/common/redux/states/keyboard"; +import { TPQueueState } from "readium-desktop/utils/redux-reducers/pqueue.reducer"; // import { NetState } from "readium-desktop/common/redux/states/net"; // import { UpdateState } from "readium-desktop/common/redux/states/update"; import { AppState } from "./app"; import { ILcpState } from "./lcp"; -import { ReaderState } from "./reader"; +import { ISessionState } from "./session"; import { StreamerState } from "./streamer"; +import { IDictWinRegistryReaderState } from "./win/registry/reader"; +import { IWinSessionLibraryState } from "./win/session/library"; +import { IDictWinSessionReaderState } from "./win/session/reader"; export interface RootState { + session: ISessionState; app: AppState; // net: NetState; i18n: I18NState; streamer: StreamerState; - reader: ReaderState; + reader: { + defaultConfig: ReaderConfig, + }; // update: UpdateState; + win: { + session: { + library: IWinSessionLibraryState, + reader: IDictWinSessionReaderState, + }, + registry: { + reader: IDictWinRegistryReaderState, + }, + }; + mode: ReaderMode; lcp: ILcpState; + publication: { + lastReadingQueue: TPQueueState; + }; keyboard: KeyboardState; } diff --git a/src/main/redux/states/session.ts b/src/main/redux/states/session.ts new file mode 100644 index 000000000..b1eb1bafc --- /dev/null +++ b/src/main/redux/states/session.ts @@ -0,0 +1,10 @@ +// ==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== + +export interface ISessionState { + state: boolean; +} diff --git a/src/main/redux/states/win/common.ts b/src/main/redux/states/win/common.ts new file mode 100644 index 000000000..e58c80de8 --- /dev/null +++ b/src/main/redux/states/win/common.ts @@ -0,0 +1,19 @@ +// ==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 { Rectangle } from "electron"; +import { + IReaderStateReader, +} from "readium-desktop/common/redux/states/renderer/readerRootState"; + +export interface IWinWindowBoundState { + windowBound: Rectangle; +} + +export interface IWinReaderReduxState { + reduxState: IReaderStateReader; +} diff --git a/src/main/redux/states/win/registry/reader.ts b/src/main/redux/states/win/registry/reader.ts new file mode 100644 index 000000000..f59ed41da --- /dev/null +++ b/src/main/redux/states/win/registry/reader.ts @@ -0,0 +1,15 @@ +// ==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 { IWinReaderReduxState, IWinWindowBoundState } from "../common"; + +export interface IWinRegistryReaderState extends IWinWindowBoundState, IWinReaderReduxState { +} + +export interface IDictWinRegistryReaderState { + [publicationIdentifier: string]: IWinRegistryReaderState; +} diff --git a/src/main/redux/states/win/session/common.ts b/src/main/redux/states/win/session/common.ts new file mode 100644 index 000000000..9d8388ebe --- /dev/null +++ b/src/main/redux/states/win/session/common.ts @@ -0,0 +1,13 @@ +// ==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 { Identifiable } from "readium-desktop/common/models/identifiable"; +import { IWinWindowBoundState } from "../common"; + +export interface IBrowserWindowState extends Identifiable, IWinWindowBoundState { + browserWindowId: number; +} diff --git a/src/renderer/reader/redux/states/index.ts b/src/main/redux/states/win/session/library.ts similarity index 58% rename from src/renderer/reader/redux/states/index.ts rename to src/main/redux/states/win/session/library.ts index 07c478840..f9f0fefb8 100644 --- a/src/renderer/reader/redux/states/index.ts +++ b/src/main/redux/states/win/session/library.ts @@ -5,9 +5,8 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { ICommonRootState } from "readium-desktop/renderer/common/redux/states"; -import { ReaderState } from "readium-desktop/renderer/reader/redux/states/reader"; +import { IBrowserWindowState } from "./common"; -export interface IReaderRootState extends ICommonRootState { - reader: ReaderState; +// tslint:disable-next-line: no-empty-interface +export interface IWinSessionLibraryState extends IBrowserWindowState { } diff --git a/src/main/redux/states/win/session/reader.ts b/src/main/redux/states/win/session/reader.ts new file mode 100644 index 000000000..ff631eca0 --- /dev/null +++ b/src/main/redux/states/win/session/reader.ts @@ -0,0 +1,19 @@ +// ==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 { IWinReaderReduxState } from "../common"; +import { IBrowserWindowState } from "./common"; + +export interface IWinSessionReaderState extends IBrowserWindowState, IWinReaderReduxState { + publicationIdentifier: string; + manifestUrl: string; + fileSystemPath: string; +} + +export interface IDictWinSessionReaderState { + [id: string]: IWinSessionReaderState; +} diff --git a/src/main/redux/store/memory.ts b/src/main/redux/store/memory.ts index ea3f936f1..d8ae2335c 100644 --- a/src/main/redux/store/memory.ts +++ b/src/main/redux/store/memory.ts @@ -5,24 +5,203 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { applyMiddleware, createStore, Store } from "redux"; -import { composeWithDevTools } from "redux-devtools-extension"; -import createSagaMiddleware from "redux-saga"; - +import * as debug_ from "debug"; +import { app } from "electron"; +import * as Ramda from "ramda"; +import { LocaleConfigIdentifier, LocaleConfigValueType } from "readium-desktop/common/config"; +import { LocatorType } from "readium-desktop/common/models/locator"; +import { + LocatorExtendedWithLocatorOnly, +} from "readium-desktop/common/redux/states/locatorInitialState"; +import { readerConfigInitialState } from "readium-desktop/common/redux/states/reader"; +import { AvailableLanguages } from "readium-desktop/common/services/translator"; +import { PromiseAllSettled } from "readium-desktop/common/utils/promise"; +import { ConfigDocument } from "readium-desktop/main/db/document/config"; +import { ConfigRepository } from "readium-desktop/main/db/repository/config"; +import { CONFIGREPOSITORY_REDUX_PERSISTENCE, diMainGet } from "readium-desktop/main/di"; import { reduxSyncMiddleware } from "readium-desktop/main/redux/middleware/sync"; import { rootReducer } from "readium-desktop/main/redux/reducers"; +import { rootSaga } from "readium-desktop/main/redux/sagas"; import { RootState } from "readium-desktop/main/redux/states"; +import { ObjectKeys } from "readium-desktop/utils/object-keys-values"; +import { TPQueueState } from "readium-desktop/utils/redux-reducers/pqueue.reducer"; +import { applyMiddleware, createStore, Store } from "redux"; +import createSagaMiddleware from "redux-saga"; +import { composeWithDevTools } from "remote-redux-devtools"; -import { rootSaga } from "readium-desktop/main/redux/sagas"; +import { reduxPersistMiddleware } from "../middleware/persistence"; +import { IDictWinRegistryReaderState } from "../states/win/registry/reader"; + +// Logger +const debug = debug_("readium-desktop:main:store:memory"); + +const REDUX_REMOTE_DEVTOOLS_PORT = 7770; + +const defaultLocale = (): LocaleConfigValueType => { + const loc = app.getLocale().split("-")[0]; + const langCodes = ObjectKeys(AvailableLanguages); + const lang = langCodes.find((l) => l === loc) || "en"; + + return { + locale: lang, + }; +}; + +async function absorbLocatorRepositoryToReduxState() { + + const locatorRepository = diMainGet("locator-repository"); + const locatorFromDb = await locatorRepository.find( + { + selector: { locatorType: LocatorType.LastReadingLocation }, + sort: [{ updatedAt: "asc" }], + }, + ); + + const lastReadingQueue: TPQueueState = []; + const registryReader: IDictWinRegistryReaderState = {}; + + for (const locator of locatorFromDb) { + if (locator.publicationIdentifier) { + + lastReadingQueue.push([locator.createdAt, locator.publicationIdentifier]); + + registryReader[locator.publicationIdentifier] = { + windowBound: { + width: 0, + height: 0, + x: 0, + y: 0, + }, + reduxState: { + config: readerConfigInitialState, + locator: LocatorExtendedWithLocatorOnly(locator.locator), + info: { + publicationIdentifier: locator.publicationIdentifier, + manifestUrl: undefined, + filesystemPath: undefined, + }, + }, + }; + + // disable at the moment, beta test + // await locatorRepository.delete(locator.identifier); + } + } + + if (lastReadingQueue.length === 0 && ObjectKeys(registryReader).length === 0) { + return undefined; + } + + return { + lastReadingQueue, + registryReader, + }; +} + +export async function initStore(configRepository: ConfigRepository): Promise> { + + let reduxStateWinRepository: ConfigDocument>; + let i18nStateRepository: ConfigDocument; + + try { + const reduxStateRepositoryPromise = configRepository.get(CONFIGREPOSITORY_REDUX_PERSISTENCE); + + const i18nStateRepositoryPromise = configRepository.get(LocaleConfigIdentifier); + + const [ + reduxStateRepositoryResult, + i18nStateRepositoryResult, + ] = await PromiseAllSettled( + [ + reduxStateRepositoryPromise, + i18nStateRepositoryPromise, + ], + ); + + if (reduxStateRepositoryResult.status === "fulfilled") { + reduxStateWinRepository = reduxStateRepositoryResult.value; + } + if (i18nStateRepositoryResult.status === "fulfilled") { + i18nStateRepository = i18nStateRepositoryResult.value; + } + } catch (err) { + // ignore + // first init + debug("ERR when get state from FS", err); + } + + let reduxStateWin = reduxStateWinRepository?.value?.win + ? reduxStateWinRepository.value + : undefined; + + const i18n = i18nStateRepository?.value?.locale + ? i18nStateRepository.value + : defaultLocale(); + + try { + // executed once time for locatorRepository to ReduxState migration + const locatorRepositoryAbsorbed = await absorbLocatorRepositoryToReduxState(); + + if (locatorRepositoryAbsorbed) { + reduxStateWin = { + ...reduxStateWin, + ...{ + publication: { + lastReadingQueue: Ramda.uniqBy( + (item) => item[1], + Ramda.concat( + reduxStateWin.publication.lastReadingQueue, + locatorRepositoryAbsorbed.lastReadingQueue, + ), + ), + }, + }, + ...{ + win: { + session: { + library: reduxStateWin.win.session.library, + reader: reduxStateWin.win.session.reader, + }, + registry: { + reader: { + ...locatorRepositoryAbsorbed.registryReader, + ...reduxStateWin.win.registry.reader, + }, + }, + }, + }, + }; + } + } catch (err) { + debug("ERR on absorbLocatorRepositoryToReduxState", err); + } + + const preloadedState = { + ...reduxStateWin, + ...{ + i18n, + }, + }; -export function initStore(): Store { const sagaMiddleware = createSagaMiddleware(); + const store = createStore( rootReducer, + preloadedState, composeWithDevTools( - applyMiddleware(reduxSyncMiddleware, sagaMiddleware), + { + port: REDUX_REMOTE_DEVTOOLS_PORT, + }, + )( + applyMiddleware( + reduxSyncMiddleware, + sagaMiddleware, + reduxPersistMiddleware, + ), ), ); + sagaMiddleware.run(rootSaga); + return store as Store; } diff --git a/src/main/services/lcp.ts b/src/main/services/lcp.ts index 5b57616d1..5c2b395ca 100644 --- a/src/main/services/lcp.ts +++ b/src/main/services/lcp.ts @@ -829,7 +829,7 @@ export class LcpManager { if (licenseUpdateJson) { let atLeastOneReaderIsOpen = false; - const readers = this.store.getState().reader?.readers; + const readers = this.store.getState().win.session.reader; if (readers) { for (const reader of Object.values(readers)) { if (reader.publicationIdentifier === publicationDocumentIdentifier) { diff --git a/src/main/services/publication.ts b/src/main/services/publication.ts index 8badb0821..2ba6364ff 100644 --- a/src/main/services/publication.ts +++ b/src/main/services/publication.ts @@ -38,13 +38,17 @@ import { TaJsonDeserialize, TaJsonSerialize } from "@r2-lcp-js/serializable"; import { OPDSPublication } from "@r2-opds-js/opds/opds2/opds2-publication"; import { PublicationParsePromise } from "@r2-shared-js/parser/publication-parser"; +import { PublicationViewConverter } from "../converter/publication"; import { getTagsFromOpdsPublication } from "../converter/tools/getTags"; import { extractCrc32OnZip } from "../crc"; +import { getLibraryWindowFromDi } from "../di"; import { lpfToAudiobookConverter } from "../lpfConverter"; +import { publicationActions } from "../redux/actions"; import { RootState } from "../redux/states"; import { Downloader } from "./downloader"; import { LcpManager } from "./lcp"; -import { WinRegistry } from "./win-registry"; + +// import { WinRegistry } from "./win-registry"; // import { IS_DEV } from "readium-desktop/preprocessor-directives"; @@ -74,8 +78,11 @@ export class PublicationService { @inject(diSymbolTable["lcp-manager"]) private readonly lcpManager!: LcpManager; - @inject(diSymbolTable["win-registry"]) - private readonly winRegistry!: WinRegistry; + @inject(diSymbolTable["publication-view-converter"]) + private readonly publicationViewConverter!: PublicationViewConverter; + + // @inject(diSymbolTable["win-registry"]) + // private readonly winRegistry!: WinRegistry; public async importEpubOrLcplFile( filePath: string, @@ -308,9 +315,44 @@ export class PublicationService { return returnPublicationDocument; } - public async deletePublication(publicationIdentifier: string) { + public async getPublication(identifier: string, checkLcpLsd: boolean = false): Promise { + + let doc: PublicationDocument; + try { + doc = await this.publicationRepository.get(identifier); + } catch (e) { + debug(`can't get ${identifier}`, e); + throw new Error(`publication not found`); // TODO translation + } + + try { + if (checkLcpLsd && doc.lcp) { + doc = await this.lcpManager.checkPublicationLicenseUpdate(doc); + } + } catch (e) { + debug(`error on checkPublicationLicenseUpdate`, e); + throw new Error(`check lcp license in publication failed`); // TODO translation + } + + try { + return this.publicationViewConverter.convertDocumentToView(doc); + } catch (e) { + debug("error on convertDocumentToView", e); + + // tslint:disable-next-line: no-floating-promises + // this.deletePublication(identifier); + + throw new Error(`${doc.title} is corrupted and should be removed`); // TODO translation + } + } + + public async deletePublication(identifier: string) { + + this.store.dispatch(readerActions.closeRequestFromPublication.build(identifier)); + + // dispatch action to update publication/lastReadingQueue reducer + this.store.dispatch(publicationActions.deletePublication.build(identifier)); - this.store.dispatch(readerActions.closeRequestFromPublication.build(publicationIdentifier)); await new Promise((res, _rej) => { setTimeout(() => { res(); @@ -318,22 +360,23 @@ export class PublicationService { }); // Remove from database - await this.publicationRepository.delete(publicationIdentifier); + await this.publicationRepository.delete(identifier); // Remove from storage - this.publicationStorage.removePublication(publicationIdentifier); + this.publicationStorage.removePublication(identifier); } public async exportPublication(publicationView: PublicationView) { - const libraryAppWindow = this.winRegistry.getLibraryWindow(); + const libraryAppWindow = getLibraryWindowFromDi(); // Open a dialog to select a folder then copy the publication in it const res = await dialog.showOpenDialog( - libraryAppWindow ? libraryAppWindow.browserWindow : undefined, + libraryAppWindow ? libraryAppWindow : undefined, { properties: ["openDirectory"], }); + if (!res.canceled) { if (res.filePaths && res.filePaths.length > 0) { let destinationPath = res.filePaths[0]; diff --git a/src/main/services/win-registry.ts b/src/main/services/win-registry.ts index 8149d4df8..83c83dd92 100644 --- a/src/main/services/win-registry.ts +++ b/src/main/services/win-registry.ts @@ -1,158 +1,158 @@ -// ==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 { BrowserWindow } from "electron"; -import { injectable } from "inversify"; -import { AppWindow, AppWindowType } from "readium-desktop/common/models/win"; -import { onWindowMoveResize } from "readium-desktop/common/rectangle/window"; -import { ObjectValues } from "readium-desktop/utils/object-keys-values"; -import { v4 as uuidv4 } from "uuid"; - -// Warning: not an array with ordered indexes! -// (cannot reliably iterate from 0 to N) -export interface WinDictionary { - [winId: number]: AppWindow; -} - -type OpenCallbackFunc = (appWindow: AppWindow) => void; -type CloseCallbackFunc = (appWindow: AppWindow) => void; - -@injectable() -export class WinRegistry { - // object map: Electron.BrowserWindow.id (number key) => AppWindow - // https://electronjs.org/docs/api/browser-window#winid-readonly - // Instead, we rely on AppWindow.registerIndex (incrementing number) - // to have a record of creation/registration stacking order - private windows: WinDictionary; - // getAllWindows() and getReaderWindows() use AppWindow.registerIndex for ordering - private _lastRegisterIndex = -1; - - private openCallbacks: OpenCallbackFunc[]; - private closeCallbacks: CloseCallbackFunc[]; - - constructor() { - this.windows = {}; - - this.unregisterWindow = this.unregisterWindow.bind(this); - this.openCallbacks = []; - this.closeCallbacks = []; - } - - public registerOpenCallback(callback: OpenCallbackFunc) { - this.openCallbacks.push(callback); - } - - public registerCloseCallback(callback: CloseCallbackFunc) { - this.closeCallbacks.push(callback); - } - - /** - * Register new electron browser window - * - * @param win Electron BrowserWindows - * @return Id of registered window - */ - public registerWindow(browserWindow: BrowserWindow, type: AppWindowType): AppWindow { - const winId = browserWindow.id; - const appWindow: AppWindow = { - identifier: uuidv4(), - type, - browserWindow, - browserWindowID: winId, - onWindowMoveResize: onWindowMoveResize(browserWindow), - - // ordered registration / creation stack - registerIndex: ++this._lastRegisterIndex, - }; - this.windows[winId] = appWindow; - - browserWindow.webContents.on("did-finish-load", () => { - // Call callbacks - for (const callback of this.openCallbacks) { - callback(appWindow); - } - }); - - // Unregister automatically - browserWindow.on("closed", () => { - this.unregisterWindow(winId); - }); - - return appWindow; - } - - /** - * Unregister electron browser window - * - * @param winId Id of registered window - */ - public unregisterWindow(winId: number) { - if (!(winId in this.windows)) { - // Window not found - return; - } - - const appWindow = this.windows[winId]; - delete this.windows[winId]; - - // Call callbacks - for (const callback of this.closeCallbacks) { - callback(appWindow); - } - } - - public getWindowByIdentifier(identifier: string): AppWindow | undefined { - return ObjectValues(this.windows).find((w) => w.identifier === identifier); - } - - // can return undefined when library window has been closed and unregistered - public getLibraryWindow(): AppWindow | undefined { - return ObjectValues(this.windows).find((w) => w.type === AppWindowType.Library); - } - - // can return empty array, but not undefined/null - public getReaderWindows() { - return ObjectValues(this.windows). - filter((w) => w.type === AppWindowType.Reader). - - // ordered registration / creation stack - sort((w1, w2) => w1.registerIndex - w2.registerIndex); - } - - // can return empty array, but not undefined/null - public getAllWindows() { - // Array - return ObjectValues(this.windows). - - // ordered registration / creation stack - sort((w1, w2) => w1.registerIndex - w2.registerIndex); - - // generic / template type does not work because dictionary not indexed by string, but by number: - // const windows = Object.values(windowsDict); - - // another style of type cohersion: - // const windows = Object.values(windowsDict) as AppWindow[]; - - // keys can be typed too: - // const keys = ObjectKeys(windowsDict); // Array - // const keys = ObjectKeysAll(windowsDict); // Array - } - - // Let's not expose internals (breaks encapsulation, also confusion with array[0...n]) - // this.windows === WinDictionary: object map of Electron.BrowserWindow.id (number key) => AppWindow - // public getWindowsDictionary() { - // return this.windows; - // } - // public getWindow(winId: number): AppWindow { - // if (!(winId in this.windows)) { - // // Window not found - // return undefined; - // } - - // return this.windows[winId]; - // } -} +// // ==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 { BrowserWindow } from "electron"; +// import { injectable } from "inversify"; +// import { AppWindow, AppWindowType } from "readium-desktop/common/models/win"; +// import { onWindowMoveResize } from "readium-desktop/common/rectangle/window"; +// import { ObjectValues } from "readium-desktop/utils/object-keys-values"; +// import * as uuid from "uuid"; + +// // Warning: not an array with ordered indexes! +// // (cannot reliably iterate from 0 to N) +// export interface WinDictionary { +// [winId: number]: AppWindow; +// } + +// type OpenCallbackFunc = (appWindow: AppWindow) => void; +// type CloseCallbackFunc = (appWindow: AppWindow) => void; + +// @injectable() +// export class WinRegistry { +// // object map: Electron.BrowserWindow.id (number key) => AppWindow +// // https://electronjs.org/docs/api/browser-window#winid-readonly +// // Instead, we rely on AppWindow.registerIndex (incrementing number) +// // to have a record of creation/registration stacking order +// private windows: WinDictionary; +// // getAllWindows() and getReaderWindows() use AppWindow.registerIndex for ordering +// private _lastRegisterIndex = -1; + +// private openCallbacks: OpenCallbackFunc[]; +// private closeCallbacks: CloseCallbackFunc[]; + +// constructor() { +// this.windows = {}; + +// this.unregisterWindow = this.unregisterWindow.bind(this); +// this.openCallbacks = []; +// this.closeCallbacks = []; +// } + +// public registerOpenCallback(callback: OpenCallbackFunc) { +// this.openCallbacks.push(callback); +// } + +// public registerCloseCallback(callback: CloseCallbackFunc) { +// this.closeCallbacks.push(callback); +// } + +// /** +// * Register new electron browser window +// * +// * @param win Electron BrowserWindows +// * @return Id of registered window +// */ +// public registerWindow(browserWindow: BrowserWindow, type: AppWindowType): AppWindow { +// const winId = browserWindow.id; +// const appWindow: AppWindow = { +// identifier: uuid.v4(), +// type, +// browserWindow, +// browserWindowID: winId, +// onWindowMoveResize: onWindowMoveResize(browserWindow), + +// // ordered registration / creation stack +// registerIndex: ++this._lastRegisterIndex, +// }; +// this.windows[winId] = appWindow; + +// browserWindow.webContents.on("did-finish-load", () => { +// // Call callbacks +// for (const callback of this.openCallbacks) { +// callback(appWindow); +// } +// }); + +// // Unregister automatically +// browserWindow.on("closed", () => { +// this.unregisterWindow(winId); +// }); + +// return appWindow; +// } + +// /** +// * Unregister electron browser window +// * +// * @param winId Id of registered window +// */ +// public unregisterWindow(winId: number) { +// if (!(winId in this.windows)) { +// // Window not found +// return; +// } + +// const appWindow = this.windows[winId]; +// delete this.windows[winId]; + +// // Call callbacks +// for (const callback of this.closeCallbacks) { +// callback(appWindow); +// } +// } + +// public getWindowByIdentifier(identifier: string): AppWindow | undefined { +// return ObjectValues(this.windows).find((w) => w.identifier === identifier); +// } + +// // can return undefined when library window has been closed and unregistered +// public getLibraryWindow(): AppWindow | undefined { +// return ObjectValues(this.windows).find((w) => w.type === AppWindowType.Library); +// } + +// // can return empty array, but not undefined/null +// public getReaderWindows() { +// return ObjectValues(this.windows). +// filter((w) => w.type === AppWindowType.Reader). + +// // ordered registration / creation stack +// sort((w1, w2) => w1.registerIndex - w2.registerIndex); +// } + +// // can return empty array, but not undefined/null +// public getAllWindows() { +// // Array +// return ObjectValues(this.windows). + +// // ordered registration / creation stack +// sort((w1, w2) => w1.registerIndex - w2.registerIndex); + +// // generic / template type does not work because dictionary not indexed by string, but by number: +// // const windows = Object.values(windowsDict); + +// // another style of type cohersion: +// // const windows = Object.values(windowsDict) as AppWindow[]; + +// // keys can be typed too: +// // const keys = ObjectKeys(windowsDict); // Array +// // const keys = ObjectKeysAll(windowsDict); // Array +// } + +// // Let's not expose internals (breaks encapsulation, also confusion with array[0...n]) +// // this.windows === WinDictionary: object map of Electron.BrowserWindow.id (number key) => AppWindow +// // public getWindowsDictionary() { +// // return this.windows; +// // } +// // public getWindow(winId: number): AppWindow { +// // if (!(winId in this.windows)) { +// // // Window not found +// // return undefined; +// // } + +// // return this.windows[winId]; +// // } +// } diff --git a/src/main/streamer.ts b/src/main/streamer.ts index 0f3534d54..f0cb85a23 100644 --- a/src/main/streamer.ts +++ b/src/main/streamer.ts @@ -9,13 +9,12 @@ import * as debug_ from "debug"; import { app } from "electron"; import * as express from "express"; import * as path from "path"; +import { computeReadiumCssJsonMessage } from "readium-desktop/common/computeReadiumCssJsonMessage"; +import { ReaderConfig } from "readium-desktop/common/models/reader"; import { diMainGet } from "readium-desktop/main/di"; import { _NODE_MODULE_RELATIVE_URL, _PACKAGING } from "readium-desktop/preprocessor-directives"; import { IEventPayload_R2_EVENT_READIUMCSS } from "@r2-navigator-js/electron/common/events"; -import { - colCountEnum, IReadiumCSS, readiumCSSDefaults, textAlignEnum, -} from "@r2-navigator-js/electron/common/readium-css-settings"; import { setupReadiumCSS } from "@r2-navigator-js/electron/main/readium-css"; import { secureSessions } from "@r2-navigator-js/electron/main/sessions"; import { Publication as R2Publication } from "@r2-shared-js/models/publication"; @@ -50,103 +49,42 @@ if (_PACKAGING === "1") { rcssPath = rcssPath.replace(/\\/g, "/"); debug("readium css path:", rcssPath); -// TODO: centralize this code, currently duplicated -// see src/renderer/components/reader/ReaderApp.jsx -function computeReadiumCssJsonMessage( +function computeReadiumCssJsonMessageInStreamer( _r2Publication: R2Publication, _link: Link | undefined, sessionInfo: string | undefined, ): IEventPayload_R2_EVENT_READIUMCSS { - const store = diMainGet("store"); - const settings = store.getState().reader.config; - debug(settings); - - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - debug("######"); - // debug(r2Publication.findFromInternal("zip")); - debug(sessionInfo); - // safeguard, just in case this applies to some unprocessed iframes, - // i.e. not the original installNavigatorDOM() call - if (sessionInfo) { - const sessionInfoStr = Buffer.from(sessionInfo, "base64").toString("utf-8"); - debug(sessionInfoStr); - const sessionInfoJson = JSON.parse(sessionInfoStr); - debug(sessionInfoJson); - } - - const cssJson: IReadiumCSS = { - - a11yNormalize: readiumCSSDefaults.a11yNormalize, - - backgroundColor: readiumCSSDefaults.backgroundColor, - - bodyHyphens: readiumCSSDefaults.bodyHyphens, - - colCount: settings.colCount === "1" ? colCountEnum.one : - (settings.colCount === "2" ? colCountEnum.two : colCountEnum.auto), - - darken: settings.dark, - - font: settings.font, - - fontSize: settings.fontSize, - - invert: settings.invert, + const winId = Buffer.from(sessionInfo || "", "base64").toString("utf-8"); + debug("winId:", winId); - letterSpacing: settings.letterSpacing, + let settings: ReaderConfig; + if (winId) { - ligatures: readiumCSSDefaults.ligatures, + const store = diMainGet("store"); + const state = store.getState(); - lineHeight: settings.lineHeight, + try { + settings = state.win.session.reader[winId].reduxState.config; - night: settings.night, + debug("PAGED: ", settings.paged, "colCount:", settings.colCount); - pageMargins: settings.pageMargins, + } catch (err) { + settings = state.reader.defaultConfig; - paged: settings.paged, - - paraIndent: readiumCSSDefaults.paraIndent, - - paraSpacing: settings.paraSpacing, - - sepia: settings.sepia, - - noFootnotes: settings.noFootnotes, - - textAlign: settings.align === textAlignEnum.left ? textAlignEnum.left : - (settings.align === textAlignEnum.right ? textAlignEnum.right : - (settings.align === textAlignEnum.justify ? textAlignEnum.justify : - (settings.align === textAlignEnum.start ? textAlignEnum.start : undefined))), - - textColor: readiumCSSDefaults.textColor, - - typeScale: readiumCSSDefaults.typeScale, - - wordSpacing: settings.wordSpacing, + debug("settings from default config"); + debug("ERROR", err); + } + } else { - mathJax: settings.enableMathJax, + const store = diMainGet("store"); + settings = store.getState().reader.defaultConfig; + } - reduceMotion: readiumCSSDefaults.reduceMotion, - }; - const jsonMsg: IEventPayload_R2_EVENT_READIUMCSS = { setCSS: cssJson }; - return jsonMsg; + return computeReadiumCssJsonMessage(settings); } -setupReadiumCSS(streamer, rcssPath, computeReadiumCssJsonMessage); +setupReadiumCSS(streamer, rcssPath, computeReadiumCssJsonMessageInStreamer); let mathJaxPath = "MathJax"; if (_PACKAGING === "1") { @@ -177,7 +115,9 @@ streamer.expressUse("/" + MATHJAX_URL_PATH, express.static(mathJaxPath, staticOp const transformer = (_publication: R2Publication, _link: Link, _url: string | undefined, str: string): string => { const store = diMainGet("store"); - const settings = store.getState().reader.config; + // TODO + // Same comment that above + const settings = store.getState().reader.defaultConfig; if (settings.enableMathJax) { const url = `${streamer.serverUrl()}/${MATHJAX_URL_PATH}/es5/tex-mml-chtml.js`; diff --git a/src/renderer/assets/styles/loader.css b/src/renderer/assets/styles/loader.css index 8b0437060..1dedf6a87 100644 --- a/src/renderer/assets/styles/loader.css +++ b/src/renderer/assets/styles/loader.css @@ -3,4 +3,15 @@ & svg { width: 5rem; } -} \ No newline at end of file +} + +.loader_small { + position: absolute; + top: 10px; + right: 10px; + z-index: 999; + + & svg { + width: 2rem; + } +} diff --git a/src/renderer/common/components/dialog/publicationInfos/publicationInfoContent.tsx b/src/renderer/common/components/dialog/publicationInfos/publicationInfoContent.tsx index 2a1f9d849..0d666dd26 100644 --- a/src/renderer/common/components/dialog/publicationInfos/publicationInfoContent.tsx +++ b/src/renderer/common/components/dialog/publicationInfos/publicationInfoContent.tsx @@ -7,12 +7,15 @@ import classNames from "classnames"; import * as React from "react"; +import { ReactReduxContext } from "react-redux"; import { I18nTyped, Translator } from "readium-desktop/common/services/translator"; import { TPublication } from "readium-desktop/common/type/publication.type"; -import { formatTime_ } from "readium-desktop/common/utils/time"; +import { formatTime } from "readium-desktop/common/utils/time"; import { IOpdsBaseLinkView } from "readium-desktop/common/views/opds"; -import { ITimeDuration } from "readium-desktop/common/views/publication"; import * as styles from "readium-desktop/renderer/assets/styles/bookDetailsDialog.css"; +import { apiActionFactory } from "readium-desktop/renderer/common/apiAction"; + +import { LocatorExtended } from "@r2-navigator-js/electron/renderer"; import Cover from "../../Cover"; import { FormatContributorWithLink } from "./FormatContributorWithLink"; @@ -32,7 +35,7 @@ export interface IProps { } const Duration = (props: { - duration: ITimeDuration; + duration: number; __: I18nTyped; }) => { @@ -42,26 +45,68 @@ const Duration = (props: { return <>; } - const { hours = 0, minutes = 0, seconds = 0 } = duration; - - const sentence = formatTime_(hours, minutes, seconds); + const sentence = formatTime(duration); return ( sentence - ? <> - - { - `${__("publication.duration.title")}: ` - } - - - { - sentence - } - -
- - : <>); + ? <> + + { + `${__("publication.duration.title")}: ` + } + + + { + sentence + } + +
+ + : <>); +}; + +const Progression = (props: { + pubId: string; + __: I18nTyped; +}) => { + + const { __, pubId } = props; + const [locatorExt, setLocatorExt] = React.useState(undefined); + + const store = React.useContext(ReactReduxContext); + + const apiAction = apiActionFactory(() => store.store); + + React.useEffect(() => { + apiAction("reader/getLastReadingLocation", pubId) + .then((_locator) => setLocatorExt(_locator)) + .catch((err) => console.error("Error to fetch api reader/getLastReadingLocation", err)); + }, [pubId]); + + if (locatorExt?.locator?.locations?.progression && locatorExt?.audioPlaybackInfo) { + + const percent = Math.round(locatorExt.locator.locations.position * 100); + const time = Math.round(locatorExt.audioPlaybackInfo.globalTime); + const duration = Math.round(locatorExt.audioPlaybackInfo.globalDuration); + const sentence = `${percent}% (${formatTime(time)} / ${formatTime(duration)})`; + + return ( + <> + + { + `${__("publication.progression.title")}: ` + } + + + { + sentence + } + +
+ + ); + } + return (<>); }; export const PublicationInfoContent: React.FC = (props) => { @@ -142,27 +187,34 @@ export const PublicationInfoContent: React.FC = (props) => { } -
- : undefined +
+ : undefined } { publication.numberOfPages ? - <> - - { - `${__("catalog.numberOfPages")}: ` - } - - - { - publication.numberOfPages - } - -
- - : undefined + <> + + { + `${__("catalog.numberOfPages")}: ` + } + + + { + publication.numberOfPages + } + +
+ + : undefined } - + + { publication.nbOfTracks ? <> diff --git a/src/renderer/common/components/dialog/publicationInfos/tag/AddTag.tsx b/src/renderer/common/components/dialog/publicationInfos/tag/AddTag.tsx index 76a3eecc4..e0833a979 100644 --- a/src/renderer/common/components/dialog/publicationInfos/tag/AddTag.tsx +++ b/src/renderer/common/components/dialog/publicationInfos/tag/AddTag.tsx @@ -75,26 +75,28 @@ export default class AddTag extends React.Component { this.setState({ newTagName: "" }); - const tagsName: string[] = []; - for (const tag of tags) { - if (typeof tag === "string") { - if (tag === tagName) { - return; + if (tagName) { + + const tagsName: string[] = []; + for (const tag of tags) { + if (typeof tag === "string") { + if (tag === tagName) { + return; + } else { + tagsName.push(tag); + } } else { - tagsName.push(tag); - } - } else { - if (tag.name === tagName) { - return; - } else { - tagsName.push(tag.name); + if (tag.name === tagName) { + return; + } else { + tagsName.push(tag.name); + } } } - } - - tagsName.push(tagName); - this.props.setTags(tagsName); + tagsName.push(tagName); + this.props.setTags(tagsName); + } } private handleChangeName = (e: TChangeEventOnInput) => { diff --git a/src/renderer/common/components/toast/ToastManager.tsx b/src/renderer/common/components/toast/ToastManager.tsx index 2c1acbc7d..f2e39fdd9 100644 --- a/src/renderer/common/components/toast/ToastManager.tsx +++ b/src/renderer/common/components/toast/ToastManager.tsx @@ -8,9 +8,9 @@ import * as React from "react"; import { connect } from "react-redux"; import { ToastType } from "readium-desktop/common/models/toast"; +import { ICommonRootState } from "readium-desktop/common/redux/states/renderer/commonRootState"; import { ToastState } from "readium-desktop/common/redux/states/toast"; import * as styles from "readium-desktop/renderer/assets/styles/toast.css"; -import { ICommonRootState } from "readium-desktop/renderer/common/redux/states"; import { v4 as uuidv4 } from "uuid"; import { TranslatorProps, withTranslator } from "../hoc/translator"; diff --git a/src/renderer/common/redux/actions/win/initRequest.ts b/src/renderer/common/redux/actions/win/initRequest.ts index 00aa22a11..d62a39fd6 100644 --- a/src/renderer/common/redux/actions/win/initRequest.ts +++ b/src/renderer/common/redux/actions/win/initRequest.ts @@ -10,16 +10,16 @@ import { Action } from "readium-desktop/common/models/redux"; export const ID = "WIN_INIT_REQUEST"; export interface Payload { - winId: string; + identifier: string; } -export function build(winId: string): +export function build(identifier: string): Action { return { type: ID, payload: { - winId, + identifier, }, }; } diff --git a/src/renderer/common/redux/api/api.ts b/src/renderer/common/redux/api/api.ts index 9ce78ce00..35b41d5c7 100644 --- a/src/renderer/common/redux/api/api.ts +++ b/src/renderer/common/redux/api/api.ts @@ -14,7 +14,7 @@ import { ReturnPromiseType } from "readium-desktop/typings/promise"; import { Dispatch } from "redux"; import { v4 as uuidv4 } from "uuid"; -import { ICommonRootState } from "../states"; +import { ICommonRootState } from "../../../../common/redux/states/renderer/commonRootState"; export function apiDispatch(dispatch: Dispatch) { return (requestId: string = uuidv4()) => diff --git a/src/renderer/common/redux/middleware/syncFactory.ts b/src/renderer/common/redux/middleware/syncFactory.ts index 7a0ca4257..d3754130d 100644 --- a/src/renderer/common/redux/middleware/syncFactory.ts +++ b/src/renderer/common/redux/middleware/syncFactory.ts @@ -8,39 +8,40 @@ import { ipcRenderer } from "electron"; import { syncIpc } from "readium-desktop/common/ipc"; import { ActionWithSender, SenderType } from "readium-desktop/common/models/sync"; -import { actionSerializer } from "readium-desktop/renderer/common/actionSerializer"; +import { ICommonRootState } from "readium-desktop/common/redux/states/renderer/commonRootState"; +import { ActionSerializer } from "readium-desktop/common/services/serializer"; import { AnyAction, Dispatch, MiddlewareAPI } from "redux"; export function syncFactory(SYNCHRONIZABLE_ACTIONS: string[]) { - return (store: MiddlewareAPI>) => - (next: Dispatch) => - ((action: ActionWithSender) => { - - // Does this action must be sent to the main process - if (SYNCHRONIZABLE_ACTIONS.indexOf(action.type) === -1) { - // Do not send - return next(action); - } - - if (action.sender && action.sender.type === SenderType.Main) { - // Do not send in loop an action already sent by main process - return next(action); - } - - // Send this action to the main process - ipcRenderer.send(syncIpc.CHANNEL, { - type: syncIpc.EventType.RendererAction, - payload: { - action: actionSerializer.serialize(action), - }, - sender: { - type: SenderType.Renderer, - winId: store.getState().win.winId, - }, - } as syncIpc.EventPayload); + return (store: MiddlewareAPI, ICommonRootState>) => + (next: Dispatch) => + ((action: ActionWithSender) => { + // Does this action must be sent to the main process + if (SYNCHRONIZABLE_ACTIONS.indexOf(action.type) === -1) { + // Do not send return next(action); - }); + } + + if (action.sender && action.sender.type === SenderType.Main) { + // Do not send in loop an action already sent by main process + return next(action); + } + + // Send this action to the main process + ipcRenderer.send(syncIpc.CHANNEL, { + type: syncIpc.EventType.RendererAction, + payload: { + action: ActionSerializer.serialize(action), + }, + sender: { + type: SenderType.Renderer, + identifier: store.getState().win.identifier, + }, + } as syncIpc.EventPayload); + + return next(action); + }); } diff --git a/src/renderer/common/redux/reducers/load.ts b/src/renderer/common/redux/reducers/load.ts new file mode 100644 index 000000000..7830d449a --- /dev/null +++ b/src/renderer/common/redux/reducers/load.ts @@ -0,0 +1,30 @@ +// ==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 { loadActions } from "readium-desktop/common/redux/actions"; + +import { ILoadState } from "../states/load"; + +const initialState: ILoadState = { + state: false, +}; + +export function loadReducer( + state: ILoadState = initialState, + action: loadActions.busy.TAction | loadActions.idle.TAction, + ): ILoadState { + if (action.type === loadActions.busy.ID) { + return { + state: true, + }; + } else if (action.type === loadActions.idle.ID) { + return { + state: false, + }; + } + return state; +} diff --git a/src/renderer/common/redux/reducers/win.ts b/src/renderer/common/redux/reducers/win.ts index d9a2bc53b..1f57e808b 100644 --- a/src/renderer/common/redux/reducers/win.ts +++ b/src/renderer/common/redux/reducers/win.ts @@ -9,7 +9,7 @@ import { winActions } from "readium-desktop/renderer/common/redux/actions"; import { WinState } from "readium-desktop/renderer/common/redux/states/win"; const initialState: WinState = { - winId: undefined, + identifier: undefined, }; export function winReducer( @@ -20,7 +20,7 @@ export function winReducer( case winActions.initRequest.ID: return { ...{ - winId: action.payload.winId, + identifier: action.payload.identifier, }, }; default: diff --git a/src/renderer/common/redux/sagas/api.ts b/src/renderer/common/redux/sagas/api.ts index 719380a50..47aeca115 100644 --- a/src/renderer/common/redux/sagas/api.ts +++ b/src/renderer/common/redux/sagas/api.ts @@ -20,7 +20,7 @@ import { put } from "redux-saga/effects"; * @param requestId id string channel * @param requestData typed api parameter */ -export function* apiSaga( +export function apiSaga( apiPath: T, requestId: string, ...requestData: Parameters @@ -28,5 +28,5 @@ export function* apiSaga( const splitPath = apiPath.split("/"); const moduleId = splitPath[0] as TModuleApi; const methodId = splitPath[1] as TMethodApi; - yield put(apiActions.request.build(requestId, moduleId, methodId, requestData)); + return put(apiActions.request.build(requestId, moduleId, methodId, requestData)); } diff --git a/src/renderer/common/redux/sagas/dialog/publicationInfoOpds.ts b/src/renderer/common/redux/sagas/dialog/publicationInfoOpds.ts index 11ea8cfdb..0e5397746 100644 --- a/src/renderer/common/redux/sagas/dialog/publicationInfoOpds.ts +++ b/src/renderer/common/redux/sagas/dialog/publicationInfoOpds.ts @@ -6,92 +6,59 @@ // ==LICENSE-END== import * as debug_ from "debug"; -import { TApiMethod } from "readium-desktop/common/api/api.type"; import { DialogTypeName } from "readium-desktop/common/models/dialog"; -import { apiActions, dialogActions } from "readium-desktop/common/redux/actions"; -import { takeTyped } from "readium-desktop/common/redux/typed-saga"; +import { dialogActions } from "readium-desktop/common/redux/actions"; +import { takeSpawnLatest } from "readium-desktop/common/redux/sagas/takeSpawnLatest"; +import { raceTyped } from "readium-desktop/common/redux/sagas/typed-saga"; import { IOpdsLinkView } from "readium-desktop/common/views/opds"; -import { ReturnPromiseType } from "readium-desktop/typings/promise"; -import { all, call, put } from "redux-saga/effects"; +import { call, delay, put, race, take } from "redux-saga/effects"; +import { opdsBrowse } from "../opdsBrowse"; -import { apiSaga } from "../api"; +// Test URL : http://readium2.herokuapp.com/opds-v1-v2-convert/http%3A%2F%2Fmanybooks.net%2Fopds%2Fnew_titles.php const REQUEST_ID = "PUBINFO_OPDS_REQUEST_ID"; // Logger -const debug = debug_("readium-desktop:renderer:redux:saga:publication-info-opds"); +const filename_ = "readium-desktop:renderer:redux:saga:publication-info-opds"; +const debug = debug_(filename_); -// global access to opdsLInksView in iterator -let linksIterator: IterableIterator; - -// While a link is available dispatch API Redux -function* browsePublication() { - const linkIterator = linksIterator.next(); - - // https://github.com/microsoft/TypeScript/issues/33353 - if (!linkIterator.done) { - const link = linkIterator.value as IOpdsLinkView; - if (link) { - yield* apiSaga("opds/browse", REQUEST_ID, link.url); - } - } -} - -// Triggered when a publication-info-opds is asked -function* checkOpdsPublicationWatcher() { - while (true) { - const action = yield* takeTyped(dialogActions.openRequest.build); - - if (action.payload?.type === DialogTypeName.PublicationInfoOpds) { - - debug("Triggered publication-info-opds"); - - const dataPayload = (action.payload as - dialogActions.openRequest.Payload).data; - const publication = dataPayload?.publication; - - debug("publication entryLinksArray", publication.entryLinks); +// Triggered when the publication data are available from the API +function* updateOpdsInfoWithEntryLink(links: IOpdsLinkView[]) { - // dispatch the publication to publication-info even not complete - yield put(dialogActions.updateRequest.build({ - publication, - coverZoom: false, - })); + for (const link of links) { - // find the entry url even if all data is already load in publication - if (publication && Array.isArray(publication.entryLinks) && publication.entryLinks[0]) { - linksIterator = publication.entryLinks.values(); + debug("updateOpdsInfoWithEntryLink", link); + if (link?.url) { + const { b: action } = yield* raceTyped({ + a: delay(5000), + b: call(opdsBrowse, link.url, REQUEST_ID), + }); - yield* browsePublication(); + if (!action) { + continue; } - } - } -} - -// Triggered when the publication data are available from the API -function* updateOpdsPublicationWatcher() { - while (true) { - const action = yield* takeTyped(apiActions.result.build); - const { requestId } = action.meta.api; - - if (requestId === REQUEST_ID) { - debug("opds publication from publicationInfo received"); const actionError = action.error; - - const httpRes = action.payload as - ReturnPromiseType; + const httpRes = action.payload; const opdsResultView = httpRes?.data; debug("Payload: ", opdsResultView); const publication = opdsResultView.publications ? opdsResultView.publications[0] : undefined; - - if (!actionError + publication.authors = publication.authors + ? Array.isArray(publication.authors) + ? publication.authors + : [publication.authors] + : undefined; + + if ( + !actionError && httpRes.isSuccess - && publication && publication?.title + && publication?.title && Array.isArray(publication.authors) ) { + + debug("dispatch"); yield put( dialogActions.updateRequest.build( { @@ -100,23 +67,57 @@ function* updateOpdsPublicationWatcher() { ), ); - // could be 401 OPDS Authentication document, - // which we ignore in this case because should not occur with "entry" URLs (unlike "borrow", for example) - } else { + // could be 401 OPDS Authentication document, + // tslint:disable-next-line: max-line-length + // which we ignore in this case because should not occur with "entry" URLs (unlike "borrow", for example) + debug("opds publication from publicationInfo received"); - if (actionError) { - debug(httpRes); - } + break; + } - yield* browsePublication(); + if (actionError) { + debug(httpRes); } } } } -export function* watchers() { - yield all([ - call(checkOpdsPublicationWatcher), - call(updateOpdsPublicationWatcher), - ]); +// Triggered when a publication-info-opds is asked +function* checkOpdsPublicationWatcher(action: dialogActions.openRequest.TAction) { + + debug("dialog open"); + + if (action.payload?.type === DialogTypeName.PublicationInfoOpds) { + + debug("Triggered publication-info-opds"); + + const dataPayload = (action.payload as + dialogActions.openRequest.Payload).data; + const publication = dataPayload?.publication; + + debug("publication entryLinksArray", publication.entryLinks); + + // dispatch the publication to publication-info even not complete + yield put(dialogActions.updateRequest.build({ + publication, + coverZoom: false, + })); + + // find the entry url even if all data is already load in publication + if (publication && Array.isArray(publication.entryLinks) && publication.entryLinks[0]) { + + yield race({ + a: call(updateOpdsInfoWithEntryLink, publication.entryLinks), + b: take(dialogActions.closeRequest.ID), + }); + } + } +} + +export function saga() { + return takeSpawnLatest( + dialogActions.openRequest.ID, + checkOpdsPublicationWatcher, + (e) => debug(e), + ); } diff --git a/src/renderer/common/redux/sagas/dialog/publicationInfoReaderAndLib.ts b/src/renderer/common/redux/sagas/dialog/publicationInfoReaderAndLib.ts index bc7ff9fe0..dea44cfa0 100644 --- a/src/renderer/common/redux/sagas/dialog/publicationInfoReaderAndLib.ts +++ b/src/renderer/common/redux/sagas/dialog/publicationInfoReaderAndLib.ts @@ -7,66 +7,90 @@ import * as debug_ from "debug"; import { TApiMethod } from "readium-desktop/common/api/api.type"; +// import { error } from "readium-desktop/common/error"; import { DialogTypeName } from "readium-desktop/common/models/dialog"; import { apiActions, dialogActions } from "readium-desktop/common/redux/actions"; -import { takeTyped } from "readium-desktop/common/redux/typed-saga"; +import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { raceTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { PublicationView } from "readium-desktop/common/views/publication"; import { ReturnPromiseType } from "readium-desktop/typings/promise"; -import { all, call, put } from "redux-saga/effects"; +import { all, call, delay, put, take } from "redux-saga/effects"; import { apiSaga } from "../api"; const REQUEST_ID = "PUBINFO_READER_AND_LIB_REQUEST_ID"; // Logger -const debug = debug_("readium-desktop:renderer:redux:saga:publication-info-readerAndLib"); +const filename_ = "readium-desktop:renderer:redux:saga:publication-info-readerAndLib"; +const debug = debug_(filename_); // Triggered when a publication-info-reader is asked -function* checkReaderAndLibPublicationWatcher() { - while (true) { - const action = yield* takeTyped(dialogActions.openRequest.build); +function* checkReaderAndLibPublication(action: dialogActions.openRequest.TAction) { + if ( + action.payload?.type === DialogTypeName.PublicationInfoReader + || action.payload?.type === DialogTypeName.PublicationInfoLib + ) { + + const dataPayload = (action.payload as + dialogActions.openRequest.Payload).data; + const id = dataPayload?.publicationIdentifier; + + // dispatch to API a publication get request + if (id) { + + const { b: getAction } = yield* raceTyped({ + a: delay(5000), + b: call(getApi, id), + }); + + if (!getAction) { + debug("timeout 5s"); + return; + } - if (action.payload?.type === DialogTypeName.PublicationInfoReader - || action.payload?.type === DialogTypeName.PublicationInfoLib) { + yield call(updateReaderAndLibPublication, getAction); + } + } +} - const dataPayload = (action.payload as - dialogActions.openRequest.Payload).data; - const id = dataPayload?.publicationIdentifier; +function* getApi(id: string) { - // dispatch to API a publication get request - if (id) { - yield* apiSaga("publication/get", REQUEST_ID, id, true); - } - } + yield apiSaga("publication/get", REQUEST_ID, id, true); + while (true) { + const action: + apiActions.result.TAction> + = yield take(apiActions.result.build); - yield call(updateReaderAndLibPublicationWatcher); + const { requestId } = action.meta.api; + if (requestId === REQUEST_ID) { + return action; + } } } // Triggered when the publication data are available from the API -function* updateReaderAndLibPublicationWatcher() { - const action = yield* takeTyped(apiActions.result.build); - const { requestId } = action.meta.api; +function* updateReaderAndLibPublication(action: apiActions.result.TAction) { + debug("reader publication from publicationInfo received"); - if (requestId === REQUEST_ID) { - debug("reader publication from publicationInfo received"); + const publicationView = action.payload; - const publicationView = action.payload as - ReturnPromiseType; + if (publicationView) { + debug("opdsPublicationResult:", publicationView); - if (publicationView) { - debug("opdsPublicationResult:", publicationView); + const publication = publicationView; - const publication = publicationView; - - yield put(dialogActions.updateRequest.build({ - publication, - })); - } + yield put(dialogActions.updateRequest.build({ + publication, + })); } } -export function* watchers() { - yield all([ - call(checkReaderAndLibPublicationWatcher), +export function saga() { + return all([ + takeSpawnLeading( + dialogActions.openRequest.ID, + checkReaderAndLibPublication, + (e) => debug(e), + ), ]); } diff --git a/src/renderer/common/redux/sagas/dialog/publicationInfosSyncTags.ts b/src/renderer/common/redux/sagas/dialog/publicationInfosSyncTags.ts index 575ac1976..359504356 100644 --- a/src/renderer/common/redux/sagas/dialog/publicationInfosSyncTags.ts +++ b/src/renderer/common/redux/sagas/dialog/publicationInfosSyncTags.ts @@ -5,16 +5,23 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import * as debug_ from "debug"; import { TApiMethod } from "readium-desktop/common/api/api.type"; +// import { error } from "readium-desktop/common/error"; import { DialogType, DialogTypeName } from "readium-desktop/common/models/dialog"; import { apiActions, dialogActions } from "readium-desktop/common/redux/actions"; -import { selectTyped } from "readium-desktop/common/redux/typed-saga"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { ICommonRootState } from "readium-desktop/common/redux/states/renderer/commonRootState"; import { PublicationView } from "readium-desktop/common/views/publication"; import { ReturnPromiseType } from "readium-desktop/typings/promise"; import { stringArrayEqual } from "readium-desktop/utils/stringArrayEqual"; -import { all, call, put, takeEvery } from "redux-saga/effects"; +import { call, put, race, take } from "redux-saga/effects"; -import { ICommonRootState } from "../../states"; +// Logger +const filename_ = "readium-desktop:renderer:redux:saga:publication-info-syncTags"; +const debug = debug_(filename_); +debug("_"); function* apiResult(action: apiActions.result.TAction) { @@ -47,15 +54,24 @@ function* apiResult(action: apiActions.result.TAction) { } function* dialogOpened(_action: dialogActions.openRequest.TAction) { - yield takeEvery(apiActions.result.build, apiResult); -} -function* dialogOpenWatcher() { - yield takeEvery(dialogActions.openRequest.build, dialogOpened); + while (true) { + const { api, can } = yield race({ + api: take(apiActions.result.ID), + can: take(dialogActions.closeRequest.ID), + }); + + if (can) { + return ; + } + + yield call(apiResult, api); + } } -export function* watchers() { - yield all([ - call(dialogOpenWatcher), - ]); +export function saga() { + return takeSpawnEvery( + dialogActions.openRequest.ID, + dialogOpened, + ); } diff --git a/src/renderer/common/redux/sagas/opdsBrowse.ts b/src/renderer/common/redux/sagas/opdsBrowse.ts new file mode 100644 index 000000000..3328e4253 --- /dev/null +++ b/src/renderer/common/redux/sagas/opdsBrowse.ts @@ -0,0 +1,35 @@ +// ==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 { TApiMethod } from "readium-desktop/common/api/api.type"; +import { apiActions } from "readium-desktop/common/redux/actions"; +import { ReturnPromiseType } from "readium-desktop/typings/promise"; +import { take } from "redux-saga/effects"; + +import { apiSaga } from "./api"; + +// Logger +const filename_ = "readium-desktop:renderer:redux:saga:opds-browse"; +const debug = debug_(filename_); + +type TA = apiActions.result.TAction>; + +export function* opdsBrowse(link: string, REQUEST_ID: string) { + + debug("opds-browse", link); + yield apiSaga("opds/browse", REQUEST_ID, link); + while (true) { + const action: TA = yield take(apiActions.result.build); + + const { requestId } = action.meta.api; + if (requestId === REQUEST_ID) { + debug("opds-browse action-received", action); + return action; + } + } +} diff --git a/src/renderer/common/redux/states/api.ts b/src/renderer/common/redux/states/api.ts index 73bbfc342..27c22e3a7 100644 --- a/src/renderer/common/redux/states/api.ts +++ b/src/renderer/common/redux/states/api.ts @@ -7,7 +7,7 @@ import { TMethodApi } from "readium-desktop/common/api/methodApi.type"; import { TModuleApi } from "readium-desktop/common/api/moduleApi.type"; -import { CodeError } from "readium-desktop/common/errors"; +import { CodeError } from "readium-desktop/common/codeError.class"; // FIXME what is the purpose of this interface ? // interface PageState { diff --git a/src/renderer/common/redux/states/load.ts b/src/renderer/common/redux/states/load.ts new file mode 100644 index 000000000..f72c6599f --- /dev/null +++ b/src/renderer/common/redux/states/load.ts @@ -0,0 +1,10 @@ +// ==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== + +export interface ILoadState { + state: boolean | undefined; +} diff --git a/src/main/redux/states/reader.ts b/src/renderer/common/redux/states/renderer/readerRootState.ts similarity index 51% rename from src/main/redux/states/reader.ts rename to src/renderer/common/redux/states/renderer/readerRootState.ts index f785d0502..050a5f6dd 100644 --- a/src/main/redux/states/reader.ts +++ b/src/renderer/common/redux/states/renderer/readerRootState.ts @@ -5,22 +5,14 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { - Reader, ReaderConfig, ReaderMode, -} from "readium-desktop/common/models/reader"; +import { ReaderConfig, ReaderInfo } from "readium-desktop/common/models/reader"; +import { ICommonRootState } from "readium-desktop/common/redux/states/renderer/commonRootState"; -export interface ReaderStateReader { - reader: Reader; +export interface IReaderRootState extends ICommonRootState { + reader: IReaderStateReader; } -export interface ReaderStateMode { - mode: ReaderMode; -} - -export interface ReaderStateConfig { +export interface IReaderStateReader { config: ReaderConfig; -} - -export interface ReaderState extends ReaderStateMode, ReaderStateConfig { - readers: { [identifier: string]: Reader }; + info: ReaderInfo; } diff --git a/src/renderer/common/redux/states/win.ts b/src/renderer/common/redux/states/win.ts index 9fa3d4dca..64a0e72d6 100644 --- a/src/renderer/common/redux/states/win.ts +++ b/src/renderer/common/redux/states/win.ts @@ -6,5 +6,5 @@ // ==LICENSE-END== export interface WinState { - winId?: string | undefined; + identifier: string | undefined; } diff --git a/src/renderer/library/components/App.tsx b/src/renderer/library/components/App.tsx index 4933ce1bb..54dee85dd 100644 --- a/src/renderer/library/components/App.tsx +++ b/src/renderer/library/components/App.tsx @@ -23,6 +23,7 @@ import PageManager from "readium-desktop/renderer/library/components/PageManager import { diLibraryGet } from "readium-desktop/renderer/library/di"; import DownloadsPanel from "./DownloadsPanel"; +import LoaderMainLoad from "./LoaderMainLoad"; export default class App extends React.Component<{}, undefined> { @@ -91,6 +92,7 @@ export default class App extends React.Component<{}, undefined> { /> + ; }} diff --git a/src/renderer/library/components/LoaderMainLoad.tsx b/src/renderer/library/components/LoaderMainLoad.tsx new file mode 100644 index 000000000..57ba72268 --- /dev/null +++ b/src/renderer/library/components/LoaderMainLoad.tsx @@ -0,0 +1,47 @@ +// ==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 { connect } from "react-redux"; +import * as LoaderIcon from "readium-desktop/renderer/assets/icons/loader.svg"; +import * as styles from "readium-desktop/renderer/assets/styles/loader.css"; +import SVG from "readium-desktop/renderer/common/components/SVG"; + +import { ILibraryRootState } from "../redux/states"; + +// tslint:disable-next-line: no-empty-interface +interface IBaseProps { +} +// IProps may typically extend: +// RouteComponentProps +// ReturnType +// ReturnType +// tslint:disable-next-line: no-empty-interface +interface IProps extends IBaseProps, ReturnType { +} + +class LoaderMainLoad extends React.Component { + + public render() { + const { mainProcessLoad } = this.props; + + if (!mainProcessLoad) { + return (<>); + } + return ( +

+ +
+ ); + } +} + +const mapStateToProps = (state: ILibraryRootState) => ({ + mainProcessLoad: state.load.state, +}); + +export default connect(mapStateToProps)(LoaderMainLoad); diff --git a/src/renderer/library/components/catalog/Catalog.tsx b/src/renderer/library/components/catalog/Catalog.tsx index e5f180be8..ff77e5b16 100644 --- a/src/renderer/library/components/catalog/Catalog.tsx +++ b/src/renderer/library/components/catalog/Catalog.tsx @@ -48,6 +48,12 @@ class Catalog extends React.Component { this.props.apiClean(this.publicationGetAllTagId); } + public componentDidUpdate(oldProps: IProps) { + if (oldProps.refreshCatalog !== this.props.refreshCatalog) { + this.getFromApi(); + } + } + public render(): React.ReactElement<{}> { const { __ } = this.props; @@ -97,11 +103,12 @@ const mapStateToProps = (state: ILibraryRootState) => ({ "publication/import", "publication/importOpdsPublicationLink", "publication/delete", - "catalog/addEntry", + // "catalog/addEntry", "publication/updateTags", - "reader/setLastReadingLocation", + // "reader/setLastReadingLocation", ]), location: state.router.location, + refreshCatalog: state.updateCatalog, // just to recall 'catalog/get' when readerActions.setReduxState is dispatched }); const mapDispatchToProps = (dispatch: Dispatch) => ({ diff --git a/src/renderer/library/components/dialog/publicationInfos/opdsControls/OpdsLinkProperties.tsx b/src/renderer/library/components/dialog/publicationInfos/opdsControls/OpdsLinkProperties.tsx index f46a8a547..b5936ab7c 100644 --- a/src/renderer/library/components/dialog/publicationInfos/opdsControls/OpdsLinkProperties.tsx +++ b/src/renderer/library/components/dialog/publicationInfos/opdsControls/OpdsLinkProperties.tsx @@ -6,7 +6,6 @@ // ==LICENSE-END== import * as moment from "moment"; -import { OPDSAvailabilityEnum } from "r2-opds-js/dist/es6-es2015/src/opds/opds2/opds2-availability"; import * as React from "react"; import { IOPDSPropertiesView } from "readium-desktop/common/views/opds"; import * as styles from "readium-desktop/renderer/assets/styles/bookDetailsDialog.css"; @@ -14,6 +13,8 @@ import { TranslatorProps, withTranslator, } from "readium-desktop/renderer/common/components/hoc/translator"; +import { OPDSAvailabilityEnum } from "@r2-opds-js/opds/opds2/opds2-availability"; + // tslint:disable-next-line: no-empty-interface interface IBaseProps extends TranslatorProps { properties: IOPDSPropertiesView | undefined; diff --git a/src/renderer/library/components/publication/PublicationListElement.tsx b/src/renderer/library/components/publication/PublicationListElement.tsx index 7dfc63011..1381114cd 100644 --- a/src/renderer/library/components/publication/PublicationListElement.tsx +++ b/src/renderer/library/components/publication/PublicationListElement.tsx @@ -12,7 +12,7 @@ import { DialogTypeName } from "readium-desktop/common/models/dialog"; import { readerActions } from "readium-desktop/common/redux/actions"; import * as dialogActions from "readium-desktop/common/redux/actions/dialog"; import { TPublication } from "readium-desktop/common/type/publication.type"; -import { IOpdsPublicationView } from "readium-desktop/common/views/opds"; +import { IOpdsContributorView, IOpdsPublicationView } from "readium-desktop/common/views/opds"; import { PublicationView } from "readium-desktop/common/views/publication"; import * as MenuIcon from "readium-desktop/renderer/assets/icons/menu.svg"; import * as styles from "readium-desktop/renderer/assets/styles/myBooks.css"; @@ -65,9 +65,18 @@ export class PublicationListElement extends React.Component { this.menuId = "menu-" + uuidv4(); } - public render(): React.ReactElement<{}> { + public render(): React.ReactElement<{}> { const pub = this.props.publicationViewMaybeOpds; - const formatedPublishers = pub.publishers.join(", "); + const publishers = pub.publishers as Array; + const formatedPublishers = publishers + .reduce( + (pv, cv) => { + if ((cv as IOpdsContributorView)?.name) { + return [...pv, `${pv}${(cv as IOpdsContributorView).name}`]; + } + return cv && typeof cv === "string" ? [...pv, cv] : pv; + }, []) + .join(", "); let formatedPublishedYear = ""; const { translator } = this.props; diff --git a/src/renderer/library/components/searchResult/AllPublicationPage.tsx b/src/renderer/library/components/searchResult/AllPublicationPage.tsx index e98b9e2a8..772448ea5 100644 --- a/src/renderer/library/components/searchResult/AllPublicationPage.tsx +++ b/src/renderer/library/components/searchResult/AllPublicationPage.tsx @@ -52,7 +52,7 @@ export class AllPublicationPage extends React.Component { this.unsubscribe = apiSubscribe([ "publication/import", "publication/delete", - "catalog/addEntry", + // "catalog/addEntry", "publication/updateTags", ], () => { apiAction("publication/findAll") diff --git a/src/renderer/library/components/searchResult/TagSearchResult.tsx b/src/renderer/library/components/searchResult/TagSearchResult.tsx index 8017f172e..f4310846c 100644 --- a/src/renderer/library/components/searchResult/TagSearchResult.tsx +++ b/src/renderer/library/components/searchResult/TagSearchResult.tsx @@ -54,7 +54,7 @@ export class TagSearchResult extends React.Component { "publication/delete", "publication/import", "publication/updateTags", - "catalog/addEntry", + // "catalog/addEntry", ], () => { const value = matchPath( this.props.location.pathname, routes["/library/search/tag"], diff --git a/src/renderer/library/components/searchResult/TextSearchResult.tsx b/src/renderer/library/components/searchResult/TextSearchResult.tsx index f72055e9a..4e82c098d 100644 --- a/src/renderer/library/components/searchResult/TextSearchResult.tsx +++ b/src/renderer/library/components/searchResult/TextSearchResult.tsx @@ -54,7 +54,7 @@ export class TextSearchResult extends React.Component { this.unsubscribe = apiSubscribe([ "publication/import", "publication/delete", - "catalog/addEntry", + // "catalog/addEntry", "publication/updateTags", ], this.searchPublications); } diff --git a/src/renderer/library/components/settings/SessionSettings.tsx b/src/renderer/library/components/settings/SessionSettings.tsx new file mode 100644 index 000000000..028f570dc --- /dev/null +++ b/src/renderer/library/components/settings/SessionSettings.tsx @@ -0,0 +1,109 @@ +// ==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 * as DoneIcon from "readium-desktop/renderer/assets/icons/done.svg"; +import * as styles from "readium-desktop/renderer/assets/styles/settings.css"; +import { + TranslatorProps, withTranslator, +} from "readium-desktop/renderer/common/components/hoc/translator"; +import { apiAction } from "readium-desktop/renderer/library/apiAction"; + +import SVG from "../../../common/components/SVG"; + +// tslint:disable-next-line: no-empty-interface +interface IBaseProps extends TranslatorProps { +} +// IProps may typically extend: +// RouteComponentProps +// ReturnType +// ReturnType +// tslint:disable-next-line: no-empty-interface +interface IProps extends IBaseProps { +} + +class SessionSettings extends React.Component { + + constructor(props: IProps) { + super(props); + this.state = { + sessionEnabled: false, + }; + } + + public componentDidMount() { + this.getSession(); + } + + public render(): React.ReactElement<{}> { + const { __ } = this.props; + return ( + <> +

{__("settings.session.title")}

+
+
+ this.setSession(true)} + checked={this.state.sessionEnabled === true} + /> + +
+
+ this.setSession(false)} + checked={this.state.sessionEnabled === false} + /> + +
+
+ + ); + } + + private getSession = () => { + apiAction("session/isEnabled") + .then((sessionEnabled) => this.setState({ sessionEnabled })) + .catch((error) => console.error("Error to fetch api publication/findAll", error)); + } + + private setSession = async (bool: boolean) => { + this.setState({ sessionEnabled: bool }); + + try { + await apiAction("session/enable", bool); + } catch { + + this.getSession(); + } + } +} + +export default withTranslator(SessionSettings); diff --git a/src/renderer/library/components/settings/Settings.tsx b/src/renderer/library/components/settings/Settings.tsx index e6a17be83..29ad21a64 100644 --- a/src/renderer/library/components/settings/Settings.tsx +++ b/src/renderer/library/components/settings/Settings.tsx @@ -13,6 +13,7 @@ import LibraryLayout from "readium-desktop/renderer/library/components/layout/Li import KeyboardSettings from "./KeyboardSettings"; import LanguageSettings from "./LanguageSettings"; +import SessionSettings from "./SessionSettings"; // tslint:disable-next-line: no-empty-interface interface IBaseProps extends TranslatorProps { @@ -35,6 +36,7 @@ class Settings extends React.Component { title={__("header.settings")} > + diff --git a/src/renderer/library/index_library.ts b/src/renderer/library/index_library.ts index 495e3abc6..d884af619 100644 --- a/src/renderer/library/index_library.ts +++ b/src/renderer/library/index_library.ts @@ -12,6 +12,7 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; import { syncIpc, winIpc } from "readium-desktop/common/ipc"; import { ActionWithSender } from "readium-desktop/common/models/sync"; +import { ActionSerializer } from "readium-desktop/common/services/serializer"; import { IS_DEV } from "readium-desktop/preprocessor-directives"; import { winActions } from "readium-desktop/renderer/common/redux/actions"; import { diLibraryGet } from "readium-desktop/renderer/library/di"; @@ -21,8 +22,6 @@ import { initGlobalConverters_GENERIC, initGlobalConverters_SHARED, } from "@r2-shared-js/init-globals"; -import { actionSerializer } from "../common/actionSerializer"; - // import { setLcpNativePluginPath } from "@r2-lcp-js/parser/epub/lcp"; // import { consoleRedirect } from "@r2-navigator-js/electron/renderer/common/console-redirect"; @@ -63,7 +62,7 @@ ipcRenderer.on(winIpc.CHANNEL, (_0: any, data: winIpc.EventPayload) => { case winIpc.EventType.IdResponse: // Initialize window const store = diLibraryGet("store"); - store.dispatch(winActions.initRequest.build(data.payload.winId)); + store.dispatch(winActions.initRequest.build(data.payload.identifier)); break; } }); @@ -77,7 +76,7 @@ ipcRenderer.on(syncIpc.CHANNEL, (_0: any, data: syncIpc.EventPayload) => { const store = diLibraryGet("store"); store.dispatch(Object.assign( {}, - actionSerializer.deserialize(data.payload.action), + ActionSerializer.deserialize(data.payload.action), { sender: data.sender, }, diff --git a/src/renderer/library/redux/middleware/sync.ts b/src/renderer/library/redux/middleware/sync.ts index c0aee91d1..b0b82bc0b 100644 --- a/src/renderer/library/redux/middleware/sync.ts +++ b/src/renderer/library/redux/middleware/sync.ts @@ -18,7 +18,7 @@ const SYNCHRONIZABLE_ACTIONS: string[] = [ readerActions.openRequest.ID, readerActions.closeRequest.ID, readerActions.detachModeRequest.ID, - readerActions.configSetRequest.ID, + // readerActions.setReduxState.ID, // readerActions.saveBookmarkRequest.ID, readerActions.fullScreenRequest.ID, diff --git a/src/renderer/library/redux/reducers/index.ts b/src/renderer/library/redux/reducers/index.ts index 8464c1b50..22f18e013 100644 --- a/src/renderer/library/redux/reducers/index.ts +++ b/src/renderer/library/redux/reducers/index.ts @@ -7,6 +7,7 @@ import { connectRouter } from "connected-react-router"; import { History } from "history"; +import { readerActions } from "readium-desktop/common/redux/actions"; import { dialogReducer } from "readium-desktop/common/redux/reducers/dialog"; import { i18nReducer } from "readium-desktop/common/redux/reducers/i18n"; import { importReducer } from "readium-desktop/common/redux/reducers/import"; @@ -15,6 +16,7 @@ import { keyboardReducer } from "readium-desktop/common/redux/reducers/keyboard" import { toastReducer } from "readium-desktop/common/redux/reducers/toast"; // import { updateReducer } from "readium-desktop/common/redux/reducers/update"; import { apiReducer } from "readium-desktop/renderer/common/redux/reducers/api"; +import { loadReducer } from "readium-desktop/renderer/common/redux/reducers/load"; import { winReducer } from "readium-desktop/renderer/common/redux/reducers/win"; import { downloadReducer } from "readium-desktop/renderer/library/redux/reducers/download"; import { historyReducer } from "readium-desktop/renderer/library/redux/reducers/history"; @@ -46,6 +48,10 @@ export const rootReducer = (history: History) => { toast: toastReducer, download: downloadReducer, history: historyReducer, + // just to recall 'catalog/get' when readerActions.setReduxState is dispatched + updateCatalog: (state: number = 0, action: readerActions.setReduxState.TAction) => + action.type === readerActions.setReduxState.ID ? Number(state) + 1 : state, keyboard: keyboardReducer, -}); + load: loadReducer, + }); }; diff --git a/src/renderer/library/redux/sagas/history.ts b/src/renderer/library/redux/sagas/history.ts index d630a8782..3f3e189dc 100644 --- a/src/renderer/library/redux/sagas/history.ts +++ b/src/renderer/library/redux/sagas/history.ts @@ -5,19 +5,17 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { takeTyped } from "readium-desktop/common/redux/typed-saga"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; import { routerActions, winActions } from "readium-desktop/renderer/library/redux/actions"; -import { all, call, put } from "redux-saga/effects"; +import { put } from "redux-saga/effects"; -function* historyWatcher() { - while (true) { - const action = yield* takeTyped(routerActions.locationChanged.build); - yield put(winActions.history.build(action.payload.location)); - } +function* historyWatcher(action: routerActions.locationChanged.TAction) { + yield put(winActions.history.build(action.payload.location)); } -export function* watchers() { - yield all([ - call(historyWatcher), - ]); +export function saga() { + return takeSpawnEvery( + routerActions.locationChanged.ID, + historyWatcher, + ); } diff --git a/src/renderer/library/redux/sagas/i18n.ts b/src/renderer/library/redux/sagas/i18n.ts index 33d21d02c..472415b13 100644 --- a/src/renderer/library/redux/sagas/i18n.ts +++ b/src/renderer/library/redux/sagas/i18n.ts @@ -6,11 +6,13 @@ // ==LICENSE-END== import { i18nActions } from "readium-desktop/common/redux/actions"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { callTyped } from "readium-desktop/common/redux/sagas/typed-saga"; import { diLibraryGet } from "readium-desktop/renderer/library/di"; -import { all, call, takeEvery } from "redux-saga/effects"; +import { call } from "redux-saga/effects"; function* setLocale(action: i18nActions.setLocale.TAction) { - const translator = diLibraryGet("translator"); + const translator = yield* callTyped(() => diLibraryGet("translator")); const translatorSetLocale = async () => await translator.setLocale(action.payload.locale); @@ -18,12 +20,9 @@ function* setLocale(action: i18nActions.setLocale.TAction) { yield call(translatorSetLocale); } -function* localeWatcher() { - yield takeEvery(i18nActions.setLocale.build, setLocale); -} - -export function* watchers() { - yield all([ - call(localeWatcher), - ]); +export function saga() { + return takeSpawnEvery( + i18nActions.setLocale.ID, + setLocale, + ); } diff --git a/src/renderer/library/redux/sagas/index.ts b/src/renderer/library/redux/sagas/index.ts index f3a240293..29408cb6f 100644 --- a/src/renderer/library/redux/sagas/index.ts +++ b/src/renderer/library/redux/sagas/index.ts @@ -5,28 +5,58 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import * as debug_ from "debug"; +import { + 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"; -import { all, call } from "redux-saga/effects"; +import { all, call, put, take } from "redux-saga/effects"; import * as publicationInfoOpds from "../../../common/redux/sagas/dialog/publicationInfoOpds"; import * as publicationInfoReaderAndLib from "../../../common/redux/sagas/dialog/publicationInfoReaderAndLib"; import * as history from "./history"; import * as i18n from "./i18n"; import * as lcp from "./lcp"; +import * as load from "./load"; import * as opds from "./opds"; import * as sameFileImport from "./sameFileImport"; import * as winInit from "./win"; +// Logger +const filename_ = "readium-desktop:renderer:library:saga:index"; +const debug = debug_(filename_); +debug("_"); + export function* rootSaga() { + + yield i18n.saga(); + + yield all({ + win: take(winActions.initRequest.ID), + i18n: take(i18nActions.setLocale.ID), + keyboard: take(keyboardActions.setShortcuts.ID), + }); + + yield put(winActions.initSuccess.build()); + + yield call(winInit.render); + yield all([ - call(i18n.watchers), - call(lcp.watchers), - call(opds.watchers), - call(winInit.watchers), - call(publicationInfoOpds.watchers), - call(publicationInfoReaderAndLib.watchers), - call(sameFileImport.watchers), - call(history.watchers), - call(publicationInfoSyncTags.watchers), + + publicationInfoOpds.saga(), + + lcp.saga(), + opds.saga(), + + publicationInfoReaderAndLib.saga(), + + sameFileImport.saga(), + history.saga(), + publicationInfoSyncTags.saga(), + + load.saga(), + ]); + } diff --git a/src/renderer/library/redux/sagas/lcp.ts b/src/renderer/library/redux/sagas/lcp.ts index cd86c78f8..e3547e197 100644 --- a/src/renderer/library/redux/sagas/lcp.ts +++ b/src/renderer/library/redux/sagas/lcp.ts @@ -8,29 +8,25 @@ import { DialogTypeName } from "readium-desktop/common/models/dialog"; import { lcpActions } from "readium-desktop/common/redux/actions"; import { dialogActions } from "readium-desktop/common/redux/actions/"; -import { takeTyped } from "readium-desktop/common/redux/typed-saga"; -import { SagaIterator } from "redux-saga"; -import { all, call, put } from "redux-saga/effects"; +import { put, takeEvery } from "redux-saga/effects"; -function* lcpUserKeyCheckRequestWatcher(): SagaIterator { - while (true) { - const action = yield* takeTyped(lcpActions.userKeyCheckRequest.build); +function* lcpUserKeyCheckRequest(action: lcpActions.userKeyCheckRequest.TAction) { + const { hint, publicationView, message } = action.payload; - const { hint, publicationView, message } = action.payload; - - // will call API.unlockPublicationWithPassphrase() - yield put(dialogActions.openRequest.build(DialogTypeName.LcpAuthentication, - { - publicationView, - hint, - message, - }, - )); - } + // will call API.unlockPublicationWithPassphrase() + yield put(dialogActions.openRequest.build( + DialogTypeName.LcpAuthentication, + { + publicationView, + hint, + message, + }, + )); } -export function* watchers() { - yield all([ - call(lcpUserKeyCheckRequestWatcher), - ]); +export function saga() { + return takeEvery( + lcpActions.userKeyCheckRequest.ID, + lcpUserKeyCheckRequest, + ); } diff --git a/src/renderer/library/redux/sagas/load.ts b/src/renderer/library/redux/sagas/load.ts new file mode 100644 index 000000000..8c99f251c --- /dev/null +++ b/src/renderer/library/redux/sagas/load.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 { apiActions, loadActions } from "readium-desktop/common/redux/actions"; +import { put, race, spawn, take } from "redux-saga/effects"; + +export const saga = () => { + return spawn(function*() { + let pending = 0; + + while (true) { + + const { a: request, b: result } = yield race({ + a: take(apiActions.request.ID), + b: take(apiActions.result.ID), + }); + + if (request) { + ++pending; + if (pending === 1) { + yield put(loadActions.busy.build()); + } + } else if (result) { + --pending; + if (pending < 1) { + pending = 0; + yield put(loadActions.idle.build()); + } + } + } + }); +}; diff --git a/src/renderer/library/redux/sagas/opds.ts b/src/renderer/library/redux/sagas/opds.ts index 2c17be99a..ed3155a90 100644 --- a/src/renderer/library/redux/sagas/opds.ts +++ b/src/renderer/library/redux/sagas/opds.ts @@ -8,14 +8,18 @@ import * as debug_ from "debug"; import { TApiMethod } from "readium-desktop/common/api/api.type"; import { apiActions } from "readium-desktop/common/redux/actions"; -import { selectTyped, takeTyped } from "readium-desktop/common/redux/typed-saga"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { raceTyped, selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; +import { IOpdsLinkView, THttpGetOpdsResultView } from "readium-desktop/common/views/opds"; +import { apiSaga } from "readium-desktop/renderer/common/redux/sagas/api"; +import { opdsBrowse } from "readium-desktop/renderer/common/redux/sagas/opdsBrowse"; import { parseOpdsBrowserRoute } from "readium-desktop/renderer/library/opds/route"; import { opdsActions, routerActions } from "readium-desktop/renderer/library/redux/actions"; import { ILibraryRootState } from "readium-desktop/renderer/library/redux/states"; import { ReturnPromiseType } from "readium-desktop/typings/promise"; import { ContentType } from "readium-desktop/utils/content-type"; -import { SagaIterator } from "redux-saga"; -import { all, call, fork, put } from "redux-saga/effects"; +import { call, put, take } from "redux-saga/effects"; +import { delay } from "typed-redux-saga"; export const BROWSE_OPDS_API_REQUEST_ID = "browseOpdsApiResult"; export const SEARCH_OPDS_API_REQUEST_ID = "searchOpdsApiResult"; @@ -25,138 +29,157 @@ export const SEARCH_TERM = "{searchTerms}"; const debug = debug_("readium-desktop:renderer:redux:saga:opds"); // https://reacttraining.com/react-router/web/api/withRouter -// tslint:disable-next-line: max-line-length -// withRouter does not subscribe to location changes like React Redux’s connect does for state changes. Instead, re-renders after location changes propagate out from the component. This means that withRouter does not re-render on route transitions unless its parent component re-renders. -function* browseWatcher(): SagaIterator { - while (true) { - const action = yield* takeTyped(routerActions.locationChanged.build); - const path = action.payload.location.pathname; - - if (path.startsWith("/opds") && path.indexOf("/browse") > 0 ) { - const parsedResult = parseOpdsBrowserRoute(path); - parsedResult.title = decodeURI(parsedResult.title); - debug("request opds browse", parsedResult); - yield put(opdsActions.browseRequest.build( +// withRouter does not subscribe to location changes like React Redux’s connect does for state changes. +// Instead, re-renders after location changes propagate out from the component. +// This means that withRouter does not re-render on route transitions unless its parent component re-renders. +function* browseWatcher(action: routerActions.locationChanged.TAction) { + const path = action.payload.location.pathname; + + if (path.startsWith("/opds") && path.indexOf("/browse") > 0) { + const parsedResult = parseOpdsBrowserRoute(path); + parsedResult.title = decodeURI(parsedResult.title); + debug("request opds browse", parsedResult); + + // re-render opds navigator + yield put( + opdsActions.browseRequest.build( parsedResult.rootFeedIdentifier, parsedResult.level, parsedResult.title, - parsedResult.url)); - yield put(apiActions.clean.build(BROWSE_OPDS_API_REQUEST_ID)); + parsedResult.url, + )); + + yield put(apiActions.clean.build(BROWSE_OPDS_API_REQUEST_ID)); + + const url = parsedResult.url; + debug("opds browse url=", url); + const { b: opdsBrowseAction } = yield* raceTyped({ + a: delay(5000), + b: call(opdsBrowse, url, BROWSE_OPDS_API_REQUEST_ID), + }); + + if (!opdsBrowseAction) { + debug("opds browse url=", url, "timeout 5s"); + return; } + + debug("opds browse data received"); + yield call(updateHeaderLinkWatcher, opdsBrowseAction); } } -function* browseRequestWatcher(): SagaIterator { - while (true) { - const action = yield* takeTyped(opdsActions.browseRequest.build); +function* updateHeaderLinkWatcher(action: apiActions.result.TAction) { - debug("opds browse catched, url requested :", action.payload.url); - yield put( - apiActions.request.build( - BROWSE_OPDS_API_REQUEST_ID, - "opds", "browse", - [action.payload.url])); - } -} + const httpRes = action.payload; + const opdsResultView = httpRes.data; + // debug("opdsResult:", opdsResultView); large dump! -function* updateHeaderLinkWatcher(): SagaIterator { - while (true) { - const action = yield* takeTyped(apiActions.result.build); - const { requestId } = action.meta.api; + if (httpRes?.isSuccess && opdsResultView) { - if (requestId === BROWSE_OPDS_API_REQUEST_ID) { - debug("opds browse data received"); - - const httpRes = action.payload as ReturnPromiseType; - const opdsResultView = httpRes.data; - // debug("opdsResult:", opdsResultView); large dump! - - if (httpRes.isSuccess && opdsResultView) { - - const links = opdsResultView.links; - if (links) { - const putLinks = { - start: links.start[0]?.url, - up: links.up[0]?.url, - bookshelf: links.bookshelf[0]?.url, - self: links.self[0]?.url, - }; - debug("opds browse data received with feed links", putLinks); - yield put(opdsActions.headerLinksUpdate.build(putLinks)); - - if (links.search && links.search.length) { - yield put( - apiActions.request.build( - SEARCH_OPDS_API_REQUEST_ID, - "opds", "getUrlWithSearchLinks", - [links.search])); - // non-blocking getUrlWithSearchLinks search request - yield fork(setSearchLinkInHeader); - } + const links = opdsResultView.links; + if (links) { + const putLinks = { + start: links.start[0]?.url, + up: links.up[0]?.url, + bookshelf: links.bookshelf[0]?.url, + self: links.self[0]?.url, + }; + debug("opds browse data received with feed links", putLinks); + yield put(opdsActions.headerLinksUpdate.build(putLinks)); + + if (links.search?.length) { + + const { b: getUrlAction } = yield* raceTyped({ + a: delay(5000), + b: call(getUrlApi, links.search), + }); + + if (!getUrlAction) { + debug("opds browse url=", links.search, "timeout 5s"); + return; } + + yield call(setSearchLinkInHeader, getUrlAction); } } } } -function* setSearchLinkInHeader(): SagaIterator { +function* getUrlApi(links: IOpdsLinkView[]) { - const action = yield* takeTyped(apiActions.result.build); - const { requestId } = action.meta.api; - let returnUrl: string; + yield apiSaga("opds/getUrlWithSearchLinks", SEARCH_OPDS_API_REQUEST_ID, links); + while (true) { + const action: + apiActions.result.TAction> + = yield take(apiActions.result.build); - if (requestId === SEARCH_OPDS_API_REQUEST_ID) { + const { requestId } = action.meta.api; + if (requestId === SEARCH_OPDS_API_REQUEST_ID) { + return action; + } + } +} - const searchRaw = action.payload as ReturnPromiseType; +function* setSearchLinkInHeader(action: apiActions.result.TAction) { - debug("opds search raw data received", searchRaw); + const searchRaw = action.payload; + debug("opds search raw data received", searchRaw); + let returnUrl: string; + try { + if (new URL(searchRaw)) { + returnUrl = searchRaw; + } + } catch (err) { try { - if (new URL(searchRaw)) { - returnUrl = searchRaw; - } - } catch (err) { - try { - const xmlDom = (new DOMParser()).parseFromString(searchRaw, ContentType.TextXml); - const urlsElem = xmlDom.documentElement.querySelectorAll("Url"); + const xmlDom = (new DOMParser()).parseFromString(searchRaw, ContentType.TextXml); + const urlsElem = xmlDom.documentElement.querySelectorAll("Url"); - for (const urlElem of urlsElem.values()) { - const type = urlElem.getAttribute("type"); + for (const urlElem of urlsElem.values()) { + const type = urlElem.getAttribute("type"); - if (type && type.includes(ContentType.AtomXml)) { - const searchUrl = urlElem.getAttribute("template"); - const url = new URL(searchUrl); + if (type && type.includes(ContentType.AtomXml)) { + const searchUrl = urlElem.getAttribute("template"); + const url = new URL(searchUrl); - if (url.search.includes(SEARCH_TERM) || url.pathname.includes(SEARCH_TERM)) { + if (url.search.includes(SEARCH_TERM) || url.pathname.includes(SEARCH_TERM)) { - // remove search filter not handle yet - let searchLink = searchUrl.replace("{atom:author}", ""); - searchLink = searchLink.replace("{atom:contributor}", ""); - searchLink = searchLink.replace("{atom:title}", ""); + // remove search filter not handle yet + let searchLink = searchUrl.replace("{atom:author}", ""); + searchLink = searchLink.replace("{atom:contributor}", ""); + searchLink = searchLink.replace("{atom:title}", ""); - returnUrl = searchLink; + returnUrl = searchLink; - } } } - } catch (errXml) { - debug("error to parse searchRaw (xml or url)"); } + } catch (errXml) { + debug("error to parse searchRaw (xml or url)"); } + } + if (returnUrl) { debug("opds searchUrl data received", returnUrl); - yield put(opdsActions.search.build({ - url: returnUrl, - level: returnUrl ? yield* selectTyped( - (state: ILibraryRootState) => state.opds.browser.breadcrumb.length) : undefined, - })); + yield put( + opdsActions.search.build( + { + url: returnUrl, + level: returnUrl + ? yield* selectTyped( + (state: ILibraryRootState) => state.opds.browser.breadcrumb.length) + : undefined, + }, + )); + } } -export function* watchers() { - yield all([ - call(browseWatcher), - call(browseRequestWatcher), - call(updateHeaderLinkWatcher), - ]); +export function saga() { + // listen on router location and trigger only on opds browse route + return takeSpawnEvery( + routerActions.locationChanged.ID, + browseWatcher, + (e) => debug(e), + ); } diff --git a/src/renderer/library/redux/sagas/sameFileImport.ts b/src/renderer/library/redux/sagas/sameFileImport.ts index 86ab63dfc..f9fa8a074 100644 --- a/src/renderer/library/redux/sagas/sameFileImport.ts +++ b/src/renderer/library/redux/sagas/sameFileImport.ts @@ -5,63 +5,70 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import * as debug_ from "debug"; import { ToastType } from "readium-desktop/common/models/toast"; import { importActions, toastActions } from "readium-desktop/common/redux/actions"; -import { selectTyped, takeTyped } from "readium-desktop/common/redux/typed-saga"; +import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading"; +import { selectTyped } from "readium-desktop/common/redux/sagas/typed-saga"; import { IOpdsLinkView } from "readium-desktop/common/views/opds"; import { apiSaga } from "readium-desktop/renderer/common/redux/sagas/api"; import { diLibraryGet } from "readium-desktop/renderer/library/di"; import { ILibraryRootState } from "readium-desktop/renderer/library/redux/states"; -import { all, call, put } from "redux-saga/effects"; +import { all, put } from "redux-saga/effects"; import { Download } from "../states/download"; const REQUEST_ID = "SAME_FILE_IMPORT_REQUEST"; -// FIXME : a lot of Downoload interface (ex: state and model) +// Logger +const filename_ = "readium-desktop:renderer:redux:saga:same-file-import"; +const debug = debug_(filename_); + +// FIXME : a lot of Download interface (ex: state and model) const findDownload = (dls: Download[], link: IOpdsLinkView) => dls.find( (dl) => dl.url === link.url, ); -function* sameFileImportWatcher() { - while (true) { - const action = yield* takeTyped(importActions.verify.build); +function* sameFileImport(action: importActions.verify.TAction) { - const { link, title, r2OpdsPublicationBase64 } = action.payload; + const { link, title, r2OpdsPublicationBase64 } = action.payload; - const downloads = yield* selectTyped( - (state: ILibraryRootState) => state.download?.downloads); + const downloads = yield* selectTyped( + (state: ILibraryRootState) => state.download?.downloads); - if (Array.isArray(downloads) - && findDownload(downloads, link)) { + if (Array.isArray(downloads) + && findDownload(downloads, link)) { - const translator = diLibraryGet("translator"); + const translator = diLibraryGet("translator"); - yield put( - toastActions.openRequest.build( - ToastType.Success, - translator.translate("message.import.alreadyImport", - { - title: title || "", - }, - ), + yield put( + toastActions.openRequest.build( + ToastType.Success, + translator.translate("message.import.alreadyImport", + { + title: title || "", + }, ), - ); + ), + ); - } else { + } else { - yield* apiSaga("publication/importOpdsPublicationLink", - REQUEST_ID, - link, - r2OpdsPublicationBase64, - ); - } + yield apiSaga("publication/importOpdsPublicationLink", + REQUEST_ID, + link, + r2OpdsPublicationBase64, + ); } } -export function* watchers() { - yield all([ - call(sameFileImportWatcher), +export function saga() { + return all([ + takeSpawnLeading( + importActions.verify.ID, + sameFileImport, + (e) => debug(e), + ), ]); } diff --git a/src/renderer/library/redux/sagas/win.ts b/src/renderer/library/redux/sagas/win.ts index 24324b142..586a6309d 100644 --- a/src/renderer/library/redux/sagas/win.ts +++ b/src/renderer/library/redux/sagas/win.ts @@ -7,26 +7,9 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; -import { i18nActions, keyboardActions } from "readium-desktop/common/redux/actions/"; -import { winActions } from "readium-desktop/renderer/common/redux/actions"; import { diLibraryGet } from "readium-desktop/renderer/library/di"; -import { SagaIterator } from "redux-saga"; -import { all, call, put, take } from "redux-saga/effects"; - -function* winInitWatcher(): SagaIterator { - yield all({ - win: take(winActions.initRequest.ID), - i18n: take(i18nActions.setLocale.ID), - keyboard: take(keyboardActions.setShortcuts.ID), - }); - - yield put(winActions.initSuccess.build()); -} - -function* winStartWatcher(): SagaIterator { - - yield take(winActions.initSuccess.ID); +export function render() { // starting point to mounting React to the DOM ReactDOM.render( React.createElement( @@ -35,10 +18,3 @@ function* winStartWatcher(): SagaIterator { document.getElementById("app"), ); } - -export function* watchers() { - yield all([ - call(winInitWatcher), - call(winStartWatcher), - ]); -} diff --git a/src/renderer/library/redux/states/index.ts b/src/renderer/library/redux/states/index.ts index bc33ec928..32f68e849 100644 --- a/src/renderer/library/redux/states/index.ts +++ b/src/renderer/library/redux/states/index.ts @@ -7,8 +7,9 @@ import { RouterState } from "connected-react-router"; import { ImportState } from "readium-desktop/common/redux/states/import"; +import { ICommonRootState } from "readium-desktop/common/redux/states/renderer/commonRootState"; import { IBreadCrumbItem } from "readium-desktop/renderer/common/models/breadcrumbItem.interface"; -import { ICommonRootState } from "readium-desktop/renderer/common/redux/states"; +import { ILoadState } from "readium-desktop/renderer/common/redux/states/load"; import { IRouterLocationState } from "../../routing"; import { DownloadState } from "./download"; @@ -27,4 +28,6 @@ export interface ILibraryRootState extends ICommonRootState { import: ImportState; download: DownloadState; history: THistoryState; + updateCatalog: number; + load: ILoadState; } diff --git a/src/renderer/reader/components/Reader.tsx b/src/renderer/reader/components/Reader.tsx index 069f901a0..8b1ed74d8 100644 --- a/src/renderer/reader/components/Reader.tsx +++ b/src/renderer/reader/components/Reader.tsx @@ -7,15 +7,18 @@ import * as classNames from "classnames"; import * as path from "path"; +import * as r from "ramda"; import * as React from "react"; import { connect } from "react-redux"; +import { computeReadiumCssJsonMessage } from "readium-desktop/common/computeReadiumCssJsonMessage"; import { DEBUG_KEYBOARD, keyboardShortcutsMatch } from "readium-desktop/common/keyboard"; import { DialogTypeName } from "readium-desktop/common/models/dialog"; import { - Reader as ReaderModel, ReaderConfig, ReaderConfigBooleans, ReaderConfigStrings, - ReaderConfigStringsAdjustables, + ReaderConfig, ReaderConfigBooleans, ReaderConfigStrings, ReaderConfigStringsAdjustables, + ReaderMode, } from "readium-desktop/common/models/reader"; import { dialogActions, readerActions } from "readium-desktop/common/redux/actions"; +import { IReaderRootState } from "readium-desktop/common/redux/states/renderer/readerRootState"; import { formatTime } from "readium-desktop/common/utils/time"; import { LocatorView } from "readium-desktop/common/views/locator"; import { PublicationView } from "readium-desktop/common/views/publication"; @@ -36,23 +39,17 @@ import { apiSubscribe } from "readium-desktop/renderer/reader/apiSubscribe"; import ReaderFooter from "readium-desktop/renderer/reader/components/ReaderFooter"; import ReaderHeader from "readium-desktop/renderer/reader/components/ReaderHeader"; import { diReaderGet } from "readium-desktop/renderer/reader/di"; -import { IReaderRootState } from "readium-desktop/renderer/reader/redux/states"; import { TChangeEventOnInput, TChangeEventOnSelect, TKeyboardEventOnAnchor, TMouseEventOnAnchor, TMouseEventOnSpan, } from "readium-desktop/typings/react"; import { TDispatch } from "readium-desktop/typings/redux"; import { ObjectKeys } from "readium-desktop/utils/object-keys-values"; +// import { encodeURIComponent_RFC3986 } from "readium-desktop/utils/url"; import { Unsubscribe } from "redux"; import { TaJsonDeserialize } from "@r2-lcp-js/serializable"; -import { - IEventPayload_R2_EVENT_CLIPBOARD_COPY, IEventPayload_R2_EVENT_READIUMCSS, -} from "@r2-navigator-js/electron/common/events"; -import { - colCountEnum, IReadiumCSS, readiumCSSDefaults, textAlignEnum, -} from "@r2-navigator-js/electron/common/readium-css-settings"; -import { getURLQueryParams } from "@r2-navigator-js/electron/renderer/common/querystring"; +import { IEventPayload_R2_EVENT_CLIPBOARD_COPY } from "@r2-navigator-js/electron/common/events"; import { getCurrentReadingLocation, handleLinkLocator, handleLinkUrl, installNavigatorDOM, isLocatorVisible, LocatorExtended, navLeftOrRight, readiumCssUpdate, setEpubReadingSystemInfo, @@ -62,6 +59,7 @@ import { reloadContent } from "@r2-navigator-js/electron/renderer/location"; import { Locator as R2Locator } from "@r2-shared-js/models/locator"; import { Publication as R2Publication } from "@r2-shared-js/models/publication"; +import { readerLocalActionSetConfig, readerLocalActionSetLocator } from "../redux/actions"; import optionsValues, { AdjustableSettingsNumber, IReaderMenuProps, IReaderOptionsProps, } from "./options-values"; @@ -84,74 +82,11 @@ const capitalizedAppName = _APP_NAME.charAt(0).toUpperCase() + _APP_NAME.substri // supportFetchAPI: true, // }); -// TODO: centralize this code, currently duplicated -// see src/main/streamer.js -const computeReadiumCssJsonMessage = (settings: ReaderConfig): IEventPayload_R2_EVENT_READIUMCSS => { - - const cssJson: IReadiumCSS = { - - a11yNormalize: readiumCSSDefaults.a11yNormalize, - - backgroundColor: readiumCSSDefaults.backgroundColor, - - bodyHyphens: readiumCSSDefaults.bodyHyphens, - - colCount: settings.colCount === "1" ? colCountEnum.one : - (settings.colCount === "2" ? colCountEnum.two : colCountEnum.auto), - - darken: settings.darken, - - font: settings.font, - - fontSize: settings.fontSize, - - invert: settings.invert, - - letterSpacing: settings.letterSpacing, - - ligatures: readiumCSSDefaults.ligatures, - - lineHeight: settings.lineHeight, - - night: settings.night, - - pageMargins: settings.pageMargins, - - paged: settings.paged, - - paraIndent: readiumCSSDefaults.paraIndent, - - paraSpacing: settings.paraSpacing, - - sepia: settings.sepia, - - noFootnotes: settings.noFootnotes, - - textAlign: settings.align === textAlignEnum.left ? textAlignEnum.left : - (settings.align === textAlignEnum.right ? textAlignEnum.right : - (settings.align === textAlignEnum.justify ? textAlignEnum.justify : - (settings.align === textAlignEnum.start ? textAlignEnum.start : undefined))), - - textColor: readiumCSSDefaults.textColor, - - typeScale: readiumCSSDefaults.typeScale, - - wordSpacing: settings.wordSpacing, - - mathJax: settings.enableMathJax, - - reduceMotion: readiumCSSDefaults.reduceMotion, - }; - const jsonMsg: IEventPayload_R2_EVENT_READIUMCSS = { setCSS: cssJson }; - return jsonMsg; -}; - -const queryParams = getURLQueryParams(); -const lcpHint = queryParams.lcpHint; +// const queryParams = getURLQueryParams(); +// const lcpHint = queryParams.lcpHint; // pub is undefined when loaded in dependency injection by library webview. // Dependency injection is shared between all the renderer view -const publicationJsonUrl = queryParams.pub; -// ?.startsWith(READIUM2_ELECTRON_HTTP_PROTOCOL) +// const publicationJsonUrl = queryParams.pub?.startsWith(READIUM2_ELECTRON_HTTP_PROTOCOL) // ? convertCustomSchemeToHttpUrl(queryParams.pub) // : queryParams.pub; @@ -175,7 +110,7 @@ interface IProps extends IBaseProps, ReturnType, ReturnT interface IState { - // publicationJsonUrl?: string; + publicationJsonUrl?: string; // title?: string; publicationView: PublicationView | undefined; @@ -194,6 +129,8 @@ interface IState { visibleBookmarkList: LocatorView[]; currentLocation: LocatorExtended; bookmarks: LocatorView[] | undefined; + + readerMode: ReaderMode; } class Reader extends React.Component { @@ -226,7 +163,7 @@ class Reader extends React.Component { this.refToolbar = React.createRef(); this.state = { - // publicationJsonUrl: "HTTP://URL", + publicationJsonUrl: "HTTP://URL", lcpHint: "LCP hint", // title: "TITLE", lcpPass: "LCP pass", @@ -244,6 +181,8 @@ class Reader extends React.Component { visibleBookmarkList: [], currentLocation: undefined, bookmarks: undefined, + + readerMode: ReaderMode.Attached, }; this.handleMenuButtonClick = this.handleMenuButtonClick.bind(this); @@ -266,6 +205,22 @@ class Reader extends React.Component { ensureKeyboardListenerIsInstalled(); this.registerAllKeyboardListeners(); + const store = diReaderGet("store"); + + const pubId = store.getState().reader.info.publicationIdentifier; + const locator = store.getState().reader.locator; + const manifestUrl = store.getState().reader.info.manifestUrl; + // const publicationJsonUrl = convertCustomSchemeToHttpUrl( + // encodeURIComponent_RFC3986( + // convertHttpUrlToCustomScheme(manifestUrl), + // ), + // ); + const publicationJsonUrl = manifestUrl; + + this.setState({ + publicationJsonUrl, + }); + setKeyDownEventHandler(keyDownEventHandler); setKeyUpEventHandler(keyUpEventHandler); @@ -273,12 +228,12 @@ class Reader extends React.Component { // publicationJsonUrl, // }); - if (lcpHint) { - this.setState({ - lcpHint, - lcpPass: this.state.lcpPass + " [" + lcpHint + "]", - }); - } + // if (lcpHint) { + // this.setState({ + // lcpHint, + // lcpPass: this.state.lcpPass + " [" + lcpHint + "]", + // }); + // } // TODO: this is a short-term hack. // Can we instead subscribe to Redux action type == CloseRequest, @@ -289,51 +244,26 @@ class Reader extends React.Component { }); }); - let docHref: string = queryParams.docHref; - let docSelector: string = queryParams.docSelector; - let docProgression: string = queryParams.docProgression; + // let docHref: string = queryParams.docHref; + // let docSelector: string = queryParams.docSelector; - if (docHref) { - // Decode base64 - docHref = window.atob(docHref); - } - if (docSelector) { - // Decode base64 - try { - docSelector = window.atob(docSelector); - } catch (err) { - console.log(docSelector); - console.log(err); - docSelector = undefined; - } - } - let progression: number | undefined; - if (docProgression) { - // Decode base64 - try { - docProgression = window.atob(docProgression); - } catch (err) { - console.log(docProgression); - console.log(err); - docProgression = undefined; - } - // percentage number - if (docProgression) { - progression = parseFloat(docProgression); - } - } + // if (docHref && docSelector) { + // // Decode base64 + // docHref = window.atob(docHref); + // docSelector = window.atob(docSelector); + // } // Note that CFI, etc. can optionally be restored too, // but navigator currently uses cssSelector as the primary - const locator: R2Locator = { - href: docHref, - locations: { - cfi: undefined, - cssSelector: docSelector, - position: undefined, - progression, - }, - }; + // const locator: R2Locator = { + // href: docHref, + // locations: { + // cfi: undefined, + // cssSelector: docSelector, + // position: undefined, + // progression: undefined, + // }, + // }; setReadingLocationSaver(this.handleReadingLocationChange); @@ -344,12 +274,14 @@ class Reader extends React.Component { "reader/addBookmark", ], this.findBookmarks); - apiAction("publication/get", queryParams.pubId, false) + apiAction("publication/get", pubId, false) .then(async (publicationView) => { - this.setState({publicationView}); - await this.loadPublicationIntoViewport(publicationView, locator); + this.setState({ publicationView }); + await this.loadPublicationIntoViewport(publicationView, locator.locator); }) .catch((error) => console.error("Error to fetch api publication/get", error)); + + this.getReaderMode(); } public async componentDidUpdate(oldProps: IProps, oldState: IState) { @@ -393,74 +325,73 @@ class Reader extends React.Component { }; return ( -
- {this.props.__("accessibility.toolbar")} - + {this.props.__("accessibility.toolbar")} + +
+ { await this.handleToggleBookmark(false); } } + isOnBookmark={this.state.visibleBookmarkList.length > 0} + readerOptionsProps={readerOptionsProps} + readerMenuProps={readerMenuProps} + displayPublicationInfo={this.displayPublicationInfo} + currentLocation={this.state.currentLocation} /> -
- { await this.handleToggleBookmark(false); } } - isOnBookmark={this.state.visibleBookmarkList.length > 0} - readerOptionsProps={readerOptionsProps} - readerMenuProps={readerMenuProps} - displayPublicationInfo={this.displayPublicationInfo} - currentLocation={this.state.currentLocation} - /> -
- - + tabIndex={-1}>{this.props.__("accessibility.mainContent")} +
+
-
+ +
); } @@ -717,7 +648,7 @@ class Reader extends React.Component { const r2PublicationStr = Buffer.from(publicationView.r2PublicationBase64, "base64").toString("utf-8"); const r2PublicationJson = JSON.parse(r2PublicationStr); const r2Publication = TaJsonDeserialize(r2PublicationJson, R2Publication); - this.setState({r2Publication}); + this.setState({ r2Publication }); if (r2Publication.Metadata && r2Publication.Metadata.Title) { const title = this.props.translator.translateContentField(r2Publication.Metadata.Title); @@ -736,8 +667,8 @@ class Reader extends React.Component { preloadPath = "file://" + path.normalize(path.join((global as any).__dirname, preloadPath)); } else { preloadPath = "r2-navigator-js/dist/" + - "es6-es2015" + - "/src/electron/renderer/webview/preload.js"; + "es6-es2015" + + "/src/electron/renderer/webview/preload.js"; if (_RENDERER_READER_BASE_URL === "file://") { // dist/prod mode (without WebPack HMR Hot Module Reload HTTP server) @@ -753,47 +684,22 @@ class Reader extends React.Component { const clipboardInterceptor = !publicationView.lcp ? undefined : (clipboardData: IEventPayload_R2_EVENT_CLIPBOARD_COPY) => { - apiAction("reader/clipboardCopy", queryParams.pubId, clipboardData) + apiAction("reader/clipboardCopy", this.props.pubId, clipboardData) .catch((error) => console.error("Error to fetch api reader/clipboardCopy", error)); }; - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - console.log("######"); - const sessionInfoJson: any = { - id: "0000-1110-222", - test: 1, - other: true, - obj: { - "sub-key": null, - }, - }; - console.log(sessionInfoJson); - const sessionInfoStr = JSON.stringify(sessionInfoJson, null, 4); - console.log(sessionInfoStr); - console.log(Buffer.from(sessionInfoStr).toString("base64")); + const store = diReaderGet("store"); + const winId = store.getState().win.identifier; installNavigatorDOM( r2Publication, - publicationJsonUrl, + this.state.publicationJsonUrl, "publication_viewport", preloadPath, locator, true, clipboardInterceptor, - sessionInfoStr, + winId, computeReadiumCssJsonMessage(this.props.readerConfig), ); } @@ -807,15 +713,16 @@ class Reader extends React.Component { } private saveReadingLocation(loc: LocatorExtended) { - apiAction("reader/setLastReadingLocation", queryParams.pubId, loc.locator) - .catch((error) => console.error("Error to fetch api reader/setLastReadingLocation", error)); - + // this.props.setLastReadingLocation(queryParams.pubId, loc.locator); + // apiAction("reader/setLastReadingLocation", this.props.pubId, loc.locator) + // .catch((error) => console.error("Error to fetch api reader/setLastReadingLocation", error)); + this.props.setLocator(loc); } private async handleReadingLocationChange(loc: LocatorExtended) { this.findBookmarks(); this.saveReadingLocation(loc); - this.setState({currentLocation: getCurrentReadingLocation()}); + this.setState({ currentLocation: getCurrentReadingLocation() }); // No need to explicitly refresh the bookmarks status here, // as componentDidUpdate() will call the function after setState(): // await this.checkBookmarks(); @@ -836,13 +743,13 @@ class Reader extends React.Component { if (!locator || locator.href === bookmark.locator.href) { if (this.state.r2Publication) { // isLocatorVisible() API only once navigator ready const isVisible = await isLocatorVisible(bookmark.locator); - if ( isVisible ) { + if (isVisible) { visibleBookmarkList.push(bookmark); } } } } - this.setState({visibleBookmarkList}); + this.setState({ visibleBookmarkList }); } private focusMainAreaLandmarkAndCloseMenu() { @@ -875,41 +782,42 @@ class Reader extends React.Component { this.focusMainAreaLandmarkAndCloseMenu(); - const newUrl = publicationJsonUrl + "/../" + url; + const newUrl = this.state.publicationJsonUrl + "/../" + url; handleLinkUrl(newUrl); } private async handleToggleBookmark(fromKeyboard?: boolean) { - if (!this.state.currentLocation || !this.state.currentLocation.locator) { + if ( !this.state.currentLocation?.locator) { return; } + const locator = this.state.currentLocation.locator; + const visibleBookmark = this.state.visibleBookmarkList; + await this.checkBookmarks(); // updates this.state.visibleBookmarkList const deleteAllVisibleBookmarks = // "toggle" only if there is a single bookmark in the content visible inside the viewport // otherwise preserve existing, and add new one (see addCurrentLocationToBookmarks below) - this.state.visibleBookmarkList.length === 1 && + visibleBookmark.length === 1 && // CTRL-B (keyboard interaction) and audiobooks: // do not toggle: never delete, just add current reading location to bookmarks !fromKeyboard && !this.state.currentLocation.audioPlaybackInfo && - (!this.state.currentLocation.locator.text?.highlight || + (!locator.text?.highlight || // "toggle" only if visible bookmark == current reading location - this.state.visibleBookmarkList[0].locator.href === this.state.currentLocation.locator.href && - // tslint:disable-next-line: max-line-length - this.state.visibleBookmarkList[0].locator.locations.cssSelector === this.state.currentLocation.locator.locations.cssSelector && - // tslint:disable-next-line: max-line-length - this.state.visibleBookmarkList[0].locator.text?.highlight === this.state.currentLocation.locator.text.highlight + visibleBookmark[0].locator.href === locator.href && + visibleBookmark[0].locator.locations.cssSelector === locator.locations.cssSelector && + visibleBookmark[0].locator.text?.highlight === locator.text.highlight ) ; if (deleteAllVisibleBookmarks) { - for (const bookmark of this.state.visibleBookmarkList) { + for (const bookmark of visibleBookmark) { try { await apiAction("reader/deleteBookmark", bookmark.identifier); } catch (e) { @@ -922,24 +830,24 @@ class Reader extends React.Component { } const addCurrentLocationToBookmarks = - !this.state.visibleBookmarkList.length || - !this.state.visibleBookmarkList.find((b) => { + !visibleBookmark.length || + !visibleBookmark.find((b) => { const identical = - b.locator.href === this.state.currentLocation.locator.href && - (b.locator.locations.progression === this.state.currentLocation.locator.locations.progression || - b.locator.locations.cssSelector && this.state.currentLocation.locator.locations.cssSelector && - b.locator.locations.cssSelector === this.state.currentLocation.locator.locations.cssSelector) && - b.locator.text?.highlight === this.state.currentLocation.locator.text?.highlight; + b.locator.href === locator.href && + (b.locator.locations.progression === locator.locations.progression || + b.locator.locations.cssSelector && locator.locations.cssSelector && + b.locator.locations.cssSelector === locator.locations.cssSelector) && + b.locator.text?.highlight === locator.text?.highlight; return identical; }) && - (this.state.currentLocation.audioPlaybackInfo || this.state.currentLocation.locator.text?.highlight); + (this.state.currentLocation.audioPlaybackInfo || locator.text?.highlight); if (addCurrentLocationToBookmarks) { let name: string | undefined; - if (this.state.currentLocation.locator?.text?.highlight) { - name = this.state.currentLocation.locator.text.highlight; + if (locator?.text?.highlight) { + name = locator.text.highlight; } else if (this.state.currentLocation.selectionInfo?.cleanText) { name = this.state.currentLocation.selectionInfo.cleanText; } else if (this.state.currentLocation.audioPlaybackInfo) { @@ -949,8 +857,9 @@ class Reader extends React.Component { const timestamp = formatTime(this.state.currentLocation.audioPlaybackInfo.globalTime); name = `${timestamp} (${percent}%)`; } + try { - await apiAction("reader/addBookmark", queryParams.pubId, this.state.currentLocation.locator, name); + await apiAction("reader/addBookmark", this.props.pubId, locator, name); } catch (e) { console.error("Error to fetch api reader/addBookmark", e); } @@ -958,16 +867,17 @@ class Reader extends React.Component { } private handleReaderClose() { - this.props.closeReader(this.props.reader); + this.props.closeReader(); } private handleReaderDetach() { - this.props.detachReader(this.props.reader); + this.props.detachReader(); + this.setState({ readerMode: ReaderMode.Detached }); } private handleFullscreenClick() { this.props.toggleFullscreen(!this.state.fullscreen); - this.setState({fullscreen: !this.state.fullscreen}); + this.setState({ fullscreen: !this.state.fullscreen }); } private handleSettingsClick() { @@ -979,8 +889,7 @@ class Reader extends React.Component { } private handleSettingsSave(readerConfig: ReaderConfig) { - const store = diReaderGet("store"); - store.dispatch(readerActions.configSetRequest.build(readerConfig)); + this.props.setConfig(readerConfig); if (this.state.r2Publication) { readiumCssUpdate(computeReadiumCssJsonMessage(readerConfig)); @@ -1010,13 +919,12 @@ class Reader extends React.Component { } } - // TODO: smarter clone? - const readerConfig = JSON.parse(JSON.stringify(this.props.readerConfig)); + const readerConfig = r.clone(this.props.readerConfig); const typedName = name as (typeof value extends string ? keyof ReaderConfigStrings : keyof ReaderConfigBooleans); const typedValue = - value as (typeof value extends string ? string : boolean); + value as (typeof value extends string ? string : boolean); readerConfig[typedName] = typedValue; if (readerConfig.paged) { @@ -1038,8 +946,7 @@ class Reader extends React.Component { } } - // TODO: smarter clone? - const readerConfig = JSON.parse(JSON.stringify(this.props.readerConfig)); + const readerConfig = r.clone(this.props.readerConfig); readerConfig[name] = optionsValues[name][valueNum]; @@ -1056,10 +963,18 @@ class Reader extends React.Component { } private findBookmarks() { - apiAction("reader/findBookmarks", queryParams.pubId) - .then((bookmarks) => this.setState({bookmarks})) + apiAction("reader/findBookmarks", this.props.pubId) + .then((bookmarks) => this.setState({ bookmarks })) .catch((error) => console.error("Error to fetch api reader/findBookmarks", error)); } + + // TODO + // replaced getMode API with an action broadcasted to every reader and catch by reducers + private getReaderMode = () => { + apiAction("reader/getMode") + .then((mode) => this.setState({ readerMode: mode })) + .catch((error) => console.error("Error to fetch api reader/getMode", error)); + } } const mapStateToProps = (state: IReaderRootState, _props: IBaseProps) => { @@ -1084,13 +999,14 @@ const mapStateToProps = (state: IReaderRootState, _props: IBaseProps) => { } return { - reader: state.reader.reader, + readerInfo: state.reader.info, readerConfig: state.reader.config, indexes, keyboardShortcuts: state.keyboard.shortcuts, - mode: state.reader.mode, infoOpen: state.dialog.open && state.dialog.type === DialogTypeName.PublicationInfoReader, + pubId: state.reader.info.publicationIdentifier, + locator: state.reader.locator, }; }; @@ -1103,11 +1019,11 @@ const mapDispatchToProps = (dispatch: TDispatch, _props: IBaseProps) => { dispatch(readerActions.fullScreenRequest.build(false)); } }, - closeReader: (reader: ReaderModel) => { - dispatch(readerActions.closeRequest.build(reader, true)); + closeReader: () => { + dispatch(readerActions.closeRequest.build()); }, - detachReader: (reader: ReaderModel) => { - dispatch(readerActions.detachModeRequest.build(reader)); + detachReader: () => { + dispatch(readerActions.detachModeRequest.build()); }, displayPublicationInfo: (pubId: string) => { dispatch(dialogActions.openRequest.build(DialogTypeName.PublicationInfoReader, @@ -1116,6 +1032,12 @@ const mapDispatchToProps = (dispatch: TDispatch, _props: IBaseProps) => { }, )); }, + setLocator: (locator: LocatorExtended) => { + dispatch(readerLocalActionSetLocator.build(locator)); + }, + setConfig: (config: ReaderConfig) => { + dispatch(readerLocalActionSetConfig.build(config)); + }, }; }; diff --git a/src/renderer/reader/components/ReaderMenu.tsx b/src/renderer/reader/components/ReaderMenu.tsx index ac92918c4..4c8041f16 100644 --- a/src/renderer/reader/components/ReaderMenu.tsx +++ b/src/renderer/reader/components/ReaderMenu.tsx @@ -8,6 +8,7 @@ import classnames from "classnames"; import * as React from "react"; import { connect } from "react-redux"; +import { IReaderRootState } from "readium-desktop/common/redux/states/renderer/readerRootState"; import { LocatorView } from "readium-desktop/common/views/locator"; import * as DeleteIcon from "readium-desktop/renderer/assets/icons/baseline-close-24px.svg"; import * as EditIcon from "readium-desktop/renderer/assets/icons/baseline-edit-24px.svg"; @@ -24,7 +25,6 @@ import { Unsubscribe } from "redux"; import { LocatorExtended } from "@r2-navigator-js/electron/renderer/index"; import { Link } from "@r2-shared-js/models/publication-link"; -import { IReaderRootState } from "../redux/states"; import { IReaderMenuProps } from "./options-values"; import SideMenu from "./sideMenu/SideMenu"; import { SectionData } from "./sideMenu/sideMenuData"; @@ -415,7 +415,7 @@ export class ReaderMenu extends React.Component { const mapStateToProps = (state: IReaderRootState, _props: IBaseProps) => { return { - pubId: state.reader.reader.publicationIdentifier, + pubId: state.reader.info.publicationIdentifier, }; }; diff --git a/src/renderer/reader/components/ReaderOptions.tsx b/src/renderer/reader/components/ReaderOptions.tsx index 3dc56624a..5a447e49e 100644 --- a/src/renderer/reader/components/ReaderOptions.tsx +++ b/src/renderer/reader/components/ReaderOptions.tsx @@ -6,8 +6,11 @@ // ==LICENSE-END== import * as React from "react"; +import { connect } from "react-redux"; import { Font } from "readium-desktop/common/models/font"; import { ReaderConfig } from "readium-desktop/common/models/reader"; +import { readerActions, toastActions } from "readium-desktop/common/redux/actions"; +import { readerConfigInitialState } from "readium-desktop/common/redux/states/reader"; import * as AutoIcon from "readium-desktop/renderer/assets/icons/auto.svg"; import * as ColumnIcon from "readium-desktop/renderer/assets/icons/colonne.svg"; import * as Column2Icon from "readium-desktop/renderer/assets/icons/colonne2.svg"; @@ -21,16 +24,18 @@ import { TranslatorProps, withTranslator, } from "readium-desktop/renderer/common/components/hoc/translator"; import SVG from "readium-desktop/renderer/common/components/SVG"; +import { TDispatch } from "readium-desktop/typings/redux"; import fontList from "readium-desktop/utils/fontList"; import { colCountEnum, textAlignEnum } from "@r2-navigator-js/electron/common/readium-css-settings"; +import { readerLocalActionSetConfig } from "../redux/actions"; import optionsValues, { IReaderOptionsProps } from "./options-values"; import SideMenu from "./sideMenu/SideMenu"; import { SectionData } from "./sideMenu/sideMenuData"; import classNames = require("classnames"); - +import { ToastType } from "readium-desktop/common/models/toast"; // tslint:disable-next-line: no-empty-interface interface IBaseProps extends TranslatorProps, IReaderOptionsProps { focusSettingMenuButton: () => void; @@ -41,7 +46,7 @@ interface IBaseProps extends TranslatorProps, IReaderOptionsProps { // ReturnType // ReturnType // tslint:disable-next-line: no-empty-interface -interface IProps extends IBaseProps { +interface IProps extends IBaseProps, ReturnType { } enum themeType { @@ -96,6 +101,11 @@ export class ReaderOptions extends React.Component { ]); } + sections.push({ + title: __("reader.settings.save.title"), + content: this.saveConfig(), + }); + return ( { ); } + private saveConfig() { + + const { readerConfig, __ } = this.props; + + return ( + +
+ + + +
+ ); + } + private mathJax() { - const {readerConfig} = this.props; + const { readerConfig } = this.props; return (
{ } private themeContent() { - const {__, readerConfig} = this.props; + const { __, readerConfig } = this.props; const withoutTheme = !readerConfig.sepia && !readerConfig.night; return (
@@ -138,7 +178,7 @@ export class ReaderOptions extends React.Component { checked={withoutTheme} />
@@ -509,4 +549,21 @@ export class ReaderOptions extends React.Component { } } -export default withTranslator(ReaderOptions); +const mapDispatchToProps = (dispatch: TDispatch, _props: IBaseProps) => { + return { + setDefaultConfig: (...config: Parameters) => { + + if (config.length === 0) { + + dispatch(readerActions.configSetDefault.build(readerConfigInitialState)); + dispatch(readerLocalActionSetConfig.build(readerConfigInitialState)); + } else { + dispatch(readerActions.configSetDefault.build(...config)); + } + + dispatch(toastActions.openRequest.build(ToastType.Success, "👍")); + }, + }; +}; + +export default connect(undefined, mapDispatchToProps)(withTranslator(ReaderOptions)); diff --git a/src/renderer/reader/components/dialog/DialogManager.tsx b/src/renderer/reader/components/dialog/DialogManager.tsx index 65367d0b8..cf2f29291 100644 --- a/src/renderer/reader/components/dialog/DialogManager.tsx +++ b/src/renderer/reader/components/dialog/DialogManager.tsx @@ -7,8 +7,8 @@ import * as React from "react"; import { connect } from "react-redux"; +import { IReaderRootState } from "readium-desktop/common/redux/states/renderer/readerRootState"; import PublicationInfo from "readium-desktop/renderer/reader/components/dialog/publicationInfos/PublicationInfo"; -import { IReaderRootState } from "readium-desktop/renderer/reader/redux/states"; // tslint:disable-next-line: no-empty-interface interface IBaseProps { diff --git a/src/renderer/reader/components/dialog/publicationInfos/PublicationInfo.tsx b/src/renderer/reader/components/dialog/publicationInfos/PublicationInfo.tsx index e82af7b63..dabfe07ce 100644 --- a/src/renderer/reader/components/dialog/publicationInfos/PublicationInfo.tsx +++ b/src/renderer/reader/components/dialog/publicationInfos/PublicationInfo.tsx @@ -10,6 +10,7 @@ import * as React from "react"; import { connect } from "react-redux"; import { DialogType, DialogTypeName } from "readium-desktop/common/models/dialog"; import * as dialogActions from "readium-desktop/common/redux/actions/dialog"; +import { IReaderRootState } from "readium-desktop/common/redux/states/renderer/readerRootState"; import { PublicationInfoContent, } from "readium-desktop/renderer/common/components/dialog/publicationInfos/publicationInfoContent"; @@ -19,7 +20,6 @@ import { import { TranslatorProps, withTranslator, } from "readium-desktop/renderer/common/components/hoc/translator"; -import { IReaderRootState } from "readium-desktop/renderer/reader/redux/states"; import { TDispatch } from "readium-desktop/typings/redux"; import TagManager from "./TagManager"; diff --git a/src/renderer/reader/components/dialog/publicationInfos/TagManager.tsx b/src/renderer/reader/components/dialog/publicationInfos/TagManager.tsx index 794cc0aa4..e0fe48687 100644 --- a/src/renderer/reader/components/dialog/publicationInfos/TagManager.tsx +++ b/src/renderer/reader/components/dialog/publicationInfos/TagManager.tsx @@ -10,6 +10,7 @@ import * as React from "react"; import { connect } from "react-redux"; import { DialogType, DialogTypeName } from "readium-desktop/common/models/dialog"; import { dialogActions } from "readium-desktop/common/redux/actions"; +import { IReaderRootState } from "readium-desktop/common/redux/states/renderer/readerRootState"; import { PublicationView } from "readium-desktop/common/views/publication"; import AddTag from "readium-desktop/renderer/common/components/dialog/publicationInfos/tag/AddTag"; import { @@ -23,7 +24,6 @@ import { } from "readium-desktop/renderer/common/components/hoc/translator"; import { deleteTag } from "readium-desktop/renderer/common/logics/publicationInfos/tags/deleteTag"; import { apiDispatch } from "readium-desktop/renderer/common/redux/api/api"; -import { IReaderRootState } from "readium-desktop/renderer/reader/redux/states"; import { TDispatch } from "readium-desktop/typings/redux"; // Logger diff --git a/src/renderer/reader/di.ts b/src/renderer/reader/di.ts index c9188d144..c458cd264 100644 --- a/src/renderer/reader/di.ts +++ b/src/renderer/reader/di.ts @@ -13,15 +13,24 @@ import { Translator } from "readium-desktop/common/services/translator"; import { initStore } from "readium-desktop/renderer/reader/redux/store/memory"; import { Store } from "redux"; +import { IReaderRootState } from "../../common/redux/states/renderer/readerRootState"; import App from "./components/App"; import { diReaderSymbolTable as diSymbolTable } from "./diSymbolTable"; -import { IReaderRootState } from "./redux/states"; // Create container used for dependency injection const container = new Container(); -const store = initStore(); -container.bind>(diSymbolTable.store).toConstantValue(store); +const createStoreFromDi = async (preloadedState: Partial) => { + + const store = initStore(preloadedState); + + container.bind>(diSymbolTable.store).toConstantValue(store); + + const locale = store.getState().i18n.locale; + await translator.setLocale(locale); + + return store; +}; // Create translator const translator = new Translator(); @@ -49,6 +58,7 @@ const { export { diGet as diReaderGet, + createStoreFromDi, lazyInject, lazyInjectNamed, lazyInjectTagged, diff --git a/src/renderer/reader/index_reader.ts b/src/renderer/reader/index_reader.ts index 8b0db9183..01297454b 100644 --- a/src/renderer/reader/index_reader.ts +++ b/src/renderer/reader/index_reader.ts @@ -11,13 +11,10 @@ import "react-dropdown/style.css"; import { ipcRenderer } from "electron"; import * as React from "react"; import * as ReactDOM from "react-dom"; -import { syncIpc, winIpc } from "readium-desktop/common/ipc"; -import { EventPayload } from "readium-desktop/common/ipc/sync"; -import { ActionWithSender } from "readium-desktop/common/models/sync"; +import { readerIpc } from "readium-desktop/common/ipc"; import { IS_DEV } from "readium-desktop/preprocessor-directives"; -import { actionSerializer } from "readium-desktop/renderer/common/actionSerializer"; import { winActions } from "readium-desktop/renderer/common/redux/actions"; -import { diReaderGet } from "readium-desktop/renderer/reader/di"; +import { createStoreFromDi } from "readium-desktop/renderer/reader/di"; import { initGlobalConverters_OPDS } from "@r2-opds-js/opds/init-globals"; import { @@ -51,32 +48,41 @@ if (IS_DEV) { }, 5000); } -ipcRenderer.on(winIpc.CHANNEL, (_0: any, data: winIpc.EventPayload) => { - switch (data.type) { - case winIpc.EventType.IdResponse: - // Initialize window - const store = diReaderGet("store"); - store.dispatch(winActions.initRequest.build(data.payload.winId)); - break; - } -}); - -// Request main process for a new id -ipcRenderer.on(syncIpc.CHANNEL, (_0: any, data: EventPayload) => { - switch (data.type) { - case syncIpc.EventType.MainAction: - // Dispatch main action to renderer reducers - const store = diReaderGet("store"); - store.dispatch(Object.assign( - {}, - actionSerializer.deserialize(data.payload.action), - { - sender: data.sender, - }, - ) as ActionWithSender); - break; - } -}); +// const ipcSyncHandler = +// (_0: any, data: syncIpc.EventPayload) => { +// switch (data.type) { +// case syncIpc.EventType.MainAction: +// // Dispatch main action to renderer reducers +// const store = diReaderGet("store"); +// store.dispatch(Object.assign( +// {}, +// actionSerializer.deserialize(data.payload.action), +// { +// sender: data.sender, +// }, +// ) as ActionWithSender); +// break; +// } +// }; + +ipcRenderer.on(readerIpc.CHANNEL, + (_0: any, data: readerIpc.EventPayload) => { + switch (data.type) { + case readerIpc.EventType.request: + // Initialize window + + createStoreFromDi(data.payload) + .then( + (store) => + store.dispatch(winActions.initRequest.build(data.payload.win.identifier)), + ) + .catch((e) => e); + // TODO display error ? + // // starting the ipc sync with redux + // ipcRenderer.on(syncIpc.CHANNEL, ipcSyncHandler); + break; + } + }); if (IS_DEV) { ipcRenderer.once("AXE_A11Y", () => { diff --git a/src/renderer/reader/redux/actions/index.ts b/src/renderer/reader/redux/actions/index.ts new file mode 100644 index 000000000..028a72a8d --- /dev/null +++ b/src/renderer/reader/redux/actions/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 readerLocalActionSetConfig from "./setConfig"; +import * as readerLocalActionSetLocator from "./setLocator"; + +export { + readerLocalActionSetConfig, + readerLocalActionSetLocator, +}; diff --git a/src/renderer/reader/redux/actions/setConfig.ts b/src/renderer/reader/redux/actions/setConfig.ts new file mode 100644 index 000000000..eb8887b75 --- /dev/null +++ b/src/renderer/reader/redux/actions/setConfig.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 { ReaderConfig } from "readium-desktop/common/models/reader"; +import { Action } from "readium-desktop/common/models/redux"; + +export const ID = "READER_SET_CONFIG_IN_RENDERER"; + +// tslint:disable-next-line: no-empty-interface +interface IPayload extends ReaderConfig { +} + +export function build(readerConfig: ReaderConfig): + Action { + + return { + type: ID, + payload: readerConfig, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/renderer/reader/redux/actions/setLocator.ts b/src/renderer/reader/redux/actions/setLocator.ts new file mode 100644 index 000000000..64b5c1f21 --- /dev/null +++ b/src/renderer/reader/redux/actions/setLocator.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 { LocatorExtended } from "@r2-navigator-js/electron/renderer"; +import { Action } from "readium-desktop/common/models/redux"; + +export const ID = "READER_SET_LOCATOR_IN_RENDERER"; + +// tslint:disable-next-line: no-empty-interface +interface IPayload extends LocatorExtended { +} + +export function build(locator: LocatorExtended): + Action { + + return { + type: ID, + payload: locator, + }; +} +build.toString = () => ID; // Redux StringableActionCreator +export type TAction = ReturnType; diff --git a/src/renderer/reader/redux/middleware/persistence.ts b/src/renderer/reader/redux/middleware/persistence.ts new file mode 100644 index 000000000..9e3cf6b54 --- /dev/null +++ b/src/renderer/reader/redux/middleware/persistence.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 ramda from "ramda"; +import { ActionWithSender } from "readium-desktop/common/models/sync"; +import { readerActions } from "readium-desktop/common/redux/actions"; +import { IReaderRootState } from "readium-desktop/common/redux/states/renderer/readerRootState"; +import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from "redux"; + +const dispatchSetReduxState = (store: MiddlewareAPI, IReaderRootState>) => { + + const state = store.getState(); + store.dispatch(readerActions.setReduxState.build(state.win.identifier, state.reader)); +}; + +export const reduxPersistMiddleware: Middleware + = (store: MiddlewareAPI, IReaderRootState>) => + (next: Dispatch) => + (action: ActionWithSender) => { + + const prevState = store.getState(); + + const returnValue = next(action); + + const nextState = store.getState(); + + if (!ramda.equals(prevState.reader, nextState.reader)) { + dispatchSetReduxState(store); + } + + return returnValue; + }; diff --git a/src/renderer/reader/redux/middleware/sync.ts b/src/renderer/reader/redux/middleware/sync.ts index c0aee91d1..38fe7e740 100644 --- a/src/renderer/reader/redux/middleware/sync.ts +++ b/src/renderer/reader/redux/middleware/sync.ts @@ -18,7 +18,8 @@ const SYNCHRONIZABLE_ACTIONS: string[] = [ readerActions.openRequest.ID, readerActions.closeRequest.ID, readerActions.detachModeRequest.ID, - readerActions.configSetRequest.ID, + readerActions.setReduxState.ID, + readerActions.configSetDefault.ID, // readerActions.saveBookmarkRequest.ID, readerActions.fullScreenRequest.ID, diff --git a/src/renderer/reader/redux/reducers/index.ts b/src/renderer/reader/redux/reducers/index.ts index 6adaa526e..6608d1a1a 100644 --- a/src/renderer/reader/redux/reducers/index.ts +++ b/src/renderer/reader/redux/reducers/index.ts @@ -9,18 +9,26 @@ import { dialogReducer } from "readium-desktop/common/redux/reducers/dialog"; import { i18nReducer } from "readium-desktop/common/redux/reducers/i18n"; import { keyboardReducer } from "readium-desktop/common/redux/reducers/keyboard"; import { toastReducer } from "readium-desktop/common/redux/reducers/toast"; +import { + IReaderRootState, IReaderStateReader, +} from "readium-desktop/common/redux/states/renderer/readerRootState"; import { apiReducer } from "readium-desktop/renderer/common/redux/reducers/api"; import { winReducer } from "readium-desktop/renderer/common/redux/reducers/win"; -import { readerReducer } from "readium-desktop/renderer/reader/redux/reducers/reader"; import { combineReducers } from "redux"; -import { IReaderRootState } from "../states"; +import { readerInfoReducer } from "./info"; +import { readerConfigReducer } from "./readerConfig"; +import { readerLocatorReducer } from "./readerLocator"; export const rootReducer = () => { return combineReducers({ api: apiReducer, i18n: i18nReducer, - reader: readerReducer, + reader: combineReducers({ // dehydrated from main process registry (preloaded state) + config: readerConfigReducer, + info: readerInfoReducer, + locator: readerLocatorReducer, + }), win: winReducer, dialog: dialogReducer, toast: toastReducer, diff --git a/src/renderer/reader/redux/states/reader.ts b/src/renderer/reader/redux/reducers/info.ts similarity index 58% rename from src/renderer/reader/redux/states/reader.ts rename to src/renderer/reader/redux/reducers/info.ts index 12a61329c..3450fa3a3 100644 --- a/src/renderer/reader/redux/states/reader.ts +++ b/src/renderer/reader/redux/reducers/info.ts @@ -5,16 +5,11 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -import { - Reader, ReaderConfig, ReaderMode, -} from "readium-desktop/common/models/reader"; +import { ReaderInfo } from "readium-desktop/common/models/reader"; -export interface ReaderState { - // Base url of started server - reader: Reader; +export function readerInfoReducer( + state: ReaderInfo = null, // injected by preloaded state +): ReaderInfo { - // Config for all readers - config: ReaderConfig; - - mode: ReaderMode; + return state; } diff --git a/src/renderer/reader/redux/reducers/reader.ts b/src/renderer/reader/redux/reducers/reader.ts deleted file mode 100644 index 824881227..000000000 --- a/src/renderer/reader/redux/reducers/reader.ts +++ /dev/null @@ -1,65 +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 { ReaderMode } from "readium-desktop/common/models/reader"; -import { readerActions } from "readium-desktop/common/redux/actions"; -import { ReaderState } from "readium-desktop/renderer/reader/redux/states/reader"; - -// TODO: centralize this code, currently duplicated -// see src/main/redux/reducers/reader.ts -const initialState: ReaderState = { - reader: undefined, - mode: ReaderMode.Attached, - // See optionsValues (AdjustableSettingsStrings) - config: { - align: "auto", - colCount: "auto", - dark: false, - font: "DEFAULT", - fontSize: "100%", - invert: false, - lineHeight: "1.5", - night: false, - paged: false, - readiumcss: true, - sepia: false, - enableMathJax: false, - pageMargins: "0.5", - wordSpacing: "0", - letterSpacing: "0", - paraSpacing: "0", - noFootnotes: undefined, - darken: undefined, - }, -}; - -export function readerReducer( - state: ReaderState = initialState, - action: readerActions.closeSuccess.TAction | - readerActions.openSuccess.TAction | - readerActions.configSetSuccess.TAction | - readerActions.detachModeSuccess.TAction, -): ReaderState { - const newState = Object.assign({}, state); - - switch (action.type) { - case readerActions.openSuccess.ID: - newState.reader = action.payload.reader; - return newState; - case readerActions.closeSuccess.ID: - delete newState.reader; - return newState; - case readerActions.detachModeSuccess.ID: - newState.mode = action.payload.mode; - return newState; - case readerActions.configSetSuccess.ID: - newState.config = action.payload.config; - return newState; - default: - return state; - } -} diff --git a/src/renderer/reader/redux/reducers/readerConfig.ts b/src/renderer/reader/redux/reducers/readerConfig.ts new file mode 100644 index 000000000..64112740d --- /dev/null +++ b/src/renderer/reader/redux/reducers/readerConfig.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 { ReaderConfig } from "readium-desktop/common/models/reader"; +import { readerConfigInitialState } from "readium-desktop/common/redux/states/reader"; +import { readerLocalActionSetConfig } from "../actions"; + +export function readerConfigReducer( + state: ReaderConfig = readerConfigInitialState, + action: readerLocalActionSetConfig.TAction, +): ReaderConfig { + + switch (action.type) { + case readerLocalActionSetConfig.ID: + + return { + ...state, + ...action.payload, + }; + default: + return state; + } +} diff --git a/src/renderer/reader/redux/reducers/readerLocator.ts b/src/renderer/reader/redux/reducers/readerLocator.ts new file mode 100644 index 000000000..116e7d4c9 --- /dev/null +++ b/src/renderer/reader/redux/reducers/readerLocator.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 { locatorInitialState } from "readium-desktop/common/redux/states/locatorInitialState"; + +import { LocatorExtended } from "@r2-navigator-js/electron/renderer"; + +import { readerLocalActionSetLocator } from "../actions"; + +export function readerLocatorReducer( + state: LocatorExtended = locatorInitialState, + action: readerLocalActionSetLocator.TAction, +): LocatorExtended { + + switch (action.type) { + case readerLocalActionSetLocator.ID: + + return { + ...action.payload, + }; + default: + return state; + } +} diff --git a/src/renderer/reader/redux/sagas/cssUpdate.ts b/src/renderer/reader/redux/sagas/cssUpdate.ts new file mode 100644 index 000000000..21a148b51 --- /dev/null +++ b/src/renderer/reader/redux/sagas/cssUpdate.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 { computeReadiumCssJsonMessage } from "readium-desktop/common/computeReadiumCssJsonMessage"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; + +import { readiumCssUpdate } from "@r2-navigator-js/electron/renderer"; + +import { readerLocalActionSetConfig } from "../actions"; + +function updateCss(action: readerLocalActionSetConfig.TAction) { + if (action.payload) { + readiumCssUpdate(computeReadiumCssJsonMessage(action.payload)); + } +} + +export function saga() { + return takeSpawnEvery( + readerLocalActionSetConfig.ID, + updateCss, + ); +} diff --git a/src/renderer/reader/redux/sagas/i18n.ts b/src/renderer/reader/redux/sagas/i18n.ts index 778b0c969..5da796db2 100644 --- a/src/renderer/reader/redux/sagas/i18n.ts +++ b/src/renderer/reader/redux/sagas/i18n.ts @@ -6,11 +6,13 @@ // ==LICENSE-END== import { i18nActions } from "readium-desktop/common/redux/actions"; +import { takeSpawnEvery } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { callTyped } from "readium-desktop/common/redux/sagas/typed-saga"; import { diReaderGet } from "readium-desktop/renderer/reader/di"; -import { all, call, takeEvery } from "redux-saga/effects"; +import { call } from "redux-saga/effects"; function* setLocale(action: i18nActions.setLocale.TAction) { - const translator = diReaderGet("translator"); + const translator = yield* callTyped(() => diReaderGet("translator")); const translatorSetLocale = async () => await translator.setLocale(action.payload.locale); @@ -18,12 +20,9 @@ function* setLocale(action: i18nActions.setLocale.TAction) { yield call(translatorSetLocale); } -function* localeWatcher() { - yield takeEvery(i18nActions.setLocale.build, setLocale); -} - -export function* watchers() { - yield all([ - call(localeWatcher), - ]); +export function saga() { + return takeSpawnEvery( + i18nActions.setLocale.ID, + setLocale, + ); } diff --git a/src/renderer/reader/redux/sagas/index.ts b/src/renderer/reader/redux/sagas/index.ts index 6e741f489..b0434f8d1 100644 --- a/src/renderer/reader/redux/sagas/index.ts +++ b/src/renderer/reader/redux/sagas/index.ts @@ -5,18 +5,39 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import * as debug_ from "debug"; +// import { error } from "readium-desktop/common/error"; +import { winActions } from "readium-desktop/renderer/common/redux/actions"; import * as publicationInfoReaderAndLib from "readium-desktop/renderer/common/redux/sagas/dialog/publicationInfoReaderAndLib"; import * as publicationInfoSyncTag from "readium-desktop/renderer/common/redux/sagas/dialog/publicationInfosSyncTags"; -import { all, call } from "redux-saga/effects"; +import { all, call, put, take } from "redux-saga/effects"; +import * as cssUpdate from "./cssUpdate"; import * as i18n from "./i18n"; +import * as ipc from "./ipc"; import * as winInit from "./win"; +// Logger +const filename_ = "readium-desktop:renderer:reader:saga:index"; +const debug = debug_(filename_); +debug("_"); + export function* rootSaga() { + + yield take(winActions.initRequest.ID); + + yield put(winActions.initSuccess.build()); + + yield call(winInit.render); + yield all([ - call(i18n.watchers), - call(winInit.watchers), - call(publicationInfoReaderAndLib.watchers), - call(publicationInfoSyncTag.watchers), + i18n.saga(), + ipc.saga(), + + publicationInfoReaderAndLib.saga(), + publicationInfoSyncTag.saga(), + + cssUpdate.saga(), + ]); } diff --git a/src/renderer/reader/redux/sagas/ipc.ts b/src/renderer/reader/redux/sagas/ipc.ts new file mode 100644 index 000000000..d45e6225c --- /dev/null +++ b/src/renderer/reader/redux/sagas/ipc.ts @@ -0,0 +1,64 @@ +// ==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 { ipcRenderer } from "electron"; +import { syncIpc } from "readium-desktop/common/ipc"; +import { ActionWithSender } from "readium-desktop/common/models/sync"; +import { takeSpawnEveryChannel } from "readium-desktop/common/redux/sagas/takeSpawnEvery"; +import { ActionSerializer } from "readium-desktop/common/services/serializer"; +import { eventChannel } from "redux-saga"; +import { put } from "redux-saga/effects"; + +// Logger +const filename_ = "readium-desktop:renderer:reader:saga:ipc"; +const debug = debug_(filename_); +debug("_"); + +function getIpcSyncChannel() { + + const channel = eventChannel( + (emit) => { + + const handler = (_0: any, data: syncIpc.EventPayload) => { + + if (data.type === syncIpc.EventType.MainAction) { + emit(data); + } + }; + + ipcRenderer.on(syncIpc.CHANNEL, handler); + + return () => { + ipcRenderer.removeListener(syncIpc.CHANNEL, handler); + }; + }, + ); + + return channel; +} + +function* ipcSyncChannel(ipcData: syncIpc.EventPayload) { + + yield put({ + ...ActionSerializer.deserialize(ipcData.payload.action), + ...{ + sender: ipcData.sender, + }, + } as ActionWithSender); +} + +export function saga() { + + const ipcChannel = getIpcSyncChannel(); + + return takeSpawnEveryChannel( + ipcChannel, + ipcSyncChannel, + (e) => debug("redux IPC sync channel error", e), + ); +} diff --git a/src/renderer/reader/redux/sagas/win.ts b/src/renderer/reader/redux/sagas/win.ts index f0cc2e57c..3ea38d1b8 100644 --- a/src/renderer/reader/redux/sagas/win.ts +++ b/src/renderer/reader/redux/sagas/win.ts @@ -7,26 +7,9 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; -import { i18nActions, keyboardActions } from "readium-desktop/common/redux/actions/"; -import { winActions } from "readium-desktop/renderer/common/redux/actions"; import { diReaderGet } from "readium-desktop/renderer/reader/di"; -import { SagaIterator } from "redux-saga"; -import { all, call, put, take } from "redux-saga/effects"; - -function* winInitWatcher(): SagaIterator { - yield all({ - win: take(winActions.initRequest.ID), - i18n: take(i18nActions.setLocale.ID), - keyboard: take(keyboardActions.setShortcuts.ID), - }); - - yield put(winActions.initSuccess.build()); -} - -function* winStartWatcher(): SagaIterator { - - yield take(winActions.initSuccess.ID); +export function render() { // starting point to mounting React to the DOM ReactDOM.render( React.createElement( @@ -35,10 +18,3 @@ function* winStartWatcher(): SagaIterator { document.getElementById("app"), ); } - -export function* watchers() { - yield all([ - call(winInitWatcher), - call(winStartWatcher), - ]); -} diff --git a/src/renderer/reader/redux/store/memory.ts b/src/renderer/reader/redux/store/memory.ts index fdcb3d862..45248d7c7 100644 --- a/src/renderer/reader/redux/store/memory.ts +++ b/src/renderer/reader/redux/store/memory.ts @@ -5,22 +5,25 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== +import { IReaderRootState } from "readium-desktop/common/redux/states/renderer/readerRootState"; +import { reduxSyncMiddleware } from "readium-desktop/renderer/reader/redux/middleware/sync"; +import { rootReducer } from "readium-desktop/renderer/reader/redux/reducers"; +import { rootSaga } from "readium-desktop/renderer/reader/redux/sagas"; import { applyMiddleware, createStore, Store } from "redux"; import { composeWithDevTools } from "redux-devtools-extension"; import createSagaMiddleware from "redux-saga"; -import { reduxSyncMiddleware } from "readium-desktop/renderer/reader/redux/middleware/sync"; -import { rootReducer } from "readium-desktop/renderer/reader/redux/reducers"; -import { rootSaga } from "readium-desktop/renderer/reader/redux/sagas"; -import { IReaderRootState } from "../states"; +import { reduxPersistMiddleware } from "../middleware/persistence"; -export function initStore(): Store { +export function initStore(preloadedState: Partial): Store { const sagaMiddleware = createSagaMiddleware(); const store = createStore( rootReducer(), + preloadedState, composeWithDevTools( applyMiddleware( reduxSyncMiddleware, + reduxPersistMiddleware, sagaMiddleware, ), ), diff --git a/src/resources/locales/de.json b/src/resources/locales/de.json index e09aa85f0..467faec61 100644 --- a/src/resources/locales/de.json +++ b/src/resources/locales/de.json @@ -21,8 +21,20 @@ "title": "Bearbeiten", "undo": "Rückgängig" }, - "hide": "Ausblenden {{appName}}", - "quit": "Beenden {{appName}}" + "hide": "Ausblenden {{- appName}}", + "quit": "Beenden {{- appName}}", + "session": { + "exit": { + "askBox": { + "button": { + "no": "Nein", + "yes": "Ja" + }, + "message": "Möchten Sie die Sitzung speichern ?", + "title": "Sitzung sichern" + } + } + } }, "catalog": { "about": { @@ -92,6 +104,13 @@ "return": "", "yes": "Ja" }, + "error": { + "errorBox": { + "error": "Fehlermeldung:", + "message": "In ist ein Fehler aufgetreten {{- filename}}", + "title": "interner {{- appName}} Leser" + } + }, "header": { "allBooks": "Alle Bücher", "books": "Meine Bücher", @@ -126,6 +145,9 @@ "alreadyImport": "Der Import von {{- title}} Es scheint, dass Sie diese Publikation bereits in Ihren Katalog aufgenommen haben.", "fail": "Der Import von {{- path}} ist fehlgeschlagen. Stellen Sie sicher, dass die Datei gültig ist.", "success": "Der Import von {{- title}} ist abgeschlossen." + }, + "open": { + "error": "Die Publikation kann nicht geöffnet werden: {{- err}}" } }, "opds": { @@ -182,6 +204,9 @@ "licenseOutOfDate": "", "licenseSignatureDateInvalid": "", "licenseSignatureInvalid": "", + "progression": { + "title": "Fortschreiten" + }, "renewButton": "Verlängern", "returnButton": "Zurückgeben", "returnedLcp": "", @@ -239,6 +264,11 @@ "lineSpacing": "Zeilenabstand", "margin": "Seitenränder", "paginated": "Seitendarstellung", + "save": { + "apply": "Speichern", + "reset": "Zurücksetzen", + "title": "Aufbau" + }, "scrolled": "Fortlaufend", "spacing": "Abstand", "text": "Text", @@ -272,6 +302,11 @@ }, "language": { "languageChoice": "Sprachauswahl" + }, + "session": { + "no": "Deaktivieren", + "title": "Sitzung speichern", + "yes": "aktivieren" } } } diff --git a/src/resources/locales/en.json b/src/resources/locales/en.json index ee88ad5e9..9ac536b92 100644 --- a/src/resources/locales/en.json +++ b/src/resources/locales/en.json @@ -21,13 +21,25 @@ "title": "Edit", "undo": "Undo" }, - "hide": "Hide {{appName}}", - "quit": "Quit {{appName}}" + "hide": "Hide {{- appName}}", + "quit": "Quit {{- appName}}", + "session": { + "exit": { + "askBox": { + "button": { + "no": "No", + "yes": "Yes" + }, + "message": "Do you want to save the session ?", + "title": "Save session" + } + } + } }, "catalog": { "about": { "button": "More information", - "title": "About {{appName}}" + "title": "About {{- appName}}" }, "addBookToLib": "Import", "addTags": "Add a tag", @@ -92,6 +104,13 @@ "return": "Do you really want to return the licence for this publication?", "yes": "Yes" }, + "error": { + "errorBox": { + "error": "Error message:", + "message": "An error has occurred in {{- filename}}", + "title": "Internal {{- appName}} Error" + } + }, "header": { "allBooks": "All books", "books": "My Books", @@ -126,6 +145,9 @@ "alreadyImport": "{{- title}} was already imported", "fail": "The import of {{- path}} failed. Verify that the file is valid", "success": "The import of {{- title}} is finished." + }, + "open": { + "error": "The publication can't be opened : {{- err}}" } }, "opds": { @@ -182,6 +204,9 @@ "licenseOutOfDate": "License is out of date", "licenseSignatureDateInvalid": "License signature date is invalid", "licenseSignatureInvalid": "License signature is invalid", + "progression": { + "title": "Progression" + }, "renewButton": "Renew", "returnButton": "Return", "returnedLcp": "This book can't be read because the LCP license has been returned.", @@ -239,6 +264,11 @@ "lineSpacing": "Line spacing", "margin": "Margin", "paginated": "Paginated", + "save": { + "apply": "save", + "reset": "reset", + "title": "Configuration" + }, "scrolled": "Scrollable", "spacing": "Spacing", "text": "Text", @@ -272,6 +302,11 @@ }, "language": { "languageChoice": "Language choice" + }, + "session": { + "no": "Disable", + "title": "Save session", + "yes": "Enable" } } } diff --git a/src/resources/locales/es.json b/src/resources/locales/es.json index 387b1f0eb..d3c914b3c 100644 --- a/src/resources/locales/es.json +++ b/src/resources/locales/es.json @@ -21,8 +21,20 @@ "title": "Editar", "undo": "Deshacer" }, - "hide": "Ocultar {{appName}}", - "quit": "Salir de {{appName}}" + "hide": "Ocultar {{- appName}}", + "quit": "Salir de {{- appName}}", + "session": { + "exit": { + "askBox": { + "button": { + "no": "", + "yes": "" + }, + "message": "", + "title": "" + } + } + } }, "catalog": { "about": { @@ -92,6 +104,13 @@ "return": "¿Seguro que quieres devolver la licencia para esta publicación?", "yes": "Sí" }, + "error": { + "errorBox": { + "error": "", + "message": "", + "title": "" + } + }, "header": { "allBooks": "Todos los libros", "books": "Mis libros", @@ -126,6 +145,9 @@ "alreadyImport": "{{- title}} ya estaba importado", "fail": "La importación de {{- path}} falló. Comprueba que el fichero es válido", "success": "La importación de {{- title}} ha finalizado." + }, + "open": { + "error": "" } }, "opds": { @@ -182,6 +204,9 @@ "licenseOutOfDate": "La licencia está desactualizada", "licenseSignatureDateInvalid": "La fecha de la firma de la licencia no es válida", "licenseSignatureInvalid": "La firma de la licencia no es válida", + "progression": { + "title": "Progresión" + }, "renewButton": "Renovar", "returnButton": "Devolver", "returnedLcp": "Este libro no se puede leer porque la licencia LCP se ha devuelto.", @@ -239,6 +264,11 @@ "lineSpacing": "Interlineado", "margin": "Margen", "paginated": "Paginado", + "save": { + "apply": "", + "reset": "", + "title": "" + }, "scrolled": "Desplazable", "spacing": "Espaciado", "text": "Texto", @@ -272,6 +302,11 @@ }, "language": { "languageChoice": "Selección de idioma" + }, + "session": { + "no": "", + "title": "", + "yes": "" } } } diff --git a/src/resources/locales/fr.json b/src/resources/locales/fr.json index da75ad6a5..2c77c1670 100644 --- a/src/resources/locales/fr.json +++ b/src/resources/locales/fr.json @@ -21,8 +21,20 @@ "title": "Édition", "undo": "Annuler" }, - "hide": "Cacher {{appName}}", - "quit": "Quitter {{appName}}" + "hide": "Masquer {{- appName}}", + "quit": "Quitter {{- appName}}", + "session": { + "exit": { + "askBox": { + "button": { + "no": "Non", + "yes": "Oui" + }, + "message": "Voulez-vous enregistrer la session ?", + "title": "Sauvegarder la session" + } + } + } }, "catalog": { "about": { @@ -92,6 +104,13 @@ "return": "Êtes vous sûr de vouloir retourner la licence pour ce livre ?", "yes": "Oui" }, + "error": { + "errorBox": { + "error": "Message d'erreur:", + "message": "Une erreur est survenue dans le fichier {{- filename}}", + "title": "Erreur interne {{- appName}}" + } + }, "header": { "allBooks": "Tous les livres", "books": "Mes Livres", @@ -126,6 +145,9 @@ "alreadyImport": "{{- title}} est déja importé", "fail": "L'importation de {{- path}} a échoué. Vérifier que le fichier est valide", "success": "L'import de {{- title}} est terminé." + }, + "open": { + "error": "La publication ne peut pas etre ouverte : {{- err}}" } }, "opds": { @@ -182,6 +204,9 @@ "licenseOutOfDate": "License expirée", "licenseSignatureDateInvalid": "Date de signature de license invalide", "licenseSignatureInvalid": "Signature de license invalide", + "progression": { + "title": "Progression" + }, "renewButton": "Renouveler", "returnButton": "Retourner", "returnedLcp": "Ce livre ne peut être lu car la licence lcp a été retournée.", @@ -239,6 +264,11 @@ "lineSpacing": "Espacement des lignes", "margin": "Marge", "paginated": "Paginé", + "save": { + "apply": "Sauvegarder", + "reset": "Réinitialiser", + "title": "Configuration" + }, "scrolled": "Défilé", "spacing": "Espacement", "text": "Texte", @@ -272,6 +302,11 @@ }, "language": { "languageChoice": "Choix de la langue" + }, + "session": { + "no": "Désactiver", + "title": "Sauvegarder la session", + "yes": "Activer" } } } diff --git a/src/resources/locales/nl.json b/src/resources/locales/nl.json index 4f1be9b04..db3d36ba2 100644 --- a/src/resources/locales/nl.json +++ b/src/resources/locales/nl.json @@ -21,8 +21,20 @@ "title": "Wijzig", "undo": "Ongedaan maken" }, - "hide": "Verbergen", - "quit": "Verlaat Thorium" + "hide": "Verbergen {{- appName}}", + "quit": "Verlaat {{- appName}}", + "session": { + "exit": { + "askBox": { + "button": { + "no": "", + "yes": "" + }, + "message": "", + "title": "" + } + } + } }, "catalog": { "about": { @@ -92,6 +104,13 @@ "return": "Wil je de licentie voor deze publicatie echt inleveren?", "yes": "Ja" }, + "error": { + "errorBox": { + "error": "", + "message": "", + "title": "" + } + }, "header": { "allBooks": "Alle boeken", "books": "Mijn boeken", @@ -126,6 +145,9 @@ "alreadyImport": "{{- title}} is al toegevoegd", "fail": "Het toevoegen van {{- path}} is mislukt. Controleer de juistheid van het bestand", "success": "Het toevoegen van {{- title}} is gelukt." + }, + "open": { + "error": "" } }, "opds": { @@ -182,6 +204,9 @@ "licenseOutOfDate": "Licentie is verlopen", "licenseSignatureDateInvalid": "Datum in de Licentie handtekening is iongeldig", "licenseSignatureInvalid": "Licentie handtekening is ongeldig", + "progression": { + "title": "" + }, "renewButton": "Vernieuw", "returnButton": "Lever in", "returnedLcp": "Dit boek kan niet gelezen worden omdat de LCP licentie is ingeleverd.", @@ -239,6 +264,11 @@ "lineSpacing": "Regelafstand", "margin": "Marge", "paginated": "Als pagina's", + "save": { + "apply": "", + "reset": "", + "title": "" + }, "scrolled": "Doorlopend", "spacing": "Afstand", "text": "Tekst", @@ -272,6 +302,11 @@ }, "language": { "languageChoice": "Taal keuze" + }, + "session": { + "no": "", + "title": "", + "yes": "" } } } diff --git a/src/typings/en.translation.d.ts b/src/typings/en.translation.d.ts index fe47a40e5..b2e48edae 100644 --- a/src/typings/en.translation.d.ts +++ b/src/typings/en.translation.d.ts @@ -33,7 +33,16 @@ declare namespace typed_i18n { readonly "undo": string }, readonly "hide": string, - readonly "quit": string + readonly "quit": string, + readonly "session": { + readonly "exit": { + readonly "askBox": { + readonly "button": { readonly "no": string, readonly "yes": string }, + readonly "message": string, + readonly "title": string + } + } + } }; (_: "app.edit", __?: {}): { readonly "copy": string, @@ -52,6 +61,32 @@ declare namespace typed_i18n { (_: "app.edit.title", __?: {}): string; (_: "app.edit.undo", __?: {}): string; (_: "app.hide", __?: {}): string; (_: "app.quit", __?: {}): string; + (_: "app.session", __?: {}): { + readonly "exit": { + readonly "askBox": { + readonly "button": { readonly "no": string, readonly "yes": string }, + readonly "message": string, + readonly "title": string + } + } +}; + (_: "app.session.exit", __?: {}): { + readonly "askBox": { + readonly "button": { readonly "no": string, readonly "yes": string }, + readonly "message": string, + readonly "title": string + } +}; + (_: "app.session.exit.askBox", __?: {}): { + readonly "button": { readonly "no": string, readonly "yes": string }, + readonly "message": string, + readonly "title": string +}; + (_: "app.session.exit.askBox.button", __?: {}): { readonly "no": string, readonly "yes": string }; + (_: "app.session.exit.askBox.button.no", __?: {}): string; + (_: "app.session.exit.askBox.button.yes", __?: {}): string; + (_: "app.session.exit.askBox.message", __?: {}): string; + (_: "app.session.exit.askBox.title", __?: {}): string; (_: "catalog", __?: {}): { readonly "about": { readonly "button": string, readonly "title": string }, readonly "addBookToLib": string, @@ -280,7 +315,8 @@ declare namespace typed_i18n { readonly "alreadyImport": string, readonly "fail": string, readonly "success": string - } + }, + readonly "open": { readonly "error": string } }; (_: "message.download", __?: {}): { readonly "error": string, @@ -298,6 +334,8 @@ declare namespace typed_i18n { (_: "message.import.alreadyImport", __?: {}): string; (_: "message.import.fail", __?: {}): string; (_: "message.import.success", __?: {}): string; + (_: "message.open", __?: {}): { readonly "error": string }; + (_: "message.open.error", __?: {}): string; (_: "opds", __?: {}): { readonly "addForm": { readonly "addButton": string, @@ -390,6 +428,7 @@ declare namespace typed_i18n { readonly "licenseOutOfDate": string, readonly "licenseSignatureDateInvalid": string, readonly "licenseSignatureInvalid": string, + readonly "progression": { readonly "title": string }, readonly "renewButton": string, readonly "returnButton": string, readonly "returnedLcp": string, @@ -415,6 +454,8 @@ declare namespace typed_i18n { (_: "publication.licenseOutOfDate", __?: {}): string; (_: "publication.licenseSignatureDateInvalid", __?: {}): string; (_: "publication.licenseSignatureInvalid", __?: {}): string; + (_: "publication.progression", __?: {}): { readonly "title": string }; + (_: "publication.progression.title", __?: {}): string; (_: "publication.renewButton", __?: {}): string; (_: "publication.returnButton", __?: {}): string; (_: "publication.returnedLcp", __?: {}): string; @@ -469,6 +510,11 @@ declare namespace typed_i18n { readonly "lineSpacing": string, readonly "margin": string, readonly "paginated": string, + readonly "save": { + readonly "apply": string, + readonly "reset": string, + readonly "title": string + }, readonly "scrolled": string, readonly "spacing": string, readonly "text": string, @@ -548,6 +594,11 @@ declare namespace typed_i18n { readonly "lineSpacing": string, readonly "margin": string, readonly "paginated": string, + readonly "save": { + readonly "apply": string, + readonly "reset": string, + readonly "title": string + }, readonly "scrolled": string, readonly "spacing": string, readonly "text": string, @@ -586,6 +637,14 @@ declare namespace typed_i18n { (_: "reader.settings.lineSpacing", __?: {}): string; (_: "reader.settings.margin", __?: {}): string; (_: "reader.settings.paginated", __?: {}): string; + (_: "reader.settings.save", __?: {}): { + readonly "apply": string, + readonly "reset": string, + readonly "title": string +}; + (_: "reader.settings.save.apply", __?: {}): string; + (_: "reader.settings.save.reset", __?: {}): string; + (_: "reader.settings.save.title", __?: {}): string; (_: "reader.settings.scrolled", __?: {}): string; (_: "reader.settings.spacing", __?: {}): string; (_: "reader.settings.text", __?: {}): string; @@ -623,7 +682,12 @@ declare namespace typed_i18n { readonly "save": string, readonly "show": string }, - readonly "language": { readonly "languageChoice": string } + readonly "language": { readonly "languageChoice": string }, + readonly "session": { + readonly "title": string, + readonly "yes": string, + readonly "no": string + } }; (_: "settings.keyboard", __?: {}): { readonly "advancedMenu": string, @@ -648,7 +712,26 @@ declare namespace typed_i18n { (_: "settings.keyboard.save", __?: {}): string; (_: "settings.keyboard.show", __?: {}): string; (_: "settings.language", __?: {}): { readonly "languageChoice": string }; - (_: "settings.language.languageChoice", __?: {}): string + (_: "settings.language.languageChoice", __?: {}): string; + (_: "settings.session", __?: {}): { readonly "title": string, readonly "yes": string, readonly "no": string }; + (_: "settings.session.title", __?: {}): string; + (_: "settings.session.yes", __?: {}): string; + (_: "settings.session.no", __?: {}): string; + (_: "error", __?: {}): { + readonly "errorBox": { + readonly "title": string, + readonly "message": string, + readonly "error": string + } +}; + (_: "error.errorBox", __?: {}): { + readonly "title": string, + readonly "message": string, + readonly "error": string +}; + (_: "error.errorBox.title", __?: {}): string; + (_: "error.errorBox.message", __?: {}): string; + (_: "error.errorBox.error", __?: {}): string } } export = typed_i18n; diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index d1b578372..d514fc9cf 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -5,48 +5,48 @@ // that can be found in the LICENSE file exposed on Github (readium) in the project repository. // ==LICENSE-END== -// https://github.com/chodorowicz/ts-debounce - -/** - * A function that emits a side effect and does not return anything. - */ - -export interface Options { - isImmediate: boolean; -} - -export function debounce Promise | any>( - func: F, - waitMilliseconds = 50, - options: Options = { - isImmediate: false, - }, -): (...args: any[]) => void { - - let timeoutId: NodeJS.Timer | undefined; - - return async function fct(this: any, ...args: any[]) { - const that = this; - - const doLater = async () => { - timeoutId = undefined; - if (!options.isImmediate) { - await Promise.resolve(func.apply(that, args)); - } - }; - - const shouldCallNow = options.isImmediate && timeoutId === undefined; - - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - - // error with TS on Timer return (Number | Timer) - // clearTimeout and setTimeout has incompatible type - timeoutId = setTimeout(doLater, waitMilliseconds) as any; - - if (shouldCallNow) { - await Promise.resolve(func.apply(that, args)); - } - }; -} +// // https://github.com/chodorowicz/ts-debounce + +// /** +// * A function that emits a side effect and does not return anything. +// */ + +// export interface Options { +// isImmediate: boolean; +// } + +// export function debounce Promise | any>( +// func: F, +// waitMilliseconds = 50, +// options: Options = { +// isImmediate: false, +// }, +// ): (...args: Parameters) => void { + +// let timeoutId: NodeJS.Timer | undefined; + +// return async function fct(this: any, ...args: Parameters) { +// const that = this; + +// const doLater = async () => { +// timeoutId = undefined; +// if (!options.isImmediate) { +// await Promise.resolve(func.apply(that, args)); +// } +// }; + +// const shouldCallNow = options.isImmediate && timeoutId === undefined; + +// if (timeoutId !== undefined) { +// clearTimeout(timeoutId); +// } + +// // error with TS on Timer return (Number | Timer) +// // clearTimeout and setTimeout has incompatible type +// timeoutId = setTimeout(doLater, waitMilliseconds) as any; + +// if (shouldCallNow) { +// await Promise.resolve(func.apply(that, args)); +// } +// }; +// } diff --git a/src/utils/redux-reducers/pqueue.reducer.ts b/src/utils/redux-reducers/pqueue.reducer.ts new file mode 100644 index 000000000..97798119f --- /dev/null +++ b/src/utils/redux-reducers/pqueue.reducer.ts @@ -0,0 +1,93 @@ +// ==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 "redux"; + +export interface ActionWithPayload + extends Action { +} + +export interface IPQueueAction, Key = number, Value = string, ActionType = string> { + type: ActionType; + selector: (action: TAction) => IPQueueState; +} + +export interface IPQueueData +< + TPushAction extends ActionWithPayload, + TPopAction extends ActionWithPayload, + Key = number, + Value = string, + ActionType = string, +> { + push: IPQueueAction; + pop: IPQueueAction; + sortFct: (a: IPQueueState, b: IPQueueState) => number; +} + +export type IPQueueState = [Key, Value]; +export type TPQueueState = Array>; + +export function priorityQueueReducer + < + TPushAction extends ActionWithPayload, + TPopAction extends ActionWithPayload, + Key = number, + Value = string, + ActionType = string, + >( + data: IPQueueData, +) { + + const reducer = + ( + queue: TPQueueState, + action: TPopAction | TPushAction, + ): TPQueueState => { + + if (!queue || !Array.isArray(queue)) { + queue = []; + } + + if (action.type === data.push.type) { + const newQueue = queue.slice(); + + const selectorItem = data.push.selector(action as TPushAction); + if (selectorItem[1]) { + + const index = newQueue.findIndex((item) => item[1] === selectorItem[1]); + if (index > -1) { + newQueue[index] = selectorItem; + } else { + newQueue.push(selectorItem); + } + newQueue.sort(data.sortFct); + + return newQueue; + } + + } else if (action.type === data.pop.type) { + + const selectorItem = data.pop.selector(action as TPopAction); + const index = queue.findIndex((item) => item[1] === selectorItem[1]); + if (index > -1) { + + const newQueue = queue.slice(); + + const left = newQueue.slice(0, index); + const right = newQueue.slice(index + 1, newQueue.length); + + return left.concat(right); + } + } + + return queue; + }; + + return reducer; +} diff --git a/src/utils/shallowEqual.ts b/src/utils/shallowEqual.ts new file mode 100644 index 000000000..a80690cc0 --- /dev/null +++ b/src/utils/shallowEqual.ts @@ -0,0 +1,42 @@ +// ==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== + +// from https://github.com/moroshko/shallow-equal/raw/master/src/objects.js + +interface IObj { + [key: string]: any; +} + +export function shallowEqual(objA: ObjA, objB: ObjB) { + + // @ts-ignore + if (objA === objB) { + return true; + } + + if (!objA || !objB) { + return false; + } + + const aKeys = Object.keys(objA); + const bKeys = Object.keys(objB); + const len = aKeys.length; + + if (bKeys.length !== aKeys.length) { + return false; + } + + for (let i = 0; i < len; i++) { + const key = aKeys[i]; + + if (objA[key] !== objB[key] || !Object.prototype.hasOwnProperty.call(objB, key)) { + return false; + } + } + + return true; +}