diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f512836..cbcb7b9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [22.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/package-lock.json b/package-lock.json index 39bf425..9364094 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "jszip": "^3.10.1", "ngx-highlightjs": "^12.0.0", "pair-pagination": "^0.0.1", - "pdfjs-dist": "^4.6.82", + "pdfjs-dist": "4.6.82", "readiverse": "^0.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -3293,6 +3293,103 @@ "win32" ] }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -5569,7 +5666,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -5618,6 +5715,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5743,14 +5877,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -5828,7 +5962,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -5840,7 +5974,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -5912,7 +6046,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5969,7 +6103,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -6235,28 +6369,21 @@ "license": "CC-BY-4.0" }, "node_modules/canvas": { - "version": "3.0.0-rc2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.0-rc2.tgz", - "integrity": "sha512-esx4bYDznnqgRX4G8kaEaf0W3q8xIc51WpmrIitDzmcoEgwnv9wSKdzT6UxWZ4wkVu5+ileofppX0TpyviJRdQ==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", "simple-get": "^3.0.3" }, "engines": { - "node": "^18.12.0 || >= 20.9.0" + "node": ">=6" } }, - "node_modules/canvas/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6301,7 +6428,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -6506,6 +6633,16 @@ "dev": true, "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -6590,7 +6727,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/connect": { @@ -6688,6 +6825,13 @@ "node": ">= 0.6" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6967,7 +7111,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -6994,16 +7138,6 @@ "node": ">=8" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -7077,6 +7211,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7261,7 +7402,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7272,7 +7412,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7282,16 +7421,6 @@ "node": ">=0.10.0" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/engine.io": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", @@ -7608,16 +7737,6 @@ "node": ">=0.8.x" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -7945,13 +8064,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT", - "optional": true - }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -7984,7 +8096,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/fsevents": { @@ -8011,6 +8123,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8063,19 +8236,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT", - "optional": true - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -8215,6 +8381,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -8450,7 +8623,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -8585,7 +8758,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -10335,7 +10508,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -10348,7 +10521,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10498,7 +10671,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "minipass": "^3.0.0", @@ -10512,7 +10685,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -10525,7 +10698,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/mkdirp": { @@ -10541,13 +10714,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT", - "optional": true - }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -10622,6 +10788,13 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -10641,13 +10814,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "license": "MIT", - "optional": true - }, "node_modules/needle": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", @@ -10710,19 +10876,6 @@ "@angular/core": ">=17.0.0" } }, - "node_modules/node-abi": { - "version": "3.71.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", - "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -10731,6 +10884,27 @@ "license": "MIT", "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -11176,6 +11350,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -11193,7 +11381,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11614,7 +11802,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11691,15 +11879,15 @@ } }, "node_modules/pdfjs-dist": { - "version": "4.8.69", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.8.69.tgz", - "integrity": "sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==", + "version": "4.6.82", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.6.82.tgz", + "integrity": "sha512-BUOryeRFwvbLe0lOU6NhkJNuVQUp06WxlJVVCsxdmJ4y5cU3O3s3/0DunVdK1PMm7v2MUw52qKYaidhDH1Z9+w==", "license": "Apache-2.0", "engines": { "node": ">=18" }, "optionalDependencies": { - "canvas": "^3.0.0-rc2", + "canvas": "^2.11.2", "path2d": "^0.2.1" } }, @@ -11912,88 +12100,6 @@ "dev": true, "license": "MIT" }, - "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prebuild-install/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prebuild-install/node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -12052,17 +12158,6 @@ "license": "MIT", "optional": true }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -12150,29 +12245,6 @@ "node": ">= 0.8" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC", - "optional": true - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -12468,7 +12540,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -12903,6 +12975,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -13501,7 +13580,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -13524,16 +13603,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13584,7 +13653,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "chownr": "^2.0.0", @@ -13598,63 +13667,11 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC", - "optional": true - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -13667,7 +13684,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -13680,7 +13697,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=8" @@ -13690,7 +13707,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -13703,7 +13720,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/terser": { @@ -13868,6 +13885,13 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, "node_modules/tree-dump": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", @@ -14068,19 +14092,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -14912,6 +14923,13 @@ "license": "MIT", "optional": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/webpack": { "version": "5.96.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", @@ -15300,6 +15318,17 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -15313,6 +15342,48 @@ "which": "bin/which" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index 1dc5cec..b25438c 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "chytanka", - "version": "0.14.28", + "version": "0.14.31", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "serve:ssr:chytanka": "node dist/chytanka/server/server.mjs" + "serve:ssr:chytanka": "node dist/chytanka/server/server.mjs", + "upv": "node scripts/update-version.js" }, "private": true, "dependencies": { @@ -29,7 +30,7 @@ "jszip": "^3.10.1", "ngx-highlightjs": "^12.0.0", "pair-pagination": "^0.0.1", - "pdfjs-dist": "^4.6.82", + "pdfjs-dist": "4.6.82", "readiverse": "^0.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/scripts/update-version.js b/scripts/update-version.js new file mode 100644 index 0000000..06b861f --- /dev/null +++ b/scripts/update-version.js @@ -0,0 +1,26 @@ +const fs = require('fs'); +const packageJson = require('../package.json'); + +const version = packageJson.version; + +const environmentFilePaths = [ + 'src/environments/environment.ts', + 'src/environments/environment.development.ts' +] + +environmentFilePaths.forEach(path => update(path)) + +function update(environmentFilePath) { + const filename = environmentFilePath.split('/').pop() + let content = fs.readFileSync(environmentFilePath, 'utf8'); + const date = new Date(Date.now()) + const versionDate = `${date.getFullYear()}.${date.getMonth()+1}.${date.getDate()}` + const V = `${version}-${versionDate}`; + const updatedContent = content.replace( + /version:\s*['"].*?['"]/, + `version: "${V}"` + ); + fs.writeFileSync(environmentFilePath, updatedContent, 'utf8'); + + console.log('\x1b[36m%s\x1b[0m',`Updated ${filename} version to: ${V}`); +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 24b806d..6229458 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,11 +2,12 @@ import { Component, HostListener, PLATFORM_ID, WritableSignal, inject, signal } import { LangService } from './shared/data-access/lang.service'; import { ActivatedRoute } from '@angular/router'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { environment } from '../environments/environment'; const SCALE_GAP = 128; @Component({ - selector: 'app-root', + selector: 'chtnk-root', template: `
`, styles: [` // :host { @@ -16,12 +17,14 @@ const SCALE_GAP = 128; standalone: false }) export class AppComponent { + private readonly document = inject(DOCUMENT); platformId = inject(PLATFORM_ID) + constructor(public lang: LangService, private route: ActivatedRoute) { this.lang.updateManifest() this.lang.updateTranslate() - if (isPlatformBrowser(this.platformId) && window.console) { + if (isPlatformBrowser(this.platformId) && window.console && environment.prod) { const msg = `What are you looking for here? The plot twist is in the next volume!` console.log(`%c${msg}`, "background-color: #166496; color: #ffd60a; font-size: 4rem; font-family: monospace; padding: 8px 16px"); } @@ -39,8 +42,6 @@ export class AppComponent { }) } - private readonly document = inject(DOCUMENT); - @HostListener('window:resize') initScaleDifference() { const w = this.document.documentElement.clientWidth; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0fada98..9beb1f7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,5 @@ import { LOCALE_ID, NgModule, isDevMode, provideZoneChangeDetection } from '@angular/core'; -import { BrowserModule, provideClientHydration } from '@angular/platform-browser'; +import { BrowserModule, provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -30,7 +30,7 @@ registerLocaleData(localeUk) SharedModule], providers: [ provideZoneChangeDetection({ eventCoalescing: true }), - provideClientHydration(), + provideClientHydration(withEventReplay()), provideHttpClient(withFetch()) ] }) diff --git a/src/app/file/data-access/file-hash.service.ts b/src/app/file/data-access/file-hash.service.ts index 876c38a..0c6f875 100644 --- a/src/app/file/data-access/file-hash.service.ts +++ b/src/app/file/data-access/file-hash.service.ts @@ -5,18 +5,41 @@ import * as CryptoJS from 'crypto-js'; providedIn: 'root' }) export class FileHashService { - public getMd5Hash(arrayBuffer: ArrayBuffer) { - const wordArray = this.arrayBufferToWordArray(arrayBuffer); - const md5Hash = CryptoJS.MD5(wordArray).toString(); - return md5Hash; - } + // getSHA256(arrayBuffer: ArrayBuffer): string { + // const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(arrayBuffer) as any); + // return CryptoJS.SHA256(wordArray).toString(CryptoJS.enc.Hex); + // } - private arrayBufferToWordArray(arrayBuffer: ArrayBuffer): CryptoJS.lib.WordArray { - const uint8Array = new Uint8Array(arrayBuffer); - const words: any[] = []; - for (let i = 0; i < uint8Array.length; i++) { - words[i >>> 2] |= uint8Array[i] << (24 - (i % 4) * 8); + async sha256(file: File): Promise { + const sha256 = CryptoJS.algo.SHA256.create(); + const sliceSize = 50_000_000; // 50 MiB + let start = 0; + + while (start < file.size) { + const slice: Uint8Array = await this.readSlice(file, start, sliceSize); + const wordArray = CryptoJS.lib.WordArray.create(slice); + sha256.update(wordArray); + start += sliceSize; } - return CryptoJS.lib.WordArray.create(words, uint8Array.length); + + return sha256.finalize().toString(); + } + + private async readSlice(file: File, start: number, size: number): Promise { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + const slice = file.slice(start, start + size); + + fileReader.onload = () => { + if (fileReader.result) { + resolve(new Uint8Array(fileReader.result as ArrayBuffer)); + } else { + reject(new Error("FileReader returned no result.")); + } + }; + fileReader.onerror = () => reject(fileReader.error || new Error("FileReader failed.")); + fileReader.readAsArrayBuffer(slice); + }); } + } diff --git a/src/app/file/data-access/file-history.service.ts b/src/app/file/data-access/file-history.service.ts new file mode 100644 index 0000000..35bce3b --- /dev/null +++ b/src/app/file/data-access/file-history.service.ts @@ -0,0 +1,87 @@ +import { isPlatformBrowser } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; +import Dexie from 'dexie'; + +const HISTORY_DB_NAME: string = `ChytankaHistoryDB`; +const HISTORY_TABLE_NAME: string = `filehistory`; + +@Injectable({ + providedIn: 'root' +}) +export class FileHistoryService { + private db!: Dexie; + + platformId = inject(PLATFORM_ID) + + constructor() { + this.createDatabase(); + } + + private createDatabase() { + this.db = new Dexie(HISTORY_DB_NAME); + this.db.version(1).stores({ + filehistory: '++id,sha256,pages,size,title,format,page,cover,arrayBuffer,created,updated' + }); + } + + async addHistory(fileHistory: any) { + if (!isPlatformBrowser(this.platformId)) return; + + const { sha256, arrayBuffer, pages, page, cover, size, title, format } = fileHistory + + // await this.db.table(HISTORY_TABLE_NAME).add({ site, post_id, title, cover }); + const existingEntry = await this.db.table(HISTORY_TABLE_NAME).where({ sha256 }).first(); + + if (existingEntry) { + // Entry already exists, update the 'updated' field + const now = new Date().toISOString(); + await this.db.table(HISTORY_TABLE_NAME).update(existingEntry.id, { arrayBuffer, updated: now }); + } else { + // Entry doesn't exist, add a new one + const now = new Date().toISOString(); + await this.db.table(HISTORY_TABLE_NAME).add({ + created: now, + updated: now, + sha256, + arrayBuffer, + pages, page, cover, size, title, format + }); + } + } + + async getAllHistory() { + return await this.db.table(HISTORY_TABLE_NAME).orderBy('updated').reverse().toArray(); + } + + async getItemBySha256(sha256: string) { + return await this.db.table(HISTORY_TABLE_NAME).where('sha256').equals(sha256).first(); +} + + + async getAllHistoryWithoutBufferArray() { + const records = await this.db.table(HISTORY_TABLE_NAME).orderBy('updated').reverse().toArray(); + + return records.map(({ arrayBuffer, ...rest }) => rest); + } + + async getTotalSizeAndCount(): Promise<{ size: number; count: number }> { + let size = 0; + let count = 0; + + await this.db.table(HISTORY_TABLE_NAME).each(item => { + size += item.size; + count++; + }); + + return { size, count }; + } + + async clearHistory() { + await this.db.table(HISTORY_TABLE_NAME).clear(); + } + + async deleteHistoryItem(itemId: number) { + await this.db.table(HISTORY_TABLE_NAME).delete(itemId); + } + +} diff --git a/src/app/file/data-access/file-settings.service.ts b/src/app/file/data-access/file-settings.service.ts new file mode 100644 index 0000000..7e1ca96 --- /dev/null +++ b/src/app/file/data-access/file-settings.service.ts @@ -0,0 +1,70 @@ +import { isPlatformServer } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID, signal, WritableSignal } from '@angular/core'; + +const SAVE_FILE_HISTORY_NAME = "saveFileToHistory" +const COPY_FILE_HISTORY_NAME = "copyFileToHistory" +const RETENTION_TIME_NAME = "retentionTime" +const STORAGE_LIMIT_NAME = "storageLimit" + +const DEFAULT_RETENTION_TIME = 30; // days +const DEFAULT_STORAGE_LIMIT = 1024; //Mb + +@Injectable({ + providedIn: 'root' +}) +export class FileSettingsService { + platformId = inject(PLATFORM_ID) + + readonly saveFileToHistory: WritableSignal = signal(false); + readonly copyFileToHistory: WritableSignal = signal(false); + readonly retentionTime: WritableSignal = signal(DEFAULT_RETENTION_TIME) + readonly storageLimit: WritableSignal = signal(DEFAULT_STORAGE_LIMIT) + + constructor() { + this.init() + } + + init() { + if (isPlatformServer(this.platformId)) return; + + const n = Boolean(localStorage.getItem(SAVE_FILE_HISTORY_NAME) == 'true'); + this.setSaveFileToHistory(n); + + const sf = Boolean(localStorage.getItem(COPY_FILE_HISTORY_NAME) == 'true'); + this.setCopyFileToHistory(sf); + + const rt = Number(localStorage.getItem(RETENTION_TIME_NAME) ?? DEFAULT_RETENTION_TIME); + this.setRetentionTime(rt) + + const sl = Number(localStorage.getItem(STORAGE_LIMIT_NAME) ?? DEFAULT_STORAGE_LIMIT); + this.setStorageLimit(sl) + } + + setSaveFileToHistory(n: boolean) { + if (isPlatformServer(this.platformId)) return; + + this.saveFileToHistory.set(n); + localStorage.setItem(SAVE_FILE_HISTORY_NAME, n.toString()) + } + + setCopyFileToHistory(n: boolean) { + if (isPlatformServer(this.platformId)) return; + + this.copyFileToHistory.set(n); + localStorage.setItem(COPY_FILE_HISTORY_NAME, n.toString()) + } + + setRetentionTime(days: number) { + if (isPlatformServer(this.platformId)) return; + + this.retentionTime.set(days); + localStorage.setItem(RETENTION_TIME_NAME, days.toString()) + } + + setStorageLimit(megabytes: number) { + if (isPlatformServer(this.platformId)) return; + + this.storageLimit.set(megabytes); + localStorage.setItem(STORAGE_LIMIT_NAME, megabytes.toString()) + } +} diff --git a/src/app/file/data-access/zip.worker.ts b/src/app/file/data-access/zip.worker.ts index 7fe606f..70f7732 100644 --- a/src/app/file/data-access/zip.worker.ts +++ b/src/app/file/data-access/zip.worker.ts @@ -11,7 +11,7 @@ addEventListener('message', ({ data }) => { .then(async zip => { const filesName: string[] = Object.keys(zip.files); - console.dir(zip.files) + // console.dir(zip.files) const comicInfoFile = getComicInfoFile(filesName) diff --git a/src/app/file/file-routing.module.ts b/src/app/file/file-routing.module.ts index e36f683..b57caa5 100644 --- a/src/app/file/file-routing.module.ts +++ b/src/app/file/file-routing.module.ts @@ -1,19 +1,19 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +const zip = () => import('./zip/zip.component').then(mod => mod.ZipComponent); +const pdf = () => import('./pdf/pdf.component').then(mod => mod.PdfComponent); +const mobi = () => import('./mobi/mobi.component').then(mod => mod.MobiComponent); + const routes: Routes = [ - { - path: 'zip', - loadComponent: () => import('./zip/zip.component').then(mod => mod.ZipComponent) - }, - { - path: 'pdf', - loadComponent: () => import('./pdf/pdf.component').then(mod => mod.PdfComponent) - }, - { - path: 'mobi', - loadComponent: () => import('./mobi/mobi.component').then(mod => mod.MobiComponent) - } + { path: 'zip/:sha256', loadComponent: zip }, + { path: 'zip', loadComponent: zip }, + + { path: 'pdf/:sha256', loadComponent: pdf }, + { path: 'pdf', loadComponent: pdf }, + + { path: 'mobi/:sha256', loadComponent: mobi }, + { path: 'mobi', loadComponent: mobi } ]; @NgModule({ diff --git a/src/app/file/zip/zip.component.html b/src/app/file/zip/zip.component.html index 0ec8be5..2e84688 100644 --- a/src/app/file/zip/zip.component.html +++ b/src/app/file/zip/zip.component.html @@ -1,5 +1,6 @@ @if(episode && episode.images && episode.images.length > 0){ - + + } @else { } \ No newline at end of file diff --git a/src/app/file/zip/zip.component.ts b/src/app/file/zip/zip.component.ts index 2e73e61..6a4c1f5 100644 --- a/src/app/file/zip/zip.component.ts +++ b/src/app/file/zip/zip.component.ts @@ -1,34 +1,61 @@ -import { Component, effect, inject, OnDestroy, OnInit } from '@angular/core'; +import { Component, effect, inject, OnDestroy, OnInit, signal } from '@angular/core'; import { FileService } from '../data-access/file.service'; import { SharedModule } from '../../shared/shared.module'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CompositionEpisode, CompositionImage } from '../../@site-modules/@common-read'; import { DomManipulationService } from '../../shared/data-access'; import { ComicInfo } from '../../shared/utils/comic-info'; import { Acbf } from '../../shared/utils/acbf'; +import { FileHashService } from '../data-access/file-hash.service'; +import { FileHistoryService } from '../data-access/file-history.service'; +import { FileSettingsService } from '../data-access/file-settings.service'; +import { map } from 'rxjs'; +import { ViewerComponent } from "../../viewer/viewer.component"; @Component({ - selector: 'app-zip', - imports: [SharedModule], - templateUrl: './zip.component.html', - styleUrl: './zip.component.scss' + selector: 'app-zip', + imports: [SharedModule, /*ViewerComponent*/], + templateUrl: './zip.component.html', + styleUrl: './zip.component.scss' }) export class ZipComponent implements OnInit, OnDestroy { private worker!: Worker; - + fileHash = inject(FileHashService) + fileHistory = inject(FileHistoryService) + fileSetts = inject(FileSettingsService) + + sha256: string | undefined; + arrayBuffer: ArrayBuffer | undefined + episode: CompositionEpisode | undefined; router = inject(Router) + private activatedRoute = inject(ActivatedRoute); dm = inject(DomManipulationService) fs = inject(FileService) + status = signal('') constructor() { this.initZipWorker() effect(() => { this.fileChange(); }); } + sha256Params: string = ''; + ngOnInit() { + this.sha256Params = this.activatedRoute.snapshot.params['sha256'] - ngOnInit() { } + if (this.sha256Params && this.sha256Params != '') + this.loadFromHistory(this.sha256Params) + } + + async loadFromHistory(sha256: string) { + const { arrayBuffer, title } = await this.fileHistory.getItemBySha256(sha256) + if (!arrayBuffer) return; + + this.sha256 = sha256 + this.arrayBuffer = arrayBuffer + this.openArrayBuffer(arrayBuffer, title, sha256) + } ngOnDestroy() { this.terminateWorker() @@ -46,10 +73,10 @@ export class ZipComponent implements OnInit, OnDestroy { .set('acbf', this.acbfHandler.bind(this)) - private acbfHandler(msg: any) { - const acbf = new Acbf(msg.data) - - } + private acbfHandler(msg: any) { + const acbf = new Acbf(msg.data) + + } private comicinfoHandler(msg: any) { const comicInfo = new ComicInfo(msg.data) @@ -66,6 +93,20 @@ export class ZipComponent implements OnInit, OnDestroy { if (this.episode) { this.episode.images = imgs + + const obj = { + arrayBuffer: (this.fileSetts.copyFileToHistory()) ? this.arrayBuffer : null, + sha256: this.sha256, + pages: this.episode.images.length, + size: this.fs.file()?.size, + page: 1, + cover: '', + title: this.fs.file()?.name, + format: 'zip' + } + + if (this.fileSetts.saveFileToHistory()) this.fileHistory.addHistory(obj) + } } private fileHandler(msg: any) { @@ -90,20 +131,30 @@ export class ZipComponent implements OnInit, OnDestroy { } fileChange() { + this.status.set(`Opening file: ${this.fs.file()?.name}`) const file = this.fs.file(); if (file && this.worker) { const reader = new FileReader(); reader.onload = (e) => { - const arrayBuffer = reader.result as ArrayBuffer; - this.episode = { - title: file.name, - images: [] - } - this.worker.postMessage({ arrayBuffer: arrayBuffer }); + this.arrayBuffer = reader.result as ArrayBuffer; + + this.openArrayBuffer(this.arrayBuffer, file.name) }; reader.readAsArrayBuffer(file); } else { - this.router.navigateByUrl('/') + // this.router.navigateByUrl('/') } } + + async openArrayBuffer(ab: ArrayBuffer, filename: string, sha256: string = '') { + if (sha256 == '') this.sha256 = await this.fileHash.sha256(this.fs.file() as File) + + this.episode = { title: filename, images: [] } + this.worker.postMessage({ arrayBuffer: ab }); + } + + onPageChange(e: { total: number, current: number[] }) { + const { current, total } = e + console.log(`${current}/${total}`); + } } diff --git a/src/app/history/ui/history-list/history-list.component.html b/src/app/history/ui/history-list/history-list.component.html index d1bfb92..a8b7375 100644 --- a/src/app/history/ui/history-list/history-list.component.html +++ b/src/app/history/ui/history-list/history-list.component.html @@ -1,11 +1,59 @@ -
- @for (item of historyItems() | async; track $index) { - - } -
- -@if (historyItems() | async; as items) { @if (items.length > 0) { - -} @else {}} \ No newline at end of file +@let files = historyFiles() | async; +@let sites = historyItems() | async; + +@if (sites && sites.length > 0) { +
+ + SITES HISTORY + @if ( sites.length > 0) { + + } + + +
+ @for (item of sites; track $index) { + + } +
+
+} + +@if (files && files.length > 0) { +
+ + FILES HISTORY | + {{fileSize() | filesize}} | + {{fileCount()}} + @if ( files.length > 0) { + + } + + +
+ @for (item of files; track item.sha256;) { + @let ab = item.arrayBuffer; +
+ +
+ } +
+
+} + +@if (files?.length ==0 && sites?.length ==0) { +none +} \ No newline at end of file diff --git a/src/app/history/ui/history-list/history-list.component.ts b/src/app/history/ui/history-list/history-list.component.ts index f54334b..f38b586 100644 --- a/src/app/history/ui/history-list/history-list.component.ts +++ b/src/app/history/ui/history-list/history-list.component.ts @@ -1,18 +1,21 @@ import { Component, WritableSignal, inject, signal } from '@angular/core'; import { HistoryService } from '../../data-access/history.service'; import { LangService } from '../../../shared/data-access/lang.service'; +import { FileHistoryService } from '../../../file/data-access/file-history.service'; @Component({ - selector: 'app-history-list', - templateUrl: './history-list.component.html', - styleUrl: './history-list.component.scss', - standalone: false + selector: 'app-history-list', + templateUrl: './history-list.component.html', + styleUrl: './history-list.component.scss', + standalone: false }) export class HistoryListComponent { public history: HistoryService = inject(HistoryService); + public fileHistory: FileHistoryService = inject(FileHistoryService); lang: LangService = inject(LangService); historyItems: WritableSignal> = signal(this.displayHistory() ?? []); + historyFiles: WritableSignal> = signal(this.displayFilesHistory() ?? []); async displayHistory() { const history = await this.history.getAllHistory(); @@ -29,4 +32,33 @@ export class HistoryListComponent { await this.history.clearHistory(); this.historyItems.update(value => this.history.getAllHistory()) } + + async displayFilesHistory() { + const history = await this.fileHistory.getAllHistory(); + this.getTotalSizeAndCount() + + return history; + } + + async clearFileHistory() { + await this.fileHistory.clearHistory(); + this.historyFiles.update(value => this.fileHistory.getAllHistory()) + this.getTotalSizeAndCount() + } + + async delFileById(id: number) { + await this.fileHistory.deleteHistoryItem(id); + this.historyFiles.update(value => this.fileHistory.getAllHistory()) + this.getTotalSizeAndCount() + } + + fileSize = signal(0); + fileCount= signal(0); + + async getTotalSizeAndCount() { + const {count, size} = await this.fileHistory.getTotalSizeAndCount() + + this.fileSize.set(size) + this.fileCount.set(count) + } } diff --git a/src/app/link-parser/data-access/link-parser.service.ts b/src/app/link-parser/data-access/link-parser.service.ts index e0dd19d..d5147f4 100644 --- a/src/app/link-parser/data-access/link-parser.service.ts +++ b/src/app/link-parser/data-access/link-parser.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { LinkParseResult, LinkParser } from '../utils'; @Injectable({ @@ -6,6 +6,21 @@ import { LinkParseResult, LinkParser } from '../utils'; }) export class LinkParserService { + supportFiles = signal([".zip", ".cbz", ".pdf", ".mobi"]) + + supportSites = signal([ + "Imgur", + "Telegra.ph", + "Reddit", + "MangaDex", + "Zenko", + "Comick", + "NHentai", + "Yandere Pools", + "Blankary", + "Pixiv" + ].sort()) + parsers: LinkParser[] = []; constructor() { } diff --git a/src/app/link-parser/link-parser/link-parser.component.html b/src/app/link-parser/link-parser/link-parser.component.html index 8047e61..21977e4 100644 --- a/src/app/link-parser/link-parser/link-parser.component.html +++ b/src/app/link-parser/link-parser/link-parser.component.html @@ -3,8 +3,10 @@ -@defer{ } -@defer{ } \ No newline at end of file +@defer (on immediate) { + + +} \ No newline at end of file diff --git a/src/app/link-parser/link-parser/link-parser.component.scss b/src/app/link-parser/link-parser/link-parser.component.scss index 366fecd..9dae161 100644 --- a/src/app/link-parser/link-parser/link-parser.component.scss +++ b/src/app/link-parser/link-parser/link-parser.component.scss @@ -6,21 +6,29 @@ height: 100dvh; background: rgb(0, 39, 65); + app-chytanka-logo-with-tags { + height: 80dvh; + } + @media (max-aspect-ratio: 1) or (max-width: 640px) { grid-template-columns: auto; - grid-template-rows: 61.8fr 38.2fr; + // grid-template-rows: 61.8fr 38.2fr; + grid-template-rows: 1fr 1fr; + + app-chytanka-logo-with-tags { + height: unset; + } } @media (prefers-color-scheme: light) { background: #eceff2; - background: radial-gradient(circle, #fffcf2 0%, #d4e4f2 100%); } } main { display: grid; row-gap: 2ch; - overflow: auto; + // overflow: auto; max-height: 100%; padding: 4ch 0; } @@ -56,6 +64,7 @@ lp-header { } :host ::ng-deep app-overlay { + &::after, &::before { content: unset; @@ -68,7 +77,7 @@ lp-header { :host:has(dialog[open]) { transform: scale(calc(1 - var(--scale-diff-x, .1)), calc(1 - var(--scale-diff-y, .1))); - filter: blur(3px); + // filter: blur(3px); } :host:has(input[type=url]:focus) { diff --git a/src/app/link-parser/link-parser/link-parser.component.ts b/src/app/link-parser/link-parser/link-parser.component.ts index bb55e4d..d0e4a78 100644 --- a/src/app/link-parser/link-parser/link-parser.component.ts +++ b/src/app/link-parser/link-parser/link-parser.component.ts @@ -1,26 +1,27 @@ -import { Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { LangService } from '../../shared/data-access/lang.service'; import { MetaTagsService } from '../../shared/data-access/meta-tags.service'; +import { LinkParserService } from '../data-access/link-parser.service'; @Component({ - selector: 'app-link-parser', - templateUrl: './link-parser.component.html', - styleUrls: [ - './link-parser.component.scss', - './link-parser.dual-screen.component.scss' - ], - standalone: false + selector: 'app-link-parser', + templateUrl: './link-parser.component.html', + styleUrls: [ + './link-parser.component.scss', + './link-parser.dual-screen.component.scss' + ], + standalone: false, + changeDetection: ChangeDetectionStrategy.OnPush }) export class LinkParserComponent { private meta: MetaTagsService = inject(MetaTagsService); private lang: LangService = inject(LangService) + public parser: LinkParserService = inject(LinkParserService) - /** - * - */ constructor() { this.initMeta() } + initMeta() { this.meta.setOg() this.meta.setTwiter() diff --git a/src/app/link-parser/ui/footer/footer.component.html b/src/app/link-parser/ui/footer/footer.component.html index a15426b..14d44fd 100644 --- a/src/app/link-parser/ui/footer/footer.component.html +++ b/src/app/link-parser/ui/footer/footer.component.html @@ -1,6 +1,7 @@ - {{lang.ph().shortTitle}} {{version}} - + @for (item of social; track $index) { diff --git a/src/app/link-parser/ui/footer/footer.component.ts b/src/app/link-parser/ui/footer/footer.component.ts index dc086ff..2128023 100644 --- a/src/app/link-parser/ui/footer/footer.component.ts +++ b/src/app/link-parser/ui/footer/footer.component.ts @@ -1,5 +1,6 @@ import { Component, inject } from '@angular/core'; import { LangService } from '../../../shared/data-access/lang.service'; +import { environment } from '../../../../environments/environment'; const SOCIAL_LINKS: any[] = [ { @@ -36,7 +37,7 @@ const SOCIAL_LINKS: any[] = [ standalone: false }) export class FooterComponent { - public readonly version: string = 'v2024.11.14' + public readonly version: string = environment.version public lang: LangService = inject(LangService); public social: any[] = SOCIAL_LINKS; diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.html b/src/app/link-parser/ui/parser-form/parser-form.component.html index 7063ecd..8ca8a13 100644 --- a/src/app/link-parser/ui/parser-form/parser-form.component.html +++ b/src/app/link-parser/ui/parser-form/parser-form.component.html @@ -1,18 +1,21 @@
-

@defer{ }

+

- +
- @defer { } +
+
+ {{lang.ph().orOpenFile}} +

{{lang.ph().slogan}}

@if (linkParams()) { -
{{lang.ph().letsgo}} diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.scss b/src/app/link-parser/ui/parser-form/parser-form.component.scss index 2abc349..a7fd6ab 100644 --- a/src/app/link-parser/ui/parser-form/parser-form.component.scss +++ b/src/app/link-parser/ui/parser-form/parser-form.component.scss @@ -9,16 +9,28 @@ } app-text-embracer { - --border-color: #166496; + --mono-color-1: #4c93c8; + --mono-color-2: #002741; + --border-color: var(--mono-color-1); --border-width: 2px; color: #ffd60a; + --shc: #166496; font-family: 'Troubleside', sans-serif; + font-weight: 900; font-size: clamp(1rem, 8vw, 5rem); justify-content: center; + -webkit-text-stroke: var(--mono-color-2) var(--border-width); + --dot-color: var(--mono-color-1); + paint-order: stroke fill; @media (prefers-color-scheme: light) { - --back-color: #fff62a; - color: #166496; + --mono-color-1: #166496; + --mono-color-2: #eceff2; + --shc: var(--mono-color-1); + color: var(--mono-color-1); + -webkit-text-stroke: var(--mono-color-1) var(--border-width); + + color: var(--mono-color-2); } @media (max-width: 1080px) { @@ -67,7 +79,7 @@ textarea { color: #166496; } - + &:focus { border-color: #166496; @@ -99,8 +111,9 @@ input[type=url]::placeholder { .slogan-wrapper { display: grid; - grid-template-columns: 1fr auto; - align-items: center; + // grid-template-columns: 1fr auto; + place-items: center; + gap: 2ch; grid-column: 2; } @@ -110,4 +123,38 @@ input[type=url]::placeholder { font-style: italic; opacity: .72; text-wrap: balance; +} + +.go-btn { + display: flex; + gap: 1ch; + align-items: center; + --dot-color: #166496; + border: 1px solid var(--dot-color); + --stroke: #002741; + --bg-1: var(--gl) 0px 0px / 4px 4px; + --bg-2: var(--gl) 0px 0px / 3px 3px, var(--gl) 1.5px 1.5px / 3px 3px; + --gl: radial-gradient(circle 1px at 0px 0px, var(--dot-color) 1px, transparent 0); + background: var(--bg-1); + + text-transform: uppercase; + font-weight: bold; + // -webkit-text-stroke: 0.5ch var(--stroke); + // paint-order: stroke fill; + + box-shadow: var(--flat-shadow-high); + + + @media (prefers-color-scheme: light) { + --dot-color: #166496; + color: #166496; + --stroke: #eceff2; + } + + &:hover, + &:focus { + --dot-color: #4c93c8; + background: var(--bg-2); + + } } \ No newline at end of file diff --git a/src/app/link-parser/ui/parser-form/parser-form.component.ts b/src/app/link-parser/ui/parser-form/parser-form.component.ts index e8f046b..0a3d7bb 100644 --- a/src/app/link-parser/ui/parser-form/parser-form.component.ts +++ b/src/app/link-parser/ui/parser-form/parser-form.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, inject, PLATFORM_ID, signal, Signal, WritableSignal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, PLATFORM_ID, signal, Signal, WritableSignal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { LangService } from '../../../shared/data-access/lang.service'; import { Base64 } from '../../../shared/utils'; @@ -11,16 +11,14 @@ import { ComickLinkParser } from '../../utils/comick-link-parser'; selector: 'app-parser-form', templateUrl: './parser-form.component.html', styleUrl: './parser-form.component.scss', - standalone: false + standalone: false, + changeDetection: ChangeDetectionStrategy.OnPush }) export class ParserFormComponent { - private router: Router = inject(Router); private route: ActivatedRoute = inject(ActivatedRoute); setts = inject(LinkParserSettingsService) platformId = inject(PLATFORM_ID) - - link: WritableSignal = signal(''); linkParams: Signal = computed(() => this.parser.parse(this.link())); linkParams64: Signal = computed(() => { @@ -31,21 +29,6 @@ export class ParserFormComponent { }; }); - supportFiles = signal([".zip", ".cbz", ".pdf", ".mobi"]) - - supportSites = signal([ - "Imgur", - "Telegra.ph", - "Reddit", - "MangaDex", - "Zenko", - "Comick", - "NHentai", - "Yandere Pools", - "Blankary", - "Pixiv" - ].sort()) - constructor(public parser: LinkParserService, public lang: LangService) { this.initParser(); } @@ -84,8 +67,6 @@ export class ParserFormComponent { if (!this.linkParams()) { this.link.set('') } } - - initUrl() { const routeParamUrl: string | null = this.route.snapshot.paramMap.get('url'); diff --git a/src/app/link-parser/ui/settings/settings.component.html b/src/app/link-parser/ui/settings/settings.component.html index e52651a..c7d4c9f 100644 --- a/src/app/link-parser/ui/settings/settings.component.html +++ b/src/app/link-parser/ui/settings/settings.component.html @@ -1,28 +1,117 @@ -
-
- -

- 🈚 {{lang.ph().language}} - {{lang.ph().getByKey(getLangValue(lang.lang()).hintPhraceKey)}} -

-

{{lang.ph().settingLangDesc}}

-
-
-
- -
-
-

- - @if(setts.autoPasteLink()) { +

+ 🛠️ Base + +
+
+

+ 🈚 {{lang.ph().language}} + {{lang.ph().getByKey(getLangValue(lang.lang()).hintPhraceKey)}} +

+

{{lang.ph().settingLangDesc}}

+
+
+
+ + @if(vibro.enabled()){ +
+
+

+ + @if(vibro.vibrationOn()) { + ON + } @else { + OFF + } +

+

+ {{'Use Vibration API'}} +

+
+ +
+ } + +
+
+

+ + @if(setts.autoPasteLink()) { ON - } @else { + } @else { OFF - } -

-

- {{lang.ph().settingAutoPasteLinkDesc}} -

-
- -
\ No newline at end of file + } +

+

+ {{lang.ph().settingAutoPasteLinkDesc}} +

+
+ +
+ + +
+ 🗐 Files + +
+
+

+ + @if(fileSetts.saveFileToHistory()) { + ON + } @else { + OFF + } +

+

+ {{'Automatically saves files opened locally.'}} +

+
+ +
+ + @if(fileSetts.saveFileToHistory()){ +
+
+

+ + @if(fileSetts.copyFileToHistory()) { + ON + } @else { + OFF + } +

+

+ {{'Adds copies of local files to history.'}} +

+
+ +
+ + @if(fileSetts.copyFileToHistory()){ +
+
+

+ + {{fileSetts.storageLimit()}} +

+

{{'Mb'}}

+
+ +
+ +
+
+

+ + {{fileSetts.retentionTime()}} +

+

{{'Days'}}

+
+ +
+ } + } + +
\ No newline at end of file diff --git a/src/app/link-parser/ui/settings/settings.component.scss b/src/app/link-parser/ui/settings/settings.component.scss index 18c2981..49535c1 100644 --- a/src/app/link-parser/ui/settings/settings.component.scss +++ b/src/app/link-parser/ui/settings/settings.component.scss @@ -1,4 +1,4 @@ -:host { +:host, fieldset { display: grid; gap: 2ch; } @@ -75,4 +75,15 @@ overflow: hidden; } } } +} + +fieldset { + padding: 2ch; + border: 1px dashed #16649680; + border-radius: var(--r); +} + +input[type=number] { + width: 8ch; + text-align: center; } \ No newline at end of file diff --git a/src/app/link-parser/ui/settings/settings.component.ts b/src/app/link-parser/ui/settings/settings.component.ts index 5532b79..5bbbb52 100644 --- a/src/app/link-parser/ui/settings/settings.component.ts +++ b/src/app/link-parser/ui/settings/settings.component.ts @@ -1,16 +1,20 @@ -import { Component, WritableSignal, effect, inject, signal } from '@angular/core'; +import { Component, WritableSignal, effect, inject, input, signal } from '@angular/core'; import { LinkParserSettingsService } from '../../data-access/link-parser-settings.service'; import { LangService } from '../../../shared/data-access/lang.service'; +import { FileSettingsService } from '../../../file/data-access/file-settings.service'; +import { VibrationService } from '../../../shared/data-access/vibration.service'; @Component({ - selector: 'app-settings', - templateUrl: './settings.component.html', - styleUrl: './settings.component.scss', - standalone: false + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrl: './settings.component.scss', + standalone: false }) export class SettingsComponent { setts = inject(LinkParserSettingsService) lang = inject(LangService) + fileSetts = inject(FileSettingsService) + vibro = inject(VibrationService) getLangValue(lang: string) { @@ -19,6 +23,44 @@ export class SettingsComponent { setAutoPasteLink(e: Event) { this.setts.setAutoPasteLink((e.target as HTMLInputElement).checked) + + this.vibro.vibrateForSettings(this.setts.autoPasteLink()) + + } + + setSaveFileToHistory(e: Event) { + this.fileSetts.setSaveFileToHistory((e.target as HTMLInputElement).checked) + + this.vibro.vibrateForSettings(this.fileSetts.saveFileToHistory()) + } + + setCopyFileToHistory(e: Event) { + this.fileSetts.setCopyFileToHistory((e.target as HTMLInputElement).checked) + + this.vibro.vibrateForSettings(this.fileSetts.copyFileToHistory()) + } + + setRetentionTime(e: Event) { + this.fileSetts.setRetentionTime(Number((e.target as HTMLInputElement).value)) + + this.vibro.vibrate() + } + + setStorageLimit(e: Event) { + this.fileSetts.setStorageLimit(Number((e.target as HTMLInputElement).value)) + + this.vibro.vibrate() } + setVibration(e: Event) { + this.vibro.setVibrationOn((e.target as HTMLInputElement).checked) + + this.vibro.vibrateForSettings(this.vibro.vibrationOn()) + } + + vibrateLangToggle(e: string) { + setTimeout(() => { + this.vibro.vibrateLangToggle(e) + }, 0); + } } diff --git a/src/app/page-not-found.component.ts b/src/app/page-not-found.component.ts index 81883dc..c831d04 100644 --- a/src/app/page-not-found.component.ts +++ b/src/app/page-not-found.component.ts @@ -3,12 +3,12 @@ import { LangService } from './shared/data-access/lang.service'; import { isPlatformBrowser } from '@angular/common'; @Component({ - selector: 'app-page-not-found', - template: ` + selector: 'app-page-not-found', + template: `

{{selectedMessage()}}
🏠

`, - styles: ` + styles: ` :host { display: grid; min-height: 100dvh; @@ -16,18 +16,40 @@ import { isPlatformBrowser } from '@angular/common'; text-align: center; } app-text-embracer { - --border-color: #166496; - --border-width: 2px; - color: #ffd60a; - margin: auto; - font-family: 'Troubleside', sans-serif; - font-size: clamp(1rem, 8vw, 5rem); - @media (max-aspect-ratio: 1) or (max-width: 640px) { - font-size: clamp(1rem, 10vw, 8rem); - } + --mono-color-1: #4c93c8; + --mono-color-2: #002741; + --border-color: var(--mono-color-1); + --border-width: 2px; + color: #ffd60a; + --shc: #166496; + font-family: 'Troubleside', sans-serif; + font-weight: 900; + font-size: clamp(1rem, 16vw, 10rem); + justify-content: center; + -webkit-text-stroke: var(--mono-color-2) var(--border-width); + --dot-color: var(--mono-color-1); + paint-order: stroke fill; + + @media (prefers-color-scheme: light) { + --mono-color-1: #166496; + --mono-color-2: #eceff2; + --shc: var(--mono-color-1); + color: var(--mono-color-1); + -webkit-text-stroke: var(--mono-color-1) var(--border-width); + + color: var(--mono-color-2); + } + + @media (max-width: 1080px) { + font-size: clamp(1rem, 14vw, 16rem); } + + @media ((orientation: portrait) and (max-aspect-ratio: 1)) { + font-size: clamp(1rem, 20vw, 16rem); + } +} `, - standalone: false + standalone: false }) export class PageNotFoundComponent { platformId = inject(PLATFORM_ID) diff --git a/src/app/shared/data-access/lang.service.ts b/src/app/shared/data-access/lang.service.ts index cfc025f..e69a78a 100644 --- a/src/app/shared/data-access/lang.service.ts +++ b/src/app/shared/data-access/lang.service.ts @@ -4,6 +4,7 @@ import { Observable, map, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { ViewModeOption } from './viewer.service'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { VibrationService } from './vibration.service'; const LANG_OPTIONS: ViewModeOption[] = [ { dir: "rtl", mode: "pages", hintPhraceKey: "english", code: "en", emoji: "🇬🇧" }, @@ -26,6 +27,8 @@ export class LangService { langOpt = LANG_OPTIONS platformId = inject(PLATFORM_ID) private readonly document = inject(DOCUMENT); + vibro = inject(VibrationService) + lang: WritableSignal = signal( (!isPlatformBrowser(this.platformId)) ? DEFAULT_LANG : @@ -48,6 +51,8 @@ export class LangService { localStorage.setItem(LANG_STORAGE_NAME, lang) this.updateTranslate(); + + this.vibro.vibrateLangToggle(this.lang()) } updateManifest() { diff --git a/src/app/shared/data-access/vibration.service.ts b/src/app/shared/data-access/vibration.service.ts new file mode 100644 index 0000000..88c77c9 --- /dev/null +++ b/src/app/shared/data-access/vibration.service.ts @@ -0,0 +1,120 @@ +import { isPlatformServer } from '@angular/common'; +import { computed, inject, Injectable, PLATFORM_ID, Signal, signal, WritableSignal } from '@angular/core'; + +const DOT = 40; // Коротка вібрація для точки +const DASH = 80; // Довга вібрація для тире +const INTRA_LETTER_PAUSE = 64; // Пауза між елементами в одній букві +const LETTER_PAUSE = 96; // Пауза між літерами +const WORD_PAUSE = 128; // Пауза між словами + +const morseCode = new Map([ + ['A', ['.', '-']], // .- + ['B', ['-', '.', '.', '.']], // -... + ['C', ['-', '.', '-', '.']], // -.-. + ['D', ['-', '.', '.']], // -.. + ['E', ['.']], // . + ['F', ['.', '.', '-', '.']], // ..-. + ['G', ['-', '-', '.']], // --. + ['H', ['.', '.', '.', '.']], // .... + ['I', ['.', '.']], // .. + ['J', ['.', '-', '-', '-']], // .--- + ['K', ['-', '.', '-']], // -.- + ['L', ['.', '-', '.', '.']], // .-.. + ['M', ['-', '-']], // -- + ['N', ['-', '.']], // -. + ['O', ['-', '-', '-']], // --- + ['P', ['.', '-', '-', '.']], // .--. + ['Q', ['-', '-', '.', '-']], // --.- + ['R', ['.', '-', '.']], // .-. + ['S', ['.', '.', '.']], // ... + ['T', ['-']], // - + ['U', ['.', '.', '-']], // ..- + ['V', ['.', '.', '.', '-']], // ...- + ['W', ['.', '-', '-']], // .-- + ['X', ['-', '.', '.', '-']], // -..- + ['Y', ['-', '.', '-', '-']], // -.-- + ['Z', ['-', '-', '.', '.']], // --.. + ['1', ['.', '-', '-', '-', '-']], // .---- + ['2', ['.', '.', '-', '-', '-']], // ..--- + ['3', ['.', '.', '.', '-', '-']], // ...-- + ['4', ['.', '.', '.', '.', '-']], // ....- + ['5', ['.', '.', '.', '.', '.']], // ..... + ['6', ['-', '.', '.', '.', '.']], // -.... + ['7', ['-', '-', '.', '.', '.']], // --... + ['8', ['-', '-', '-', '.', '.']], // ---.. + ['9', ['-', '-', '-', '-', '.']], // ----. + ['0', ['-', '-', '-', '-', '-']], // ----- + [' ', []] +]); + +@Injectable({ + providedIn: 'root' +}) +export class VibrationService { + vibrationOn: WritableSignal = signal(false); + + enabled = computed(() => { + return (!isPlatformServer(this._platformId) && ("vibrate" in navigator)) + }) + + private _platformId = inject(PLATFORM_ID) + + constructor() { + this.initVibrationOn() + } + + initVibrationOn() { + if (isPlatformServer(this._platformId)) return; + + const n = Boolean(localStorage.getItem('vibrationOn') == 'true'); + this.vibrationOn.set(n); + } + + setVibrationOn(n: boolean) { + if (isPlatformServer(this._platformId)) return; + + this.vibrationOn.set(n); + localStorage.setItem('vibrationOn', n.toString()) + } + + vibrate(pattern: VibratePattern = DOT) { + + if (isPlatformServer(this._platformId) || !this.vibrationOn()) return + + navigator.vibrate(0); + navigator.vibrate(pattern) + } + + vibrateForSettings = (isEnabled: boolean) => this.vibrate(this.getVibrationPattern(isEnabled ? "ON" : "OFF")); + + vibrateLangToggle = (lang: string) => this.vibrate(this.getVibrationPattern(lang.toUpperCase())) + + getVibrationPattern(text: string) { + + let pattern: VibratePattern = []; + + text.toUpperCase().split('').forEach((char, index, array) => { + const morse = morseCode.get(char); + if (morse) { + morse.forEach((signal, signalIndex) => { + pattern.push(signal === '.' ? DOT : DASH); // Вібрація для точки або тире + if (signalIndex < morse.length - 1) { + pattern.push(INTRA_LETTER_PAUSE); // Пауза між елементами букви + } + }); + + // Пауза між літерами + if (index < array.length - 1) { + pattern.push(LETTER_PAUSE); + } + } + + // Пауза між словами + if (index < array.length - 1 && array[index + 1] === ' ') { + pattern.push(WORD_PAUSE); + } + }); + + return pattern; + } +} diff --git a/src/app/shared/data-access/viewer.service.ts b/src/app/shared/data-access/viewer.service.ts index 1323890..a1f26f7 100644 --- a/src/app/shared/data-access/viewer.service.ts +++ b/src/app/shared/data-access/viewer.service.ts @@ -26,7 +26,7 @@ export class ViewerService { public viewModeOption: WritableSignal = signal(VIEV_MODE_OPTIONS[0]); - nightlight: WritableSignal = signal(0); + nightlight: WritableSignal = signal(6500); keyboard: boolean = (isPlatformBrowser(this.platformId)) && (navigator as any).keyboard; diff --git a/src/app/shared/pipes/filesize.pipe.ts b/src/app/shared/pipes/filesize.pipe.ts new file mode 100644 index 0000000..3a42a17 --- /dev/null +++ b/src/app/shared/pipes/filesize.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'filesize', + standalone: false +}) +export class FileSizePipe implements PipeTransform { + transform(size: number, extension: string = 'MB') { + return (size / (1024 * 1024)).toFixed(2) + extension; + } +} \ No newline at end of file diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9149997..3d54611 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -23,6 +23,10 @@ import { ViewerFooterComponent } from './ui/viewer/components/viewer-footer/view import { ViewerHeaderComponent } from './ui/viewer/components/viewer-header/viewer-header.component'; import { MangaPageEvenComponent } from './ui/manga-page/manga-page-even.component'; import { FileChangeComponent } from './ui/file-change/file-change.component'; +import { ChytankaLogoWithTagsComponent } from './ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component'; +import { FileSizePipe } from './pipes/filesize.pipe'; +import { RoughPaperComponent } from './ui/filters/rough-paper/rough-paper.component'; +import { SharpenComponent } from './ui/filters/sharpen/sharpen.component'; @@ -48,13 +52,17 @@ import { FileChangeComponent } from './ui/file-change/file-change.component'; ViewerFooterComponent, ViewerHeaderComponent, MangaPageEvenComponent, - FileChangeComponent + FileChangeComponent, + ChytankaLogoWithTagsComponent, + FileSizePipe ], imports: [ CommonModule, FormsModule, - RouterModule + RouterModule, + RoughPaperComponent, + SharpenComponent ], - exports: [TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent] + exports: [TruncatePipe, TextEmbracerComponent, ViewerComponent, OverlayComponent, ViewModeBarComponent, MadeInUkraineComponent, DialogComponent, LangToggleComponent, TitleCardComponent, LoadingComponent, SeparatorComponent, FileChangeComponent, ChytankaLogoWithTagsComponent, FileSizePipe] }) export class SharedModule { } diff --git a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html new file mode 100644 index 0000000..f047f7f --- /dev/null +++ b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.html @@ -0,0 +1,38 @@ +@for (tag of siteTags(); track $index) { +@let c = getRandomCoordinates(); +@let s = getRandom(0.8, 1.28); +
  • {{formatTagname(tag)}}
  • +} +@for (tag of fileTags(); track $index) { +@let c = getRandomCoordinates(); +@let s = getRandom(0.8, 1.28); +
  • {{formatTagname(tag)}}
  • +} + + + + + + + + + + + + + + + + + + + + + + + + +v{{version}} \ No newline at end of file diff --git a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.scss b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.scss new file mode 100644 index 0000000..0aced22 --- /dev/null +++ b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.scss @@ -0,0 +1,132 @@ +:host { + position: relative; + // overflow: hidden; + min-height: 0; + min-width: 0; + display: grid; + place-items: stretch; +} + +#logoPath { + + fill: #000118; + + @media (prefers-color-scheme: light) { + fill: #166496; + } +} + +.one-bit { + filter: url('#oneBit'); +} + +.filters { + position: fixed; + user-select: none; + pointer-events: none; +} + +svg, +img { + display: block; + min-width: 0; + min-height: 0; + user-select: none; + pointer-events: none; +} + +.version { + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%,-50%); + font-weight: bold; + text-align: center; + opacity: .5; + color: #fff; + + @media (prefers-color-scheme: light) { + color: #000; + } + + a, + a:hover { + color: inherit; + } + + a:hover { + text-decoration: underline; + } +} + +.tag { + z-index: 1; + transition: all var(--t) linear; + position: absolute; + padding: .25ch; + text-align: center; + border-radius: .25ch; + font-size: small; + font-family: 'Courier New', Courier, monospace; + font-weight: bold; + --b: #ffd60a; + --f: black; + background-color: var(--b); + color: var(--f); + box-shadow: var(--flat-shadow-medium); + list-style: none; + + &:hover { + cursor: none; + scale: 1.5 !important; + z-index: 2; + } + + &.reddit { + --b: #FF4500; + --f: white; + } + + &.pixiv { + --b: #0091d4; + --f: white; + } + + &.blankary { + --b: #000; + --f: white; + } + + // &.comick {} + + &.imgur { + --b: #1bb76e; + } + + &.mangadex { + --b: #ff6740; + } + + // &.nhentai {} + + &.zenko { + --b: #078DEE; + --f: white; + } + + &.zip { + --b: #0074D9; + } + + &.cbz { + --b: rgb(216, 68, 68); + } + + &.pdf { + --b: #0074D9; + } + + &.mobi { + --b: #4CAF50; + --f: #FFC107; + } +} \ No newline at end of file diff --git a/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.ts b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.ts new file mode 100644 index 0000000..5adf6e3 --- /dev/null +++ b/src/app/shared/ui/chytanka-logo-with-tags/chytanka-logo-with-tags.component.ts @@ -0,0 +1,33 @@ +import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; +import { environment } from '../../../../environments/environment'; + +type Coordinate = { x: number; y: number }; + +@Component({ + selector: 'app-chytanka-logo-with-tags', + standalone: false, + + templateUrl: './chytanka-logo-with-tags.component.html', + styleUrl: './chytanka-logo-with-tags.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ChytankaLogoWithTagsComponent { + version = environment.version; + fileTags = input([]) + siteTags = input([]) + + formatTagname(tagname: string) { + return tagname.replaceAll('.', '').toLowerCase() + } + + getRandom = (min: number = 10, max: number = 90): number => { + return Math.random() * (max - min) + min; + }; + + getRandomCoordinates(): Coordinate { + const x = this.getRandom(); + const y = this.getRandom(); + + return { x: parseFloat(x.toFixed(2)), y: parseFloat(y.toFixed(2)) }; + } +} diff --git a/src/app/shared/ui/dialog/dialog.component.scss b/src/app/shared/ui/dialog/dialog.component.scss index e32cb60..b4fc055 100644 --- a/src/app/shared/ui/dialog/dialog.component.scss +++ b/src/app/shared/ui/dialog/dialog.component.scss @@ -33,6 +33,7 @@ dialog { .dialog-wrapper { display: grid; max-height: 100%; + width: 100%; grid-template: auto 1fr auto / 1fr; width: min(80vw, 60ch); } @@ -90,7 +91,7 @@ dialog { } &[open] { - display: flex; + // display: flex; box-shadow: var(--flat-shadow-high); } } \ No newline at end of file diff --git a/src/app/shared/ui/file-change/file-change.component.ts b/src/app/shared/ui/file-change/file-change.component.ts index b75285d..ac3721d 100644 --- a/src/app/shared/ui/file-change/file-change.component.ts +++ b/src/app/shared/ui/file-change/file-change.component.ts @@ -1,37 +1,43 @@ -import { Component, HostListener, inject, input, OnInit } from '@angular/core'; +import { Component, HostListener, inject, input, OnInit, PLATFORM_ID } from '@angular/core'; import { FileService } from '../../../file/data-access/file.service'; import { Router } from '@angular/router'; import { FILE_PATH } from '../../../app-routing.module'; import { LangService } from '../../data-access/lang.service'; +import { isPlatformServer } from '@angular/common'; @Component({ - selector: 'app-file-change', - templateUrl: './file-change.component.html', - styleUrl: './file-change.component.scss', - standalone: false + selector: 'app-file-change', + templateUrl: './file-change.component.html', + styleUrl: './file-change.component.scss', + standalone: false }) export class FileChangeComponent implements OnInit { + platformId = inject(PLATFORM_ID) + fs = inject(FileService) + router = inject(Router) + lang = inject(LangService) + + accept = input([]) + + input: HTMLInputElement | undefined; + showDragAndDropZone: boolean = false; + ngOnInit(): void { + if (isPlatformServer(this.platformId)) return; + this.initFileInput(); if ("launchQueue" in window) { (window as any).launchQueue.setConsumer(async (launchParams: FileSystemFileHandle) => { - console.log(launchParams); - - const file: File = await launchParams.getFile(); + console.log((launchParams as any).files[0]); + + // const file: File = await launchParams.getFile(); + const file: File = (launchParams as any).files[0] as File; this.fileHandler(file) }); } } - // should be input - // accept = [".zip", ".cbz", 'application/vnd.comicbook+zip', 'application/zip', '.pdf'] - accept = input([]) - - fs = inject(FileService) - router = inject(Router) - lang = inject(LangService) - onFileSelected(event: any) { const file: File = event.target.files[0]; @@ -54,18 +60,16 @@ export class FileChangeComponent implements OnInit { getRouteType(file: File): string | undefined { const fileType = file.type || file.name.split('.').pop()?.toLowerCase(); - + if (!fileType) return undefined; - + if (fileType.includes('pdf')) return 'pdf'; if (fileType.includes('mobi')) return 'mobi'; if (/zip|cbz/.test(fileType)) return 'zip'; - + return undefined; } - input = document.createElement('input') - initFileInput() { this.input = document.createElement('input') @@ -75,12 +79,10 @@ export class FileChangeComponent implements OnInit { this.input.oninput = (e: Event) => { this.onFileSelected(e) } - } openFileDialog() { - - this.input.click(); + if (this.input) this.input.click(); } @HostListener('window:keydown', ['$event']) @@ -109,6 +111,4 @@ export class FileChangeComponent implements OnInit { this.fileHandler(file) } - - showDragAndDropZone: boolean = false; } diff --git a/src/app/shared/ui/filters/rough-paper/rough-paper.component.ts b/src/app/shared/ui/filters/rough-paper/rough-paper.component.ts new file mode 100644 index 0000000..15fe0a4 --- /dev/null +++ b/src/app/shared/ui/filters/rough-paper/rough-paper.component.ts @@ -0,0 +1,37 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'fx-rough-paper', + imports: [], + template: ` + + + + + + +`, + styles: ` + svg { + position: fixed; + user-select: none; + pointer-events: none; + width: 0; + height: 0; + z-index: -1; + } +` +}) +export class RoughPaperComponent { + baseFrequency = input(0.04) + numOctaves = input(5) + azimuth = input(45) + elevation = input(100) + lightingColor = input("#ffffff") +} diff --git a/src/app/shared/ui/filters/sharpen/sharpen.component.ts b/src/app/shared/ui/filters/sharpen/sharpen.component.ts new file mode 100644 index 0000000..0f65185 --- /dev/null +++ b/src/app/shared/ui/filters/sharpen/sharpen.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'fx-sharpen', + imports: [], + template: ` + + + + + `, + styles: ` + svg { + position: fixed; + user-select: none; + pointer-events: none; + width: 0; + height: 0; + z-index: -1; + } + ` +}) +export class SharpenComponent { + +} diff --git a/src/app/shared/ui/manga-page/manga-page-even.component.scss b/src/app/shared/ui/manga-page/manga-page-even.component.scss index 03e7dd8..e49a26c 100644 --- a/src/app/shared/ui/manga-page/manga-page-even.component.scss +++ b/src/app/shared/ui/manga-page/manga-page-even.component.scss @@ -25,7 +25,7 @@ section { display: grid; place-items: center; overflow: hidden; - + background: var(--bg-1); counter-increment: read-order; &:nth-child(1), diff --git a/src/app/shared/ui/manga-page/manga-page.component.scss b/src/app/shared/ui/manga-page/manga-page.component.scss index 1c603bf..5b9588a 100644 --- a/src/app/shared/ui/manga-page/manga-page.component.scss +++ b/src/app/shared/ui/manga-page/manga-page.component.scss @@ -21,6 +21,7 @@ section { place-items: center; overflow: hidden; counter-increment: read-order; + background: var(--bg-1); &::after { content: counter(read-order); diff --git a/src/app/shared/ui/pages-indicator/pages-indicator.component.ts b/src/app/shared/ui/pages-indicator/pages-indicator.component.ts index 30cd313..f3e275b 100644 --- a/src/app/shared/ui/pages-indicator/pages-indicator.component.ts +++ b/src/app/shared/ui/pages-indicator/pages-indicator.component.ts @@ -30,7 +30,7 @@ export class PagesIndicatorComponent { if(this.activeIndexs().length ==0) return [] const totalPages = this.images().length const currentPage = Math.max(...this.activeIndexs().map(v => v + 1)) - console.log(currentPage); + // console.log(currentPage); return pairPagination({ totalPages, currentPage }) as number[] }) diff --git a/src/app/shared/ui/text-embracer/text-embracer.component.html b/src/app/shared/ui/text-embracer/text-embracer.component.html index 0bd8c04..2875690 100644 --- a/src/app/shared/ui/text-embracer/text-embracer.component.html +++ b/src/app/shared/ui/text-embracer/text-embracer.component.html @@ -1,3 +1,14 @@ @for(item of letters(); track item) { -{{item}} -} \ No newline at end of file +{{item}} +} + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/shared/ui/text-embracer/text-embracer.component.scss b/src/app/shared/ui/text-embracer/text-embracer.component.scss index 9b18765..f7ad1b4 100644 --- a/src/app/shared/ui/text-embracer/text-embracer.component.scss +++ b/src/app/shared/ui/text-embracer/text-embracer.component.scss @@ -3,6 +3,7 @@ --border-width: 1px; --border-style: solid; --back-color: transparent; + --shc: var(--border-color); margin: 0; text-transform: uppercase; text-align: center; @@ -11,7 +12,8 @@ display: flex; gap: .1ch; - & > span { + &>span { + text-box: ex alphabetic; aspect-ratio: 2/3; display: grid; border: var(--border-width) var(--border-style) var(--border-color); @@ -19,7 +21,26 @@ height: 1.5lh; place-content: center; background-color: var(--back-color); - --shc: var(--border-color); - text-shadow: 0.025ch .025ch var(--shc), 0.05ch .05ch var(--shc), 0.075ch .075ch var(--shc), 0.1ch .1ch var(--shc); + --bg-1: var(--gl) 0px 0px / 4px 4px; + --bg-2: var(--gl) 0px 0px / 3px 3px, var(--gl) 1.5px 1.5px / 3px 3px; + --gl: radial-gradient(circle 1px at 0px 0px, var(--dot-color) 1px, transparent 0); + background: var(--bg-1); + + text-shadow: 0.025ch .025ch var(--shc), + 0.05ch .05ch var(--shc), + 0.075ch .075ch var(--shc), + 0.1ch .1ch var(--shc); + transition: all var(--t) ease-in-out; + + &:hover { + background: var(--bg-2); + } + + span { + left: -0.05ch; + top: -0.05ch; + position: relative; + // filter: url("#oneBitShadow"); + } } } \ No newline at end of file diff --git a/src/app/shared/ui/title-card/title-card.component.html b/src/app/shared/ui/title-card/title-card.component.html index e0a8355..90a5ffd 100644 --- a/src/app/shared/ui/title-card/title-card.component.html +++ b/src/app/shared/ui/title-card/title-card.component.html @@ -1,12 +1,27 @@ +@let isLink = (value?.site && value?.post_id); + +@if (isLink) {
    - +
    - - {{value?.site}} + + {{getSiteTag()}}  + @if(value.size){ {{value.size | filesize}}  } + @if(value.page && value.pages){ {{value.page}}/{{value.pages}}  } + {{value.title ?? lang.ph().untitled}} + +} @else { +
    + +
    +
    + {{value.title ?? lang.ph().untitled}} @if(value.page && value.pages){ {{value.page}}/{{value.pages}} } +
    +} {{value.updated | date : 'short'}} \ No newline at end of file diff --git a/src/app/shared/ui/title-card/title-card.component.scss b/src/app/shared/ui/title-card/title-card.component.scss index e7cc06d..254046a 100644 --- a/src/app/shared/ui/title-card/title-card.component.scss +++ b/src/app/shared/ui/title-card/title-card.component.scss @@ -52,4 +52,5 @@ background-color: #166496; color: white; filter: grayscale(1); + text-transform: uppercase; } \ No newline at end of file diff --git a/src/app/shared/ui/title-card/title-card.component.ts b/src/app/shared/ui/title-card/title-card.component.ts index 676c83e..436b47f 100644 --- a/src/app/shared/ui/title-card/title-card.component.ts +++ b/src/app/shared/ui/title-card/title-card.component.ts @@ -2,11 +2,11 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject import { LangService } from '../../data-access/lang.service'; @Component({ - selector: 'app-title-card', - templateUrl: './title-card.component.html', - styleUrl: './title-card.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false + selector: 'app-title-card', + templateUrl: './title-card.component.html', + styleUrl: './title-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class TitleCardComponent { @Input() value: any = {}; @@ -18,4 +18,13 @@ export class TitleCardComponent { } lang = inject(LangService); + + getRouteLink() { + const foo = (this.value?.site instanceof Array) ? this.value?.site : [this.value?.site] + return ['', ...foo, this.value?.post_id] + } + + getSiteTag() { + return (this.value?.site instanceof Array) ? this.value?.site.pop() : this.value?.site + } } diff --git a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.html b/src/app/shared/ui/viewer/components/hint-page/hint-page.component.html index 4d6a996..6f69ae1 100644 --- a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.html +++ b/src/app/shared/ui/viewer/components/hint-page/hint-page.component.html @@ -1,7 +1,6 @@ - - + {{lang.ph().getByKey(viewer.viewModeOption().hintPhraceKey)}} {{viewer.viewModeOption().emoji}} @@ -24,7 +23,6 @@ - @@ -47,6 +45,4 @@ }
    - - \ No newline at end of file diff --git a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.scss b/src/app/shared/ui/viewer/components/hint-page/hint-page.component.scss index 0b8405c..1e3f532 100644 --- a/src/app/shared/ui/viewer/components/hint-page/hint-page.component.scss +++ b/src/app/shared/ui/viewer/components/hint-page/hint-page.component.scss @@ -14,8 +14,9 @@ svg { text { text-anchor: middle; - font-family: 'Troubleside', monospace; + font-family: monospace; direction: ltr; + text-transform: uppercase; } &.abs { @@ -35,5 +36,6 @@ svg { font-style: italic; opacity: .8; font-family: 'EB Garamond'; + text-transform: unset; } } \ No newline at end of file diff --git a/src/app/shared/ui/viewer/viewer.component.html b/src/app/shared/ui/viewer/viewer.component.html index 3c36e46..edc75f9 100644 --- a/src/app/shared/ui/viewer/viewer.component.html +++ b/src/app/shared/ui/viewer/viewer.component.html @@ -126,5 +126,7 @@ } @defer{ - + + + } \ No newline at end of file diff --git a/src/app/shared/ui/viewer/viewer.component.ts b/src/app/shared/ui/viewer/viewer.component.ts index 61455c6..dd0119f 100644 --- a/src/app/shared/ui/viewer/viewer.component.ts +++ b/src/app/shared/ui/viewer/viewer.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Input, PLATFORM_ID, Signal, ViewChild, WritableSignal, computed, inject, signal } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Input, PLATFORM_ID, Signal, ViewChild, WritableSignal, computed, inject, output, signal } from '@angular/core'; import { CompositionEpisode } from '../../../@site-modules/@common-read'; import { ViewerService, DomManipulationService } from '../../data-access'; import { ActivatedRoute, Router } from '@angular/router'; @@ -16,22 +16,24 @@ const CHTNK_LIST_RESPONCE_EVENT_NAME = 'listresponse' const CHTNK_LIST_REQUEST_EVENT_NAME = 'listrequest' @Component({ - selector: 'app-viewer', - templateUrl: './viewer.component.html', - styleUrls: [ - './viewer.component.scss', - './viewer.pages.component.scss', - './viewer.long.component.scss', - '../../../shared/ui/@styles/details.scss', - '../../../shared/ui/@styles/input-group.scss' - ], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false + selector: 'app-viewer', + templateUrl: './viewer.component.html', + styleUrls: [ + './viewer.component.scss', + './viewer.pages.component.scss', + './viewer.long.component.scss', + '../../../shared/ui/@styles/details.scss', + '../../../shared/ui/@styles/input-group.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class ViewerComponent implements AfterViewInit { readonly separator: string = '│' showNsfw: WritableSignal = signal(false); + pagechange = output<{ total: number, current: number[] }>() + @Input() episode: CompositionEpisode | undefined = undefined; @Input() playlist: Playlist = []; @Input() playlistLink: string = ""; @@ -129,18 +131,18 @@ export class ViewerComponent implements AfterViewInit { } // this.showOverlay = false; - console.log(); - + // console.log(); + // if (JSON.stringify(this.activeIndexs()) !== JSON.stringify(activeIndxs)) - this.activeIndexs.set(activeIndxs); + this.activeIndexs.set(activeIndxs); + const total = this.episode?.images.length + const current = activeIndxs.map(i => i + 1) + this.embedHelper.postMessage({ total, current }, CHTNK_CHANGE_PAGE_EVENT_NAME); - this.embedHelper.postMessage({ - total: this.episode?.images.length, - current: activeIndxs.map(i => i + 1) - }, CHTNK_CHANGE_PAGE_EVENT_NAME); + this.pagechange.emit({ total: Number(total), current }) } diff --git a/src/app/shared/ui/warm-control/warm-control.component.html b/src/app/shared/ui/warm-control/warm-control.component.html index ef022ab..f724312 100644 --- a/src/app/shared/ui/warm-control/warm-control.component.html +++ b/src/app/shared/ui/warm-control/warm-control.component.html @@ -1,4 +1,7 @@ - --> + + \ No newline at end of file diff --git a/src/app/shared/ui/warm-filter/warm-filter.component.html b/src/app/shared/ui/warm-filter/warm-filter.component.html index 7ebb324..d687dfa 100644 --- a/src/app/shared/ui/warm-filter/warm-filter.component.html +++ b/src/app/shared/ui/warm-filter/warm-filter.component.html @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/src/app/shared/ui/warm-filter/warm-filter.component.ts b/src/app/shared/ui/warm-filter/warm-filter.component.ts index 612315d..3ad3e57 100644 --- a/src/app/shared/ui/warm-filter/warm-filter.component.ts +++ b/src/app/shared/ui/warm-filter/warm-filter.component.ts @@ -1,15 +1,54 @@ -import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, HostBinding, input, Input, signal } from '@angular/core'; @Component({ - selector: 'app-warm-filter', - templateUrl: './warm-filter.component.html', - styleUrl: './warm-filter.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false + selector: 'app-warm-filter', + templateUrl: './warm-filter.component.html', + styleUrl: './warm-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false }) export class WarmFilterComponent { - @Input() - @HostBinding('style.opacity') - value: number = 0; + kelvin = input(5000) + + matrixKelvin = computed(() => { + const { r, g, b } = this.kelvinToRgbMatrix(this.kelvin()) + + return `${r} 0 0 0 0 0 ${g} 0 0 0 0 0 ${b} 0 0 0 0 0 1 0` + }) + + + + + kelvinToRgbMatrix(kelvin: number): { r: number, g: number, b: number } { + if (kelvin < 1000 || kelvin > 6500) { + throw new Error("Kelvin value must be between 1000 and 6500."); + } + + // Нормалізація значення K + const temperature = kelvin / 100; + + // Розрахунок червоного компонента + const red = temperature <= 66 + ? 255 + : Math.min(255, 329.698727446 * Math.pow(temperature - 60, -0.1332047592)); + + // Розрахунок зеленого компонента + const green = temperature <= 66 + ? Math.min(255, 99.4708025861 * Math.log(temperature) - 161.1195681661) + : Math.min(255, 288.1221695283 * Math.pow(temperature - 60, -0.0755148492)); + + // Розрахунок синього компонента + const blue = temperature >= 66 + ? 255 + : temperature <= 19 + ? 0 + : Math.min(255, 138.5177312231 * Math.log(temperature - 10) - 305.0447927307); + + // Перетворення значень у діапазон [0, 1] для фільтра feColorMatrix + const normalize = (value: number) => value / 255; + + return { r: normalize(red), g: normalize(green), b: normalize(blue) }; + } + } diff --git a/src/app/shared/utils/phrases.ts b/src/app/shared/utils/phrases.ts index 76a4f5a..dd9e0ab 100644 --- a/src/app/shared/utils/phrases.ts +++ b/src/app/shared/utils/phrases.ts @@ -1,7 +1,8 @@ export class Phrases { title: string = "Chytanka — read it easily and comfortably!"; shortTitle: string = "Chytanka"; - enterLink: string = "Enter link to episode on "; + enterLink: string = "Enter link to episode on the supported sites"; + orOpenFile = "or open file" slogan: string = "and read it easily and comfortably!"; letsgo: string = "Let's go" dataLoadErr: string = "Data loading error. Please try again later." diff --git a/src/app/viewer/components/page/page.component.html b/src/app/viewer/components/page/page.component.html new file mode 100644 index 0000000..95a0b70 --- /dev/null +++ b/src/app/viewer/components/page/page.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/viewer/components/page/page.component.scss b/src/app/viewer/components/page/page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/viewer/components/page/page.component.ts b/src/app/viewer/components/page/page.component.ts new file mode 100644 index 0000000..8ddb695 --- /dev/null +++ b/src/app/viewer/components/page/page.component.ts @@ -0,0 +1,9 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'chtnk-page', + imports: [], + templateUrl: './page.component.html', + styleUrl: './page.component.scss' +}) +export class PageComponent {} diff --git a/src/app/viewer/components/pages/pages.component.html b/src/app/viewer/components/pages/pages.component.html new file mode 100644 index 0000000..95a0b70 --- /dev/null +++ b/src/app/viewer/components/pages/pages.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/viewer/components/pages/pages.component.scss b/src/app/viewer/components/pages/pages.component.scss new file mode 100644 index 0000000..4f27991 --- /dev/null +++ b/src/app/viewer/components/pages/pages.component.scss @@ -0,0 +1,20 @@ +:host.right-to-left, +:host.left-to-right { + display: flex; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + scrollbar-width: none; + user-select: none; + touch-action: pan-x; +} + +:host.right-to-left, +:host.vertical { + direction: rtl; +} + +:host.left-to-right { + direction: ltr; +} \ No newline at end of file diff --git a/src/app/viewer/components/pages/pages.component.ts b/src/app/viewer/components/pages/pages.component.ts new file mode 100644 index 0000000..10ab5a0 --- /dev/null +++ b/src/app/viewer/components/pages/pages.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'chtnk-pages', + imports: [], + templateUrl: './pages.component.html', + styleUrl: './pages.component.scss' +}) +export class PagesComponent { + +} diff --git a/src/app/viewer/viewer.component.html b/src/app/viewer/viewer.component.html new file mode 100644 index 0000000..110eb08 --- /dev/null +++ b/src/app/viewer/viewer.component.html @@ -0,0 +1,20 @@ +@let pages = episode()?.images ?? []; + + +

    🌻📖

    + @for (page of pages; track $index) { + + @defer (on viewport) { + + + + } @placeholder { +
    + } + + } + + 🌻📚📖📕📘📗📙📓📒 + @if (pages.length % 2 != 0) { } + +
    diff --git a/src/app/viewer/viewer.component.scss b/src/app/viewer/viewer.component.scss new file mode 100644 index 0000000..e2c8a1a --- /dev/null +++ b/src/app/viewer/viewer.component.scss @@ -0,0 +1,32 @@ +:host { + display: grid; + height: 100vh; + height: 100dvh; + position: relative; + align-content: center; +} + +chtnk-page { + display: block; + width: 50%; + flex: 1 0 auto; + position: relative; +} + +chtnk-page:nth-child(odd) { + scroll-snap-align: start; +} + +img { + max-height: 100%; + max-width: 100%; + display: block; +} + +.right-to-left chtnk-page:nth-child(odd) { + direction: ltr; +} + +.left-to-right chtnk-page:nth-child(odd) { + direction: rtl; +} diff --git a/src/app/viewer/viewer.component.ts b/src/app/viewer/viewer.component.ts new file mode 100644 index 0000000..7fc1f8e --- /dev/null +++ b/src/app/viewer/viewer.component.ts @@ -0,0 +1,24 @@ +import { Component, input, signal } from '@angular/core'; +import { CompositionEpisode } from '../@site-modules/@common-read'; +import { PageComponent } from "./components/page/page.component"; +import { PagesComponent } from "./components/pages/pages.component"; + +@Component({ + standalone: true, + selector: 'chtnk-viewer', + templateUrl: './viewer.component.html', + styleUrl: './viewer.component.scss', + imports: [PageComponent, PagesComponent], + host: { + '[class.panels]': 'panels()', + '(click)': 'tooglePanels()', + } +}) +export class ViewerComponent { + panels = signal(true); + episode = input(); + + tooglePanels() { + this.panels.update(v => !v); + } +} diff --git a/src/assets/icons/icon-512x512.svg b/src/assets/icons/icon-512x512.svg index bea1afa..dff6029 100755 --- a/src/assets/icons/icon-512x512.svg +++ b/src/assets/icons/icon-512x512.svg @@ -1,34 +1,13 @@ - - - - - - + + + - - - - + - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/assets/langs/uk.json b/src/assets/langs/uk.json index bcf2766..80ce579 100644 --- a/src/assets/langs/uk.json +++ b/src/assets/langs/uk.json @@ -1,7 +1,8 @@ { "title": "Читанка — читай легко і комфортно!", "shortTitle": "Читанка", - "enterLink": "Введи посилання на епізод з ", + "enterLink": "Введи посилання на епізод з сайтів, що підтримуютсья", + "orOpenFile":" або відкрий файл", "slogan": "і читай легко та зручно!", "letsgo": "Вйо до", "dataLoadErr": "Помилка завантаження даних. Будь ласка, спробуйте пізніше.", diff --git a/src/assets/new-logo.svg b/src/assets/new-logo.svg new file mode 100644 index 0000000..f19a087 --- /dev/null +++ b/src/assets/new-logo.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/no-image.svg b/src/assets/no-image.svg new file mode 100644 index 0000000..b2c86ee --- /dev/null +++ b/src/assets/no-image.svg @@ -0,0 +1,8 @@ + + + + + + Waiting for the pixels to align + + diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index c1bf151..deb7bc4 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,6 +1,8 @@ const PROXY = `https://proxy.chytanka.ink/api?url=` export const environment = { + version: "0.14.31-2025.5.3", + prod: false, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`, imgurHost: 'https://api.imgur.com/3/album/', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index c1bf151..33ab2fb 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,6 +1,8 @@ const PROXY = `https://proxy.chytanka.ink/api?url=` export const environment = { + version: "0.14.31-2025.5.3", + prod: true, proxy: PROXY, blankaryoHost: `https://blankary.com/page/`, imgurHost: 'https://api.imgur.com/3/album/', diff --git a/src/favicon.ico b/src/favicon.ico index 4d06197..bf63ddf 100644 Binary files a/src/favicon.ico and b/src/favicon.ico differ diff --git a/src/index.html b/src/index.html index 33dc1a6..7f5e648 100644 --- a/src/index.html +++ b/src/index.html @@ -45,11 +45,11 @@ - +
    ⏳ loading...
    -
    + diff --git a/src/server.ts b/src/server.ts index 0f3612e..e2091ae 100644 --- a/src/server.ts +++ b/src/server.ts @@ -53,7 +53,7 @@ app.use('/**', (req, res, next) => { * Start the server if this module is the main entry point. * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. */ -if (isMainModule(import.meta.url)) { +if (isMainModule(import.meta.url) || process.env['FORCE_START']) { const port = process.env['PORT'] || 4000; app.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); diff --git a/src/styles.scss b/src/styles.scss index b79a499..171d819 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -5,10 +5,14 @@ } :root { + // filter: grayscale(1); --blur: 1ch; --t: 133.333334ms; //266.666667ms; //calc(1s / 60 * 16); --surface: hsl(203.44 8% 16%); --text: hsl(200 5% 80%); + --dot-color: #000; + --bg-1: var(--gl) 0px 0px / 4px 4px; + --gl: radial-gradient(circle 1px at 0px 0px, var(--dot-color) 1px, transparent 0); color-scheme: light dark; background-color: var(--surface); color: var(--text); @@ -167,20 +171,29 @@ a[target="_blank"]:after { } :root { - --shc: #a8923e; //rgb(0, 39, 65); + --shc: #ffd60a; //rgb(0, 39, 65); + --avarage-l: 0.48; + --avarage-l-2: 0.36; @media (prefers-color-scheme: light) { - --shc: #74b5e8; + --shc: #166496; } & { - --flat-shadow-high: 1px 1px var(--shc), - 2px 2px var(--shc), - 3px 3px 0 -1px var(--shc), - 4px 4px 0 -1px var(--shc); + --shadow-color: oklch(from var(--shc) var(--avarage-l-2) c h); + --shadow-distance: .25rem; + --shadow-distance-2: .5rem; - --flat-shadow-medium: 1px 1px var(--shc), - 2px 2px var(--shc); + // --flat-shadow-high: 1px 1px var(--shc), + // 2px 2px var(--shc), + // 3px 3px 0 -1px var(--shc), + // 4px 4px 0 -1px var(--shc); + + // --flat-shadow-medium: 1px 1px var(--shc), + // 2px 2px var(--shc); + + --flat-shadow-medium: 1px 1px var(--surface), var(--shadow-distance) var(--shadow-distance) var(--shadow-color); + --flat-shadow-high: 1px 1px var(--surface), var(--shadow-distance-2) var(--shadow-distance-2) var(--shadow-color); } }