From d20fa38df56b506c8af1b06a9bde02cbb82f6217 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 12:38:58 +0300 Subject: [PATCH 01/22] version update 0.9.8 --- electron/package.json | 4 ++-- package-lock.json | 6 +++--- package.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/electron/package.json b/electron/package.json index 69d3fa5a..154b8dbf 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "shikicinema", - "version": "0.9.7", + "version": "0.9.8", "description": "Shikicinema - player app for Shikimori", "author": "Smarthard", "repository": { @@ -48,4 +48,4 @@ "capacitor", "electron" ] -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2a0ed31e..febd465f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "shikicinema", - "version": "0.9.7", + "version": "0.9.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shikicinema", - "version": "0.9.7", + "version": "0.9.8", "license": "BSD-2-Clause", "workspaces": [ "electron" @@ -103,7 +103,7 @@ }, "electron": { "name": "shikicinema", - "version": "0.9.7", + "version": "0.9.8", "hasInstallScript": true, "license": "BSD-2-Clause", "dependencies": { diff --git a/package.json b/package.json index e8ad6b0c..c9f22c8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shikicinema", - "version": "0.9.7", + "version": "0.9.8", "description": "Returns video player on Shikimori", "main": "src/shikicinema.js", "engines": { @@ -132,4 +132,4 @@ "typescript": "~5.8.3", "webpack": "^5.89.0" } -} +} \ No newline at end of file From cf06489c59aec22632e9a9a8d124c935e33870f8 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 12:45:13 +0300 Subject: [PATCH 02/22] add ngxtension --- package-lock.json | 1372 ++++++++++++++++++++++++++++----------------- package.json | 2 + tsconfig.json | 1 + 3 files changed, 858 insertions(+), 517 deletions(-) diff --git a/package-lock.json b/package-lock.json index febd465f..5dded602 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "ngx-scrollbar": "^18.0.0", "ngx-tippy-wrapper": "^6.3.0", "ngx-visibility": "2.0.0", + "ngxtension": "^5.1.0", "rxjs": "~7.8.1", "tslib": "^2.6.2" }, @@ -89,6 +90,7 @@ "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.1.0", + "ngxtension-plugin": "^5.1.0", "npm-run-all": "^4.1.5", "puppeteer": "24.9.0", "terser-webpack-plugin": "^5.3.10", @@ -387,9 +389,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", - "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", "cpu": [ "arm" ], @@ -401,9 +403,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", - "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", "cpu": [ "arm64" ], @@ -415,9 +417,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", - "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", "cpu": [ "arm64" ], @@ -429,9 +431,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", - "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", "cpu": [ "x64" ], @@ -443,9 +445,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", - "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", "cpu": [ "arm64" ], @@ -457,9 +459,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", - "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", "cpu": [ "x64" ], @@ -471,9 +473,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", - "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", "cpu": [ "arm" ], @@ -485,9 +487,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", - "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", "cpu": [ "arm" ], @@ -499,9 +501,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", - "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", "cpu": [ "arm64" ], @@ -513,9 +515,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", - "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", "cpu": [ "arm64" ], @@ -527,9 +529,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", - "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", "cpu": [ "loong64" ], @@ -541,9 +543,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", - "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", "cpu": [ "ppc64" ], @@ -555,9 +557,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", - "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", "cpu": [ "riscv64" ], @@ -569,9 +571,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", - "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", "cpu": [ "s390x" ], @@ -583,9 +585,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", - "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", "cpu": [ "x64" ], @@ -597,9 +599,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", - "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", "cpu": [ "x64" ], @@ -611,9 +613,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", - "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", "cpu": [ "arm64" ], @@ -625,9 +627,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", - "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", "cpu": [ "ia32" ], @@ -639,9 +641,9 @@ "peer": true }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", - "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", "cpu": [ "x64" ], @@ -652,12 +654,6 @@ ], "peer": true }, - "node_modules/@angular-devkit/build-angular/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true - }, "node_modules/@angular-devkit/build-angular/node_modules/@types/node": { "version": "24.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", @@ -716,13 +712,13 @@ "dev": true }, "node_modules/@angular-devkit/build-angular/node_modules/rollup": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", - "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", "dev": true, "peer": true, "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -732,26 +728,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.43.0", - "@rollup/rollup-android-arm64": "4.43.0", - "@rollup/rollup-darwin-arm64": "4.43.0", - "@rollup/rollup-darwin-x64": "4.43.0", - "@rollup/rollup-freebsd-arm64": "4.43.0", - "@rollup/rollup-freebsd-x64": "4.43.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", - "@rollup/rollup-linux-arm-musleabihf": "4.43.0", - "@rollup/rollup-linux-arm64-gnu": "4.43.0", - "@rollup/rollup-linux-arm64-musl": "4.43.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-musl": "4.43.0", - "@rollup/rollup-linux-s390x-gnu": "4.43.0", - "@rollup/rollup-linux-x64-gnu": "4.43.0", - "@rollup/rollup-linux-x64-musl": "4.43.0", - "@rollup/rollup-win32-arm64-msvc": "4.43.0", - "@rollup/rollup-win32-ia32-msvc": "4.43.0", - "@rollup/rollup-win32-x64-msvc": "4.43.0", + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" } }, @@ -4405,6 +4401,34 @@ "node": "*" } }, + "node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.46.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", @@ -6722,6 +6746,17 @@ "node": ">= 10" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", + "integrity": "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==", + "dev": true, + "dependencies": { + "@emnapi/core": "^1.1.0", + "@emnapi/runtime": "^1.1.0", + "@tybys/wasm-util": "^0.9.0" + } + }, "node_modules/@ngrx/effects": { "version": "19.2.1", "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.2.1.tgz", @@ -6997,6 +7032,218 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/@nx/devkit": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.8.2.tgz", + "integrity": "sha512-rr9p2/tZDQivIpuBUpZaFBK6bZ+b5SAjZk75V4tbCUqGW3+5OPuVvBPm+X+7PYwUF6rwSpewxkjWNeGskfCe+Q==", + "dev": true, + "dependencies": { + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "minimatch": "9.0.3", + "semver": "^7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0", + "yargs-parser": "21.1.1" + }, + "peerDependencies": { + "nx": ">= 19 <= 21" + } + }, + "node_modules/@nx/devkit/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@nx/devkit/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nx/devkit/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.8.2.tgz", + "integrity": "sha512-t+bmCn6sRPNGU6hnSyWNvbQYA/KgsxGZKYlaCLRwkNhI2akModcBUqtktJzCKd1XHDqs6EkEFBWjFr8/kBEkSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.8.2.tgz", + "integrity": "sha512-pt/wmDLM31Es8/EzazlyT5U+ou2l60rfMNFGCLqleHEQ0JUTc0KWnOciBLbHIQFiPsCQZJFEKyfV5V/ncePmmw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.8.2.tgz", + "integrity": "sha512-joZxFbgJfkHkB9uMIJr73Gpnm9pnpvr0XKGbWC409/d2x7q1qK77tKdyhGm+A3+kaZFwstNVPmCUtUwJYyU6LA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.8.2.tgz", + "integrity": "sha512-98O/qsxn4vIMPY/FyzvmVrl7C5yFhCUVk0/4PF+PA2SvtQ051L1eMRY6bq/lb69qfN6szJPZ41PG5mPx0NeLZw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.8.2.tgz", + "integrity": "sha512-h6a+HxwfSpxsi4KpxGgPh9GDBmD2E+XqGCdfYpobabxqEBvlnIlJyuDhlRR06cTWpuNXHpRdrVogmV6m/YbtDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.8.2.tgz", + "integrity": "sha512-4Ev+jM0VAxDHV/dFgMXjQTCXS4I8W4oMe7FSkXpG8RUn6JK659DC8ExIDPoGIh+Cyqq6r6mw1CSia+ciQWICWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.8.2.tgz", + "integrity": "sha512-nR0ev+wxu+nQYRd7bhqggOxK7UfkV6h+Ko1mumUFyrM5GvPpz/ELhjJFSnMcOkOMcvH0b6G5uTBJvN1XWCkbmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.8.2.tgz", + "integrity": "sha512-ost41l5yc2aq2Gc9bMMpaPi/jkXqbXEMEPHrxWKuKmaek3K2zbVDQzvBBNcQKxf/mlCsrqN4QO0mKYSRRqag5A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.8.2.tgz", + "integrity": "sha512-0SEOqT/daBG5WtM9vOGilrYaAuf1tiALdrFavY62+/arXYxXemUKmRI5qoKDTnvoLMBGkJs6kxhMO5b7aUXIvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.8.2.tgz", + "integrity": "sha512-iIsY+tVqes/NOqTbJmggL9Juie/iaDYlWgXA9IUv88FE9thqWKhVj4/tCcPjsOwzD+1SVna3YISEEFsx5UV4ew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", @@ -7727,9 +7974,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", - "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", "cpu": [ "riscv64" ], @@ -8013,6 +8260,33 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "dev": true }, + "node_modules/@ts-morph/common": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.23.0.tgz", + "integrity": "sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.3", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -8059,6 +8333,15 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -9139,6 +9422,59 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true }, + "node_modules/@yarnpkg/parsers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.2.tgz", + "integrity": "sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==", + "dev": true, + "dependencies": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/@zkochan/js-yaml": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", + "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/7zip-bin": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", @@ -9496,154 +9832,10 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "peer": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "peer": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/archiver-utils/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, - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "peer": true - }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/archiver/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==", - "dev": true, - "peer": 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/are-docs-informative": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, "engines": { "node": ">=14" @@ -9926,6 +10118,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -11141,6 +11344,12 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -11231,22 +11440,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "peer": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -11551,33 +11744,6 @@ "buffer": "^5.1.0" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, - "peer": true, - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "peer": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -12448,34 +12614,6 @@ "node": ">=14.0.0" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", - "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", - "dev": true, - "peer": true, - "dependencies": { - "app-builder-lib": "24.13.3", - "archiver": "^5.3.1", - "builder-util": "24.13.1", - "fs-extra": "^10.1.0" - } - }, - "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -12785,6 +12923,18 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/ensure-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ensure-error/-/ensure-error-2.1.0.tgz", @@ -14354,12 +14504,48 @@ "node": ">= 0.6" } }, + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1" + } + }, + "node_modules/front-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/front-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/front-matter/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "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==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fs-extra": { "version": "11.1.1", @@ -17488,77 +17674,24 @@ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==" }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "node_modules/leek": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/leek/-/leek-0.0.24.tgz", + "integrity": "sha512-6PVFIYXxlYF0o6hrAsHtGpTmi06otkwNrMcmQ0K96SeSRHPREPa9J3nJZ1frliVH7XT0XFswoJFQoXsDukzGNQ==", "dev": true, - "peer": true, "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" + "debug": "^2.1.0", + "lodash.assign": "^3.2.0", + "rsvp": "^3.0.21" } }, - "node_modules/lazystream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "peer": true - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/leek/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "peer": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/leek": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/leek/-/leek-0.0.24.tgz", - "integrity": "sha512-6PVFIYXxlYF0o6hrAsHtGpTmi06otkwNrMcmQ0K96SeSRHPREPa9J3nJZ1frliVH7XT0XFswoJFQoXsDukzGNQ==", - "dev": true, - "dependencies": { - "debug": "^2.1.0", - "lodash.assign": "^3.2.0", - "rsvp": "^3.0.21" - } - }, - "node_modules/leek/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" + "ms": "2.0.0" } }, "node_modules/leek/node_modules/ms": { @@ -18001,32 +18134,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true, - "peer": true - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true, - "peer": true - }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true, - "peer": true - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -18045,13 +18157,6 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "peer": true - }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -18080,13 +18185,6 @@ "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", "dev": true }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, - "peer": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -19031,6 +19129,46 @@ "@angular/core": "^19.0.0" } }, + "node_modules/ngxtension": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ngxtension/-/ngxtension-5.1.0.tgz", + "integrity": "sha512-ZYs5Fkgrcu+aa/GBqIf/2N7SUZoYam+Fjmu7WxP9rIm4Mx0SECzq+glpWDwBYyPtyfCBwuo6Z8dSPyyzR7PTFQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@use-gesture/vanilla": "^10.0.0", + "rxjs": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@use-gesture/vanilla": { + "optional": true + } + } + }, + "node_modules/ngxtension-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ngxtension-plugin/-/ngxtension-plugin-5.1.0.tgz", + "integrity": "sha512-8oH3SE36Sh0cfRub45FTKkaYVcB0ronw8wjwUJjQscL5wABkHruHTdpA+23QInxyhL8jcVbR6uYGrpjY0ikpxw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "^18.0.1", + "@nx/devkit": "^20.0.0", + "nx": "^20.0.0", + "ts-morph": "^22.0.0" + } + }, + "node_modules/ngxtension-plugin/node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.3.tgz", + "integrity": "sha512-zdrA8mR98X+U4YgHzUKmivRU+PxzwOL/j8G7eTOvBuq8GPzsP+hvak+tyxlgeGm9HsvpFj9ERHLtJ0xDUPs8fg==", + "dev": true + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -19148,6 +19286,12 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -19534,6 +19678,278 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nx": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-20.8.2.tgz", + "integrity": "sha512-mDKpbH3vEpUFDx0rrLh+tTqLq1PYU8KiD/R7OVZGd1FxQxghx2HOl32MiqNsfPcw6AvKlXhslbwIESV+N55FLQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@napi-rs/wasm-runtime": "0.2.4", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.2", + "@zkochan/js-yaml": "0.0.7", + "axios": "^1.8.3", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^8.0.1", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "enquirer": "~2.3.6", + "figures": "3.2.0", + "flat": "^5.0.2", + "front-matter": "^4.0.2", + "ignore": "^5.0.4", + "jest-diff": "^29.4.1", + "jsonc-parser": "3.2.0", + "lines-and-columns": "2.0.3", + "minimatch": "9.0.3", + "node-machine-id": "1.1.12", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "ora": "5.3.0", + "resolve.exports": "2.0.3", + "semver": "^7.5.3", + "string-width": "^4.2.3", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "yaml": "^2.6.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js", + "nx-cloud": "bin/nx-cloud.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "20.8.2", + "@nx/nx-darwin-x64": "20.8.2", + "@nx/nx-freebsd-x64": "20.8.2", + "@nx/nx-linux-arm-gnueabihf": "20.8.2", + "@nx/nx-linux-arm64-gnu": "20.8.2", + "@nx/nx-linux-arm64-musl": "20.8.2", + "@nx/nx-linux-x64-gnu": "20.8.2", + "@nx/nx-linux-x64-musl": "20.8.2", + "@nx/nx-win32-arm64-msvc": "20.8.2", + "@nx/nx-win32-x64-msvc": "20.8.2" + }, + "peerDependencies": { + "@swc-node/register": "^1.8.0", + "@swc/core": "^1.3.85" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/nx/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/nx/node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/nx/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/nx/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/nx/node_modules/lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/nx/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nx/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/ora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", + "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nx/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==", + "dev": 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/nx/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/nx/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -20055,6 +20471,12 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -20881,29 +21303,6 @@ "node": ">= 6" } }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "peer": true, - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -23419,6 +23818,16 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-morph": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-22.0.0.tgz", + "integrity": "sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.23.0", + "code-block-writer": "^13.0.1" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -24853,6 +25262,18 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -24920,89 +25341,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "peer": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "peer": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/zip-stream/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, - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/zip-stream/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", diff --git a/package.json b/package.json index c9f22c8f..2e904b16 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "ngx-scrollbar": "^18.0.0", "ngx-tippy-wrapper": "^6.3.0", "ngx-visibility": "2.0.0", + "ngxtension": "^5.1.0", "rxjs": "~7.8.1", "tslib": "^2.6.2" }, @@ -124,6 +125,7 @@ "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.1.0", + "ngxtension-plugin": "^5.1.0", "npm-run-all": "^4.1.5", "puppeteer": "24.9.0", "terser-webpack-plugin": "^5.3.10", diff --git a/tsconfig.json b/tsconfig.json index 64851bb6..eba04908 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "es2020", "dom" ], + "skipLibCheck": true, "paths": { "@app-env/*": [ "/../src/environments/*" From 89310779f22c27097ffef548aaf96e4cb46f3269 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 13:26:22 +0300 Subject: [PATCH 03/22] use repeat pipe for skeletons --- .../searchbar-results.component.html | 2 +- .../searchbar-results/searchbar-results.component.ts | 3 ++- .../components/card-grid/card-grid.component.html | 2 +- .../home/components/card-grid/card-grid.component.ts | 3 ++- .../components/comments/comments.component.html | 2 +- .../player/components/comments/comments.component.ts | 4 ++-- .../episode-selector-item.component.scss | 4 ++++ .../episode-selector/episode-selector.component.html | 5 +++-- .../episode-selector/episode-selector.component.ts | 7 ++----- .../pipes/make-empty-array/make-empty-array.pipe.ts | 12 ------------ 10 files changed, 18 insertions(+), 26 deletions(-) delete mode 100644 src/app/shared/pipes/make-empty-array/make-empty-array.pipe.ts diff --git a/src/app/core/components/searchbar-results/searchbar-results.component.html b/src/app/core/components/searchbar-results/searchbar-results.component.html index aca6e4d3..6be45898 100644 --- a/src/app/core/components/searchbar-results/searchbar-results.component.html +++ b/src/app/core/components/searchbar-results/searchbar-results.component.html @@ -96,7 +96,7 @@ - @for (_ of fakeResults; track $index) { + @for (_ of 5 | repeat; track $index) { (5).fill(0); results = input(); diff --git a/src/app/modules/home/components/card-grid/card-grid.component.html b/src/app/modules/home/components/card-grid/card-grid.component.html index 4fd5ce43..e3099ecc 100644 --- a/src/app/modules/home/components/card-grid/card-grid.component.html +++ b/src/app/modules/home/components/card-grid/card-grid.component.html @@ -20,7 +20,7 @@ } - @for (_ of userAnimeRatesSkeleton; track $index) { + @for (_ of 30 | repeat; track $index) {
(30).fill(0); userAnimeRates = input(); diff --git a/src/app/modules/player/components/comments/comments.component.html b/src/app/modules/player/components/comments/comments.component.html index 39c01d3b..6bf2524d 100644 --- a/src/app/modules/player/components/comments/comments.component.html +++ b/src/app/modules/player/components/comments/comments.component.html @@ -37,7 +37,7 @@ } - @for(_ of (3 | makeEmptyArray); track $index) { + @for(_ of 3 | repeat; track $index) {
}
diff --git a/src/app/modules/player/components/comments/comments.component.ts b/src/app/modules/player/components/comments/comments.component.ts index ec448a0e..37d32b14 100644 --- a/src/app/modules/player/components/comments/comments.component.ts +++ b/src/app/modules/player/components/comments/comments.component.ts @@ -20,11 +20,11 @@ import { ToastController, } from '@ionic/angular/standalone'; import { NgTemplateOutlet } from '@angular/common'; +import { RepeatPipe } from 'ngxtension/repeat-pipe'; import { TranslocoPipe, TranslocoService } from '@jsverse/transloco'; import { Comment } from '@app/shared/types/shikimori/comment'; import { CommentComponent } from '@app/modules/player/components/comment/comment.component'; -import { MakeEmptyArrayPipe } from '@app/shared/pipes/make-empty-array/make-empty-array.pipe'; import { ResourceIdType } from '@app/shared/types/resource-id.type'; import { SortByCreatedAtPipe } from '@app/shared/pipes/sort-by-created-at/sort-by-created-at.pipe'; import { isShowLastItemsPipe } from '@app/modules/player/pipes/is-show-last-items.pipe'; @@ -38,12 +38,12 @@ import { trackById } from '@app/shared/utils/common-ngfor-tracking'; CommentComponent, SortByCreatedAtPipe, isShowLastItemsPipe, - MakeEmptyArrayPipe, IonButton, IonLabel, IonSpinner, NgTemplateOutlet, TranslocoPipe, + RepeatPipe, ], providers: [ ModalController, diff --git a/src/app/modules/player/components/episode-selector-item/episode-selector-item.component.scss b/src/app/modules/player/components/episode-selector-item/episode-selector-item.component.scss index 4138a18c..5d5930dd 100644 --- a/src/app/modules/player/components/episode-selector-item/episode-selector-item.component.scss +++ b/src/app/modules/player/components/episode-selector-item/episode-selector-item.component.scss @@ -4,6 +4,10 @@ overflow: hidden; // for ionic ripple effect --> + &.skeleton { + color: transparent; + } + user-select: none; display: flex; align-items: center; diff --git a/src/app/modules/player/components/episode-selector/episode-selector.component.html b/src/app/modules/player/components/episode-selector/episode-selector.component.html index 04bb9636..00f3f4f0 100644 --- a/src/app/modules/player/components/episode-selector/episode-selector.component.html +++ b/src/app/modules/player/components/episode-selector/episode-selector.component.html @@ -5,7 +5,8 @@ appearance="compact" visibility="hover">
- @for (episode of episodes(); track episode) { + @for (i of maxEpisode() | repeat; track $index) { + @let episode = i + 1; @let isNotAired = episode > maxAiredEpisode(); } @empty { @if (isLoading()) { - @for (episode of episodesSkeleton; track $index) { + @for (episode of 50 | repeat; track $index) { } } @else { diff --git a/src/app/modules/player/components/episode-selector/episode-selector.component.ts b/src/app/modules/player/components/episode-selector/episode-selector.component.ts index 7a44f309..393ef768 100644 --- a/src/app/modules/player/components/episode-selector/episode-selector.component.ts +++ b/src/app/modules/player/components/episode-selector/episode-selector.component.ts @@ -4,7 +4,6 @@ import { ElementRef, HostBinding, ViewEncapsulation, - computed, effect, inject, input, @@ -14,6 +13,7 @@ import { } from '@angular/core'; import { NgScrollbar } from 'ngx-scrollbar'; import { NgxTippyModule } from 'ngx-tippy-wrapper'; +import { RepeatPipe } from 'ngxtension/repeat-pipe'; import { TranslocoService } from '@jsverse/transloco'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -26,6 +26,7 @@ import { EpisodeSelectorItemComponent } from '@app/modules/player/components/epi NgScrollbar, NgxTippyModule, EpisodeSelectorItemComponent, + RepeatPipe, ], templateUrl: './episode-selector.component.html', styleUrl: './episode-selector.component.scss', @@ -41,8 +42,6 @@ export class EpisodeSelectorComponent { readonly episodesScrollbar = viewChild('episodesScrollbar'); readonly episodesEl = viewChildren('episodeEl'); - readonly episodesSkeleton = new Array(50); - readonly notAiredText = toSignal( this.transloco.selectTranslate('PLAYER_MODULE.PLAYER_PAGE.PLAYER.EPISODE_IS_NOT_AIRED'), ); @@ -55,8 +54,6 @@ export class EpisodeSelectorComponent { selection = output(); - episodes = computed(() => new Array(this.maxEpisode()).fill(0).map((_, index) => index + 1)); - private scrollToEpisode(episode: number) { this.episodesScrollbar()?.scrollToElement(`#episode-${episode}`, { duration: 800 }); } diff --git a/src/app/shared/pipes/make-empty-array/make-empty-array.pipe.ts b/src/app/shared/pipes/make-empty-array/make-empty-array.pipe.ts deleted file mode 100644 index 24aa499d..00000000 --- a/src/app/shared/pipes/make-empty-array/make-empty-array.pipe.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'makeEmptyArray', - standalone: true, - pure: true, -}) -export class MakeEmptyArrayPipe implements PipeTransform { - transform(count: number): Array { - return new Array(count).fill(0); - } -} From c71c8431dc85baebb362fefad7921f5790fe94a1 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 16:20:01 +0300 Subject: [PATCH 04/22] fix initial episode scrolling --- .../episode-selector.component.html | 3 -- .../episode-selector.component.ts | 29 +++++++++---------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/app/modules/player/components/episode-selector/episode-selector.component.html b/src/app/modules/player/components/episode-selector/episode-selector.component.html index 00f3f4f0..d052e6c9 100644 --- a/src/app/modules/player/components/episode-selector/episode-selector.component.html +++ b/src/app/modules/player/components/episode-selector/episode-selector.component.html @@ -1,5 +1,4 @@ maxAiredEpisode(); diff --git a/src/app/modules/player/components/episode-selector/episode-selector.component.ts b/src/app/modules/player/components/episode-selector/episode-selector.component.ts index 393ef768..6e754d3f 100644 --- a/src/app/modules/player/components/episode-selector/episode-selector.component.ts +++ b/src/app/modules/player/components/episode-selector/episode-selector.component.ts @@ -1,15 +1,13 @@ import { ChangeDetectionStrategy, Component, - ElementRef, HostBinding, ViewEncapsulation, - effect, + afterRender, inject, input, output, viewChild, - viewChildren, } from '@angular/core'; import { NgScrollbar } from 'ngx-scrollbar'; import { NgxTippyModule } from 'ngx-tippy-wrapper'; @@ -39,8 +37,7 @@ export class EpisodeSelectorComponent { private readonly transloco = inject(TranslocoService); - readonly episodesScrollbar = viewChild('episodesScrollbar'); - readonly episodesEl = viewChildren('episodeEl'); + private readonly scrollbar = viewChild(NgScrollbar); readonly notAiredText = toSignal( this.transloco.selectTranslate('PLAYER_MODULE.PLAYER_PAGE.PLAYER.EPISODE_IS_NOT_AIRED'), @@ -54,19 +51,19 @@ export class EpisodeSelectorComponent { selection = output(); - private scrollToEpisode(episode: number) { - this.episodesScrollbar()?.scrollToElement(`#episode-${episode}`, { duration: 800 }); + constructor() { + afterRender({ + read: () => { + if (!this.isLoading()) { + this.scrollToEpisode(this.selected()); + } + }, + }); } - onEpisodeSelectionChangeEffect = effect(() => { - const selectedEpisode = this.selected(); - const episodesEl = this.episodesEl(); - const scrollbarEl = this.episodesScrollbar(); - - if (scrollbarEl && episodesEl?.length) { - this.scrollToEpisode(selectedEpisode); - } - }); + private scrollToEpisode(episode: number): void { + this.scrollbar()?.scrollToElement(`#episode-${episode}`, { duration: 800 }); + } onEpisodeSelect(episode: number): void { this.selection.emit(episode); From ffbcd792040638d646fdd58c2b86f0c37e42add8 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 17:28:04 +0300 Subject: [PATCH 05/22] align availability warning icon --- .../video-selector-item.component.html | 2 +- .../video-selector-item.component.scss | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/modules/player/components/video-selector-item/video-selector-item.component.html b/src/app/modules/player/components/video-selector-item/video-selector-item.component.html index 3e6323df..664f8c68 100644 --- a/src/app/modules/player/components/video-selector-item/video-selector-item.component.html +++ b/src/app/modules/player/components/video-selector-item/video-selector-item.component.html @@ -5,7 +5,7 @@ class="author-accordion__item"> - {{ author() }} + {{ author() }} @if (!isAvailableForAllEpisodes()) { Date: Mon, 23 Jun 2025 18:05:29 +0300 Subject: [PATCH 06/22] extract includes pipe --- .../video-selector/video-selector.component.html | 2 +- .../video-selector/video-selector.component.ts | 4 +++- src/app/shared/pipes/includes/includes.pipe.ts | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/pipes/includes/includes.pipe.ts diff --git a/src/app/modules/player/components/video-selector/video-selector.component.html b/src/app/modules/player/components/video-selector/video-selector.component.html index 60676356..10a8fb32 100644 --- a/src/app/modules/player/components/video-selector/video-selector.component.html +++ b/src/app/modules/player/components/video-selector/video-selector.component.html @@ -12,7 +12,7 @@ [kindDisplayMode]="kindDisplayMode()" [selected]="selected()" [videos]="videos() | filterByAuthor: author: defaultAuthorName()" - [isAvailableForAllEpisodes]="!warnAvailability()?.includes(author)" + [isAvailableForAllEpisodes]="!(warnAvailability() | includes: author)" (toggleOpen)="onAuthorSectionToggle($event)" (selectVideo)="onSelectionChange($event)" /> diff --git a/src/app/modules/player/components/video-selector/video-selector.component.ts b/src/app/modules/player/components/video-selector/video-selector.component.ts index 65b318e9..c996cc87 100644 --- a/src/app/modules/player/components/video-selector/video-selector.component.ts +++ b/src/app/modules/player/components/video-selector/video-selector.component.ts @@ -17,6 +17,7 @@ import { TranslocoService } from '@jsverse/transloco'; import { toSignal } from '@angular/core/rxjs-interop'; import { FilterByAuthorPipe } from '@app/shared/pipes/filter-by-author/filter-by-author.pipe'; +import { IncludesPipe } from '@app/shared/pipes/includes/includes.pipe'; import { PlayerKindDisplayMode } from '@app/store/settings/types/player-kind-display-mode.type'; import { VideoInfoInterface } from '@app/modules/player/types'; import { VideoSelectorItemComponent } from '@app/modules/player/components/video-selector-item'; @@ -29,6 +30,7 @@ import { cleanAuthorName } from '@app/shared/utils/clean-author-name.function'; imports: [ IonAccordionGroup, FilterByAuthorPipe, + IncludesPipe, NgScrollbar, VideoSelectorItemComponent, ], @@ -48,7 +50,7 @@ export class VideoSelectorComponent { selected = input(); videos = input(); kindDisplayMode = input(); - warnAvailability = input(); + warnAvailability = input([]); selection = output(); diff --git a/src/app/shared/pipes/includes/includes.pipe.ts b/src/app/shared/pipes/includes/includes.pipe.ts new file mode 100644 index 00000000..d451d21a --- /dev/null +++ b/src/app/shared/pipes/includes/includes.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'includes', + pure: false, + standalone: true, +}) +export class IncludesPipe implements PipeTransform { + transform(array: ArrayLike, value: T): boolean { + return array instanceof Array + ? array.includes(value) + : Array.from(array)?.includes(value); + } +} From ed385c7b86f68ffd49852c05fd1f0f9fde4c47a8 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 18:06:48 +0300 Subject: [PATCH 07/22] fix author availability for modal --- .../video-selector-modal.component.html | 1 + .../video-selector-modal.component.ts | 23 ++++++++----------- src/app/modules/player/player.page.ts | 1 + 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html index e2c1db27..de609711 100644 --- a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html +++ b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html @@ -26,6 +26,7 @@ class="video-selector-modal__video-selector" [videos]="videos | filterByKind: selectedKind" [selected]="selectedVideo" + [warnAvailability]="videos | authorAvailabilityWarning: lastAiredEpisode" (selection)="onVideoChange($event)" />
diff --git a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts index 96c63a2e..faeb14ec 100644 --- a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts +++ b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts @@ -1,12 +1,10 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, - ElementRef, HostBinding, Input, - NgZone, ViewEncapsulation, + inject, } from '@angular/core'; import { IonButton, @@ -18,13 +16,14 @@ import { ModalController, } from '@ionic/angular/standalone'; +import { AuthorAvailabilityWarningPipe } from '@app/modules/player/pipes'; import { FilterByKindPipe } from '@app/shared/pipes/filter-by-kind/filter-by-kind.pipe'; import { GetActiveKindsPipe } from '@app/shared/pipes/get-active-kinds/get-active-kinds.pipe'; import { KindSelectorComponent } from '@app/modules/player/components/kind-selector/kind-selector.component'; import { VideoInfoInterface, VideoKindEnum } from '@app/modules/player/types'; import { VideoSelectorComponent } from '@app/modules/player/components/video-selector/video-selector.component'; - +// TODO: модалка не хочет переводиться на сигналы - перепроверить позже @Component({ selector: 'app-video-selector-modal', standalone: true, @@ -34,6 +33,7 @@ import { VideoSelectorComponent } from '@app/modules/player/components/video-sel IonButtons, IonButton, IonIcon, + AuthorAvailabilityWarningPipe, FilterByKindPipe, GetActiveKindsPipe, VideoSelectorComponent, @@ -48,12 +48,17 @@ export class VideoSelectorModalComponent extends IonModal { @HostBinding('class.video-selector-modal') private videoSelectorModalClass = true; + private readonly _modalController = inject(ModalController); + private _selectedKind: VideoKindEnum; private _selectedVideo: VideoInfoInterface; @Input() videos: VideoInfoInterface[]; + @Input() + lastAiredEpisode: number; + @Input() set selectedKind(kind: VideoKindEnum) { this._selectedKind = kind; @@ -72,16 +77,6 @@ export class VideoSelectorModalComponent extends IonModal { return this._selectedVideo; } - // TODO: инжект ModalController - костыль, нужно убрать вместе со всем конструктором (см. нижнее todo) - constructor( - private readonly _modalController: ModalController, - private readonly _changeDetectorRef: ChangeDetectorRef, - private readonly _elementRef: ElementRef, - private readonly _zone: NgZone, - ) { - super(_changeDetectorRef, _elementRef, _zone); - } - onKindChange(kind: VideoKindEnum): void { this._selectedKind = kind; } diff --git a/src/app/modules/player/player.page.ts b/src/app/modules/player/player.page.ts index 567de2e6..c90cea15 100644 --- a/src/app/modules/player/player.page.ts +++ b/src/app/modules/player/player.page.ts @@ -341,6 +341,7 @@ export class PlayerPage implements OnInit { videos: this.episodeVideos(), selectedKind: this.currentKind(), selectedVideo: this.currentVideo(), + lastAiredEpisode: this.lastAiredEpisode(), }; const { VideoSelectorModalComponent } = await import('@app/modules/player/components/video-selector-modal'); From 1874c77681f69953750a0d1a6381d92c6431e594 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 18:19:15 +0300 Subject: [PATCH 08/22] speed up availability calculations a bit --- .../player/pipes/author-availability-warning.pipe.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/modules/player/pipes/author-availability-warning.pipe.ts b/src/app/modules/player/pipes/author-availability-warning.pipe.ts index 8a3c2cfc..829947df 100644 --- a/src/app/modules/player/pipes/author-availability-warning.pipe.ts +++ b/src/app/modules/player/pipes/author-availability-warning.pipe.ts @@ -15,12 +15,8 @@ export class AuthorAvailabilityWarningPipe implements PipeTransform { for (const targetAuthor of authors) { const authorVideos = videos?.filter(({ author }) => author === targetAuthor); const authorEpisodes = new Set(authorVideos?.map(({ episode }) => episode)); - const episodesSum = [...authorEpisodes].reduce((acc, episode) => acc + episode, 0); - const targetEpSum = (lastAiredEpisode + 1) * (lastAiredEpisode / 2); - // считаем сумму от первого до последнего вышедшего эпизода - // сравниваем с суммой уникальных эпизодов, доступных в видео - if (targetEpSum !== episodesSum) { + if (authorEpisodes.size !== lastAiredEpisode) { availabilityIssueAuthros.push(targetAuthor); } } From 4273bbf10c13893843738c2ce4ef0d702c10d25f Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 18:26:11 +0300 Subject: [PATCH 09/22] add modal title translation --- .../video-selector-modal.component.html | 2 +- .../video-selector-modal.component.ts | 2 ++ .../episode-selector-modal.component.html | 4 +++- .../episode-selector-modal.component.ts | 18 +++++------------- src/assets/i18n/en.json | 6 ++++++ src/assets/i18n/ru.json | 6 ++++++ src/assets/i18n/uk.json | 6 ++++++ 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html index de609711..660e0d8a 100644 --- a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html +++ b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html @@ -1,5 +1,5 @@ - Выберите видео + {{ 'PLAYER_MODULE.VIDEO_SELECT_MODAL.TITLE' | transloco }} diff --git a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts index faeb14ec..8f284f38 100644 --- a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts +++ b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts @@ -15,6 +15,7 @@ import { IonToolbar, ModalController, } from '@ionic/angular/standalone'; +import { TranslocoPipe } from '@jsverse/transloco'; import { AuthorAvailabilityWarningPipe } from '@app/modules/player/pipes'; import { FilterByKindPipe } from '@app/shared/pipes/filter-by-kind/filter-by-kind.pipe'; @@ -36,6 +37,7 @@ import { VideoSelectorComponent } from '@app/modules/player/components/video-sel AuthorAvailabilityWarningPipe, FilterByKindPipe, GetActiveKindsPipe, + TranslocoPipe, VideoSelectorComponent, KindSelectorComponent, ], diff --git a/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.html b/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.html index c3f2cbb4..b7883e6a 100644 --- a/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.html +++ b/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.html @@ -1,5 +1,7 @@ - Выберите эпизод + + {{ 'PLAYER_MODULE.VIDEO_SELECT_MODAL.TITLE' | transloco }} + diff --git a/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.ts b/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.ts index ec628f25..226ea963 100644 --- a/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.ts +++ b/src/app/shared/components/episode-selector-modal/episode-selector-modal.component.ts @@ -1,12 +1,10 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, - ElementRef, HostBinding, Input, - NgZone, ViewEncapsulation, + inject, } from '@angular/core'; import { IonButton, @@ -20,6 +18,7 @@ import { IonToolbar, ModalController, } from '@ionic/angular/standalone'; +import { TranslocoPipe } from '@jsverse/transloco'; @Component({ selector: 'app-episode-selector-modal', @@ -33,6 +32,7 @@ import { IonPicker, IonPickerColumn, IonPickerColumnOption, + TranslocoPipe, ], templateUrl: './episode-selector-modal.component.html', styleUrl: './episode-selector-modal.component.scss', @@ -43,6 +43,8 @@ export class EpisodeSelectorModalComponent extends IonModal { @HostBinding('class.episode-selector-modal') private episodeSelectorModalClass = true; + private readonly _modalController = inject(ModalController); + private _selected = 1; @Input({ required: true }) @@ -57,16 +59,6 @@ export class EpisodeSelectorModalComponent extends IonModal { return this._selected; } - // TODO: инжект ModalController - костыль, нужно убрать вместе со всем конструктором (см. нижнее todo) - constructor( - private readonly _modalController: ModalController, - private readonly _changeDetectorRef: ChangeDetectorRef, - private readonly _elementRef: ElementRef, - private readonly _zone: NgZone, - ) { - super(_changeDetectorRef, _elementRef, _zone); - } - onSelectedChange(event: CustomEvent): void { const episode = event?.detail?.value; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 6ca74d53..bee6161e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -189,6 +189,12 @@ } } } + }, + "VIDEO_SELECT_MODAL": { + "TITLE": "Select video" + }, + "EPISODE_SELECT_MODAL": { + "TITLE": "Select episode" } }, "SETTINGS_MODULE": { diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index 72594c9f..bf9bba74 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -194,6 +194,12 @@ } } } + }, + "VIDEO_SELECT_MODAL": { + "TITLE": "Выберите видео" + }, + "EPISODE_SELECT_MODAL": { + "TITLE": "Выберите эпизод" } }, "SETTINGS_MODULE": { diff --git a/src/assets/i18n/uk.json b/src/assets/i18n/uk.json index a51b86df..b959bc59 100644 --- a/src/assets/i18n/uk.json +++ b/src/assets/i18n/uk.json @@ -189,6 +189,12 @@ } } } + }, + "VIDEO_SELECT_MODAL": { + "TITLE": "Виберіть відео" + }, + "EPISODE_SELECT_MODAL": { + "TITLE": "Виберіть епізод" } }, "SETTINGS_MODULE": { From d34d59f04a76685ba08fed3b29fa7dfc64ab61c4 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 23 Jun 2025 18:43:38 +0300 Subject: [PATCH 10/22] refactor input --- .../video-upload-modal/video-upload-modal.component.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/app/modules/player/components/video-upload-modal/video-upload-modal.component.ts b/src/app/modules/player/components/video-upload-modal/video-upload-modal.component.ts index 6c856d70..e019590a 100644 --- a/src/app/modules/player/components/video-upload-modal/video-upload-modal.component.ts +++ b/src/app/modules/player/components/video-upload-modal/video-upload-modal.component.ts @@ -71,19 +71,11 @@ export class VideoUploadModalComponent extends IonModal implements OnInit { private readonly transloco = inject(TranslocoService); private readonly _modalController = inject(ModalController); - private _episode: number; - @Input() anime: AnimeBriefInfoInterface; @Input() - set episode(episode: number) { - this._episode = episode; - } - - get episode(): number { - return this._episode; - } + episode: number; uploadForm: FormGroup; From 1f879d7d9c7371e1f047250d4081503e229e0219 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Tue, 24 Jun 2025 20:08:19 +0300 Subject: [PATCH 11/22] remove default value from input paramenters --- src/app/modules/player/player.page.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/modules/player/player.page.ts b/src/app/modules/player/player.page.ts index c90cea15..0e6c6340 100644 --- a/src/app/modules/player/player.page.ts +++ b/src/app/modules/player/player.page.ts @@ -151,8 +151,8 @@ export class PlayerPage implements OnInit { private readonly modalController = inject(ModalController); private readonly destroyRef = inject(DestroyRef); - readonly animeId = input(null); - readonly episode = input(); + readonly animeId = input.required(); + readonly episode = input.required(); private readonly userCommentFormEl = viewChild('userCommentForm', { read: ElementRef }); From 16ffc52139de5d0f7a134763ed73eadd68da8644 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Wed, 25 Jun 2025 17:03:28 +0300 Subject: [PATCH 12/22] add custom sort for user anime list --- src/app/core/used-icons.config.ts | 1 + src/app/modules/home/home.page.html | 18 +- src/app/modules/home/home.page.ts | 170 +++++++++--------- .../pipes/sort-rates-by-date-visited.pipe.ts | 3 +- .../home/types/anime-grid.interface.ts | 2 - src/app/modules/home/utils/index.ts | 1 + .../sort-rates-by-date-visited.function.ts | 5 + src/app/modules/settings/settings.page.html | 31 +++- src/app/modules/settings/settings.page.ts | 27 ++- src/app/modules/settings/utils/index.ts | 1 + ...ime-status-order-to-form-array.function.ts | 7 + .../settings/utils/settings-form.interface.ts | 2 + .../default-anime-status-order.config.ts | 8 + .../settings/reducers/settings.reducer.ts | 3 + .../types/settings-store.interface.ts | 2 + src/assets/i18n/en.json | 7 + src/assets/i18n/ru.json | 7 + src/assets/i18n/uk.json | 7 + 18 files changed, 201 insertions(+), 101 deletions(-) create mode 100644 src/app/modules/home/utils/index.ts create mode 100644 src/app/modules/home/utils/sort-rates-by-date-visited.function.ts create mode 100644 src/app/modules/settings/utils/map-anime-status-order-to-form-array.function.ts create mode 100644 src/app/shared/config/default-anime-status-order.config.ts diff --git a/src/app/core/used-icons.config.ts b/src/app/core/used-icons.config.ts index 4dab7e66..6d477852 100644 --- a/src/app/core/used-icons.config.ts +++ b/src/app/core/used-icons.config.ts @@ -14,6 +14,7 @@ export { serverOutline, settingsOutline, trashOutline, + playOutline, playBackOutline, playForwardOutline, send, diff --git a/src/app/modules/home/home.page.html b/src/app/modules/home/home.page.html index 4e136c0b..b63548b9 100644 --- a/src/app/modules/home/home.page.html +++ b/src/app/modules/home/home.page.html @@ -1,22 +1,12 @@ - - + @for (status of animeStatusOrder(); track status) { + @let grid = animeGridMap.get(status); - - @for (grid of animeGrids; track $index) { this.settings()?.useCustomAnimeStatusOrder); + readonly animeStatusOrder = computed(() => this.useCustomAnimeStatusOrder() + ? this.settings()?.userAnimeStatusOrder + : DEFAULT_ANIME_STATUS_ORDER, + ); + readonly isTranslationsLoaded$ = this.transloco.events$.pipe( filter((e) => e.type === 'translationLoadSuccess'), ); - currentUser$: Observable; + readonly currentUser$ = this.store.select(selectShikimoriCurrentUser); - recent$: Observable; + readonly recent$ = combineLatest([ + this.store.select(selectRecentAnimes), + this.store.select(selectCachedAnimes), + ]).pipe( + map(([recentAnimes, cachedAnimes]) => recentAnimesToRates(recentAnimes, cachedAnimes)), + map((recentRates) => sortRatesByDateVisited(recentRates?.slice(0, 6))), + shareReplay(1), + ); - planned$: Observable; - watching$: Observable; - rewatching$: Observable; - completed$: Observable; - onHold$: Observable; - dropped$: Observable; + readonly planned$ = this.store.select(selectRatesByStatus('planned')); + readonly watching$ = this.store.select(selectRatesByStatus('watching')); + readonly rewatching$ = this.store.select(selectRatesByStatus('rewatching')); + readonly completed$ = this.store.select(selectRatesByStatus('completed')); + readonly onHold$ = this.store.select(selectRatesByStatus('on_hold')); + readonly dropped$ = this.store.select(selectRatesByStatus('dropped')); - isRecentLoaded$: Observable; - isPlannedLoaded$: Observable; - isWatchingLoaded$: Observable; - isRewatchingLoaded$: Observable; - isCompletedLoaded$: Observable; - isOnHoldLoaded$: Observable; - isDroppedLoaded$: Observable; + readonly isPlannedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('planned')); + readonly isWatchingLoaded$ = this.store.select(selectIsRatesLoadedByStatus('watching')); + readonly isRewatchingLoaded$ = this.store.select(selectIsRatesLoadedByStatus('rewatching')); + readonly isCompletedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('completed')); + readonly isOnHoldLoaded$ = this.store.select(selectIsRatesLoadedByStatus('on_hold')); + readonly isDroppedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('dropped')); - animeGrids: AnimeGridInterface[]; + animeGridMap: Map; hiddenGridMap: Map; - sectionVisibilitySubject$: Subject; + sectionVisibilitySubject$ = new Subject(); ngOnInit() { this.initValues(); @@ -149,64 +162,57 @@ export class HomePage implements OnInit { initValues(): void { this.hiddenGridMap = new Map(); - this.sectionVisibilitySubject$ = new Subject(); - - this.currentUser$ = this.store.select(selectShikimoriCurrentUser); - - this.recent$ = combineLatest([ - this.store.select(selectRecentAnimes), - this.store.select(selectCachedAnimes), - ]).pipe( - map(([recentAnimes, cachedAnimes]) => recentAnimesToRates(recentAnimes, cachedAnimes)), - shareReplay(1), - ); - - this.planned$ = this.store.select(selectRatesByStatus('planned')); - this.watching$ = this.store.select(selectRatesByStatus('watching')); - this.rewatching$ = this.store.select(selectRatesByStatus('rewatching')); - this.completed$ = this.store.select(selectRatesByStatus('completed')); - this.onHold$ = this.store.select(selectRatesByStatus('on_hold')); - this.dropped$ = this.store.select(selectRatesByStatus('dropped')); - - this.isPlannedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('planned')); - this.isWatchingLoaded$ = this.store.select(selectIsRatesLoadedByStatus('watching')); - this.isRewatchingLoaded$ = this.store.select(selectIsRatesLoadedByStatus('rewatching')); - this.isCompletedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('completed')); - this.isOnHoldLoaded$ = this.store.select(selectIsRatesLoadedByStatus('on_hold')); - this.isDroppedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('dropped')); - - this.animeGrids = [ - { - status: 'planned', - rates: this.planned$, - isLoaded: this.isPlannedLoaded$, - }, - { - status: 'watching', - rates: this.watching$, - isLoaded: this.isWatchingLoaded$, - }, - { - status: 'rewatching', - rates: this.rewatching$, - isLoaded: this.isRewatchingLoaded$, - }, - { - status: 'completed', - rates: this.completed$, - isLoaded: this.isCompletedLoaded$, - }, - { - status: 'on_hold', - rates: this.onHold$, - isLoaded: this.isOnHoldLoaded$, - }, - { - status: 'dropped', - rates: this.dropped$, - isLoaded: this.isDroppedLoaded$, - }, - ]; + this.animeGridMap = new Map([ + [ + 'recent', + { + rates: this.recent$.pipe(), + isLoaded: of(true), + }, + ], + [ + 'planned', + { + rates: this.planned$, + isLoaded: this.isPlannedLoaded$, + }, + ], + [ + 'watching', + { + rates: this.watching$, + isLoaded: this.isWatchingLoaded$, + }, + ], + [ + 'rewatching', + { + rates: this.rewatching$, + isLoaded: this.isRewatchingLoaded$, + }, + ], + [ + 'completed', + { + rates: this.completed$, + isLoaded: this.isCompletedLoaded$, + }, + ], + [ + 'on_hold', + { + rates: this.onHold$, + isLoaded: this.isOnHoldLoaded$, + }, + ], + [ + 'dropped', + { + rates: this.dropped$, + isLoaded: this.isDroppedLoaded$, + }, + ], + ]); } toggleHiddenGridStatus(rateStatus: UserRateStatusType): void { diff --git a/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts b/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts index 974f9f82..4681cbec 100644 --- a/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts +++ b/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts @@ -1,6 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; +import { sortRatesByDateVisited } from '@app/modules/home/utils'; @Pipe({ name: 'sortRatesByDateVisited', @@ -8,6 +9,6 @@ import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; }) export class SortRatesByDateVisitedPipe implements PipeTransform { transform(userRates: UserAnimeRate[] = []): UserAnimeRate[] { - return userRates.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at)); + return sortRatesByDateVisited(userRates); } } diff --git a/src/app/modules/home/types/anime-grid.interface.ts b/src/app/modules/home/types/anime-grid.interface.ts index dc0513d5..b38e2371 100644 --- a/src/app/modules/home/types/anime-grid.interface.ts +++ b/src/app/modules/home/types/anime-grid.interface.ts @@ -1,10 +1,8 @@ import { Observable } from 'rxjs'; import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; export interface AnimeGridInterface { - status: UserRateStatusType; rates: Observable; isLoaded: Observable; label?: string; diff --git a/src/app/modules/home/utils/index.ts b/src/app/modules/home/utils/index.ts new file mode 100644 index 00000000..a6398f88 --- /dev/null +++ b/src/app/modules/home/utils/index.ts @@ -0,0 +1 @@ +export { sortRatesByDateVisited } from './sort-rates-by-date-visited.function'; diff --git a/src/app/modules/home/utils/sort-rates-by-date-visited.function.ts b/src/app/modules/home/utils/sort-rates-by-date-visited.function.ts new file mode 100644 index 00000000..dc39f9d6 --- /dev/null +++ b/src/app/modules/home/utils/sort-rates-by-date-visited.function.ts @@ -0,0 +1,5 @@ +import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; + +export function sortRatesByDateVisited(userRates: UserAnimeRate[]): UserAnimeRate[] { + return userRates.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at)); +} diff --git a/src/app/modules/settings/settings.page.html b/src/app/modules/settings/settings.page.html index 145e85d0..fc1ec93a 100644 --- a/src/app/modules/settings/settings.page.html +++ b/src/app/modules/settings/settings.page.html @@ -71,7 +71,7 @@ + + + @let useCustomOrder = useCustomAnimeStatusOrder?.value; + @let helperText = 'SETTINGS_MODULE.SETTINGS_PAGE.SETTING_GROUPS.USER_ANIMES_PAGE.USE_CUSTOM_ANIME_STATUS_ORDER.HELP'| transloco; + + + {{ 'SETTINGS_MODULE.SETTINGS_PAGE.SETTING_GROUPS.USER_ANIMES_PAGE.USE_CUSTOM_ANIME_STATUS_ORDER.LABEL' | transloco }} + + + @if (useCustomOrder) { + + @for (status of userAnimeStatusOrderCtrl?.value; track status) { + + {{ 'HOME_MODULE.HOME_PAGE.GRIDS.' + (status | uppercase) | transloco }} + + + } + + } + + ('auto'), playerKindDisplayMode: new FormControl('special-only'), shikimoriDomain: new FormControl(this.defaultShikimoriDomain), + useCustomAnimeStatusOrder: new FormControl(false), + userAnimeStatusOrder: mapAnimeStatusOrderToFormArray(DEFAULT_ANIME_STATUS_ORDER), }); readonly themeCtrl = this.settingsForm?.get('theme'); readonly playerModeCtrl = this.settingsForm?.get('playerMode'); readonly playerKindDisplayModeCtrl = this.settingsForm?.get('playerKindDisplayMode'); readonly shikimoriDomainCtrl = this.settingsForm?.get('shikimoriDomain'); + readonly useCustomAnimeStatusOrder = this.settingsForm?.get('useCustomAnimeStatusOrder'); + readonly userAnimeStatusOrderCtrl = this.settingsForm?.get('userAnimeStatusOrder'); readonly localStorageLimit = this.persistenceService.getMaxByxes(); readonly localStorageUsage$ = new BehaviorSubject(this.persistenceService.getUsedBytes()); @@ -190,4 +204,15 @@ export class SettingsPage implements OnInit { goToLastPage(): void { this.router.navigateByUrl(this.lastVisitedPage()); } + + reorderAnimeStatusSections(event: CustomEvent): void { + const newAnimeStatusOrder = [...this.userAnimeStatusOrderCtrl.value]; + const itemToMove = newAnimeStatusOrder.splice(event.detail.from, 1)[0]; + + newAnimeStatusOrder.splice(event.detail.to, 0, itemToMove); + + this.userAnimeStatusOrderCtrl.patchValue(newAnimeStatusOrder); + + event.detail.complete(true); + } } diff --git a/src/app/modules/settings/utils/index.ts b/src/app/modules/settings/utils/index.ts index cade5da8..8bb27299 100644 --- a/src/app/modules/settings/utils/index.ts +++ b/src/app/modules/settings/utils/index.ts @@ -1,2 +1,3 @@ export { mapSettinsFormToState } from './map-settings-form-to-state.function'; export { SettingsFormInterface } from './settings-form.interface'; +export { mapAnimeStatusOrderToFormArray } from './map-anime-status-order-to-form-array.function'; diff --git a/src/app/modules/settings/utils/map-anime-status-order-to-form-array.function.ts b/src/app/modules/settings/utils/map-anime-status-order-to-form-array.function.ts new file mode 100644 index 00000000..162a56cb --- /dev/null +++ b/src/app/modules/settings/utils/map-anime-status-order-to-form-array.function.ts @@ -0,0 +1,7 @@ +import { FormArray, FormControl } from '@angular/forms'; + +export function mapAnimeStatusOrderToFormArray(statusesOrder: string[] = []): FormArray> { + const controls = statusesOrder.map((status) => new FormControl(status)); + + return new FormArray(controls); +} diff --git a/src/app/modules/settings/utils/settings-form.interface.ts b/src/app/modules/settings/utils/settings-form.interface.ts index 087a330b..61f9ab22 100644 --- a/src/app/modules/settings/utils/settings-form.interface.ts +++ b/src/app/modules/settings/utils/settings-form.interface.ts @@ -8,4 +8,6 @@ export interface SettingsFormInterface { playerMode: PlayerModeType; playerKindDisplayMode: PlayerKindDisplayMode; shikimoriDomain: string; + isUserAnimeStatusReorder: boolean; + userAnimeStatusOrder: string[]; } diff --git a/src/app/shared/config/default-anime-status-order.config.ts b/src/app/shared/config/default-anime-status-order.config.ts new file mode 100644 index 00000000..ffd10fa7 --- /dev/null +++ b/src/app/shared/config/default-anime-status-order.config.ts @@ -0,0 +1,8 @@ +export const DEFAULT_ANIME_STATUS_ORDER = [ + 'recent', + 'planned', + 'watching', + 'rewatching', + 'on_hold', + 'dropped', +]; diff --git a/src/app/store/settings/reducers/settings.reducer.ts b/src/app/store/settings/reducers/settings.reducer.ts index a5847b9f..bf47774c 100644 --- a/src/app/store/settings/reducers/settings.reducer.ts +++ b/src/app/store/settings/reducers/settings.reducer.ts @@ -1,5 +1,6 @@ import { createReducer, on } from '@ngrx/store'; +import { DEFAULT_ANIME_STATUS_ORDER } from '@app/shared/config/default-anime-status-order.config'; import { SettingsStoreInterface } from '@app/store/settings/types/settings-store.interface'; import { defaultAvailableLangs } from '@app/core/providers/transloco/transloco.provider'; import { @@ -25,6 +26,8 @@ const initialState: SettingsStoreInterface = { kindPreferences: {}, domainPreferences: {}, lastPage: '/home', + useCustomAnimeStatusOrder: false, + userAnimeStatusOrder: DEFAULT_ANIME_STATUS_ORDER, }; const reducer = createReducer( diff --git a/src/app/store/settings/types/settings-store.interface.ts b/src/app/store/settings/types/settings-store.interface.ts index 1b7717fb..046c677a 100644 --- a/src/app/store/settings/types/settings-store.interface.ts +++ b/src/app/store/settings/types/settings-store.interface.ts @@ -16,4 +16,6 @@ export interface SettingsStoreInterface { kindPreferences: PreferencesInterface; domainPreferences: PreferencesInterface; lastPage: string; + useCustomAnimeStatusOrder: boolean; + userAnimeStatusOrder: string[]; } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index bee6161e..f3871ffb 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -257,6 +257,13 @@ } } }, + "USER_ANIMES_PAGE": { + "TITLE": "Anime List", + "USE_CUSTOM_ANIME_STATUS_ORDER": { + "LABEL": "Custom anime list order", + "HELP": "Drag and drop items to sort them as you like" + } + }, "PROFILE_SETTINGS": { "TITLE": "Profile", "CONNECTION_INFO": { diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index bf9bba74..30204dde 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -262,6 +262,13 @@ } } }, + "USER_ANIMES_PAGE": { + "TITLE": "Список Аниме", + "USE_CUSTOM_ANIME_STATUS_ORDER": { + "LABEL": "Пользовательская сортировка списка аниме", + "HELP": "Перетаскивайте элементы, чтобы отсортировать по своему вкусу" + } + }, "PROFILE_SETTINGS": { "TITLE": "Профиль", "CONNECTION_INFO": { diff --git a/src/assets/i18n/uk.json b/src/assets/i18n/uk.json index b959bc59..5a389c72 100644 --- a/src/assets/i18n/uk.json +++ b/src/assets/i18n/uk.json @@ -257,6 +257,13 @@ } } }, + "USER_ANIMES_PAGE": { + "TITLE": "Список Аніме", + "USE_CUSTOM_ANIME_STATUS_ORDER": { + "LABEL": "Користувальницьке сортування списку аніме", + "HELP": "Перетягуйте елементи, щоб відсортувати на свій смак" + } + }, "PROFILE_SETTINGS": { "TITLE": "Профіль", "CONNECTION_INFO": { From 048341dbc37ee3584d620afaa3a30a07d5cc860e Mon Sep 17 00:00:00 2001 From: Smarthard Date: Fri, 27 Jun 2025 17:16:24 +0300 Subject: [PATCH 13/22] add domain filters setting --- src/app/core/used-icons.config.ts | 1 + src/app/modules/settings/settings.page.html | 39 +++++++++++++++++++ src/app/modules/settings/settings.page.scss | 8 ++++ src/app/modules/settings/settings.page.ts | 30 ++++++++++++++ src/app/shared/validators/index.ts | 3 ++ src/app/shared/validators/url.validator.ts | 13 +++++++ .../settings/reducers/settings.reducer.ts | 1 + .../types/settings-store.interface.ts | 1 + src/assets/i18n/en.json | 11 ++++++ src/assets/i18n/ru.json | 11 ++++++ src/assets/i18n/uk.json | 11 ++++++ 11 files changed, 129 insertions(+) create mode 100644 src/app/shared/validators/index.ts create mode 100644 src/app/shared/validators/url.validator.ts diff --git a/src/app/core/used-icons.config.ts b/src/app/core/used-icons.config.ts index 6d477852..00b0b8b3 100644 --- a/src/app/core/used-icons.config.ts +++ b/src/app/core/used-icons.config.ts @@ -26,4 +26,5 @@ export { ellipsisVerticalOutline, checkmarkDoneOutline, warningOutline, + addOutline, } from 'ionicons/icons'; diff --git a/src/app/modules/settings/settings.page.html b/src/app/modules/settings/settings.page.html index fc1ec93a..da8ea227 100644 --- a/src/app/modules/settings/settings.page.html +++ b/src/app/modules/settings/settings.page.html @@ -122,6 +122,45 @@ {{ 'SETTINGS_MODULE.SETTINGS_PAGE.SETTING_GROUPS.PLAYER_PAGE.PLAYER_KIND_DISPLAY_MODE.OPTIONS.ALL' | transloco }} + + + + {{ 'SETTINGS_MODULE.SETTINGS_PAGE.SETTING_GROUPS.PLAYER_PAGE.FILTER_PLAYER_DOMAINS.LABEL' | transloco }} + + + + @for (domain of filterPlayerDomainsCtrl?.value; track domain) { + + {{ domain }} + + + + {{ 'SETTINGS_MODULE.SETTINGS_PAGE.SETTING_GROUPS.PLAYER_PAGE.FILTER_PLAYER_DOMAINS.ADD_NEW_DOMAIN.DELETE_BTN' | transloco }} + + + } + + +
+ + + + + +
+
diff --git a/src/app/modules/settings/settings.page.scss b/src/app/modules/settings/settings.page.scss index 8fc5ff11..154f4167 100644 --- a/src/app/modules/settings/settings.page.scss +++ b/src/app/modules/settings/settings.page.scss @@ -30,6 +30,14 @@ margin: .5rem; } + .filter-player-domains { + &__input { + display: flex; + align-items: flex-start; + gap: 1rem; + } + } + .storage { &__used-info { display: flex; diff --git a/src/app/modules/settings/settings.page.ts b/src/app/modules/settings/settings.page.ts index 6237b752..b5eebc62 100644 --- a/src/app/modules/settings/settings.page.ts +++ b/src/app/modules/settings/settings.page.ts @@ -21,8 +21,11 @@ import { IonButton, IonContent, IonIcon, + IonInput, IonItem, + IonItemGroup, IonLabel, + IonList, IonProgressBar, IonReorder, IonReorderGroup, @@ -49,6 +52,7 @@ import { SettingsGroupComponent } from '@app/modules/settings/components/setting import { ThemeSettingsType } from '@app/store/settings/types/theme-settings.type'; import { ToHumanReadableBytesPipe } from '@app/shared/pipes/to-human-readable-bytes/to-human-readable-bytes.pipe'; import { authShikimoriAction, logoutShikimoriAction } from '@app/store/auth/actions/auth.actions'; +import { getDomain } from '@app/shared/utils/get-domain.function'; import { mapAnimeStatusOrderToFormArray, mapSettinsFormToState } from '@app/modules/settings/utils'; import { resetCacheAction } from '@app/store/cache/actions'; import { selectIsAuthenticated } from '@app/store/auth/selectors/auth.selectors'; @@ -60,6 +64,7 @@ import { } from '@app/store/shikimori/selectors/shikimori.selectors'; import { updateSettingsAction } from '@app/store/settings/actions/settings.actions'; import { updateShikimoriDomainAction } from '@app/store/shikimori/actions'; +import { urlValidator } from '@app/shared/validators'; @Component({ @@ -83,7 +88,10 @@ import { updateShikimoriDomainAction } from '@app/store/shikimori/actions'; IonReorderGroup, IonReorder, IonItem, + IonItemGroup, + IonInput, IonLabel, + IonList, SettingsGroupComponent, ProfileInfoComponent, ToHumanReadableBytesPipe, @@ -131,6 +139,7 @@ export class SettingsPage implements OnInit { shikimoriDomain: new FormControl(this.defaultShikimoriDomain), useCustomAnimeStatusOrder: new FormControl(false), userAnimeStatusOrder: mapAnimeStatusOrderToFormArray(DEFAULT_ANIME_STATUS_ORDER), + filterPlayerDomains: new FormControl([]), }); readonly themeCtrl = this.settingsForm?.get('theme'); @@ -139,6 +148,8 @@ export class SettingsPage implements OnInit { readonly shikimoriDomainCtrl = this.settingsForm?.get('shikimoriDomain'); readonly useCustomAnimeStatusOrder = this.settingsForm?.get('useCustomAnimeStatusOrder'); readonly userAnimeStatusOrderCtrl = this.settingsForm?.get('userAnimeStatusOrder'); + readonly filterPlayerDomainsCtrl = this.settingsForm?.get('filterPlayerDomains'); + readonly addFilterDomainCtrl = new FormControl('', [urlValidator()]); readonly localStorageLimit = this.persistenceService.getMaxByxes(); readonly localStorageUsage$ = new BehaviorSubject(this.persistenceService.getUsedBytes()); @@ -215,4 +226,23 @@ export class SettingsPage implements OnInit { event.detail.complete(true); } + + addNewDomainFilter(): void { + if (this.addFilterDomainCtrl.valid) { + const { value: rawDomain } = this.addFilterDomainCtrl; + const newDomain = getDomain(rawDomain); + const { value: oldDomains } = this.filterPlayerDomainsCtrl; + const newFilters = [...new Set([...oldDomains, newDomain])]; + + this.filterPlayerDomainsCtrl.setValue(newFilters); + this.addFilterDomainCtrl.reset(); + } + } + + deleteDomainFilter(domain: string): void { + const { value: domains } = this.filterPlayerDomainsCtrl; + const newDomains = (domains || [])?.filter((d) => d !== domain); + + this.filterPlayerDomainsCtrl.setValue(newDomains); + } } diff --git a/src/app/shared/validators/index.ts b/src/app/shared/validators/index.ts new file mode 100644 index 00000000..c59040f3 --- /dev/null +++ b/src/app/shared/validators/index.ts @@ -0,0 +1,3 @@ +export { NoWhitespacesValidator } from './no-whitespaces.validator'; +export { ResourceStateValidator } from './resource-state.validator'; +export { urlValidator } from './url.validator'; diff --git a/src/app/shared/validators/url.validator.ts b/src/app/shared/validators/url.validator.ts new file mode 100644 index 00000000..c7f882e4 --- /dev/null +++ b/src/app/shared/validators/url.validator.ts @@ -0,0 +1,13 @@ +import { FormControl } from '@angular/forms'; + +export function urlValidator() { + return (control: FormControl) => { + try { + new URL(control?.value); + } catch (e) { + return { isNotUrl: true }; + } + + return null; + }; +} diff --git a/src/app/store/settings/reducers/settings.reducer.ts b/src/app/store/settings/reducers/settings.reducer.ts index bf47774c..383888f9 100644 --- a/src/app/store/settings/reducers/settings.reducer.ts +++ b/src/app/store/settings/reducers/settings.reducer.ts @@ -28,6 +28,7 @@ const initialState: SettingsStoreInterface = { lastPage: '/home', useCustomAnimeStatusOrder: false, userAnimeStatusOrder: DEFAULT_ANIME_STATUS_ORDER, + filterPlayerDomains: [], }; const reducer = createReducer( diff --git a/src/app/store/settings/types/settings-store.interface.ts b/src/app/store/settings/types/settings-store.interface.ts index 046c677a..d0abca37 100644 --- a/src/app/store/settings/types/settings-store.interface.ts +++ b/src/app/store/settings/types/settings-store.interface.ts @@ -18,4 +18,5 @@ export interface SettingsStoreInterface { lastPage: string; useCustomAnimeStatusOrder: boolean; userAnimeStatusOrder: string[]; + filterPlayerDomains: string[] } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index f3871ffb..cf9b10f1 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -255,6 +255,17 @@ "SPECIAL-ONLY": "Display only on videos of Blu-Ray, DVD or VHS quality", "ALL": "Display for all video types" } + }, + "FILTER_PLAYER_DOMAINS": { + "LABEL": "Domain Filter", + "ADD_NEW_DOMAIN": { + "LABEL": "Add domain", + "PLACEHOLDER": "Copy the link to the player and paste it here", + "DELETE_BTN": "Delete", + "VALIDATION_ERRORS": { + "IS_NOT_URL": "Incorrect link" + } + } } }, "USER_ANIMES_PAGE": { diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index 30204dde..abc7b4fb 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -260,6 +260,17 @@ "SPECIAL-ONLY": "Отображать только на видео с типом Blue-Ray, DVD и VHS качества", "ALL": "Отображать для всех типов видео" } + }, + "FILTER_PLAYER_DOMAINS": { + "LABEL": "Фильтр доменов", + "ADD_NEW_DOMAIN": { + "LABEL": "Добавление домена", + "PLACEHOLDER": "Скопируйте ссылку на плеер и вставьте сюда", + "DELETE_BTN": "Удалить", + "VALIDATION_ERRORS": { + "IS_NOT_URL": "Некорректная ссылка" + } + } } }, "USER_ANIMES_PAGE": { diff --git a/src/assets/i18n/uk.json b/src/assets/i18n/uk.json index 5a389c72..c16ed912 100644 --- a/src/assets/i18n/uk.json +++ b/src/assets/i18n/uk.json @@ -255,6 +255,17 @@ "SPECIAL-ONLY": "Відображати тільки на відео з типом Blu-Ray, DVD та VHS якості", "ALL": "Відображати для всіх типів відео" } + }, + "FILTER_PLAYER_DOMAINS": { + "LABEL": "Фільтр доменів", + "ADD_NEW_DOMAIN": { + "LABEL": "Додавання домену", + "PLACEHOLDER": "Скопіюйте посилання на програвач і вставте сюди", + "DELETE_BTN": "Видалити", + "VALIDATION_ERRORS": { + "IS_NOT_URL": "Некоректне посилання" + } + } } }, "USER_ANIMES_PAGE": { From 5d050a55ccac90caad7378f8c17ba2a3509d5aab Mon Sep 17 00:00:00 2001 From: Smarthard Date: Mon, 30 Jun 2025 13:57:27 +0300 Subject: [PATCH 14/22] add player domain filters --- src/app/core/used-icons.config.ts | 1 + .../video-selector.component.html | 13 ++++++++++ .../video-selector.component.scss | 8 +++++++ .../video-selector.component.ts | 15 ++++++++++-- src/app/modules/player/player.page.html | 2 ++ src/app/modules/player/player.page.ts | 24 +++++++++++++++++-- .../filter-videos-by-domains.function.ts | 8 +++++++ src/app/modules/player/utils/index.ts | 1 + .../settings/selectors/settings.selectors.ts | 5 ++++ src/assets/i18n/en.json | 6 +++++ src/assets/i18n/ru.json | 6 +++++ src/assets/i18n/uk.json | 6 +++++ 12 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 src/app/modules/player/utils/filter-videos-by-domains.function.ts diff --git a/src/app/core/used-icons.config.ts b/src/app/core/used-icons.config.ts index 00b0b8b3..cc76f658 100644 --- a/src/app/core/used-icons.config.ts +++ b/src/app/core/used-icons.config.ts @@ -27,4 +27,5 @@ export { checkmarkDoneOutline, warningOutline, addOutline, + funnelOutline, } from 'ionicons/icons'; diff --git a/src/app/modules/player/components/video-selector/video-selector.component.html b/src/app/modules/player/components/video-selector/video-selector.component.html index 10a8fb32..203151cc 100644 --- a/src/app/modules/player/components/video-selector/video-selector.component.html +++ b/src/app/modules/player/components/video-selector/video-selector.component.html @@ -16,6 +16,19 @@ (toggleOpen)="onAuthorSectionToggle($event)" (selectVideo)="onSelectionChange($event)" /> + } @empty { +
+ @if (hasUnfilteredVideos()) { + + {{ 'PLAYER_MODULE.PLAYER_PAGE.VIDEO_SELECTOR.HAS_UNFILTERED.REMINDER_TEXT' | transloco }} + + + + + {{ 'PLAYER_MODULE.PLAYER_PAGE.VIDEO_SELECTOR.HAS_UNFILTERED.DISABLE_FILTERS_BTN' | transloco }} + + } +
} diff --git a/src/app/modules/player/components/video-selector/video-selector.component.scss b/src/app/modules/player/components/video-selector/video-selector.component.scss index aea9c002..d8b7ea8e 100644 --- a/src/app/modules/player/components/video-selector/video-selector.component.scss +++ b/src/app/modules/player/components/video-selector/video-selector.component.scss @@ -9,4 +9,12 @@ --scrollbar-thumb-color: var(--shc-scrollbar-color); --scrollbar-track-color: var(--shc-scrollbar-background-color); } + + &__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + } } diff --git a/src/app/modules/player/components/video-selector/video-selector.component.ts b/src/app/modules/player/components/video-selector/video-selector.component.ts index c996cc87..eb1c17f4 100644 --- a/src/app/modules/player/components/video-selector/video-selector.component.ts +++ b/src/app/modules/player/components/video-selector/video-selector.component.ts @@ -11,9 +11,14 @@ import { signal, untracked, } from '@angular/core'; -import { IonAccordionGroup } from '@ionic/angular/standalone'; +import { + IonAccordionGroup, + IonButton, + IonIcon, + IonText, +} from '@ionic/angular/standalone'; import { NgScrollbar } from 'ngx-scrollbar'; -import { TranslocoService } from '@jsverse/transloco'; +import { TranslocoPipe, TranslocoService } from '@jsverse/transloco'; import { toSignal } from '@angular/core/rxjs-interop'; import { FilterByAuthorPipe } from '@app/shared/pipes/filter-by-author/filter-by-author.pipe'; @@ -29,9 +34,13 @@ import { cleanAuthorName } from '@app/shared/utils/clean-author-name.function'; standalone: true, imports: [ IonAccordionGroup, + IonButton, + IonIcon, + IonText, FilterByAuthorPipe, IncludesPipe, NgScrollbar, + TranslocoPipe, VideoSelectorItemComponent, ], templateUrl: './video-selector.component.html', @@ -51,8 +60,10 @@ export class VideoSelectorComponent { videos = input(); kindDisplayMode = input(); warnAvailability = input([]); + hasUnfilteredVideos = input(false); selection = output(); + disableFilters = output(); readonly openedByDefaultAuthors = signal([]); diff --git a/src/app/modules/player/player.page.html b/src/app/modules/player/player.page.html index e89522fb..df00df72 100644 --- a/src/app/modules/player/player.page.html +++ b/src/app/modules/player/player.page.html @@ -49,7 +49,9 @@ [selected]="currentVideo()" [kindDisplayMode]="playerKindDisplayMode()" [warnAvailability]="videos() | authorAvailabilityWarning: lastAiredEpisode()" + [hasUnfilteredVideos]="hasUnfilteredVideos()" (selection)="onVideoChange($event)" + (disableFilters)="onDisableDomainFilters()" /> getAnimeName(this.anime(), this.userSelectedLanguage())); isWatched = computed(() => isEpisodeWatched(this.episodeQ(), this.userRate())); isRewatching = computed(() => this.userRate()?.status === 'rewatching'); - episodeVideos = computed(() => filterByEpisode(this.videos(), this.episodeQ())); + + isDomainFilterOn = signal(true); + episodeVideosUnfiltered = computed(() => filterByEpisode(this.videos(), this.episodeQ())); + episodeVideosFiltered = computed(() => filterVideosByDomains(this.episodeVideosUnfiltered(), this.domainFilters())); + episodeVideos = computed(() => this.isDomainFilterOn() + ? this.episodeVideosFiltered() + : this.episodeVideosUnfiltered(), + ); + hasUnfilteredVideos = computed(() => this.episodeVideosUnfiltered()?.length > 0 && this.isDomainFilterOn()); + nextEpisodeAt = computed(() => { const nextEpisodeAt = this.anime()?.next_episode_at; const isCurrentEpisodeNotAired = this.episodeQ() > this.lastAiredEpisode(); @@ -472,4 +488,8 @@ export class PlayerPage implements OnInit { onCancelCommentEdit(): void { this.editComment.set(null); } + + onDisableDomainFilters(): void { + this.isDomainFilterOn.set(false); + } } diff --git a/src/app/modules/player/utils/filter-videos-by-domains.function.ts b/src/app/modules/player/utils/filter-videos-by-domains.function.ts new file mode 100644 index 00000000..aba4ff82 --- /dev/null +++ b/src/app/modules/player/utils/filter-videos-by-domains.function.ts @@ -0,0 +1,8 @@ +import { VideoInfoInterface } from '@app/modules/player/types'; +import { getDomain } from '@app/shared/utils/get-domain.function'; + + +export function filterVideosByDomains(videos: VideoInfoInterface[], filterDomains: string[]): VideoInfoInterface[] { + const filterDomainsSet = new Set(filterDomains); + return videos?.filter(({ url }) => !filterDomainsSet.has(getDomain(url))); +} diff --git a/src/app/modules/player/utils/index.ts b/src/app/modules/player/utils/index.ts index 3ddac4a0..0ac3f6a7 100644 --- a/src/app/modules/player/utils/index.ts +++ b/src/app/modules/player/utils/index.ts @@ -1,3 +1,4 @@ +export { filterVideosByDomains } from './filter-videos-by-domains.function'; export { isEpisodeWatched } from './is-episode-watched.function'; export { getLastAiredEpisode } from './get-last-aired-episode.function'; export { getLastUnwatchedEpisode } from './get-last-unwatched-episode.function'; diff --git a/src/app/store/settings/selectors/settings.selectors.ts b/src/app/store/settings/selectors/settings.selectors.ts index e83b386f..fdcb1d98 100644 --- a/src/app/store/settings/selectors/settings.selectors.ts +++ b/src/app/store/settings/selectors/settings.selectors.ts @@ -88,3 +88,8 @@ export const selectLastVisitedPage = createSelector( selectSettings, (state) => state.lastPage, ); + +export const selectDomainFilters = createSelector( + selectSettings, + (state) => state.filterPlayerDomains, +); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index cf9b10f1..39d81eef 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -188,6 +188,12 @@ "500": "Retry later" } } + }, + "VIDEO_SELECTOR": { + "HAS_UNFILTERED": { + "REMINDER_TEXT": "There are videos here, but you need to", + "DISABLE_FILTERS_BTN": "turn off filters" + } } }, "VIDEO_SELECT_MODAL": { diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index abc7b4fb..fdf2d055 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -193,6 +193,12 @@ "500": "Попробуйте позднее" } } + }, + "VIDEO_SELECTOR": { + "HAS_UNFILTERED": { + "REMINDER_TEXT": "Тут есть видео, но нужно", + "DISABLE_FILTERS_BTN": "Отключить Фильтры" + } } }, "VIDEO_SELECT_MODAL": { diff --git a/src/assets/i18n/uk.json b/src/assets/i18n/uk.json index c16ed912..e05d1987 100644 --- a/src/assets/i18n/uk.json +++ b/src/assets/i18n/uk.json @@ -188,6 +188,12 @@ "500": "Спробуйте пізніше" } } + }, + "VIDEO_SELECTOR": { + "HAS_UNFILTERED": { + "REMINDER_TEXT": "Тут є відео, але потрібно", + "DISABLE_FILTERS_BTN": "Вимкнути Фільтри" + } } }, "VIDEO_SELECT_MODAL": { From 496becaa61fbf06442da57b0bc766ba5c74a021d Mon Sep 17 00:00:00 2001 From: Smarthard Date: Thu, 3 Jul 2025 14:59:56 +0300 Subject: [PATCH 15/22] refactor side-panel to use signals --- .../side-panel/side-panel.component.html | 6 +-- .../side-panel/side-panel.component.ts | 40 +++++++------------ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/app/modules/player/components/side-panel/side-panel.component.html b/src/app/modules/player/components/side-panel/side-panel.component.html index 3c3dd499..8884e069 100644 --- a/src/app/modules/player/components/side-panel/side-panel.component.html +++ b/src/app/modules/player/components/side-panel/side-panel.component.html @@ -1,8 +1,8 @@ -@if (isMinified) { +@if (isMinified()) { @@ -10,7 +10,7 @@ {{ 'PLAYER_MODULE.PLAYER_PAGE.UPLOAD_VIDEO.FORM_ACTIONS.UPLOAD' | transloco }} diff --git a/src/app/modules/player/components/side-panel/side-panel.component.ts b/src/app/modules/player/components/side-panel/side-panel.component.ts index ad590b47..966b3827 100644 --- a/src/app/modules/player/components/side-panel/side-panel.component.ts +++ b/src/app/modules/player/components/side-panel/side-panel.component.ts @@ -1,11 +1,10 @@ import { ChangeDetectionStrategy, Component, - EventEmitter, - HostBinding, - Input, - Output, ViewEncapsulation, + inject, + input, + output, } from '@angular/core'; import { IonButton, IonIcon, ModalController } from '@ionic/angular/standalone'; import { RouterLink } from '@angular/router'; @@ -27,36 +26,27 @@ import { VideoInfoInterface } from '@app/modules/player/types'; styleUrl: './side-panel.component.scss', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'class': 'side-panel', + '[class.side-panel--minified]': 'isMinified()', + }, }) export class SidePanelComponent { - @HostBinding('class.side-panel') - private sidePanelClass = true; + private readonly modalController = inject(ModalController); - @Input() - anime: AnimeBriefInfoInterface; + anime = input.required(); + episode = input.required(); - @Input() - episode: number; + isLoading = input(true); + isMinified = input(false); - @Input() - isLoading: boolean = true; - - @Input() - @HostBinding('class.side-panel--minified') - isMinified: boolean = false; - - @Output() - uploaded = new EventEmitter(); - - constructor( - private readonly modalController: ModalController, - ) {} + uploaded = output(); async onOpenUploadModal(): Promise { const cssClass = 'side-panel__upload-modal'; const componentProps = { - anime: this.anime, - episode: this.episode, + anime: this.anime(), + episode: this.episode(), }; const { VideoUploadModalComponent } = await import('@app/modules/player/components/video-upload-modal'); From 4565876115d13b1d6a91dfc91315bae6ec73ed76 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Fri, 4 Jul 2025 13:46:14 +0300 Subject: [PATCH 16/22] add domain filters toggle button --- .../components/side-panel/side-panel.component.html | 12 +++++++++--- .../components/side-panel/side-panel.component.ts | 1 + src/app/modules/player/player.page.html | 3 ++- src/app/modules/player/player.page.ts | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app/modules/player/components/side-panel/side-panel.component.html b/src/app/modules/player/components/side-panel/side-panel.component.html index 8884e069..7f37a148 100644 --- a/src/app/modules/player/components/side-panel/side-panel.component.html +++ b/src/app/modules/player/components/side-panel/side-panel.component.html @@ -16,7 +16,13 @@ {{ 'PLAYER_MODULE.PLAYER_PAGE.UPLOAD_VIDEO.FORM_ACTIONS.UPLOAD' | transloco }} - - - +
+ + + + + + + +
} diff --git a/src/app/modules/player/components/side-panel/side-panel.component.ts b/src/app/modules/player/components/side-panel/side-panel.component.ts index 966b3827..116b60c1 100644 --- a/src/app/modules/player/components/side-panel/side-panel.component.ts +++ b/src/app/modules/player/components/side-panel/side-panel.component.ts @@ -41,6 +41,7 @@ export class SidePanelComponent { isMinified = input(false); uploaded = output(); + filtersToggle = output(); async onOpenUploadModal(): Promise { const cssClass = 'side-panel__upload-modal'; diff --git a/src/app/modules/player/player.page.html b/src/app/modules/player/player.page.html index df00df72..2ea5c1ef 100644 --- a/src/app/modules/player/player.page.html +++ b/src/app/modules/player/player.page.html @@ -51,7 +51,7 @@ [warnAvailability]="videos() | authorAvailabilityWarning: lastAiredEpisode()" [hasUnfilteredVideos]="hasUnfilteredVideos()" (selection)="onVideoChange($event)" - (disableFilters)="onDisableDomainFilters()" + (disableFilters)="setDomainFilters(false)" /> } diff --git a/src/app/modules/player/player.page.ts b/src/app/modules/player/player.page.ts index a9ac0029..bd295482 100644 --- a/src/app/modules/player/player.page.ts +++ b/src/app/modules/player/player.page.ts @@ -489,7 +489,7 @@ export class PlayerPage implements OnInit { this.editComment.set(null); } - onDisableDomainFilters(): void { - this.isDomainFilterOn.set(false); + setDomainFilters(isEnabled: boolean): void { + this.isDomainFilterOn.set(isEnabled); } } From c4ac679a6f9b3d54b939251055f35db510851254 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Fri, 4 Jul 2025 13:51:24 +0300 Subject: [PATCH 17/22] fix skeletons for mobile/tablets --- src/app/modules/player/player.page.html | 2 +- src/app/modules/player/player.page.scss | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/app/modules/player/player.page.html b/src/app/modules/player/player.page.html index 2ea5c1ef..b9f3c48b 100644 --- a/src/app/modules/player/player.page.html +++ b/src/app/modules/player/player.page.html @@ -36,7 +36,7 @@ (selection)="onKindChange($event)" /> - Date: Sat, 5 Jul 2025 16:24:40 +0300 Subject: [PATCH 18/22] fix video selector for mobiles --- .../video-selector-modal.component.html | 13 +++-- .../video-selector-modal.component.ts | 56 ++++++++----------- .../pipes/author-availability-warning.pipe.ts | 16 +----- src/app/modules/player/player.page.html | 2 +- src/app/modules/player/player.page.ts | 41 ++++++++------ .../utils/authors-availability.function.ts | 18 ++++++ src/app/modules/player/utils/index.ts | 1 + 7 files changed, 76 insertions(+), 71 deletions(-) create mode 100644 src/app/modules/player/utils/authors-availability.function.ts diff --git a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html index 660e0d8a..f967de7f 100644 --- a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html +++ b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.html @@ -17,16 +17,19 @@
diff --git a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts index 8f284f38..28914774 100644 --- a/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts +++ b/src/app/modules/player/components/video-selector-modal/video-selector-modal.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, - HostBinding, - Input, + Signal, ViewEncapsulation, + WritableSignal, inject, } from '@angular/core'; import { @@ -21,10 +21,10 @@ import { AuthorAvailabilityWarningPipe } from '@app/modules/player/pipes'; import { FilterByKindPipe } from '@app/shared/pipes/filter-by-kind/filter-by-kind.pipe'; import { GetActiveKindsPipe } from '@app/shared/pipes/get-active-kinds/get-active-kinds.pipe'; import { KindSelectorComponent } from '@app/modules/player/components/kind-selector/kind-selector.component'; +import { PlayerKindDisplayMode } from '@app/store/settings/types'; import { VideoInfoInterface, VideoKindEnum } from '@app/modules/player/types'; import { VideoSelectorComponent } from '@app/modules/player/components/video-selector/video-selector.component'; -// TODO: модалка не хочет переводиться на сигналы - перепроверить позже @Component({ selector: 'app-video-selector-modal', standalone: true, @@ -45,46 +45,34 @@ import { VideoSelectorComponent } from '@app/modules/player/components/video-sel styleUrl: './video-selector-modal.component.scss', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'video-selector-modal', + }, }) export class VideoSelectorModalComponent extends IonModal { - @HostBinding('class.video-selector-modal') - private videoSelectorModalClass = true; - private readonly _modalController = inject(ModalController); - private _selectedKind: VideoKindEnum; - private _selectedVideo: VideoInfoInterface; - - @Input() - videos: VideoInfoInterface[]; - - @Input() - lastAiredEpisode: number; - - @Input() - set selectedKind(kind: VideoKindEnum) { - this._selectedKind = kind; - } - - get selectedKind(): VideoKindEnum { - return this._selectedKind; - } - - @Input() - set selectedVideo(video: VideoInfoInterface) { - this._selectedVideo = video; - } + // TODO: перекидывание напрямую сигналов, а не их значений не выглядит хорошей идеей + public videos: Signal; + public episodeVideos: Signal; + public kindDisplayMode: Signal; + public hasUnfilteredVideos: Signal; + public lastAiredEpisode: Signal; - get selectedVideo(): VideoInfoInterface { - return this._selectedVideo; - } + public isDomainFilterOn: WritableSignal; + public selectedKind: WritableSignal; + public selectedVideo: WritableSignal; onKindChange(kind: VideoKindEnum): void { - this._selectedKind = kind; + this.selectedKind.set(kind); } onVideoChange(video: VideoInfoInterface): void { - this._selectedVideo = video; + this.selectedVideo.set(video); + } + + onToggleDomainFilters(): void { + this.isDomainFilterOn.set(false); } cancel(): void { @@ -93,6 +81,6 @@ export class VideoSelectorModalComponent extends IonModal { } submit(): void { - this._modalController.dismiss(this.selectedVideo, 'submit'); + this._modalController.dismiss(this.selectedVideo(), 'submit'); } } diff --git a/src/app/modules/player/pipes/author-availability-warning.pipe.ts b/src/app/modules/player/pipes/author-availability-warning.pipe.ts index 829947df..1d954dd6 100644 --- a/src/app/modules/player/pipes/author-availability-warning.pipe.ts +++ b/src/app/modules/player/pipes/author-availability-warning.pipe.ts @@ -1,5 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; + import { VideoInfoInterface } from '@app/modules/player/types'; +import { authorAvailability } from '@app/modules/player/utils'; @Pipe({ @@ -9,18 +11,6 @@ import { VideoInfoInterface } from '@app/modules/player/types'; }) export class AuthorAvailabilityWarningPipe implements PipeTransform { transform(videos: VideoInfoInterface[], lastAiredEpisode: number): string[] { - const availabilityIssueAuthros: string[] = []; - const authors = new Set(videos?.map(({ author }) => author)); - - for (const targetAuthor of authors) { - const authorVideos = videos?.filter(({ author }) => author === targetAuthor); - const authorEpisodes = new Set(authorVideos?.map(({ episode }) => episode)); - - if (authorEpisodes.size !== lastAiredEpisode) { - availabilityIssueAuthros.push(targetAuthor); - } - } - - return availabilityIssueAuthros; + return authorAvailability(videos, lastAiredEpisode); } } diff --git a/src/app/modules/player/player.page.html b/src/app/modules/player/player.page.html index b9f3c48b..a220c981 100644 --- a/src/app/modules/player/player.page.html +++ b/src/app/modules/player/player.page.html @@ -48,7 +48,7 @@ [videos]="episodeVideos() | filterByKind: currentKind()" [selected]="currentVideo()" [kindDisplayMode]="playerKindDisplayMode()" - [warnAvailability]="videos() | authorAvailabilityWarning: lastAiredEpisode()" + [warnAvailability]="authorAvailability()" [hasUnfilteredVideos]="hasUnfilteredVideos()" (selection)="onVideoChange($event)" (disableFilters)="setDomainFilters(false)" diff --git a/src/app/modules/player/player.page.ts b/src/app/modules/player/player.page.ts index bd295482..9f1d8b9d 100644 --- a/src/app/modules/player/player.page.ts +++ b/src/app/modules/player/player.page.ts @@ -38,10 +38,6 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { timer } from 'rxjs'; import { AnimeBriefInfoInterface } from '@app/shared/types/shikimori/anime-brief-info.interface'; -import { - AuthorAvailabilityWarningPipe, - ToUploaderPipe, -} from '@app/modules/player/pipes'; import { Comment } from '@app/shared/types/shikimori/comment'; import { CommentsComponent } from '@app/modules/player/components/comments/comments.component'; import { ControlPanelComponent } from '@app/modules/player/components/control-panel/control-panel.component'; @@ -56,12 +52,20 @@ import { ShikimoriAnimeLinkPipe } from '@app/shared/pipes/shikimori-anime-link/s import { SidePanelComponent } from '@app/modules/player/components/side-panel/side-panel.component'; import { SkeletonBlockComponent } from '@app/shared/components/skeleton-block/skeleton-block.component'; import { SwipeDirective } from '@app/shared/directives/swipe.directive'; +import { ToUploaderPipe } from '@app/modules/player/pipes'; import { UploaderComponent } from '@app/modules/player/components/uploader/uploader.component'; import { UserCommentFormComponent } from '@app/modules/player/components/user-comment-form/user-comment-form.component'; import { VideoInfoInterface } from '@app/modules/player/types'; import { VideoKindEnum } from '@app/modules/player/types/video-kind.enum'; import { VideoSelectorComponent } from '@app/modules/player/components/video-selector/video-selector.component'; import { authShikimoriAction } from '@app/store/auth/actions/auth.actions'; +import { + authorAvailability, + filterVideosByDomains, + getLastAiredEpisode, + getMaxEpisode, + isEpisodeWatched, +} from '@app/modules/player/utils'; import { deleteCommentAction, editCommentAction, @@ -74,12 +78,6 @@ import { watchAnimeSuccessAction, } from '@app/modules/player/store/actions'; import { filterByEpisode } from '@app/shared/utils/filter-by-episode.function'; -import { - filterVideosByDomains, - getLastAiredEpisode, - getMaxEpisode, - isEpisodeWatched, -} from '@app/modules/player/utils'; import { filterVideosByPreferences } from '@app/modules/player/utils/filter-videos-by-preferences.function'; import { getAnimeName } from '@app/shared/utils/get-anime-name.function'; import { getDomain } from '@app/shared/utils/get-domain.function'; @@ -134,7 +132,6 @@ import { visitAnimePageAction } from '@app/modules/home/store/recent-animes/acti UserCommentFormComponent, ShikimoriAnimeLinkPipe, GetShikimoriPagePipe, - AuthorAvailabilityWarningPipe, SidePanelComponent, IonText, IonContent, @@ -225,6 +222,8 @@ export class PlayerPage implements OnInit { return isCurrentEpisodeNotAired ? nextEpisodeAt : null; }); + authorAvailability = computed(() => authorAvailability(this.videos(), this.lastAiredEpisode())); + currentVideo = signal(null); currentKind = signal(null); isOrientationPortrait = signal(false); @@ -353,11 +352,17 @@ export class PlayerPage implements OnInit { // TODO: для модалок нужно придумать какой-то сервис - слишком много бойлерплейта async onOpenVideoSelectorModal(): Promise { + const prevVideo = this.currentVideo(); + const componentProps = { - videos: this.episodeVideos(), - selectedKind: this.currentKind(), - selectedVideo: this.currentVideo(), - lastAiredEpisode: this.lastAiredEpisode(), + videos: this.videos, + episodeVideos: this.episodeVideos, + kindDisplayMode: this.playerKindDisplayMode, + isDomainFilterOn: this.isDomainFilterOn, + hasUnfilteredVideos: this.hasUnfilteredVideos, + selectedKind: this.currentKind, + selectedVideo: this.currentVideo, + lastAiredEpisode: this.lastAiredEpisode, }; const { VideoSelectorModalComponent } = await import('@app/modules/player/components/video-selector-modal'); @@ -368,10 +373,10 @@ export class PlayerPage implements OnInit { modal.present(); - const { data: newSelected, role } = await modal.onDidDismiss(); + const { role } = await modal.onDidDismiss(); - if (role === 'submit') { - this.onVideoChange(newSelected); + if (role === 'cancel') { + this.onVideoChange(prevVideo); } } diff --git a/src/app/modules/player/utils/authors-availability.function.ts b/src/app/modules/player/utils/authors-availability.function.ts new file mode 100644 index 00000000..7ddf6cc5 --- /dev/null +++ b/src/app/modules/player/utils/authors-availability.function.ts @@ -0,0 +1,18 @@ +import { VideoInfoInterface } from '@app/modules/player/types'; + + +export function authorAvailability(videos: VideoInfoInterface[], lastAiredEpisode: number): string[] { + const availabilityIssueAuthros: string[] = []; + const authors = new Set(videos?.map(({ author }) => author)); + + for (const targetAuthor of authors) { + const authorVideos = videos?.filter(({ author }) => author === targetAuthor); + const authorEpisodes = new Set(authorVideos?.map(({ episode }) => episode)); + + if (authorEpisodes.size !== lastAiredEpisode) { + availabilityIssueAuthros.push(targetAuthor); + } + } + + return availabilityIssueAuthros; +} diff --git a/src/app/modules/player/utils/index.ts b/src/app/modules/player/utils/index.ts index 0ac3f6a7..ff69e4ea 100644 --- a/src/app/modules/player/utils/index.ts +++ b/src/app/modules/player/utils/index.ts @@ -4,3 +4,4 @@ export { getLastAiredEpisode } from './get-last-aired-episode.function'; export { getLastUnwatchedEpisode } from './get-last-unwatched-episode.function'; export { toUserRatesUpdate } from './to-user-rates-update.function'; export { getMaxEpisode } from './get-max-episodes.function'; +export { authorAvailability } from './authors-availability.function'; From acdaa1b0e0af5b402143e1ea5c8a5313885646f4 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Sat, 5 Jul 2025 16:47:54 +0300 Subject: [PATCH 19/22] round player and anime title --- src/app/modules/player/components/player/player.component.scss | 2 ++ src/app/modules/player/player.page.html | 1 + 2 files changed, 3 insertions(+) diff --git a/src/app/modules/player/components/player/player.component.scss b/src/app/modules/player/components/player/player.component.scss index 56f3ef9f..5dda220b 100644 --- a/src/app/modules/player/components/player/player.component.scss +++ b/src/app/modules/player/components/player/player.component.scss @@ -2,12 +2,14 @@ .player { display: flex; + border-radius: .25rem; &__video { flex: 1 0; overflow: hidden; background-color: var(--box-shadow); border: 0; + border-radius: inherit; &--has-banner { display: flex; diff --git a/src/app/modules/player/player.page.html b/src/app/modules/player/player.page.html index a220c981..5d2103f9 100644 --- a/src/app/modules/player/player.page.html +++ b/src/app/modules/player/player.page.html @@ -10,6 +10,7 @@ } @else { From 6024ad9c505c2d95edfd61695872dc03ee11b545 Mon Sep 17 00:00:00 2001 From: Smarthard Date: Sun, 6 Jul 2025 05:07:43 +0300 Subject: [PATCH 20/22] extract anime-rate-section component --- .../anime-rate-section.component.html | 33 ++++++ .../anime-rate-section.component.scss | 22 ++++ .../anime-rate-section.component.ts | 63 ++++++++++ .../components/anime-rate-section/index.ts | 1 + .../home/components/card-grid/index.ts | 1 + src/app/modules/home/home.page.html | 64 ++-------- src/app/modules/home/home.page.scss | 50 +++----- src/app/modules/home/home.page.ts | 111 ++++++------------ 8 files changed, 182 insertions(+), 163 deletions(-) create mode 100644 src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html create mode 100644 src/app/modules/home/components/anime-rate-section/anime-rate-section.component.scss create mode 100644 src/app/modules/home/components/anime-rate-section/anime-rate-section.component.ts create mode 100644 src/app/modules/home/components/anime-rate-section/index.ts create mode 100644 src/app/modules/home/components/card-grid/index.ts diff --git a/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html new file mode 100644 index 00000000..6ec4d97c --- /dev/null +++ b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html @@ -0,0 +1,33 @@ + + + diff --git a/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.scss b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.scss new file mode 100644 index 00000000..e122a517 --- /dev/null +++ b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.scss @@ -0,0 +1,22 @@ +.anime-rate-section { + &__label { + margin: 0 0 1rem; + padding: 5px 1rem; + display: flex; + justify-content: space-between; + align-items: center; + + color: var(--ion-color-dark-tint); + background-color: var(--ion-color-light-tint); + } + + &__text { + font-weight: bold; + font-size: 15px; + } + + &__icon { + font-size: 25px; + cursor: pointer; + } +} diff --git a/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.ts b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.ts new file mode 100644 index 00000000..0aeb77d2 --- /dev/null +++ b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.ts @@ -0,0 +1,63 @@ +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, + computed, + input, + output, +} from '@angular/core'; +import { IonButton, IonIcon, IonText } from '@ionic/angular/standalone'; +import { NgxVisibilityDirective } from 'ngx-visibility'; +import { TranslocoPipe } from '@jsverse/transloco'; +import { UpperCasePipe } from '@angular/common'; + +import { CardGridComponent } from '@app/modules/home/components/card-grid'; +import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; + +@Component({ + selector: 'app-anime-rate-section', + imports: [ + NgxVisibilityDirective, + TranslocoPipe, + IonIcon, + IonButton, + IonText, + UpperCasePipe, + CardGridComponent, + ], + templateUrl: './anime-rate-section.component.html', + styleUrl: './anime-rate-section.component.scss', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'class': 'anime-rate-section', + '[id]': 'status()', + '[class.ion-hide]': 'isSectionHidden(isLoaded(), rates())', + }, +}) +export class AnimeRateSectionComponent { + label = input.required(); + status = input.required(); + rates = input.required(); + isHidden = input(false); + isLoaded = input(false); + + visible = output(); + toggleHidden = output(); + + ratesCount = computed(() => this.rates()?.length || 0); + + isSectionHidden(isLoaded: boolean, rates: UserAnimeRate[]): boolean { + return isLoaded && !rates?.length; + } + + onSectionVisible(isVisible: boolean): void { + if (isVisible) { + this.visible.emit(); + } + } + + onToggleHidden(): void { + this.toggleHidden.emit(); + } +} diff --git a/src/app/modules/home/components/anime-rate-section/index.ts b/src/app/modules/home/components/anime-rate-section/index.ts new file mode 100644 index 00000000..448a85d2 --- /dev/null +++ b/src/app/modules/home/components/anime-rate-section/index.ts @@ -0,0 +1 @@ +export { AnimeRateSectionComponent } from './anime-rate-section.component'; diff --git a/src/app/modules/home/components/card-grid/index.ts b/src/app/modules/home/components/card-grid/index.ts new file mode 100644 index 00000000..0cf9dc9e --- /dev/null +++ b/src/app/modules/home/components/card-grid/index.ts @@ -0,0 +1 @@ +export { CardGridComponent } from './card-grid.component'; diff --git a/src/app/modules/home/home.page.html b/src/app/modules/home/home.page.html index b63548b9..e41e8cbe 100644 --- a/src/app/modules/home/home.page.html +++ b/src/app/modules/home/home.page.html @@ -1,60 +1,16 @@ - + @for (status of animeStatusOrder(); track status) { @let grid = animeGridMap.get(status); - } - - -
-
- - {{ label | transloco | uppercase }} - - @if (isLoaded) { - ({{ rates?.length || 0 }}) - } - - - - - -
- - -
-
diff --git a/src/app/modules/home/home.page.scss b/src/app/modules/home/home.page.scss index 7dec1fce..43836ade 100644 --- a/src/app/modules/home/home.page.scss +++ b/src/app/modules/home/home.page.scss @@ -1,43 +1,25 @@ @use 'src/scss/mixins' as *; -.anime-rates { - padding: 0; - margin: 0; +.home-page { - @include media-breakpoint-up('sm') { - margin: .5rem; - } - - @include media-breakpoint-up('md') { - margin: 1.25rem; - } - - @include media-breakpoint-up('lg') { - margin: 1.5rem 5rem; - } - - @include media-breakpoint-up('xl') { - margin: 1.5rem 10rem; - } - - &__label { - margin: 0 0 1rem; - padding: 5px 1rem; + &__anime-rates { display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; - color: var(--ion-color-dark-tint); - background-color: var(--ion-color-light-tint); - } + @include media-breakpoint-up('sm') { + margin: .5rem; + } - &__text { - font-weight: bold; - font-size: 15px; - } + @include media-breakpoint-up('md') { + margin: 1.25rem; + } + + @include media-breakpoint-up('lg') { + margin: 1.5rem 5rem; + } - &__icon { - font-size: 25px; - cursor: pointer; + @include media-breakpoint-up('xl') { + margin: 1.5rem 10rem; + } } } diff --git a/src/app/modules/home/home.page.ts b/src/app/modules/home/home.page.ts index 3177d695..15d941ff 100644 --- a/src/app/modules/home/home.page.ts +++ b/src/app/modules/home/home.page.ts @@ -1,6 +1,5 @@ import { AsyncPipe, - NgTemplateOutlet, UpperCasePipe, } from '@angular/common'; import { @@ -10,41 +9,26 @@ import { OnInit, ViewEncapsulation, computed, + effect, inject, } from '@angular/core'; -import { - IonButton, - IonContent, - IonIcon, - IonText, -} from '@ionic/angular/standalone'; -import { NgxVisibilityDirective } from 'ngx-visibility'; +import { IonContent } from '@ionic/angular/standalone'; import { Store } from '@ngrx/store'; -import { - Subject, - combineLatest, - of, -} from 'rxjs'; import { Title } from '@angular/platform-browser'; -import { TranslocoPipe, TranslocoService } from '@jsverse/transloco'; +import { TranslocoService } from '@jsverse/transloco'; +import { combineLatest, of } from 'rxjs'; import { filter, map, shareReplay, - skipWhile, - take, - tap, - withLatestFrom, } from 'rxjs/operators'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { toSignal } from '@angular/core/rxjs-interop'; import { AnimeGridInterface } from '@app/modules/home/types/anime-grid.interface'; -import { CardGridComponent } from '@app/modules/home/components/card-grid/card-grid.component'; +import { AnimeRateSectionComponent } from '@app/modules/home/components/anime-rate-section'; import { DEFAULT_ANIME_STATUS_ORDER } from '@app/shared/config/default-anime-status-order.config'; import { ResourceIdType } from '@app/shared/types/resource-id.type'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; -import { VisibilityChangeInterface } from '@app/modules/home/types/visibility-change.interface'; import { loadAnimeRateByStatusAction, selectIsRatesLoadedByStatus, @@ -63,19 +47,16 @@ import { sortRatesByDateVisited } from '@app/modules/home/utils'; changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [ - NgxVisibilityDirective, AsyncPipe, UpperCasePipe, - NgTemplateOutlet, - TranslocoPipe, - CardGridComponent, - IonIcon, - IonButton, - IonText, IonContent, + AnimeRateSectionComponent, ], templateUrl: 'home.page.html', styleUrls: ['home.page.scss'], + host: { + class: 'home-page', + }, }) export class HomePage implements OnInit { private readonly store = inject(Store); @@ -91,11 +72,11 @@ export class HomePage implements OnInit { : DEFAULT_ANIME_STATUS_ORDER, ); - readonly isTranslationsLoaded$ = this.transloco.events$.pipe( - filter((e) => e.type === 'translationLoadSuccess'), + readonly isTranslationsLoaded = toSignal( + this.transloco.events$.pipe(filter((e) => e.type === 'translationLoadSuccess')), ); - readonly currentUser$ = this.store.select(selectShikimoriCurrentUser); + readonly currentUser = this.store.selectSignal(selectShikimoriCurrentUser); readonly recent$ = combineLatest([ this.store.select(selectRecentAnimes), @@ -121,44 +102,24 @@ export class HomePage implements OnInit { readonly isDroppedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('dropped')); animeGridMap: Map; - hiddenGridMap: Map; - sectionVisibilitySubject$ = new Subject(); - ngOnInit() { this.initValues(); - this.initSubscriptions(); } - initSubscriptions(): void { - combineLatest([ - this.currentUser$, - this.isTranslationsLoaded$, - ]).pipe( - skipWhile(([currentUser, hasTranslation]) => !currentUser?.id && !hasTranslation), - take(1), - tap(([{ id, nickname }]) => { - const title = this.transloco.translate('HOME_MODULE.HOME_PAGE.PAGE_TITLE', { nickname }); - - this.getUserAnimeRatesByStatus(id, 'planned'); - this.title.setTitle(title); - }), - takeUntilDestroyed(this.destroyRef), - ).subscribe(); - - this.sectionVisibilitySubject$ - .pipe( - takeUntilDestroyed(this.destroyRef), - withLatestFrom(this.currentUser$), - tap(([event, currentUser]) => { - if (currentUser?.id && event.isVisible) { - this.getUserAnimeRatesByStatus(currentUser.id, event.section); - } - }), - ) - .subscribe(); - } + currentUserChangeEffect = effect(() => { + const currentUser = this.currentUser(); + const hasTranslation = this.isTranslationsLoaded(); + + if (currentUser?.id && hasTranslation) { + const { id, nickname } = currentUser; + const title = this.transloco.translate('HOME_MODULE.HOME_PAGE.PAGE_TITLE', { nickname }); + + this.getUserAnimeRatesByStatus(id, 'planned'); + this.title.setTitle(title); + } + }); initValues(): void { this.hiddenGridMap = new Map(); @@ -215,18 +176,22 @@ export class HomePage implements OnInit { ]); } - toggleHiddenGridStatus(rateStatus: UserRateStatusType): void { - const status = this.hiddenGridMap.get(rateStatus) || false; + toggleHiddenGridStatus(rateStatus: string): void { + const status = this.hiddenGridMap.get(rateStatus as UserRateStatusType) || false; - this.hiddenGridMap.set(rateStatus, !status); + this.hiddenGridMap.set(rateStatus as UserRateStatusType, !status); } - getHiddenGridStatus(rateStatus: UserRateStatusType): boolean { - return this.hiddenGridMap.get(rateStatus) || false; + getHiddenGridStatus(rateStatus: string): boolean { + return this.hiddenGridMap.get(rateStatus as UserRateStatusType) || false; } - onSectionVisibilityChange(section: UserRateStatusType, isVisible: boolean): void { - this.sectionVisibilitySubject$.next({ section, isVisible }); + onSectionVisibilityChange(section: string): void { + const currentUser = this.currentUser(); + + if (currentUser?.id && section !== 'recent') { + this.getUserAnimeRatesByStatus(currentUser.id, section as UserRateStatusType); + } } getUserAnimeRatesByStatus(userId: ResourceIdType, status: UserRateStatusType): void { @@ -234,8 +199,4 @@ export class HomePage implements OnInit { this.store.dispatch(loadAnimeRateByStatusAction({ userId, status })); } } - - isSectionHidden(isLoaded: boolean, rates: UserAnimeRate[]): boolean { - return isLoaded && !rates?.length; - } } From 0bcfd455efa31fc032999b9c36046c7ad7db524f Mon Sep 17 00:00:00 2001 From: Smarthard Date: Thu, 10 Jul 2025 20:51:17 +0300 Subject: [PATCH 21/22] add shikimori graphql types for user rates --- package-lock.json | 19 +- package.json | 4 +- src/app/core/providers/avif/index.ts | 2 + .../avif/is-supports-avif.provider.ts | 10 + .../providers/avif/is-supports-avif.token.ts | 4 + .../anime-rate-section.component.html | 11 +- .../anime-rate-section.component.ts | 20 +- .../card-grid-item.component.html | 70 ++++--- .../card-grid-item.component.scss | 9 +- .../card-grid-item.component.ts | 3 +- .../card-grid/card-grid.component.html | 27 +-- .../card-grid/card-grid.component.scss | 2 +- .../card-grid/card-grid.component.ts | 38 ++-- src/app/modules/home/home.page.html | 14 +- src/app/modules/home/home.page.ts | 179 +++++------------- src/app/modules/home/home.routes.ts | 6 +- .../home/pipes/filter-rates-by-status.pipe.ts | 14 ++ .../modules/home/pipes/get-anime-kind.pipe.ts | 25 +++ .../modules/home/pipes/get-anime-name.pipe.ts | 25 +++ .../home/pipes/get-anime-poster.pipe.ts | 22 +++ .../home/pipes/get-anime-release-date.pipe.ts | 29 +++ src/app/modules/home/pipes/index.ts | 10 + .../pipes/sort-rates-by-anime-rating.pipe.ts | 28 +++ .../pipes/sort-rates-by-date-visited.pipe.ts | 5 +- .../home/pipes/sort-rates-by-name.pipe.ts | 26 +++ .../pipes/sort-rates-by-user-score.pipe.ts | 37 ++++ .../actions/anime-rate-metadata.actions.ts | 21 ++ .../actions/anime-rate-paging.actions.ts | 14 -- .../home/store/anime-rates/actions/index.ts | 2 +- .../actions/load-anime-rate.action.ts | 26 +-- .../effects/anime-rates.effects.ts | 130 ++++--------- .../modules/home/store/anime-rates/index.ts | 1 - .../reducers/anime-rates.meta-reducer.ts | 16 ++ .../reducers/anime-rates.reducer.ts | 61 +++--- .../home/store/anime-rates/reducers/index.ts | 3 +- .../selectors/anime-rates.selectors.ts | 36 ++-- .../types/anime-rate-metadata.interface.ts | 6 + .../types/anime-rates-store.interface.ts | 42 +--- .../home/store/anime-rates/types/index.ts | 3 +- .../utils/anime-rates-store-key.helpers.ts | 33 ---- .../anime-rates/utils/anime-rates.helpers.ts | 10 - .../home/store/anime-rates/utils/index.ts | 2 - .../anime-to-user-anime-rate.function.ts | 20 +- .../utils/recent-animes-to-rates.function.ts | 9 +- .../home/types/anime-grid.interface.ts | 9 - .../types/extended-user-rate-status.type.ts | 3 + src/app/modules/home/types/index.ts | 1 + .../home/types/visibility-change.interface.ts | 6 - .../utils/get-anime-rate-name.function.ts | 16 ++ src/app/modules/home/utils/index.ts | 4 + .../sort-rates-by-anime-name.function.ts | 20 ++ .../sort-rates-by-anime-rating.function.ts | 23 +++ .../sort-rates-by-date-visited.function.ts | 4 +- .../sort-rates-by-user-score.function.ts | 25 +++ .../player/store/effects/player.effects.ts | 5 +- .../abstract-image-card.component.ts | 5 +- .../image-card/image-card.component.html | 3 +- .../image-card/image-card.component.scss | 1 + .../image-card/image-card.component.ts | 2 +- .../default-anime-status-order.config.ts | 5 +- .../get-anime-name/get-anime-name.pipe.ts | 16 -- .../get-player-link/get-player-link.pipe.ts | 6 +- src/app/shared/providers/index.ts | 4 + .../shikimori-image-loader.factory.ts | 8 +- .../smarthard-image-loader.factory.ts | 25 +++ .../smarthard-image-loader.provider.ts | 18 ++ .../services/shikimori-client.service.ts | 79 +++++--- src/app/shared/types/index.ts | 9 + .../shikimori/graphql/anime.interface.ts | 67 +++++++ .../graphql/character-role.interface.ts | 9 + .../shikimori/graphql/character.interface.ts | 23 +++ .../graphql/external-link.interface.ts | 9 + .../shikimori/graphql/genre.interface.ts | 10 + .../graphql/incomplete-date.interface.ts | 6 + .../shared/types/shikimori/graphql/index.ts | 22 +++ .../graphql/person-role.interface.ts | 9 + .../shikimori/graphql/person.interface.ts | 24 +++ .../shikimori/graphql/poster.interface.ts | 18 ++ .../shikimori/graphql/related.interface.ts | 10 + ...e-rates-metadata-gql-response.interface.ts | 29 +++ ...user-anime-rates-gql-response.interface.ts | 7 + .../shikimori/graphql/score-stat.interface.ts | 4 + .../shikimori/graphql/screenshot.interface.ts | 8 + .../graphql/status-stat.interface.ts | 6 + .../shikimori/graphql/studio.interface.ts | 7 + .../shikimori/graphql/topic.interface.ts | 14 ++ .../graphql/user-anime-rate-gql.interface.ts | 17 ++ .../shikimori/graphql/video.interface.ts | 10 + .../shared/types/shikimori/helpers/index.ts | 1 + src/app/shared/types/shikimori/index.ts | 25 +++ .../types/shikimori/mappers/grapql.mappers.ts | 126 ++++++++++++ .../shared/types/shikimori/mappers/index.ts | 8 + .../shikimori/queries/find-anime-query.ts | 18 +- .../shared/types/shikimori/queries/index.ts | 3 + .../shared/types/shikimori/user-anime-rate.ts | 14 +- .../shikimori/user-brief-rate.interface.ts | 5 +- .../types/shikimori/user-rate-target.enum.ts | 4 + .../types/shikimori/user-stats.interface.ts | 3 +- src/app/shared/utils/common-ngfor-tracking.ts | 6 +- src/app/shared/utils/entities.utils.ts | 15 ++ src/app/shared/utils/get-path.function.ts | 16 +- .../shared/utils/is-supports-avif.function.ts | 20 ++ .../shared/utils/rx-anime-rates-functions.ts | 33 ---- .../utils/split-array-to-chunks.funtion.ts | 8 + src/app/shared/utils/zip.utils.ts | 16 ++ src/app/store/app-state.providers.ts | 2 + .../settings/reducers/settings.reducer.ts | 1 - .../settings/selectors/settings.selectors.ts | 5 - .../types/settings-store.interface.ts | 1 - src/assets/1x1.avif | Bin 0 -> 450 bytes src/index.html | 3 + src/main.ts | 2 + 112 files changed, 1369 insertions(+), 646 deletions(-) create mode 100644 src/app/core/providers/avif/index.ts create mode 100644 src/app/core/providers/avif/is-supports-avif.provider.ts create mode 100644 src/app/core/providers/avif/is-supports-avif.token.ts create mode 100644 src/app/modules/home/pipes/filter-rates-by-status.pipe.ts create mode 100644 src/app/modules/home/pipes/get-anime-kind.pipe.ts create mode 100644 src/app/modules/home/pipes/get-anime-name.pipe.ts create mode 100644 src/app/modules/home/pipes/get-anime-poster.pipe.ts create mode 100644 src/app/modules/home/pipes/get-anime-release-date.pipe.ts create mode 100644 src/app/modules/home/pipes/index.ts create mode 100644 src/app/modules/home/pipes/sort-rates-by-anime-rating.pipe.ts create mode 100644 src/app/modules/home/pipes/sort-rates-by-name.pipe.ts create mode 100644 src/app/modules/home/pipes/sort-rates-by-user-score.pipe.ts create mode 100644 src/app/modules/home/store/anime-rates/actions/anime-rate-metadata.actions.ts delete mode 100644 src/app/modules/home/store/anime-rates/actions/anime-rate-paging.actions.ts create mode 100644 src/app/modules/home/store/anime-rates/reducers/anime-rates.meta-reducer.ts create mode 100644 src/app/modules/home/store/anime-rates/types/anime-rate-metadata.interface.ts delete mode 100644 src/app/modules/home/store/anime-rates/utils/anime-rates-store-key.helpers.ts delete mode 100644 src/app/modules/home/store/anime-rates/utils/anime-rates.helpers.ts delete mode 100644 src/app/modules/home/store/anime-rates/utils/index.ts delete mode 100644 src/app/modules/home/types/anime-grid.interface.ts create mode 100644 src/app/modules/home/types/extended-user-rate-status.type.ts create mode 100644 src/app/modules/home/types/index.ts delete mode 100644 src/app/modules/home/types/visibility-change.interface.ts create mode 100644 src/app/modules/home/utils/get-anime-rate-name.function.ts create mode 100644 src/app/modules/home/utils/sort-rates-by-anime-name.function.ts create mode 100644 src/app/modules/home/utils/sort-rates-by-anime-rating.function.ts create mode 100644 src/app/modules/home/utils/sort-rates-by-user-score.function.ts delete mode 100644 src/app/shared/pipes/get-anime-name/get-anime-name.pipe.ts create mode 100644 src/app/shared/providers/index.ts create mode 100644 src/app/shared/providers/smarthard-image-loader.factory.ts create mode 100644 src/app/shared/providers/smarthard-image-loader.provider.ts create mode 100644 src/app/shared/types/index.ts create mode 100644 src/app/shared/types/shikimori/graphql/anime.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/character-role.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/character.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/external-link.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/genre.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/incomplete-date.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/index.ts create mode 100644 src/app/shared/types/shikimori/graphql/person-role.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/person.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/poster.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/related.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/responses/anime-rates-metadata-gql-response.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/responses/user-anime-rates-gql-response.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/score-stat.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/screenshot.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/status-stat.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/studio.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/topic.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/user-anime-rate-gql.interface.ts create mode 100644 src/app/shared/types/shikimori/graphql/video.interface.ts create mode 100644 src/app/shared/types/shikimori/helpers/index.ts create mode 100644 src/app/shared/types/shikimori/index.ts create mode 100644 src/app/shared/types/shikimori/mappers/grapql.mappers.ts create mode 100644 src/app/shared/types/shikimori/mappers/index.ts create mode 100644 src/app/shared/types/shikimori/queries/index.ts create mode 100644 src/app/shared/types/shikimori/user-rate-target.enum.ts create mode 100644 src/app/shared/utils/entities.utils.ts create mode 100644 src/app/shared/utils/is-supports-avif.function.ts delete mode 100644 src/app/shared/utils/rx-anime-rates-functions.ts create mode 100644 src/app/shared/utils/split-array-to-chunks.funtion.ts create mode 100644 src/app/shared/utils/zip.utils.ts create mode 100644 src/assets/1x1.avif diff --git a/package-lock.json b/package-lock.json index 5dded602..14a5e168 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,11 +36,11 @@ "@ngrx/store": "^19.0.0", "date-fns": "^4.1.0", "deep-object-diff": "^1.1.9", + "fflate": "^0.8.2", "ionicons": "^8.0.8", "ngrx-store-localstorage": "19.0.0", "ngx-scrollbar": "^18.0.0", "ngx-tippy-wrapper": "^6.3.0", - "ngx-visibility": "2.0.0", "ngxtension": "^5.1.0", "rxjs": "~7.8.1", "tslib": "^2.6.2" @@ -14146,6 +14146,11 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -19117,18 +19122,6 @@ "tslib": "^2.4.0" } }, - "node_modules/ngx-visibility": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ngx-visibility/-/ngx-visibility-2.0.0.tgz", - "integrity": "sha512-y0R7OJqH2dFliOAgEX30vHi64slogGvL1fAylg/Kp/qTUPazEj5iAZ3oi4rgsv4vuUvgyU2oH5iCOKbIzyYO6Q==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": "^19.0.0", - "@angular/core": "^19.0.0" - } - }, "node_modules/ngxtension": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ngxtension/-/ngxtension-5.1.0.tgz", diff --git a/package.json b/package.json index 2e904b16..5d9a0b07 100644 --- a/package.json +++ b/package.json @@ -71,11 +71,11 @@ "@ngrx/store": "^19.0.0", "date-fns": "^4.1.0", "deep-object-diff": "^1.1.9", + "fflate": "^0.8.2", "ionicons": "^8.0.8", "ngrx-store-localstorage": "19.0.0", "ngx-scrollbar": "^18.0.0", "ngx-tippy-wrapper": "^6.3.0", - "ngx-visibility": "2.0.0", "ngxtension": "^5.1.0", "rxjs": "~7.8.1", "tslib": "^2.6.2" @@ -134,4 +134,4 @@ "typescript": "~5.8.3", "webpack": "^5.89.0" } -} \ No newline at end of file +} diff --git a/src/app/core/providers/avif/index.ts b/src/app/core/providers/avif/index.ts new file mode 100644 index 00000000..1943c142 --- /dev/null +++ b/src/app/core/providers/avif/index.ts @@ -0,0 +1,2 @@ +export { IS_SUPPORTS_AVIF } from './is-supports-avif.token'; +export { provideIsSupportsAvif } from './is-supports-avif.provider'; diff --git a/src/app/core/providers/avif/is-supports-avif.provider.ts b/src/app/core/providers/avif/is-supports-avif.provider.ts new file mode 100644 index 00000000..566da09c --- /dev/null +++ b/src/app/core/providers/avif/is-supports-avif.provider.ts @@ -0,0 +1,10 @@ +import { Provider } from '@angular/core'; + +import { IS_SUPPORTS_AVIF } from '@app/core/providers/avif/is-supports-avif.token'; +import { isSupportsAvif } from '@app/shared/utils/is-supports-avif.function'; + +export function provideIsSupportsAvif(): Provider[] { + return [ + { provide: IS_SUPPORTS_AVIF, useFactory: isSupportsAvif }, + ]; +} diff --git a/src/app/core/providers/avif/is-supports-avif.token.ts b/src/app/core/providers/avif/is-supports-avif.token.ts new file mode 100644 index 00000000..e94e323e --- /dev/null +++ b/src/app/core/providers/avif/is-supports-avif.token.ts @@ -0,0 +1,4 @@ +import { InjectionToken } from '@angular/core'; +import { Observable } from 'rxjs'; + +export const IS_SUPPORTS_AVIF = new InjectionToken>('IS_SUPPORTS_AVIF'); diff --git a/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html index 6ec4d97c..ccca94ea 100644 --- a/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html +++ b/src/app/modules/home/components/anime-rate-section/anime-rate-section.component.html @@ -1,11 +1,8 @@ - diff --git a/src/app/modules/home/components/card-grid/card-grid.component.scss b/src/app/modules/home/components/card-grid/card-grid.component.scss index 3e7a0455..95753989 100644 --- a/src/app/modules/home/components/card-grid/card-grid.component.scss +++ b/src/app/modules/home/components/card-grid/card-grid.component.scss @@ -37,7 +37,7 @@ .skeleton-item { display: grid; - gap: 0.25rem; + gap: 0.125rem; // 2 cards grid-template-rows: 8fr 1fr; diff --git a/src/app/modules/home/components/card-grid/card-grid.component.ts b/src/app/modules/home/components/card-grid/card-grid.component.ts index 612065c6..ad311ca3 100644 --- a/src/app/modules/home/components/card-grid/card-grid.component.ts +++ b/src/app/modules/home/components/card-grid/card-grid.component.ts @@ -1,50 +1,66 @@ +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, - HostBinding, ViewEncapsulation, inject, input, + signal, } from '@angular/core'; -import { NgTemplateOutlet } from '@angular/common'; import { RepeatPipe } from 'ngxtension/repeat-pipe'; import { TranslocoService } from '@jsverse/transloco'; import { toSignal } from '@angular/core/rxjs-interop'; import { CardGridItemComponent } from '@app/modules/home/components/card-grid-item/card-grid-item.component'; -import { GetAnimeNamePipe } from '@app/shared/pipes/get-anime-name/get-anime-name.pipe'; +import { + GetAnimeKindPipe, + GetAnimeNamePipe, + GetAnimePosterPipe, + GetAnimeReleaseDatePipe, +} from '@app/modules/home/pipes'; import { GetPlayerLinkPipe } from '@app/shared/pipes/get-player-link/get-player-link.pipe'; +import { IS_SUPPORTS_AVIF } from '@app/core/providers/avif'; import { SkeletonBlockComponent } from '@app/shared/components/skeleton-block/skeleton-block.component'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; -import { provideShikimoriImageLoader } from '@app/shared/providers/shikimori-image-loader.provider'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; +import { provideSmarthardNetImageLoader } from '@app/shared/providers'; +import { trackById } from '@app/shared/utils/common-ngfor-tracking'; @Component({ selector: 'app-card-grid', templateUrl: './card-grid.component.html', styleUrls: ['./card-grid.component.scss'], imports: [ + AsyncPipe, NgTemplateOutlet, SkeletonBlockComponent, CardGridItemComponent, GetPlayerLinkPipe, GetAnimeNamePipe, RepeatPipe, + GetAnimePosterPipe, + GetAnimeKindPipe, + GetAnimeReleaseDatePipe, ], providers: [ - provideShikimoriImageLoader(96), + provideSmarthardNetImageLoader(), ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, + host: { class: 'anime-grid' }, }) export class CardGridComponent { - @HostBinding('class.anime-grid') - private animeGridClass = true; - private readonly _transloco = inject(TranslocoService); + readonly trackById = trackById; + + readonly isSupportsAvif = toSignal(inject(IS_SUPPORTS_AVIF)); readonly currentLang = toSignal(this._transloco.langChanges$); - userAnimeRates = input(); + // TODO: добавить подключение настройки для экономия трафика + readonly isHiRes = signal(true); + + userAnimeRates = input.required(); - isLoading = input(); + isLoading = input(true); + hasPriority = input(false); } diff --git a/src/app/modules/home/home.page.html b/src/app/modules/home/home.page.html index e41e8cbe..bf1556e5 100644 --- a/src/app/modules/home/home.page.html +++ b/src/app/modules/home/home.page.html @@ -1,15 +1,19 @@ - @for (status of animeStatusOrder(); track status) { - @let grid = animeGridMap.get(status); + @for (status of animeStatusOrder(); track status; let index = $index) { + @let hasPriority = index <= 1; + @let userRatesForStatus = userAnimeRatesWithRecent() | filterRatesByStatus: status; + @let sortedUserRates = status === 'recent' + ? (userRatesForStatus | slice: 0: 6 | sortRatesByDateVisited) + : (userRatesForStatus | sortRatesByUserScore: 'original': false | async); } diff --git a/src/app/modules/home/home.page.ts b/src/app/modules/home/home.page.ts index 15d941ff..d7cf92a9 100644 --- a/src/app/modules/home/home.page.ts +++ b/src/app/modules/home/home.page.ts @@ -1,12 +1,7 @@ -import { - AsyncPipe, - UpperCasePipe, -} from '@angular/common'; +import { AsyncPipe, SlicePipe, UpperCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, - DestroyRef, - OnInit, ViewEncapsulation, computed, effect, @@ -16,30 +11,32 @@ import { IonContent } from '@ionic/angular/standalone'; import { Store } from '@ngrx/store'; import { Title } from '@angular/platform-browser'; import { TranslocoService } from '@jsverse/transloco'; -import { combineLatest, of } from 'rxjs'; -import { - filter, - map, - shareReplay, -} from 'rxjs/operators'; +import { combineLatest } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { rxEffect } from 'ngxtension/rx-effect'; import { toSignal } from '@angular/core/rxjs-interop'; -import { AnimeGridInterface } from '@app/modules/home/types/anime-grid.interface'; import { AnimeRateSectionComponent } from '@app/modules/home/components/anime-rate-section'; import { DEFAULT_ANIME_STATUS_ORDER } from '@app/shared/config/default-anime-status-order.config'; -import { ResourceIdType } from '@app/shared/types/resource-id.type'; -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; +import { ExtendedUserRateStatusType } from '@app/modules/home/types'; +import { FilterRatesByStatusPipe } from '@app/modules/home/pipes/filter-rates-by-status.pipe'; +import { + SortRatesByDateVisitedPipe, + SortRatesByUserScorePipe, +} from '@app/modules/home/pipes'; import { - loadAnimeRateByStatusAction, - selectIsRatesLoadedByStatus, - selectRatesByStatus, + loadAllUserAnimeRatesAction, + selectIsMetadataLoading, + selectRates, } from '@app/modules/home/store/anime-rates'; import { recentAnimesToRates } from '@app/modules/home/store/recent-animes/utils/recent-animes-to-rates.function'; import { selectCachedAnimes } from '@app/store/cache/selectors/cache.selectors'; import { selectRecentAnimes } from '@app/modules/home/store/recent-animes'; import { selectSettings } from '@app/store/settings/selectors/settings.selectors'; -import { selectShikimoriCurrentUser } from '@app/store/shikimori/selectors/shikimori.selectors'; -import { sortRatesByDateVisited } from '@app/modules/home/utils'; +import { + selectShikimoriCurrentUser, + selectShikimoriCurrentUserNickname, +} from '@app/store/shikimori/selectors/shikimori.selectors'; @Component({ @@ -49,8 +46,12 @@ import { sortRatesByDateVisited } from '@app/modules/home/utils'; imports: [ AsyncPipe, UpperCasePipe, + SlicePipe, IonContent, AnimeRateSectionComponent, + FilterRatesByStatusPipe, + SortRatesByUserScorePipe, + SortRatesByDateVisitedPipe, ], templateUrl: 'home.page.html', styleUrls: ['home.page.scss'], @@ -58,145 +59,55 @@ import { sortRatesByDateVisited } from '@app/modules/home/utils'; class: 'home-page', }, }) -export class HomePage implements OnInit { +export class HomePage { private readonly store = inject(Store); private readonly title = inject(Title); private readonly transloco = inject(TranslocoService); - private readonly destroyRef = inject(DestroyRef); readonly settings = this.store.selectSignal(selectSettings); + readonly currentUser = this.store.selectSignal(selectShikimoriCurrentUser); + + readonly userAnimeRates = this.store.selectSignal(selectRates); + readonly isMetadataLoading = this.store.selectSignal(selectIsMetadataLoading); readonly useCustomAnimeStatusOrder = computed(() => this.settings()?.useCustomAnimeStatusOrder); readonly animeStatusOrder = computed(() => this.useCustomAnimeStatusOrder() - ? this.settings()?.userAnimeStatusOrder + ? this.settings()?.userAnimeStatusOrder as ExtendedUserRateStatusType[] : DEFAULT_ANIME_STATUS_ORDER, ); - readonly isTranslationsLoaded = toSignal( - this.transloco.events$.pipe(filter((e) => e.type === 'translationLoadSuccess')), + readonly pageTitle$ = this.store.select(selectShikimoriCurrentUserNickname).pipe( + switchMap((nickname) => this.transloco.selectTranslate('HOME_MODULE.HOME_PAGE.PAGE_TITLE', { nickname })), ); - readonly currentUser = this.store.selectSignal(selectShikimoriCurrentUser); - - readonly recent$ = combineLatest([ + readonly recent = toSignal(combineLatest([ this.store.select(selectRecentAnimes), this.store.select(selectCachedAnimes), - ]).pipe( - map(([recentAnimes, cachedAnimes]) => recentAnimesToRates(recentAnimes, cachedAnimes)), - map((recentRates) => sortRatesByDateVisited(recentRates?.slice(0, 6))), - shareReplay(1), - ); + ]).pipe(map(([recentAnimes, cachedAnimes]) => recentAnimesToRates(recentAnimes, cachedAnimes)))); - readonly planned$ = this.store.select(selectRatesByStatus('planned')); - readonly watching$ = this.store.select(selectRatesByStatus('watching')); - readonly rewatching$ = this.store.select(selectRatesByStatus('rewatching')); - readonly completed$ = this.store.select(selectRatesByStatus('completed')); - readonly onHold$ = this.store.select(selectRatesByStatus('on_hold')); - readonly dropped$ = this.store.select(selectRatesByStatus('dropped')); - - readonly isPlannedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('planned')); - readonly isWatchingLoaded$ = this.store.select(selectIsRatesLoadedByStatus('watching')); - readonly isRewatchingLoaded$ = this.store.select(selectIsRatesLoadedByStatus('rewatching')); - readonly isCompletedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('completed')); - readonly isOnHoldLoaded$ = this.store.select(selectIsRatesLoadedByStatus('on_hold')); - readonly isDroppedLoaded$ = this.store.select(selectIsRatesLoadedByStatus('dropped')); - - animeGridMap: Map; - hiddenGridMap: Map; - - ngOnInit() { - this.initValues(); - } + readonly userAnimeRatesWithRecent = computed( + () => [...this.userAnimeRates(), ...this.recent()], + ); - currentUserChangeEffect = effect(() => { - const currentUser = this.currentUser(); - const hasTranslation = this.isTranslationsLoaded(); + readonly hiddenGridMap = new Map(); - if (currentUser?.id && hasTranslation) { - const { id, nickname } = currentUser; - const title = this.transloco.translate('HOME_MODULE.HOME_PAGE.PAGE_TITLE', { nickname }); + loadAnimeRatesEffect = effect(() => { + const userId = this.currentUser()?.id; - this.getUserAnimeRatesByStatus(id, 'planned'); - this.title.setTitle(title); + if (userId) { + this.store.dispatch(loadAllUserAnimeRatesAction({ userId })); } }); - initValues(): void { - this.hiddenGridMap = new Map(); - this.animeGridMap = new Map([ - [ - 'recent', - { - rates: this.recent$.pipe(), - isLoaded: of(true), - }, - ], - [ - 'planned', - { - rates: this.planned$, - isLoaded: this.isPlannedLoaded$, - }, - ], - [ - 'watching', - { - rates: this.watching$, - isLoaded: this.isWatchingLoaded$, - }, - ], - [ - 'rewatching', - { - rates: this.rewatching$, - isLoaded: this.isRewatchingLoaded$, - }, - ], - [ - 'completed', - { - rates: this.completed$, - isLoaded: this.isCompletedLoaded$, - }, - ], - [ - 'on_hold', - { - rates: this.onHold$, - isLoaded: this.isOnHoldLoaded$, - }, - ], - [ - 'dropped', - { - rates: this.dropped$, - isLoaded: this.isDroppedLoaded$, - }, - ], - ]); - } + setPageTitleEffect = rxEffect(this.pageTitle$, (title) => this.title.setTitle(title)); - toggleHiddenGridStatus(rateStatus: string): void { - const status = this.hiddenGridMap.get(rateStatus as UserRateStatusType) || false; + toggleHiddenGridStatus(rateStatus: ExtendedUserRateStatusType): void { + const status = this.hiddenGridMap.get(rateStatus) || false; - this.hiddenGridMap.set(rateStatus as UserRateStatusType, !status); + this.hiddenGridMap.set(rateStatus, !status); } - getHiddenGridStatus(rateStatus: string): boolean { - return this.hiddenGridMap.get(rateStatus as UserRateStatusType) || false; - } - - onSectionVisibilityChange(section: string): void { - const currentUser = this.currentUser(); - - if (currentUser?.id && section !== 'recent') { - this.getUserAnimeRatesByStatus(currentUser.id, section as UserRateStatusType); - } - } - - getUserAnimeRatesByStatus(userId: ResourceIdType, status: UserRateStatusType): void { - if (status !== 'recent' as UserRateStatusType) { - this.store.dispatch(loadAnimeRateByStatusAction({ userId, status })); - } + getHiddenGridStatus(rateStatus: ExtendedUserRateStatusType): boolean { + return this.hiddenGridMap.get(rateStatus) || false; } } diff --git a/src/app/modules/home/home.routes.ts b/src/app/modules/home/home.routes.ts index 1e9b1b73..6e7c6602 100644 --- a/src/app/modules/home/home.routes.ts +++ b/src/app/modules/home/home.routes.ts @@ -1,4 +1,3 @@ -import { NgxVisibilityService } from 'ngx-visibility'; import { Routes } from '@angular/router'; import { provideEffects } from '@ngrx/effects'; import { provideState } from '@ngrx/store'; @@ -12,9 +11,8 @@ export const HOME_ROUTES: Routes = [ path: '', component: HomePage, providers: [ - NgxVisibilityService, - provideState({ name: 'animeRates', reducer: animeRatesReducer }), - provideState({ name: 'recentAnimes', reducer: recentAnimesReducer }), + provideState('animeRates', animeRatesReducer), + provideState('recentAnimes', recentAnimesReducer), provideEffects( AnimeRatesEffects, RecentAnimesEffects, diff --git a/src/app/modules/home/pipes/filter-rates-by-status.pipe.ts b/src/app/modules/home/pipes/filter-rates-by-status.pipe.ts new file mode 100644 index 00000000..b057abf5 --- /dev/null +++ b/src/app/modules/home/pipes/filter-rates-by-status.pipe.ts @@ -0,0 +1,14 @@ +import { ExtendedUserRateStatusType } from '@app/modules/home/types'; +import { Pipe, PipeTransform } from '@angular/core'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; + +@Pipe({ + name: 'filterRatesByStatus', + standalone: true, + pure: true, +}) +export class FilterRatesByStatusPipe implements PipeTransform { + transform(rates: UserBriefRateInterface[], status: ExtendedUserRateStatusType): UserBriefRateInterface[] { + return rates?.filter((rate) => rate?.status === status); + } +} diff --git a/src/app/modules/home/pipes/get-anime-kind.pipe.ts b/src/app/modules/home/pipes/get-anime-kind.pipe.ts new file mode 100644 index 00000000..c290cdf2 --- /dev/null +++ b/src/app/modules/home/pipes/get-anime-kind.pipe.ts @@ -0,0 +1,25 @@ +import { Observable, map } from 'rxjs'; +import { + Pipe, + PipeTransform, + inject, +} from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { AnimeKindType } from '@app/shared/types/shikimori'; +import { ResourceIdType } from '@app/shared/types'; +import { selectRatesMetadata } from '@app/modules/home/store/anime-rates'; + +@Pipe({ + name: 'getAnimeKind', + standalone: true, + pure: true, +}) +export class GetAnimeKindPipe implements PipeTransform { + readonly store = inject(Store); + readonly ratesMetadata$ = this.store.select(selectRatesMetadata); + + transform(animeId: ResourceIdType): Observable { + return this.ratesMetadata$.pipe(map((metadata) => metadata?.[animeId]?.kind)); + } +} diff --git a/src/app/modules/home/pipes/get-anime-name.pipe.ts b/src/app/modules/home/pipes/get-anime-name.pipe.ts new file mode 100644 index 00000000..a0cfcba0 --- /dev/null +++ b/src/app/modules/home/pipes/get-anime-name.pipe.ts @@ -0,0 +1,25 @@ +import { Observable, map } from 'rxjs'; +import { + Pipe, + PipeTransform, + inject, +} from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { ResourceIdType } from '@app/shared/types'; +import { getAnimeRateName } from '@app/modules/home/utils'; +import { selectRatesMetadata } from '@app/modules/home/store/anime-rates'; + +@Pipe({ + name: 'getAnimeName', + standalone: true, + pure: true, +}) +export class GetAnimeNamePipe implements PipeTransform { + readonly store = inject(Store); + readonly ratesMetadata$ = this.store.select(selectRatesMetadata); + + transform(animeId: ResourceIdType, language: string): Observable { + return this.ratesMetadata$.pipe(map((metadata) => getAnimeRateName(metadata?.[animeId], language))); + } +} diff --git a/src/app/modules/home/pipes/get-anime-poster.pipe.ts b/src/app/modules/home/pipes/get-anime-poster.pipe.ts new file mode 100644 index 00000000..d0270553 --- /dev/null +++ b/src/app/modules/home/pipes/get-anime-poster.pipe.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { ResourceIdType } from '@app/shared/types'; +import { environment } from '@app-env/environment'; + +@Pipe({ + name: 'getAnimePoster', + standalone: true, + pure: true, +}) +export class GetAnimePosterPipe implements PipeTransform { + static readonly HOST = `${environment.smarthard.apiURI}/static/animes`; + + transform(animeId: ResourceIdType, isHiResPref = true, isSupportsAvif = true): string { + const hiResUrl = isSupportsAvif + ? `${GetAnimePosterPipe.HOST}/${animeId}.avif` + : `${GetAnimePosterPipe.HOST}/${animeId}.jpeg`; + const lowResUrl = `${GetAnimePosterPipe.HOST}/${animeId}.webp`; + + return isHiResPref ? hiResUrl : lowResUrl; + } +} diff --git a/src/app/modules/home/pipes/get-anime-release-date.pipe.ts b/src/app/modules/home/pipes/get-anime-release-date.pipe.ts new file mode 100644 index 00000000..2615759c --- /dev/null +++ b/src/app/modules/home/pipes/get-anime-release-date.pipe.ts @@ -0,0 +1,29 @@ +import { Observable, map } from 'rxjs'; +import { + Pipe, + PipeTransform, + inject, +} from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { ResourceIdType } from '@app/shared/types'; +import { selectRatesMetadata } from '@app/modules/home/store/anime-rates'; + +@Pipe({ + name: 'getAnimeReleaseDate', + standalone: true, + pure: true, +}) +export class GetAnimeReleaseDatePipe implements PipeTransform { + readonly store = inject(Store); + readonly ratesMetadata$ = this.store.select(selectRatesMetadata); + + transform(animeId: ResourceIdType): Observable { + return this.ratesMetadata$.pipe(map((metadata) => { + const { releasedOn, airedOn } = metadata?.[animeId] || {}; + const date = releasedOn?.date || airedOn?.date; + + return date ? new Date(date)?.toISOString() : null; + })); + } +} diff --git a/src/app/modules/home/pipes/index.ts b/src/app/modules/home/pipes/index.ts new file mode 100644 index 00000000..0bb3bee0 --- /dev/null +++ b/src/app/modules/home/pipes/index.ts @@ -0,0 +1,10 @@ +export { FilterRatesByStatusPipe } from './filter-rates-by-status.pipe'; +export { GetAnimeKindPipe } from './get-anime-kind.pipe'; +export { GetAnimeNamePipe } from './get-anime-name.pipe'; +export { GetAnimePosterPipe } from './get-anime-poster.pipe'; +export { GetAnimeReleaseDatePipe } from './get-anime-release-date.pipe'; +export { RectangleFitPipe } from './rectangle-fit.pipe'; +export { SortRatesByDateVisitedPipe } from './sort-rates-by-date-visited.pipe'; +export { SortRatesByAnimeNamePipe } from './sort-rates-by-name.pipe'; +export { SortRatesByAnimeRatingPipe } from './sort-rates-by-anime-rating.pipe'; +export { SortRatesByUserScorePipe } from './sort-rates-by-user-score.pipe'; diff --git a/src/app/modules/home/pipes/sort-rates-by-anime-rating.pipe.ts b/src/app/modules/home/pipes/sort-rates-by-anime-rating.pipe.ts new file mode 100644 index 00000000..051d7b31 --- /dev/null +++ b/src/app/modules/home/pipes/sort-rates-by-anime-rating.pipe.ts @@ -0,0 +1,28 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { AnimeRatesMetadata } from '@app/modules/home/store/anime-rates'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; +import { sortRatesByAnimeRating } from '@app/modules/home/utils'; + +@Pipe({ + name: 'sortRatesByAnimeRating', + standalone: true, + pure: true, +}) +export class SortRatesByAnimeRatingPipe implements PipeTransform { + transform( + rates: UserBriefRateInterface[], + ratesMetadata: AnimeRatesMetadata, + language: string, + isCaseSensitive = false, + isAsc = true, + ): UserBriefRateInterface[] { + return rates?.sort((a, b) => sortRatesByAnimeRating( + ratesMetadata?.[a.target_id], + ratesMetadata?.[b.target_id], + language, + isCaseSensitive, + isAsc, + )); + } +} diff --git a/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts b/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts index 4681cbec..d7215ee4 100644 --- a/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts +++ b/src/app/modules/home/pipes/sort-rates-by-date-visited.pipe.ts @@ -1,14 +1,15 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; import { sortRatesByDateVisited } from '@app/modules/home/utils'; @Pipe({ name: 'sortRatesByDateVisited', standalone: true, + pure: true, }) export class SortRatesByDateVisitedPipe implements PipeTransform { - transform(userRates: UserAnimeRate[] = []): UserAnimeRate[] { + transform(userRates: UserBriefRateInterface[]): UserBriefRateInterface[] { return sortRatesByDateVisited(userRates); } } diff --git a/src/app/modules/home/pipes/sort-rates-by-name.pipe.ts b/src/app/modules/home/pipes/sort-rates-by-name.pipe.ts new file mode 100644 index 00000000..d7ec3820 --- /dev/null +++ b/src/app/modules/home/pipes/sort-rates-by-name.pipe.ts @@ -0,0 +1,26 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { AnimeRatesMetadata } from '@app/modules/home/store/anime-rates'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; +import { sortRatesByAnimeName } from '@app/modules/home/utils'; + +@Pipe({ + name: 'sortRatesByAnimeName', + standalone: true, + pure: true, +}) +export class SortRatesByAnimeNamePipe implements PipeTransform { + transform( + rates: UserBriefRateInterface[], + ratesMetadata: AnimeRatesMetadata, + language: string, + isCaseSensitive = false, + ): UserBriefRateInterface[] { + return rates?.sort((a, b) => sortRatesByAnimeName( + ratesMetadata?.[a.target_id], + ratesMetadata?.[b.target_id], + language, + isCaseSensitive, + )); + } +} diff --git a/src/app/modules/home/pipes/sort-rates-by-user-score.pipe.ts b/src/app/modules/home/pipes/sort-rates-by-user-score.pipe.ts new file mode 100644 index 00000000..d7b4abc1 --- /dev/null +++ b/src/app/modules/home/pipes/sort-rates-by-user-score.pipe.ts @@ -0,0 +1,37 @@ +import { Observable, map } from 'rxjs'; +import { + Pipe, + PipeTransform, + inject, +} from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; +import { selectRatesMetadata } from '@app/modules/home/store/anime-rates'; +import { sortRatesByUserScore } from '@app/modules/home/utils'; + +@Pipe({ + name: 'sortRatesByUserScore', + standalone: true, + pure: true, +}) +export class SortRatesByUserScorePipe implements PipeTransform { + readonly store = inject(Store); + readonly ratesMetadata$ = this.store.select(selectRatesMetadata); + + transform( + rates: UserBriefRateInterface[], + language: string, + isCaseSensitive = false, + isAsc = true, + ): Observable { + return this.ratesMetadata$.pipe(map((metadata) => rates?.sort((a, b) => sortRatesByUserScore( + a, + b, + metadata, + language, + isCaseSensitive, + isAsc, + )))); + } +} diff --git a/src/app/modules/home/store/anime-rates/actions/anime-rate-metadata.actions.ts b/src/app/modules/home/store/anime-rates/actions/anime-rate-metadata.actions.ts new file mode 100644 index 00000000..bbc01d2b --- /dev/null +++ b/src/app/modules/home/store/anime-rates/actions/anime-rate-metadata.actions.ts @@ -0,0 +1,21 @@ +import { createAction, props } from '@ngrx/store'; + +import { AnimeRatesMetadataGQL } from '@app/shared/types/shikimori/graphql'; +import { ResourceIdType } from '@app/shared/types'; + +export const getAnimeRatesMetadataAction = createAction( + '[Anime Rates] get anime rates metadata', + props<{ animeIds: ResourceIdType[] }>(), +); + +export const getAnimeRatesMetadataSuccessAction = createAction( + '[Anime Rates] get anime rates metadata success', + props<{ metadata: AnimeRatesMetadataGQL[] }>(), +); + +export const getAnimeRatesMetadataFailureAction = createAction( + '[Anime Rates] get anime rates metadata failure', + props<{ errors: any }>(), +); + + diff --git a/src/app/modules/home/store/anime-rates/actions/anime-rate-paging.actions.ts b/src/app/modules/home/store/anime-rates/actions/anime-rate-paging.actions.ts deleted file mode 100644 index 52d1ff99..00000000 --- a/src/app/modules/home/store/anime-rates/actions/anime-rate-paging.actions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createAction, props } from '@ngrx/store'; - -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; - -export const incrementPageForStatusAction = createAction( - '[Anime Rates] increment page for status', - props<{ status: UserRateStatusType }>(), -); - -export const allPagesLoadedForStatusAction = createAction( - '[Anime Rates] all rates for status loaded', - props<{ status: UserRateStatusType; maxItemsLoaded: number }>(), -); - diff --git a/src/app/modules/home/store/anime-rates/actions/index.ts b/src/app/modules/home/store/anime-rates/actions/index.ts index 3691a31e..0b74c3a2 100644 --- a/src/app/modules/home/store/anime-rates/actions/index.ts +++ b/src/app/modules/home/store/anime-rates/actions/index.ts @@ -1,2 +1,2 @@ -export * from './anime-rate-paging.actions'; export * from './load-anime-rate.action'; +export * from './anime-rate-metadata.actions'; diff --git a/src/app/modules/home/store/anime-rates/actions/load-anime-rate.action.ts b/src/app/modules/home/store/anime-rates/actions/load-anime-rate.action.ts index e605d736..3c957a20 100644 --- a/src/app/modules/home/store/anime-rates/actions/load-anime-rate.action.ts +++ b/src/app/modules/home/store/anime-rates/actions/load-anime-rate.action.ts @@ -1,21 +1,23 @@ import { createAction, props } from '@ngrx/store'; -import { ResourceIdType } from '@app/shared/types/resource-id.type'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; +import { ResourceIdType } from '@app/shared/types'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; -export const loadAnimeRateByStatusAction = createAction( - '[Anime Rates] load anime rate by status', - props<{ status: UserRateStatusType; userId: ResourceIdType }>(), +export const loadAllUserAnimeRatesAction = createAction( + '[Anime Rates] load all user anime rates', + props<{ userId: ResourceIdType }>(), ); -export const loadAnimeRateByStatusSuccessAction = createAction( - '[Anime Rates] load anime rate by status success', - props<{ status: UserRateStatusType; userId: ResourceIdType; rates: UserAnimeRate[]; newRates: UserAnimeRate[] }>(), +export const loadAllUserAnimeRatesSuccessAction = createAction( + '[Anime Rates] load all user anime rates success', + props<{ + userId: ResourceIdType; + rates: UserBriefRateInterface[]; + }>(), ); -export const loadAnimeRateByStatusFailureAction = createAction( - '[Anime Rates] load anime rate by status failure', - props<{ status: UserRateStatusType; errors: unknown }>(), +export const loadAllUserAnimeRatesFailureAction = createAction( + '[Anime Rates] load all user anime rates failure', + props<{ errors: unknown }>(), ); diff --git a/src/app/modules/home/store/anime-rates/effects/anime-rates.effects.ts b/src/app/modules/home/store/anime-rates/effects/anime-rates.effects.ts index 8202d852..dd41f4f3 100644 --- a/src/app/modules/home/store/anime-rates/effects/anime-rates.effects.ts +++ b/src/app/modules/home/store/anime-rates/effects/anime-rates.effects.ts @@ -3,112 +3,64 @@ import { createEffect, ofType, } from '@ngrx/effects'; -import { EMPTY, of } from 'rxjs'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; import { catchError, + concatMap, delay, exhaustMap, - filter, map, - mergeMap, - withLatestFrom, + switchMap, } from 'rxjs/operators'; -import { concatLatestFrom } from '@ngrx/operators'; +import { of } from 'rxjs'; -import { Action, Store } from '@ngrx/store'; -import { AnimeNameSortingConfig } from '@app/shared/utils/rx-anime-rates-functions'; -import { SettingsStoreInterface } from '@app/store/settings/types/settings-store.interface'; import { ShikimoriClient } from '@app/shared/services/shikimori-client.service'; +import { UserRateTargetEnum } from '@app/shared/types/shikimori'; +import { concatLatestFrom } from '@ngrx/operators'; import { - allPagesLoadedForStatusAction, - incrementPageForStatusAction, -} from '@app/modules/home/store/anime-rates/actions/anime-rate-paging.actions'; -import { - isDuplicateArrayFilter, - sortAnimeRatesByUserRating, -} from '@app/modules/home/store/anime-rates/utils/anime-rates.helpers'; -import { - loadAnimeRateByStatusAction, - loadAnimeRateByStatusFailureAction, - loadAnimeRateByStatusSuccessAction, -} from '@app/modules/home/store/anime-rates/actions/load-anime-rate.action'; -import { selectAnimePaginationSize } from '@app/store/settings/selectors/settings.selectors'; -import { - selectIsRatesLoadedByStatus, - selectRatesByStatus, - selectRatesPageByStatus, -} from '@app/modules/home/store/anime-rates/selectors/anime-rates.selectors'; -import { updateSettingsAction } from '@app/store/settings/actions/settings.actions'; + getAnimeRatesMetadataAction, + getAnimeRatesMetadataFailureAction, + getAnimeRatesMetadataSuccessAction, + loadAllUserAnimeRatesAction, + loadAllUserAnimeRatesFailureAction, + loadAllUserAnimeRatesSuccessAction, +} from '@app/modules/home/store/anime-rates/actions'; +import { selectRatesMetadata } from '@app/modules/home/store/anime-rates/selectors'; +import { splitArrayToChunks } from '@app/shared/utils/split-array-to-chunks.funtion'; @Injectable() export class AnimeRatesEffects { - loadAnimeRateByStatusEffect$ = createEffect(() => this.actions$.pipe( - ofType(loadAnimeRateByStatusAction), - concatLatestFrom(({ status }) => [ - this.store.select(selectRatesPageByStatus(status)), - this.store.select(selectIsRatesLoadedByStatus(status)), - this.store.select(selectAnimePaginationSize), - ]), - exhaustMap(([{ status, userId }, page, isLoad, limit]) => !isLoad - ? this.shikimoriClient.getUserAnimeRates(userId, { status, page, limit }) - .pipe( - withLatestFrom(this.store.select(selectRatesByStatus(status))), - mergeMap(([newRates, oldRates]) => { - const animeNameSortCfg: AnimeNameSortingConfig = { - compareOriginalName: true, - caseSensitive: false, - }; - const isAllRatesLoaded = newRates?.length < limit; - const maxItemsLoaded = page * limit; - const rates = [...oldRates, ...newRates] - .sort(sortAnimeRatesByUserRating(animeNameSortCfg)) - .filter(isDuplicateArrayFilter); - const actions: Action[] = [ - loadAnimeRateByStatusSuccessAction({ status, rates, userId, newRates }), - ]; + private readonly actions$ = inject(Actions); + private readonly store = inject(Store); + private readonly shikimori = inject(ShikimoriClient); - actions.push(isAllRatesLoaded - ? allPagesLoadedForStatusAction({ status, maxItemsLoaded }) - : incrementPageForStatusAction({ status }), - ); + readonly METADATA_QUERY_LIMIT = 50; + readonly metadata$ = this.store.select(selectRatesMetadata); - return actions; - }), - catchError((errors) => of(loadAnimeRateByStatusFailureAction({ status, errors }))), - ) - : EMPTY), - )); - - scheduleNextPageLoadEffect$ = createEffect(() => this.actions$.pipe( - ofType(loadAnimeRateByStatusSuccessAction), - // schedule next page of rates if not all have loaded - // it would dispatch an extra load action with delay - // until items amount is more or equal than page limit - concatLatestFrom(() => this.store.select(selectAnimePaginationSize)), - filter(([{ newRates }, limit]) => newRates?.length >= limit), - // Shikimori API is limited by 5 rps, 90 rpm! - delay(1000), - mergeMap(([action]) => of(action).pipe( - map(({ userId, status }) => loadAnimeRateByStatusAction({ userId, status })), - catchError((errors) => of(loadAnimeRateByStatusFailureAction({ errors, status: action.status }))), + loadAllUserAnimeRates$ = createEffect(() => this.actions$.pipe( + ofType(loadAllUserAnimeRatesAction), + exhaustMap(({ userId }) => this.shikimori.getUserRates(userId, UserRateTargetEnum.ANIME).pipe( + map((rates) => loadAllUserAnimeRatesSuccessAction({ userId, rates })), + catchError((errors) => of(loadAllUserAnimeRatesFailureAction({ errors }))), )), )); - allPagesLoadedForStatus$ = createEffect(() => this.actions$.pipe( - ofType(allPagesLoadedForStatusAction), - concatLatestFrom(() => this.store.select(selectAnimePaginationSize)), - filter(([{ maxItemsLoaded }, pageSize]) => maxItemsLoaded > pageSize), - map(([{ maxItemsLoaded }]) => { - const config: Pick = { animePaginationSize: maxItemsLoaded }; - - return updateSettingsAction({ config }); - }), + generateMetadataActions$ = createEffect(() => this.actions$.pipe( + ofType(loadAllUserAnimeRatesSuccessAction), + map(({ rates }) => rates?.map(({ target_id: targetId }) => targetId)), + concatLatestFrom(() => this.metadata$), + map(([animeIds, metadata]) => animeIds?.filter((animeId) => !metadata?.[animeId])), + map((animeIds) => splitArrayToChunks(animeIds, this.METADATA_QUERY_LIMIT)), + switchMap((chuckedIds) => chuckedIds.map((animeIds) => getAnimeRatesMetadataAction({ animeIds }))), )); - constructor( - private actions$: Actions, - private store: Store, - private shikimoriClient: ShikimoriClient, - ) {} + loadMetadata$ = createEffect(() => this.actions$.pipe( + ofType(getAnimeRatesMetadataAction), + delay(333), + concatMap(({ animeIds }) => this.shikimori.getUserAnimeRatesMetadataGQL(animeIds).pipe( + map((metadata) => getAnimeRatesMetadataSuccessAction({ metadata })), + catchError((errors) => of(getAnimeRatesMetadataFailureAction({ errors }))), + )), + )); } diff --git a/src/app/modules/home/store/anime-rates/index.ts b/src/app/modules/home/store/anime-rates/index.ts index 7aaf942c..e6b028c9 100644 --- a/src/app/modules/home/store/anime-rates/index.ts +++ b/src/app/modules/home/store/anime-rates/index.ts @@ -3,4 +3,3 @@ export * from './effects'; export * from './reducers'; export * from './selectors'; export * from './types'; -export * from './utils'; diff --git a/src/app/modules/home/store/anime-rates/reducers/anime-rates.meta-reducer.ts b/src/app/modules/home/store/anime-rates/reducers/anime-rates.meta-reducer.ts new file mode 100644 index 00000000..e9c104f3 --- /dev/null +++ b/src/app/modules/home/store/anime-rates/reducers/anime-rates.meta-reducer.ts @@ -0,0 +1,16 @@ +import { ActionReducer } from '@ngrx/store'; +import { localStorageSync } from 'ngrx-store-localstorage'; + +import { AppStoreInterface } from '@app/store/app-store.interface'; +import { unzip, zip } from '@app/shared/utils/zip.utils'; + +export const animeRatesLocalStorageSyncReducer = (r: ActionReducer) => localStorageSync({ + keys: [{ + animeRates: { + encrypt: zip, + decrypt: unzip, + filter: ['rates', 'metadata'], + }, + }], + rehydrate: true, +})(r); diff --git a/src/app/modules/home/store/anime-rates/reducers/anime-rates.reducer.ts b/src/app/modules/home/store/anime-rates/reducers/anime-rates.reducer.ts index c48f37a1..71273be7 100644 --- a/src/app/modules/home/store/anime-rates/reducers/anime-rates.reducer.ts +++ b/src/app/modules/home/store/anime-rates/reducers/anime-rates.reducer.ts @@ -1,59 +1,46 @@ import { createReducer, on } from '@ngrx/store'; -import { AnimeRatesStoreInterface } from '@app/modules/home/store/anime-rates/types/anime-rates-store.interface'; +import { AnimeRatesStoreInterface } from '@app/modules/home/store/anime-rates/types'; +import { entityArrayToMap } from '@app/shared/utils/entities.utils'; import { - allPagesLoadedForStatusAction, - incrementPageForStatusAction, -} from '@app/modules/home/store/anime-rates/actions/anime-rate-paging.actions'; -import { - getRateLoadedKey, - getRatePageKey, - getRateStoreKey, -} from '@app/modules/home/store/anime-rates/utils/anime-rates-store-key.helpers'; -import { loadAnimeRateByStatusSuccessAction } from '@app/modules/home/store/anime-rates/actions/load-anime-rate.action'; + getAnimeRatesMetadataAction, + getAnimeRatesMetadataSuccessAction, + loadAllUserAnimeRatesSuccessAction, +} from '@app/modules/home/store/anime-rates/actions'; const initialState: AnimeRatesStoreInterface = { - planned: [], - watching: [], - rewatching: [], - completed: [], - onHold: [], - dropped: [], - plannedPage: 1, - watchingPage: 1, - completedPage: 1, - rewatchingPage: 1, - onHoldPage: 1, - droppedPage: 1, - isCompletedLoaded: false, - isDroppedLoaded: false, - isOnHoldLoaded: false, - isPlannedLoaded: false, - isRewatchingLoaded: false, - isWatchingLoaded: false, + rates: {}, + isRatesLoading: true, + metadata: {}, + metaSize: 0, }; const reducer = createReducer( initialState, on( - allPagesLoadedForStatusAction, - (state, { status }) => ({ + loadAllUserAnimeRatesSuccessAction, + (state, { rates }) => ({ ...state, - [getRateLoadedKey(status)]: true, + isRatesLoading: false, + rates: entityArrayToMap(rates), }), ), on( - loadAnimeRateByStatusSuccessAction, - (state, { status, rates }) => ({ + getAnimeRatesMetadataAction, + (state, { animeIds }) => ({ ...state, - [getRateStoreKey(status)]: rates, + metaSize: (state.metaSize || 0) + (animeIds?.length || 0), }), ), on( - incrementPageForStatusAction, - (state, { status }) => ({ + getAnimeRatesMetadataSuccessAction, + (state, { metadata }) => ({ ...state, - [getRatePageKey(status)]: state[getRatePageKey(status)] + 1, + metadata: { + ...state?.metadata, + ...entityArrayToMap(metadata), + }, + metaSize: (state.metaSize || 0) - (metadata?.length || 0), }), ), ); diff --git a/src/app/modules/home/store/anime-rates/reducers/index.ts b/src/app/modules/home/store/anime-rates/reducers/index.ts index 6e00e266..213910c7 100644 --- a/src/app/modules/home/store/anime-rates/reducers/index.ts +++ b/src/app/modules/home/store/anime-rates/reducers/index.ts @@ -1 +1,2 @@ -export * from './anime-rates.reducer'; +export { animeRatesReducer } from './anime-rates.reducer'; +export { animeRatesLocalStorageSyncReducer } from './anime-rates.meta-reducer'; diff --git a/src/app/modules/home/store/anime-rates/selectors/anime-rates.selectors.ts b/src/app/modules/home/store/anime-rates/selectors/anime-rates.selectors.ts index d2bc6efa..d1aa394f 100644 --- a/src/app/modules/home/store/anime-rates/selectors/anime-rates.selectors.ts +++ b/src/app/modules/home/store/anime-rates/selectors/anime-rates.selectors.ts @@ -1,38 +1,26 @@ import { createFeatureSelector, createSelector } from '@ngrx/store'; -import { AnimeRatesStoreInterface } from '@app/modules/home/store/anime-rates/types/anime-rates-store.interface'; -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; -import { - getRateLoadedKey, - getRatePageKey, - getRateStoreKey, -} from '@app/modules/home/store/anime-rates/utils/anime-rates-store-key.helpers'; +import { AnimeRatesStoreInterface } from '@app/modules/home/store/anime-rates/types'; +import { entityMapToArray } from '@app/shared/utils/entities.utils'; export const selectAnimeRates = createFeatureSelector('animeRates'); -export const selectRatesByStatus = (status: UserRateStatusType) => createSelector( +export const selectRates = createSelector( selectAnimeRates, - (state) => { - const key = getRateStoreKey(status); - - return state?.[key] || []; - }, + ({ rates }) => entityMapToArray(rates), ); -export const selectRatesPageByStatus = (status: UserRateStatusType) => createSelector( +export const selectIsRatesLoading = createSelector( selectAnimeRates, - (state) => { - const key = getRatePageKey(status); - - return state?.[key]; - }, + ({ isRatesLoading }) => isRatesLoading, ); -export const selectIsRatesLoadedByStatus = (status: UserRateStatusType) => createSelector( +export const selectRatesMetadata = createSelector( selectAnimeRates, - (state) => { - const key = getRateLoadedKey(status); + ({ metadata }) => metadata, +); - return state?.[key]; - }, +export const selectIsMetadataLoading = createSelector( + selectAnimeRates, + ({ metaSize }) => metaSize > 0, ); diff --git a/src/app/modules/home/store/anime-rates/types/anime-rate-metadata.interface.ts b/src/app/modules/home/store/anime-rates/types/anime-rate-metadata.interface.ts new file mode 100644 index 00000000..8d22412b --- /dev/null +++ b/src/app/modules/home/store/anime-rates/types/anime-rate-metadata.interface.ts @@ -0,0 +1,6 @@ +import { AnimeRatesMetadataGQL } from '@app/shared/types/shikimori/graphql'; +import { ResourceIdType } from '@app/shared/types'; + +export interface AnimeRatesMetadata { + [animeId: ResourceIdType]: AnimeRatesMetadataGQL; +} diff --git a/src/app/modules/home/store/anime-rates/types/anime-rates-store.interface.ts b/src/app/modules/home/store/anime-rates/types/anime-rates-store.interface.ts index 693ecd15..e54f7d99 100644 --- a/src/app/modules/home/store/anime-rates/types/anime-rates-store.interface.ts +++ b/src/app/modules/home/store/anime-rates/types/anime-rates-store.interface.ts @@ -1,34 +1,10 @@ -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; - -interface RatesData { - planned: UserAnimeRate[]; - watching: UserAnimeRate[]; - rewatching: UserAnimeRate[]; - completed: UserAnimeRate[]; - onHold: UserAnimeRate[]; - dropped: UserAnimeRate[]; -} - -interface RatesPages { - plannedPage: number; - watchingPage: number; - rewatchingPage: number; - completedPage: number; - onHoldPage: number; - droppedPage: number; +import { AnimeRatesMetadata } from '@app/modules/home/store/anime-rates/types/anime-rate-metadata.interface'; +import { ResourceIdType } from '@app/shared/types'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; + +export interface AnimeRatesStoreInterface { + rates: { [animeId: ResourceIdType]: UserBriefRateInterface }; + isRatesLoading: boolean; + metadata: AnimeRatesMetadata; + metaSize: number; } - -interface RatesLoadStatus { - isPlannedLoaded: boolean; - isWatchingLoaded: boolean; - isRewatchingLoaded: boolean; - isCompletedLoaded: boolean; - isOnHoldLoaded: boolean; - isDroppedLoaded: boolean; -} - -export type StatusKeysType = keyof RatesData; -export type StatusPageType = keyof RatesPages; -export type LoadStatusKeysType = keyof RatesLoadStatus; - -export interface AnimeRatesStoreInterface extends RatesData, RatesPages, RatesLoadStatus {} diff --git a/src/app/modules/home/store/anime-rates/types/index.ts b/src/app/modules/home/store/anime-rates/types/index.ts index 2f21f0b6..6174b75d 100644 --- a/src/app/modules/home/store/anime-rates/types/index.ts +++ b/src/app/modules/home/store/anime-rates/types/index.ts @@ -1 +1,2 @@ -export * from './anime-rates-store.interface'; +export { AnimeRatesStoreInterface } from './anime-rates-store.interface'; +export { AnimeRatesMetadata } from './anime-rate-metadata.interface'; diff --git a/src/app/modules/home/store/anime-rates/utils/anime-rates-store-key.helpers.ts b/src/app/modules/home/store/anime-rates/utils/anime-rates-store-key.helpers.ts deleted file mode 100644 index ac56ed32..00000000 --- a/src/app/modules/home/store/anime-rates/utils/anime-rates-store-key.helpers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - LoadStatusKeysType, - StatusKeysType, - StatusPageType, -} from '@app/modules/home/store/anime-rates'; -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; - -export function getRateLoadedKey(status: UserRateStatusType): LoadStatusKeysType { - switch (status) { - case 'planned': - return 'isPlannedLoaded'; - case 'watching': - return 'isWatchingLoaded'; - case 'rewatching': - return 'isRewatchingLoaded'; - case 'completed': - return 'isCompletedLoaded'; - case 'on_hold': - return 'isOnHoldLoaded'; - case 'dropped': - return 'isDroppedLoaded'; - } -} - -export function getRateStoreKey(status: UserRateStatusType): StatusKeysType { - return status === 'on_hold' ? 'onHold' : status; -} - -export function getRatePageKey(status: UserRateStatusType): StatusPageType { - const originKey = getRateStoreKey(status); - - return `${originKey}Page`; -} diff --git a/src/app/modules/home/store/anime-rates/utils/anime-rates.helpers.ts b/src/app/modules/home/store/anime-rates/utils/anime-rates.helpers.ts deleted file mode 100644 index 01f13e38..00000000 --- a/src/app/modules/home/store/anime-rates/utils/anime-rates.helpers.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AnimeNameSortingConfig, sortByAnimeUserRating } from '@app/shared/utils/rx-anime-rates-functions'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; - -export function isDuplicateArrayFilter(item: UserAnimeRate, pos: number, array: UserAnimeRate[]) { - return !pos || item.anime.id !== array[pos - 1].anime.id; -} - -export function sortAnimeRatesByUserRating(animeNameSortCfg?: AnimeNameSortingConfig) { - return (a: UserAnimeRate, b: UserAnimeRate) => sortByAnimeUserRating(a, b, animeNameSortCfg); -} diff --git a/src/app/modules/home/store/anime-rates/utils/index.ts b/src/app/modules/home/store/anime-rates/utils/index.ts deleted file mode 100644 index 9a679d91..00000000 --- a/src/app/modules/home/store/anime-rates/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './anime-rates-store-key.helpers'; -export * from './anime-rates.helpers'; diff --git a/src/app/modules/home/store/recent-animes/utils/anime-to-user-anime-rate.function.ts b/src/app/modules/home/store/recent-animes/utils/anime-to-user-anime-rate.function.ts index 956e2165..d8408b4a 100644 --- a/src/app/modules/home/store/recent-animes/utils/anime-to-user-anime-rate.function.ts +++ b/src/app/modules/home/store/recent-animes/utils/anime-to-user-anime-rate.function.ts @@ -1,30 +1,28 @@ -import { AnimeBriefInfoInterface } from '@app/shared/types/shikimori/anime-brief-info.interface'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; +import { + AnimeBriefInfoInterface, + UserBriefRateInterface, + UserRateStatusType, + UserRateTargetEnum, +} from '@app/shared/types/shikimori'; export function animeToUserAnimeRate( anime: AnimeBriefInfoInterface, watchedEpisode = 0, visited: string = null, -): UserAnimeRate { +): UserBriefRateInterface { return { id: -1, status: 'recent' as UserRateStatusType, - anime: { - ...anime, - english: anime.english as never, - japanese: anime.japanese as never, - }, episodes: watchedEpisode, score: 0, chapters: 0, rewatches: 0, volumes: 0, target_id: anime.id, - target_type: 'Anime', + target_type: UserRateTargetEnum.ANIME, text: '', text_html: '', - created_at: new Date().toISOString(), + created_at: visited, updated_at: visited, user_id: null, }; diff --git a/src/app/modules/home/store/recent-animes/utils/recent-animes-to-rates.function.ts b/src/app/modules/home/store/recent-animes/utils/recent-animes-to-rates.function.ts index 83a79197..b981b107 100644 --- a/src/app/modules/home/store/recent-animes/utils/recent-animes-to-rates.function.ts +++ b/src/app/modules/home/store/recent-animes/utils/recent-animes-to-rates.function.ts @@ -1,10 +1,13 @@ import { AnimeCacheType } from '@app/store/cache/types'; import { RecentAnimePages } from '@app/store/settings/types'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; import { animeToUserAnimeRate } from '@app/modules/home/store/recent-animes/utils/anime-to-user-anime-rate.function'; -export function recentAnimesToRates(recent: RecentAnimePages = {}, cachedAnimes: AnimeCacheType = {}): UserAnimeRate[] { - const userAnimeRates: UserAnimeRate[] = []; +export function recentAnimesToRates( + recent: RecentAnimePages = {}, + cachedAnimes: AnimeCacheType = {}, +): UserBriefRateInterface[] { + const userAnimeRates: UserBriefRateInterface[] = []; for (const [animeId, { visited, episode }] of Object.entries(recent)) { const animeFromCache = cachedAnimes[animeId]?.anime; diff --git a/src/app/modules/home/types/anime-grid.interface.ts b/src/app/modules/home/types/anime-grid.interface.ts deleted file mode 100644 index b38e2371..00000000 --- a/src/app/modules/home/types/anime-grid.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Observable } from 'rxjs'; - -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; - -export interface AnimeGridInterface { - rates: Observable; - isLoaded: Observable; - label?: string; -} diff --git a/src/app/modules/home/types/extended-user-rate-status.type.ts b/src/app/modules/home/types/extended-user-rate-status.type.ts new file mode 100644 index 00000000..1eac71b3 --- /dev/null +++ b/src/app/modules/home/types/extended-user-rate-status.type.ts @@ -0,0 +1,3 @@ +import { UserRateStatusType } from '@app/shared/types/shikimori'; + +export type ExtendedUserRateStatusType = UserRateStatusType | 'recent'; diff --git a/src/app/modules/home/types/index.ts b/src/app/modules/home/types/index.ts new file mode 100644 index 00000000..39d2afce --- /dev/null +++ b/src/app/modules/home/types/index.ts @@ -0,0 +1 @@ +export { ExtendedUserRateStatusType } from './extended-user-rate-status.type'; diff --git a/src/app/modules/home/types/visibility-change.interface.ts b/src/app/modules/home/types/visibility-change.interface.ts deleted file mode 100644 index eed69934..00000000 --- a/src/app/modules/home/types/visibility-change.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; - -export interface VisibilityChangeInterface { - section: UserRateStatusType; - isVisible: boolean; -} diff --git a/src/app/modules/home/utils/get-anime-rate-name.function.ts b/src/app/modules/home/utils/get-anime-rate-name.function.ts new file mode 100644 index 00000000..55d49bde --- /dev/null +++ b/src/app/modules/home/utils/get-anime-rate-name.function.ts @@ -0,0 +1,16 @@ +import { AnimeRatesMetadataGQL } from '@app/shared/types/shikimori/graphql'; + +export function getAnimeRateName(rateMetadata: AnimeRatesMetadataGQL, language: string): string { + const defaultName = rateMetadata?.name; + + switch (language) { + case 'ru': + return rateMetadata?.russian || defaultName; + case 'jp': + return rateMetadata?.japanese || defaultName; + case 'en': + return rateMetadata?.english || defaultName; + default: + return defaultName; + } +} diff --git a/src/app/modules/home/utils/index.ts b/src/app/modules/home/utils/index.ts index a6398f88..a673ab1c 100644 --- a/src/app/modules/home/utils/index.ts +++ b/src/app/modules/home/utils/index.ts @@ -1 +1,5 @@ export { sortRatesByDateVisited } from './sort-rates-by-date-visited.function'; +export { getAnimeRateName } from './get-anime-rate-name.function'; +export { sortRatesByAnimeName } from './sort-rates-by-anime-name.function'; +export { sortRatesByAnimeRating } from './sort-rates-by-anime-rating.function'; +export { sortRatesByUserScore } from './sort-rates-by-user-score.function'; diff --git a/src/app/modules/home/utils/sort-rates-by-anime-name.function.ts b/src/app/modules/home/utils/sort-rates-by-anime-name.function.ts new file mode 100644 index 00000000..bc666ad9 --- /dev/null +++ b/src/app/modules/home/utils/sort-rates-by-anime-name.function.ts @@ -0,0 +1,20 @@ +import { AnimeRatesMetadataGQL } from '@app/shared/types/shikimori/graphql'; +import { getAnimeRateName } from '@app/modules/home/utils/get-anime-rate-name.function'; + +export function sortRatesByAnimeName( + rateA: AnimeRatesMetadataGQL, + rateB: AnimeRatesMetadataGQL, + language: string, + isCaseSensitive = false, +): number { + const nameA = getAnimeRateName(rateA, language); + const nameB = getAnimeRateName(rateB, language); + const locales = language === 'en' ? ['en', 'jp'] : ['en', language]; + const options: Intl.CollatorOptions = { + sensitivity: isCaseSensitive ? 'case' : 'base', + }; + + if (!nameA || !nameB) return 0; + + return nameA.localeCompare(nameB, locales, options); +} diff --git a/src/app/modules/home/utils/sort-rates-by-anime-rating.function.ts b/src/app/modules/home/utils/sort-rates-by-anime-rating.function.ts new file mode 100644 index 00000000..1bfc17ed --- /dev/null +++ b/src/app/modules/home/utils/sort-rates-by-anime-rating.function.ts @@ -0,0 +1,23 @@ +import { AnimeRatesMetadataGQL } from '@app/shared/types/shikimori/graphql'; +import { sortRatesByAnimeName } from '@app/modules/home/utils/sort-rates-by-anime-name.function'; + + +export function sortRatesByAnimeRating( + rateA: AnimeRatesMetadataGQL, + rateB: AnimeRatesMetadataGQL, + language: string, + isCaseSensitive = false, + isAsc = true, +): number { + const scoreA = rateA?.score; + const scoreB = rateB?.score; + const compare = isAsc + ? scoreB - scoreA + : scoreA - scoreB; + + if (!scoreA || !scoreB) return 0; + + return compare === 0 + ? sortRatesByAnimeName(rateA, rateB, language, isCaseSensitive) + : compare; +} diff --git a/src/app/modules/home/utils/sort-rates-by-date-visited.function.ts b/src/app/modules/home/utils/sort-rates-by-date-visited.function.ts index dc39f9d6..dfe0744e 100644 --- a/src/app/modules/home/utils/sort-rates-by-date-visited.function.ts +++ b/src/app/modules/home/utils/sort-rates-by-date-visited.function.ts @@ -1,5 +1,5 @@ -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; -export function sortRatesByDateVisited(userRates: UserAnimeRate[]): UserAnimeRate[] { +export function sortRatesByDateVisited(userRates: UserBriefRateInterface[]): UserBriefRateInterface[] { return userRates.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at)); } diff --git a/src/app/modules/home/utils/sort-rates-by-user-score.function.ts b/src/app/modules/home/utils/sort-rates-by-user-score.function.ts new file mode 100644 index 00000000..56ba300f --- /dev/null +++ b/src/app/modules/home/utils/sort-rates-by-user-score.function.ts @@ -0,0 +1,25 @@ +import { AnimeRatesMetadata } from '@app/modules/home/store/anime-rates'; +import { UserBriefRateInterface } from '@app/shared/types/shikimori'; +import { sortRatesByAnimeName } from '@app/modules/home/utils/sort-rates-by-anime-name.function'; + + +export function sortRatesByUserScore( + a: UserBriefRateInterface, + b: UserBriefRateInterface, + ratesMetadata: AnimeRatesMetadata, + language: string, + isCaseSensitive = false, + isAsc = true, +): number { + const rateA = ratesMetadata?.[a?.target_id]; + const rateB = ratesMetadata?.[b?.target_id]; + const scoreA = a?.score || 0; + const scoreB = b?.score || 0; + const compare = isAsc + ? scoreB - scoreA + : scoreA - scoreB; + + return compare === 0 + ? sortRatesByAnimeName(rateA, rateB, language, isCaseSensitive) + : compare; +} diff --git a/src/app/modules/player/store/effects/player.effects.ts b/src/app/modules/player/store/effects/player.effects.ts index 49b4922e..53523d0a 100644 --- a/src/app/modules/player/store/effects/player.effects.ts +++ b/src/app/modules/player/store/effects/player.effects.ts @@ -22,6 +22,7 @@ import { merge, of, switchMap } from 'rxjs'; import { KodikClient, ShikicinemaV1Client, ShikimoriClient } from '@app/shared/services'; import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; +import { UserRateTargetEnum } from '@app/shared/types/shikimori'; import { addVideosAction, deleteCommentAction, @@ -124,7 +125,7 @@ export class PlayerEffects { ...rate || {} as UserAnimeRate, user_id: user.id, target_id: animeId, - target_type: 'Anime', + target_type: UserRateTargetEnum.ANIME, episodes, status, rewatches, @@ -261,7 +262,7 @@ export class PlayerEffects { getUserRate$ = createEffect(() => this.actions$.pipe( ofType(getUserRateAction), debounceTime(50), - switchMap(({ userId, animeId }) => this.shikimori.getUserRate(userId, animeId, 'Anime').pipe( + switchMap(({ userId, animeId }) => this.shikimori.getUserRate(userId, animeId, UserRateTargetEnum.ANIME).pipe( map((userRates) => getUserRateSuccessAction({ animeId, userRate: userRates?.[0] || null })), catchError((error) => { // возможно удален пользователем на самом сайте Шикимори diff --git a/src/app/shared/components/abstract-image-card/abstract-image-card.component.ts b/src/app/shared/components/abstract-image-card/abstract-image-card.component.ts index 045ed843..9c8a7ebe 100644 --- a/src/app/shared/components/abstract-image-card/abstract-image-card.component.ts +++ b/src/app/shared/components/abstract-image-card/abstract-image-card.component.ts @@ -4,6 +4,7 @@ import { ViewEncapsulation, input, output, + signal, } from '@angular/core'; @Component({ @@ -25,10 +26,10 @@ export class AbstractImageCardComponent { imageLoad = output(); - protected isLoading: boolean; + protected isLoading = signal(true); protected onImageLoad(image: EventTarget): void { - this.isLoading = false; + this.isLoading.set(false); this.imageLoad.emit(image as HTMLImageElement); } } diff --git a/src/app/shared/components/image-card/image-card.component.html b/src/app/shared/components/image-card/image-card.component.html index 45298cc3..d2eebf74 100644 --- a/src/app/shared/components/image-card/image-card.component.html +++ b/src/app/shared/components/image-card/image-card.component.html @@ -1,4 +1,4 @@ -@if (isLoading) { +@if (isLoading()) { diff --git a/src/app/shared/components/image-card/image-card.component.scss b/src/app/shared/components/image-card/image-card.component.scss index dd7125b3..862f8f77 100644 --- a/src/app/shared/components/image-card/image-card.component.scss +++ b/src/app/shared/components/image-card/image-card.component.scss @@ -1,4 +1,5 @@ .image-card { + position: relative; background-color: var(--ion-color-light); overflow: hidden; border-radius: .25rem; diff --git a/src/app/shared/components/image-card/image-card.component.ts b/src/app/shared/components/image-card/image-card.component.ts index 685cef7d..563bf9e0 100644 --- a/src/app/shared/components/image-card/image-card.component.ts +++ b/src/app/shared/components/image-card/image-card.component.ts @@ -38,7 +38,7 @@ export class ImageCardComponent extends AbstractImageCardComponent { override backgroundSize = input('cover'); - override isLoading = true; + hasPriority = input(false); readonly loadedImg$ = outputToObservable(this.imageLoad); } diff --git a/src/app/shared/config/default-anime-status-order.config.ts b/src/app/shared/config/default-anime-status-order.config.ts index ffd10fa7..a65dfd36 100644 --- a/src/app/shared/config/default-anime-status-order.config.ts +++ b/src/app/shared/config/default-anime-status-order.config.ts @@ -1,8 +1,11 @@ +import { ExtendedUserRateStatusType } from '@app/modules/home/types'; + export const DEFAULT_ANIME_STATUS_ORDER = [ 'recent', 'planned', 'watching', 'rewatching', + 'completed', 'on_hold', 'dropped', -]; +] as ExtendedUserRateStatusType[]; diff --git a/src/app/shared/pipes/get-anime-name/get-anime-name.pipe.ts b/src/app/shared/pipes/get-anime-name/get-anime-name.pipe.ts deleted file mode 100644 index 286070cf..00000000 --- a/src/app/shared/pipes/get-anime-name/get-anime-name.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -import { AnimeBriefInfoInterface } from '@app/shared/types/shikimori/anime-brief-info.interface'; -import { AnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; -import { getAnimeName } from '@app/shared/utils/get-anime-name.function'; - -@Pipe({ - name: 'getAnimeName', - standalone: true, - pure: true, -}) -export class GetAnimeNamePipe implements PipeTransform { - transform(animeRate: AnimeBriefInfoInterface | AnimeRate, language: string): string { - return getAnimeName(animeRate, language); - } -} diff --git a/src/app/shared/pipes/get-player-link/get-player-link.pipe.ts b/src/app/shared/pipes/get-player-link/get-player-link.pipe.ts index 329d12f6..dafe669d 100644 --- a/src/app/shared/pipes/get-player-link/get-player-link.pipe.ts +++ b/src/app/shared/pipes/get-player-link/get-player-link.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { AnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; +import { ResourceIdType } from '@app/shared/types'; @Pipe({ name: 'getPlayerLink', @@ -8,9 +8,7 @@ import { AnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; standalone: true, }) export class GetPlayerLinkPipe implements PipeTransform { - transform(anime: AnimeRate): string { - const { id: animeId } = anime; - + transform(animeId: ResourceIdType): string { return `/player/${animeId}`; } } diff --git a/src/app/shared/providers/index.ts b/src/app/shared/providers/index.ts new file mode 100644 index 00000000..4215f1e3 --- /dev/null +++ b/src/app/shared/providers/index.ts @@ -0,0 +1,4 @@ +export { shikimoriImageLoader } from './shikimori-image-loader.factory'; +export { provideShikimoriImageLoader } from './shikimori-image-loader.provider'; +export { smarthardNetImageLoader } from './smarthard-image-loader.factory'; +export { provideSmarthardNetImageLoader } from './smarthard-image-loader.provider'; diff --git a/src/app/shared/providers/shikimori-image-loader.factory.ts b/src/app/shared/providers/shikimori-image-loader.factory.ts index 824baca7..f63aa014 100644 --- a/src/app/shared/providers/shikimori-image-loader.factory.ts +++ b/src/app/shared/providers/shikimori-image-loader.factory.ts @@ -48,9 +48,15 @@ export const shikimoriImageLoader = (): ImageLoader => { return (config: ImageLoaderConfig): string => { const path = getPath(config.src); const domain = domainSignal() || defaultShikimoriDomain; + const isShikimori = config?.src?.includes('shikimori'); const isMissingImg = config?.src?.includes('globals/missing_original'); + const isGQLPoster = config?.src?.includes('/poster/'); - if (isMissingImg) { + if (!isShikimori) { + return config?.src; + } + + if (isMissingImg || isGQLPoster) { return `${domain}${path}`; } diff --git a/src/app/shared/providers/smarthard-image-loader.factory.ts b/src/app/shared/providers/smarthard-image-loader.factory.ts new file mode 100644 index 00000000..d1c28e1d --- /dev/null +++ b/src/app/shared/providers/smarthard-image-loader.factory.ts @@ -0,0 +1,25 @@ +import { ImageLoader, ImageLoaderConfig } from '@angular/common'; + +import { getPath } from '@app/shared/utils/get-path.function'; + +// TODO: реализовать выбор меньших разрешений для ограниченного интернет соединения +export const smarthardNetImageLoader = (): ImageLoader => { + const domain = 'https://smarthard.net'; + + return (config: ImageLoaderConfig): string => { + const path = getPath(config.src); + const isSmarthardNet = config?.src?.startsWith(domain); + + if (!isSmarthardNet) { + return config?.src; + } + + const { isPlaceholder = false } = config; + const [imagePath, extension] = path?.split('.'); + const extensionPath = isPlaceholder + ? '-placeholder.jpeg' + : `.${extension}`; + + return `${domain}${imagePath}${extensionPath}`; + }; +}; diff --git a/src/app/shared/providers/smarthard-image-loader.provider.ts b/src/app/shared/providers/smarthard-image-loader.provider.ts new file mode 100644 index 00000000..a58fb6f1 --- /dev/null +++ b/src/app/shared/providers/smarthard-image-loader.provider.ts @@ -0,0 +1,18 @@ +import { IMAGE_CONFIG, IMAGE_LOADER } from '@angular/common'; +import { Provider } from '@angular/core'; + +import { smarthardNetImageLoader } from '@app/shared/providers/smarthard-image-loader.factory'; + + +export function provideSmarthardNetImageLoader(): Provider[] { + return [ + { + provide: IMAGE_CONFIG, + useValue: {}, + }, + { + provide: IMAGE_LOADER, + useFactory: smarthardNetImageLoader, + }, + ]; +} diff --git a/src/app/shared/services/shikimori-client.service.ts b/src/app/shared/services/shikimori-client.service.ts index b3f1cdc2..3471a1b8 100644 --- a/src/app/shared/services/shikimori-client.service.ts +++ b/src/app/shared/services/shikimori-client.service.ts @@ -13,26 +13,35 @@ import { take, } from 'rxjs/operators'; -import { AnimeBriefInfoInterface } from '@app/shared/types/shikimori/anime-brief-info.interface'; -import { Comment } from '@app/shared/types/shikimori/comment'; -import { CommentableEnum } from '@app/shared//types/shikimori/commentable.enum'; -import { CreateComment } from '@app/shared/types/shikimori/create-comment.interface'; -import { Credentials } from '@app/shared/types/shikimori/credentials'; -import { EpisodeNotification } from '@app/shared//types/shikimori/episode-notification.interface'; -import { EpisodeNotificationResponse } from '@app/shared/types/shikimori/episode-notification-response.interface'; -import { FindAnimeQuery } from '@app/shared/types/shikimori/queries/find-anime-query'; -import { ResourceIdType } from '@app/shared/types/resource-id.type'; +import { + AnimeBriefInfoInterface, + Comment, + CommentableEnum, + CreateComment, + Credentials, + EpisodeNotification, + EpisodeNotificationResponse, + Topic, + UserAnimeRate, + UserBriefInfoInterface, + UserBriefRateInterface, + UserInterface, + UserRateTargetEnum, +} from '@app/shared/types/shikimori'; +import { AnimeRatesMetadataGQLResponse, UserAnimeRatesGQLResponse } from '@app/shared/types/shikimori/graphql'; +import { FindAnimeQuery, UserAnimeRatesQuery } from '@app/shared/types/shikimori/queries'; +import { ResourceIdType } from '@app/shared/types'; import { ShikimoriCredentials } from '@app/store/auth/types/auth-store.interface'; -import { Topic } from '@app/shared/types/shikimori/topic'; -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; -import { UserAnimeRatesQuery } from '@app/shared/types/shikimori/queries/user-anime-rates-query'; -import { UserBriefInfoInterface } from '@app/shared/types/shikimori/user-brief-info.interface'; -import { UserBriefRateInterface } from '@app/shared/types/shikimori/user-brief-rate.interface'; -import { UserInterface } from '@app/shared/types/shikimori/user.interface'; import { environment } from '@app-env/environment'; +import { + mapAnimeRatesMetadataGQL, + mapAnimeRatesMetadataGQLQuery, + mapUserAnimeRatesGQL, + mapUserRatesGQLQuery, + toShikimoriCredentials, +} from '@app/shared/types/shikimori/mappers'; import { selectShikimoriDomain } from '@app/store/shikimori/selectors'; -import { setPaginationToParams } from '@app/shared/types/shikimori/helpers/pagination-helper'; -import { toShikimoriCredentials } from '@app/shared/types/shikimori/mappers/auth.mappers'; +import { setPaginationToParams } from '@app/shared/types/shikimori/helpers'; @Injectable({ @@ -123,12 +132,10 @@ export class ShikimoriClient { ); } - getUserRates(userId?: ResourceIdType): Observable { - let params = new HttpParams(); - - if (userId) { - params = params.set('user_id', userId); - } + getUserRates(userId: ResourceIdType, targetType: UserRateTargetEnum): Observable { + const params = new HttpParams() + .set('user_id', userId) + .set('target_type', targetType); return this.shikimoriDomain$.pipe( take(1), @@ -155,6 +162,30 @@ export class ShikimoriClient { ); } + getUserAnimeRatesMetadataGQL(animeIds: ResourceIdType[]) { + const query = mapAnimeRatesMetadataGQLQuery(animeIds); + + return this.shikimoriDomain$.pipe( + take(1), + switchMap( + (domain) => this.http.post(`${domain}/api/graphql`, { query }) + .pipe(map(mapAnimeRatesMetadataGQL)), + ), + ); + } + + getUserAnimeRatesGQL(animeRatesQuery: UserAnimeRatesQuery): Observable { + const query = mapUserRatesGQLQuery(animeRatesQuery); + + return this.shikimoriDomain$.pipe( + take(1), + switchMap( + (domain) => this.http.post(`${domain}/api/graphql`, { query }) + .pipe(map(mapUserAnimeRatesGQL)), + ), + ); + } + findAnimes(query?: FindAnimeQuery): Observable { let params = setPaginationToParams(query); @@ -178,7 +209,7 @@ export class ShikimoriClient { getUserRate( userId: ResourceIdType, targetId: ResourceIdType, - targetType: 'Anime' | 'Manga', + targetType: UserRateTargetEnum, ): Observable { const params = new HttpParams() .set('user_id', userId) diff --git a/src/app/shared/types/index.ts b/src/app/shared/types/index.ts new file mode 100644 index 00000000..2f321ce0 --- /dev/null +++ b/src/app/shared/types/index.ts @@ -0,0 +1,9 @@ +export { Bytes, BytesSI } from './bytes.type'; +export { ResourceIdType } from './resource-id.type'; +export { ResourceStateEnum } from './resource-state.enum'; +export { ResultOpenTarget, SearchbarResult } from './searchbar.types'; +export { UploaderIdType } from './uploader-id.type'; +export { VideoMapperFn } from './video-mapper.type'; +export { DELETED_UPLOADER, KODIK_UPLOADER } from './well-known-uploader-ids'; +export { WELL_KNOWN_UPLOADERS_TOKEN } from './well-known-uploaders.token'; +export { WellKnownType } from './well-known-uploaders.type'; diff --git a/src/app/shared/types/shikimori/graphql/anime.interface.ts b/src/app/shared/types/shikimori/graphql/anime.interface.ts new file mode 100644 index 00000000..d415b5c7 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/anime.interface.ts @@ -0,0 +1,67 @@ +import { AnimeKindType } from '@app/shared/types/shikimori/anime-kind.type'; +import { AnimeRatingType } from '@app/shared/types/shikimori/queries/find-anime-query'; +import { AnimeReleaseStatus } from '@app/shared/types/shikimori/user-anime-rate'; +import { CharacterRoleGQL } from '@app/shared/types/shikimori/graphql/character-role.interface'; +import { ExternalLinkGQL } from '@app/shared/types/shikimori/graphql/external-link.interface'; +import { GenreGQL } from '@app/shared/types/shikimori/graphql/genre.interface'; +import { IncompleteDateGQL } from '@app/shared/types/shikimori/graphql/incomplete-date.interface'; +import { PersonRoleGQL } from '@app/shared/types/shikimori/graphql/person-role.interface'; +import { PosterGQL } from '@app/shared/types/shikimori/graphql/poster.interface'; +import { RelatedGQL } from '@app/shared/types/shikimori/graphql/related.interface'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; +import { ScoreStatGQL } from '@app/shared/types/shikimori/graphql/score-stat.interface'; +import { ScreenshotGQL } from '@app/shared/types/shikimori/graphql/screenshot.interface'; +import { StatusStatGQL } from '@app/shared/types/shikimori/graphql/status-stat.interface'; +import { StudioGQL } from '@app/shared/types/shikimori/graphql/studio.interface'; +import { TopicGQL } from '@app/shared/types/shikimori/graphql/topic.interface'; +import { UserAnimeRateGQL } from '@app/shared/types/shikimori/graphql/user-anime-rate-gql.interface'; +import { VideoGQL } from '@app/shared/types/shikimori/graphql/video.interface'; + +export interface AnimeGQL { + airedOn: IncompleteDateGQL; + characterRoles: CharacterRoleGQL; + chronology: AnimeGQL[]; + createdAt: string; + description: string; + descriptionHtml: string; + descriptionSource: string; + duration: number; + english: string; + episodes: number; + episodesAired: number; + externalLinks: ExternalLinkGQL[]; + fandubbers: string[]; + fansubbers: string[]; + franchise: string; + genres: GenreGQL[]; + id: ResourceIdType; + isCensored: boolean; + japanese: string; + kind: AnimeKindType; + licenseNameRu: string; + licensors: string[]; + malId: ResourceIdType; + name: string; + nextEpisodeAt: string; + opengraphImageUrl: string; + origin: string; + personRoles: PersonRoleGQL; + poster: PosterGQL; + rating: AnimeRatingType; + related: RelatedGQL[]; + releasedOn: IncompleteDateGQL; + russian: string; + score: number; + scoresStats: ScoreStatGQL; + screenshots: ScreenshotGQL[]; + season: string; + status: AnimeReleaseStatus; + statusesStats: StatusStatGQL; + studios: StudioGQL; + synonyms: string[] + topic: TopicGQL; + updatedAt: string; + url: string; + userRate: UserAnimeRateGQL; + videos: VideoGQL[]; +} diff --git a/src/app/shared/types/shikimori/graphql/character-role.interface.ts b/src/app/shared/types/shikimori/graphql/character-role.interface.ts new file mode 100644 index 00000000..70546237 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/character-role.interface.ts @@ -0,0 +1,9 @@ +import { CharacterGQL } from '@app/shared/types/shikimori/graphql/character.interface'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface CharacterRoleGQL { + character: CharacterGQL; + id: ResourceIdType; + rolesEn: string[]; + rolesRu: string[]; +} diff --git a/src/app/shared/types/shikimori/graphql/character.interface.ts b/src/app/shared/types/shikimori/graphql/character.interface.ts new file mode 100644 index 00000000..94636712 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/character.interface.ts @@ -0,0 +1,23 @@ +import { PosterGQL } from '@app/shared/types/shikimori/graphql/poster.interface'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; +import { TopicGQL } from '@app/shared/types/shikimori/graphql/topic.interface'; + +export interface CharacterGQL { + id: ResourceIdType; + createdAt: string; + description: string; + descriptionHtml: string; + descriptionSource: string; + isAnime: boolean; + isManga: boolean; + isRanobe: boolean; + japanese: string; + malId: ResourceIdType; + name: string; + poster: PosterGQL; + russian: string; + synonyms: string[]; + topic: TopicGQL; + updatedAt: string; + url: string; +} diff --git a/src/app/shared/types/shikimori/graphql/external-link.interface.ts b/src/app/shared/types/shikimori/graphql/external-link.interface.ts new file mode 100644 index 00000000..0624ea5e --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/external-link.interface.ts @@ -0,0 +1,9 @@ +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface ExternalLinkGQL { + createdAt: string; + id: ResourceIdType; + kind: string; + updatedAt: string; + url: string; +} diff --git a/src/app/shared/types/shikimori/graphql/genre.interface.ts b/src/app/shared/types/shikimori/graphql/genre.interface.ts new file mode 100644 index 00000000..5e123f6f --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/genre.interface.ts @@ -0,0 +1,10 @@ +import { ResourceIdType } from '@app/shared/types/resource-id.type'; +import { UserRateTargetEnum } from '@app/shared/types/shikimori/user-rate-target.enum'; + +export interface GenreGQL { + entryType: UserRateTargetEnum; + id: ResourceIdType; + kind: string; + name: string; + russian: string; +} diff --git a/src/app/shared/types/shikimori/graphql/incomplete-date.interface.ts b/src/app/shared/types/shikimori/graphql/incomplete-date.interface.ts new file mode 100644 index 00000000..2ff35393 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/incomplete-date.interface.ts @@ -0,0 +1,6 @@ +export interface IncompleteDateGQL { + date: string; + day: number; + month: number; + year: number; +} diff --git a/src/app/shared/types/shikimori/graphql/index.ts b/src/app/shared/types/shikimori/graphql/index.ts new file mode 100644 index 00000000..2576d84c --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/index.ts @@ -0,0 +1,22 @@ +/* responses */ +export { UserAnimeRatesGQLResponse } from './responses/user-anime-rates-gql-response.interface'; +export { AnimeRatesMetadataGQL, AnimeRatesMetadataGQLResponse } from './responses/anime-rates-metadata-gql-response.interface'; + +/* common shikimori's GQL entities */ +export { AnimeGQL } from './anime.interface'; +export { CharacterRoleGQL } from './character-role.interface'; +export { CharacterGQL } from './character.interface'; +export { ExternalLinkGQL } from './external-link.interface'; +export { GenreGQL } from './genre.interface'; +export { IncompleteDateGQL } from './incomplete-date.interface'; +export { PersonRoleGQL } from './person-role.interface'; +export { PersonGQL } from './person.interface'; +export { PosterGQL } from './poster.interface'; +export { RelatedGQL } from './related.interface'; +export { ScoreStatGQL } from './score-stat.interface'; +export { ScreenshotGQL } from './screenshot.interface'; +export { StatusStatGQL } from './status-stat.interface'; +export { StudioGQL } from './studio.interface'; +export { TopicGQL } from './topic.interface'; +export { UserAnimeRateGQL } from './user-anime-rate-gql.interface'; +export { VideoGQL } from './video.interface'; diff --git a/src/app/shared/types/shikimori/graphql/person-role.interface.ts b/src/app/shared/types/shikimori/graphql/person-role.interface.ts new file mode 100644 index 00000000..19f7ce12 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/person-role.interface.ts @@ -0,0 +1,9 @@ +import { PersonGQL } from '@app/shared/types/shikimori/graphql/person.interface'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface PersonRoleGQL { + id: ResourceIdType; + person: PersonGQL; + rolesEn: string[]; + rolesRu: string[]; +} diff --git a/src/app/shared/types/shikimori/graphql/person.interface.ts b/src/app/shared/types/shikimori/graphql/person.interface.ts new file mode 100644 index 00000000..66a3470a --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/person.interface.ts @@ -0,0 +1,24 @@ +import { IncompleteDateGQL } from '@app/shared/types/shikimori/graphql/incomplete-date.interface'; +import { PosterGQL } from '@app/shared/types/shikimori/graphql/poster.interface'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; +import { TopicGQL } from '@app/shared/types/shikimori/graphql/topic.interface'; + +export interface PersonGQL { + id: ResourceIdType; + birthOn: IncompleteDateGQL; + createdAt: string; + deceasedOn: IncompleteDateGQL; + isMangaka: boolean; + isProducer: boolean; + isSeyu: boolean; + japanese: string; + malId: ResourceIdType; + name: string; + poster: PosterGQL; + russian: string; + synonyms: string[]; + topic: TopicGQL; + updatedAt: string; + url: string; + website: string; +} diff --git a/src/app/shared/types/shikimori/graphql/poster.interface.ts b/src/app/shared/types/shikimori/graphql/poster.interface.ts new file mode 100644 index 00000000..e3dc6324 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/poster.interface.ts @@ -0,0 +1,18 @@ +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface PosterGQL { + id: ResourceIdType; + main2xUrl: string; + mainAlt2xUrl: string; + mainAltUrl: string; + mainUrl: string; + mini2xUrl: string; + miniAlt2xUrl: string; + miniAltUrl: string; + miniUrl: string; + originalUrl: string; + preview2xUrl: string; + previewAlt2xUrl: string; + previewAltUrl: string; + previewUrl: string; +} diff --git a/src/app/shared/types/shikimori/graphql/related.interface.ts b/src/app/shared/types/shikimori/graphql/related.interface.ts new file mode 100644 index 00000000..db82c0ed --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/related.interface.ts @@ -0,0 +1,10 @@ +import { AnimeGQL } from '@app/shared/types/shikimori/graphql/anime.interface'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface RelatedGQL { + anime: AnimeGQL; + id: ResourceIdType; + manga: any; + relationKind: string; + relationText: string; +} diff --git a/src/app/shared/types/shikimori/graphql/responses/anime-rates-metadata-gql-response.interface.ts b/src/app/shared/types/shikimori/graphql/responses/anime-rates-metadata-gql-response.interface.ts new file mode 100644 index 00000000..211b71c5 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/responses/anime-rates-metadata-gql-response.interface.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/indent */ +import { AnimeGQL } from '@app/shared/types/shikimori/graphql/anime.interface'; + +export type AnimeRatesMetadataGQL = Pick; + +export interface AnimeRatesMetadataGQLResponse { + data: { + animes: AnimeRatesMetadataGQL[]; + } +} diff --git a/src/app/shared/types/shikimori/graphql/responses/user-anime-rates-gql-response.interface.ts b/src/app/shared/types/shikimori/graphql/responses/user-anime-rates-gql-response.interface.ts new file mode 100644 index 00000000..504b31bb --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/responses/user-anime-rates-gql-response.interface.ts @@ -0,0 +1,7 @@ +import { UserAnimeRateGQL } from '@app/shared/types/shikimori/graphql/user-anime-rate-gql.interface'; + +export interface UserAnimeRatesGQLResponse { + data: { + userRates: UserAnimeRateGQL[]; + } +} diff --git a/src/app/shared/types/shikimori/graphql/score-stat.interface.ts b/src/app/shared/types/shikimori/graphql/score-stat.interface.ts new file mode 100644 index 00000000..1803dc34 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/score-stat.interface.ts @@ -0,0 +1,4 @@ +export interface ScoreStatGQL { + count: number; + score: number; +} diff --git a/src/app/shared/types/shikimori/graphql/screenshot.interface.ts b/src/app/shared/types/shikimori/graphql/screenshot.interface.ts new file mode 100644 index 00000000..7f939887 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/screenshot.interface.ts @@ -0,0 +1,8 @@ +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface ScreenshotGQL { + id: ResourceIdType; + originalUrl: string; + x166Url: string; + x332Url: string; +} diff --git a/src/app/shared/types/shikimori/graphql/status-stat.interface.ts b/src/app/shared/types/shikimori/graphql/status-stat.interface.ts new file mode 100644 index 00000000..56510776 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/status-stat.interface.ts @@ -0,0 +1,6 @@ +import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; + +export interface StatusStatGQL { + count: number; + status: UserRateStatusType; +} diff --git a/src/app/shared/types/shikimori/graphql/studio.interface.ts b/src/app/shared/types/shikimori/graphql/studio.interface.ts new file mode 100644 index 00000000..a9b269ce --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/studio.interface.ts @@ -0,0 +1,7 @@ +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface StudioGQL { + id: ResourceIdType; + imageUrl: string; + name: string; +} diff --git a/src/app/shared/types/shikimori/graphql/topic.interface.ts b/src/app/shared/types/shikimori/graphql/topic.interface.ts new file mode 100644 index 00000000..b41263c1 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/topic.interface.ts @@ -0,0 +1,14 @@ +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface TopicGQL { + body: string; + commentsCount: number; + createdAt: string; + htmlBody: string; + id: ResourceIdType; + tags: string[]; + title: string; + type: string; + updatedAt: string; + url: string; +} diff --git a/src/app/shared/types/shikimori/graphql/user-anime-rate-gql.interface.ts b/src/app/shared/types/shikimori/graphql/user-anime-rate-gql.interface.ts new file mode 100644 index 00000000..4789effb --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/user-anime-rate-gql.interface.ts @@ -0,0 +1,17 @@ +import { AnimeGQL } from '@app/shared/types/shikimori/graphql/anime.interface'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; +import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; + +export interface UserAnimeRateGQL { + anime: AnimeGQL; + chapters: number; + createdAt: string; + episodes: number; + id: ResourceIdType; + rewatches: number; + score: number; + status: UserRateStatusType; + text: string; + updatedAt: string; + volumes: number; +} diff --git a/src/app/shared/types/shikimori/graphql/video.interface.ts b/src/app/shared/types/shikimori/graphql/video.interface.ts new file mode 100644 index 00000000..9343c3f7 --- /dev/null +++ b/src/app/shared/types/shikimori/graphql/video.interface.ts @@ -0,0 +1,10 @@ +import { ResourceIdType } from '@app/shared/types/resource-id.type'; + +export interface VideoGQL { + id: ResourceIdType; + imageUrl: string; + kind: string; + name: string; + playerUrl: string; + url: string; +} diff --git a/src/app/shared/types/shikimori/helpers/index.ts b/src/app/shared/types/shikimori/helpers/index.ts new file mode 100644 index 00000000..bbf0bdab --- /dev/null +++ b/src/app/shared/types/shikimori/helpers/index.ts @@ -0,0 +1 @@ +export { parsePagination, setPaginationToParams } from './pagination-helper'; diff --git a/src/app/shared/types/shikimori/index.ts b/src/app/shared/types/shikimori/index.ts new file mode 100644 index 00000000..555f1da1 --- /dev/null +++ b/src/app/shared/types/shikimori/index.ts @@ -0,0 +1,25 @@ +export { AnimeBriefInfoInterface } from './anime-brief-info.interface'; +export { AnimeKindType } from './anime-kind.type'; +export { ApiErrorInfo } from './api-error-info.interface'; +export { Comment } from './comment'; +export { CommentableEnum } from './commentable.enum'; +export { CreateComment } from './create-comment.interface'; +export { Credentials } from './credentials'; +export { EpisodeNotificationResponse } from './episode-notification-response.interface'; +export { EpisodeNotification } from './episode-notification.interface'; +export { MangaKindType } from './manga-kind.type'; +export { ShikimoriMediaNameType } from './shikimori-media-name.type'; +export { Topic } from './topic'; +export { + AnimeReleaseStatus, + MangaReleaseStatus, + UserAnimeRate, + UserMangaRate, +} from './user-anime-rate'; +export { UserBriefInfoInterface } from './user-brief-info.interface'; +export { UserBriefRateInterface } from './user-brief-rate.interface'; +export { UserImagesInterface } from './user-images.interface'; +export { UserRateStatusType } from './user-rate-status.type'; +export { UserStatsInterface } from './user-stats.interface'; +export { UserInterface } from './user.interface'; +export { UserRateTargetEnum } from './user-rate-target.enum'; diff --git a/src/app/shared/types/shikimori/mappers/grapql.mappers.ts b/src/app/shared/types/shikimori/mappers/grapql.mappers.ts new file mode 100644 index 00000000..77c24572 --- /dev/null +++ b/src/app/shared/types/shikimori/mappers/grapql.mappers.ts @@ -0,0 +1,126 @@ +import { AnimeRate, UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; +import { + AnimeRatesMetadataGQL, + AnimeRatesMetadataGQLResponse, + UserAnimeRateGQL, + UserAnimeRatesGQLResponse, +} from '@app/shared/types/shikimori/graphql'; +import { ResourceIdType } from '@app/shared/types'; +import { UserAnimeRatesQuery } from '@app/shared/types/shikimori/queries'; +import { UserRateTargetEnum } from '@app/shared/types/shikimori/user-rate-target.enum'; + +export function mapUserRatesGQLQuery(query: UserAnimeRatesQuery) { + const { + page, + limit, + status, + } = query; + + return ` + { + userRates( + page: ${page}, + limit: ${limit}, + status: ${status}, + targetType: Anime, + ) { + id + score + status + text + episodes + chapters + volumes + text + rewatches + createdAt + updatedAt + anime { + id + name + russian + url + score + airedOn { date } + releasedOn { date } + kind + status + } + } + } + `.replace(/\n/g, ''); +} + +export function mapAnimeRatesMetadataGQLQuery(animeIds: ResourceIdType[]) { + return ` + { + animes(ids: "${animeIds.join(',')}", limit: 50, censored: false) { + id + name + russian + english + japanese + kind + rating + score + airedOn { date } + releasedOn { date } + genres { id name } + studios { id name } + } + } + `.replace(/\n/g, ''); +} + +export function mapGqlToV2UserAnimeRate(animeUserRateGQL: UserAnimeRateGQL): UserAnimeRate { + const { + id, + status, + anime, + score, + createdAt, + episodes, + rewatches, + text, + chapters, + updatedAt, + } = animeUserRateGQL; + + return { + id, + status, + chapters, + score, + episodes, + rewatches, + text, + anime: { + id: anime.id, + name: anime.name, + russian: anime.russian, + url: anime.url, + score: `${anime.score}`, + aired_on: anime.airedOn.date, + released_on: anime.releasedOn.date, + kind: anime.kind, + status: anime.status, + episodes: anime.episodes, + episodes_aired: anime.episodesAired, + } as AnimeRate, + created_at: createdAt, + target_id: anime.id, + target_type: UserRateTargetEnum.ANIME, + text_html: text, + updated_at: updatedAt, + user_id: null, + volumes: null, + }; +} + +export function mapUserAnimeRatesGQL(response: UserAnimeRatesGQLResponse): UserAnimeRate[] { + return response?.data?.userRates?.map(mapGqlToV2UserAnimeRate); +} + +export function mapAnimeRatesMetadataGQL(response: AnimeRatesMetadataGQLResponse): AnimeRatesMetadataGQL[] { + return response?.data?.animes; +} diff --git a/src/app/shared/types/shikimori/mappers/index.ts b/src/app/shared/types/shikimori/mappers/index.ts new file mode 100644 index 00000000..ed051d28 --- /dev/null +++ b/src/app/shared/types/shikimori/mappers/index.ts @@ -0,0 +1,8 @@ +export { toShikimoriCredentials } from './auth.mappers'; +export { + mapUserRatesGQLQuery, + mapAnimeRatesMetadataGQL, + mapAnimeRatesMetadataGQLQuery, + mapGqlToV2UserAnimeRate, + mapUserAnimeRatesGQL, +} from './grapql.mappers'; diff --git a/src/app/shared/types/shikimori/queries/find-anime-query.ts b/src/app/shared/types/shikimori/queries/find-anime-query.ts index a0a25874..a358171d 100644 --- a/src/app/shared/types/shikimori/queries/find-anime-query.ts +++ b/src/app/shared/types/shikimori/queries/find-anime-query.ts @@ -2,20 +2,10 @@ import { AnimeKindType } from '@app/shared/types/shikimori/anime-kind.type'; import { PaginationRequest } from '@app/shared/types/shikimori/queries/pagination-request'; import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; -type AnimeOrderType = 'id' -| 'id_desc' -| 'ranked' -| 'kind' -| 'popularity' -|'name' -|'aired_on' -|'episodes' -|'status' -|'created_at' -|'created_at_desc' -|'random'; +// eslint-disable-next-line max-len +type AnimeOrderType = 'id' | 'id_desc' | 'ranked' | 'kind' | 'popularity' | 'name' | 'aired_on' | 'episodes' | 'status' | 'created_at' | 'created_at_desc' | 'random'; -type AnimeStatusType = 'anons' | 'ongoing' | 'released'; +export type AnimeStatusType = 'anons' | 'ongoing' | 'released'; /** * @description "S" - less than 10 minutes, @@ -23,7 +13,7 @@ type AnimeStatusType = 'anons' | 'ongoing' | 'released'; * "F" - more than 30 minutes */ type AnimeDurationType = 'S' | 'D' | 'F'; -type AnimeRatingType = 'none' | 'g' | 'pg' | 'pg_13' | 'r' | 'r_plus' | 'rx'; +export type AnimeRatingType = 'none' | 'g' | 'pg' | 'pg_13' | 'r' | 'r_plus' | 'rx'; export interface FindAnimeQuery extends PaginationRequest { order?: AnimeOrderType; diff --git a/src/app/shared/types/shikimori/queries/index.ts b/src/app/shared/types/shikimori/queries/index.ts new file mode 100644 index 00000000..9512d216 --- /dev/null +++ b/src/app/shared/types/shikimori/queries/index.ts @@ -0,0 +1,3 @@ +export { FindAnimeQuery } from './find-anime-query'; +export { PaginationRequest } from './pagination-request'; +export { UserAnimeRatesQuery } from './user-anime-rates-query'; diff --git a/src/app/shared/types/shikimori/user-anime-rate.ts b/src/app/shared/types/shikimori/user-anime-rate.ts index 2ba98eb9..d2f618f4 100644 --- a/src/app/shared/types/shikimori/user-anime-rate.ts +++ b/src/app/shared/types/shikimori/user-anime-rate.ts @@ -1,13 +1,15 @@ import { AnimeKindType } from '@app/shared/types/shikimori/anime-kind.type'; import { MangaKindType } from '@app/shared/types/shikimori/manga-kind.type'; +import { ResourceIdType } from '@app/shared/types/resource-id.type'; import { UserImagesInterface } from '@app/shared/types/shikimori/user-images.interface'; import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; +import { UserRateTargetEnum } from '@app/shared/types/shikimori/user-rate-target.enum'; export type AnimeReleaseStatus = 'anons' | 'ongoing' | 'released'; export type MangaReleaseStatus = 'anons' | 'ongoing' | 'released' | 'paused' | 'discontinued'; export interface RateUserInfo { - id: number; + id: ResourceIdType; nickname: string; avatar: string; image: UserImagesInterface; @@ -22,7 +24,7 @@ export interface RateImage { } export interface Rate { - id: number; + id: ResourceIdType; name: string; russian: string; image: RateImage; @@ -50,7 +52,7 @@ export interface MangaRate extends Rate { } interface UserFullRate { - id: number; + id: ResourceIdType; score: number; status: UserRateStatusType; text: string | null; @@ -61,9 +63,9 @@ interface UserFullRate { rewatches: number; created_at: string; updated_at: string; - user_id: number; - target_id: number; - target_type: 'Anime' | 'Manga'; + user_id: ResourceIdType; + target_id: ResourceIdType; + target_type: UserRateTargetEnum; anime: T; manga: T; } diff --git a/src/app/shared/types/shikimori/user-brief-rate.interface.ts b/src/app/shared/types/shikimori/user-brief-rate.interface.ts index 02bdd773..936476e7 100644 --- a/src/app/shared/types/shikimori/user-brief-rate.interface.ts +++ b/src/app/shared/types/shikimori/user-brief-rate.interface.ts @@ -1,12 +1,11 @@ import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; - -type RateTargetType = 'Anime' | 'Manga'; +import { UserRateTargetEnum } from '@app/shared/types/shikimori/user-rate-target.enum'; export interface UserBriefRateInterface { id: number; user_id: number; target_id: number; - target_type: RateTargetType; + target_type: UserRateTargetEnum; score: number; status: UserRateStatusType; rewatches: number; diff --git a/src/app/shared/types/shikimori/user-rate-target.enum.ts b/src/app/shared/types/shikimori/user-rate-target.enum.ts new file mode 100644 index 00000000..e781b9ae --- /dev/null +++ b/src/app/shared/types/shikimori/user-rate-target.enum.ts @@ -0,0 +1,4 @@ +export enum UserRateTargetEnum { + ANIME = 'Anime', + MANGA = 'Manga', +} diff --git a/src/app/shared/types/shikimori/user-stats.interface.ts b/src/app/shared/types/shikimori/user-stats.interface.ts index 8ebca1ee..28285a02 100644 --- a/src/app/shared/types/shikimori/user-stats.interface.ts +++ b/src/app/shared/types/shikimori/user-stats.interface.ts @@ -1,4 +1,5 @@ import { UserRateStatusType } from '@app/shared/types/shikimori/user-rate-status.type'; +import { UserRateTargetEnum } from '@app/shared/types/shikimori/user-rate-target.enum'; interface StatusInterface { anime: T[]; @@ -10,7 +11,7 @@ interface StatStatusInterface { grouped_id: UserRateStatusType; name: UserRateStatusType; size: number; - type: 'Anime' | 'Manga'; + type: UserRateTargetEnum; } type ScoreNameType = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; diff --git a/src/app/shared/utils/common-ngfor-tracking.ts b/src/app/shared/utils/common-ngfor-tracking.ts index 59999a74..167f27bb 100644 --- a/src/app/shared/utils/common-ngfor-tracking.ts +++ b/src/app/shared/utils/common-ngfor-tracking.ts @@ -1,5 +1,9 @@ import { ResourceIdType } from '@app/shared/types/resource-id.type'; export function trackById(index: number, item: T) { - return item?.id || index; + if (item?.id && `${item.id}` !== '-1') { + return item.id; + } else { + return index; + } } diff --git a/src/app/shared/utils/entities.utils.ts b/src/app/shared/utils/entities.utils.ts new file mode 100644 index 00000000..8826d44f --- /dev/null +++ b/src/app/shared/utils/entities.utils.ts @@ -0,0 +1,15 @@ +import { ResourceIdType } from '@app/shared/types'; + +export function entityArrayToMap(entities: T[]) { + const json = {}; + + for (const entity of entities) { + json[entity.id] = entity; + } + + return json; +} + +export function entityMapToArray(entities: { [id: ResourceIdType]: T }): T[] { + return Object.values(entities || {})?.map((val) => val) || []; +} diff --git a/src/app/shared/utils/get-path.function.ts b/src/app/shared/utils/get-path.function.ts index 48bfa00a..4439e27b 100644 --- a/src/app/shared/utils/get-path.function.ts +++ b/src/app/shared/utils/get-path.function.ts @@ -1,7 +1,17 @@ export function getPath(url: string): string { - const path = url?.startsWith('/') - ? url - : new URL(url).pathname; + let path = url; + + try { + const toUrl = url?.startsWith('/') + ? url + : new URL(url); + + path = toUrl instanceof URL + ? toUrl.pathname + toUrl.search + toUrl.hash + : url; + } catch (e) { + path = url; + } return path; } diff --git a/src/app/shared/utils/is-supports-avif.function.ts b/src/app/shared/utils/is-supports-avif.function.ts new file mode 100644 index 00000000..d1e3b272 --- /dev/null +++ b/src/app/shared/utils/is-supports-avif.function.ts @@ -0,0 +1,20 @@ +import { + Observable, + from, + shareReplay, + startWith, +} from 'rxjs'; + +export function isSupportsAvif(): Observable { + return from( + new Promise((resolve) => { + const image = new Image(); + image.onerror = () => resolve(false); + image.onload = () => resolve(true); + image.src = '/assets/1x1.avif'; + }).catch(() => false), + ).pipe( + startWith(undefined), + shareReplay(1), + ); +} diff --git a/src/app/shared/utils/rx-anime-rates-functions.ts b/src/app/shared/utils/rx-anime-rates-functions.ts deleted file mode 100644 index 492131ee..00000000 --- a/src/app/shared/utils/rx-anime-rates-functions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { UserAnimeRate } from '@app/shared/types/shikimori/user-anime-rate'; - -export interface AnimeNameSortingConfig { - caseSensitive: boolean; - compareOriginalName: boolean; -} - -export function sortByAnimeName( - rateA: UserAnimeRate, - rateB: UserAnimeRate, - { compareOriginalName, caseSensitive }: AnimeNameSortingConfig, -) { - const nameA = compareOriginalName ? rateA.anime.name : rateA.anime.russian; - const nameB = compareOriginalName ? rateB.anime.name : rateB.anime.russian; - const locales: string[] = compareOriginalName ? ['en', 'jp'] : ['en', 'ru']; - const options: Intl.CollatorOptions = { - sensitivity: caseSensitive ? 'case' : 'base', - }; - - return nameA.localeCompare(nameB, locales, options); -} - -export function sortByAnimeUserRating( - rateA: UserAnimeRate, - rateB: UserAnimeRate, - config: AnimeNameSortingConfig, -) { - const cmp = +rateB.score - +rateA.score; - - return cmp === 0 - ? sortByAnimeName(rateA, rateB, config) - : cmp; -} diff --git a/src/app/shared/utils/split-array-to-chunks.funtion.ts b/src/app/shared/utils/split-array-to-chunks.funtion.ts new file mode 100644 index 00000000..7a22722b --- /dev/null +++ b/src/app/shared/utils/split-array-to-chunks.funtion.ts @@ -0,0 +1,8 @@ +export function splitArrayToChunks(array: T[], chunkSize: number): T[][] { + const chunksCount = Math.ceil(array.length / chunkSize); + + return new Array(chunksCount) + .fill(0) + .map((_, index) => index * chunkSize) + .map((startIndex) => array.slice(startIndex, startIndex + chunkSize)); +} diff --git a/src/app/shared/utils/zip.utils.ts b/src/app/shared/utils/zip.utils.ts new file mode 100644 index 00000000..6f3f5de6 --- /dev/null +++ b/src/app/shared/utils/zip.utils.ts @@ -0,0 +1,16 @@ +import { gunzipSync, gzipSync } from 'fflate'; + +export function zip(str: string): string { + const ascii = encodeURIComponent(str); + const array = new TextEncoder().encode(ascii); + const zip = gzipSync(array); + return btoa(String.fromCharCode(...zip)); +} + +export function unzip(base64: string): string { + const raw = atob(base64); + const array = Uint8Array.from(raw, (c) => c.charCodeAt(0)); + const unzip = gunzipSync(array); + const ascii = new TextDecoder().decode(unzip); + return decodeURIComponent(ascii); +} diff --git a/src/app/store/app-state.providers.ts b/src/app/store/app-state.providers.ts index 5a5033e6..301164a3 100644 --- a/src/app/store/app-state.providers.ts +++ b/src/app/store/app-state.providers.ts @@ -11,6 +11,7 @@ import { SettingsEffects } from '@app/store/settings/effects/settings.effects'; import { ShikicinemaEffects } from '@app/store/shikicinema/effects/shikicinema.effects'; import { ShikimoriClient } from '@app/shared/services/shikimori-client.service'; import { ShikimoriEffects } from '@app/store/shikimori/effects/shikimori.effects'; +import { animeRatesLocalStorageSyncReducer } from '@app/modules/home/store/anime-rates'; import { authEffectFactory } from '@app/store/auth/factories/auth-effects.factories'; import { authLocalStorageSyncReducer } from '@app/store/auth/reducers/auth.meta-reducer'; import { authReducer } from '@app/store/auth/reducers/auth.reducer'; @@ -33,6 +34,7 @@ const storeConfig: RootStoreConfig = { settingsLocalStorageSyncReducer, shikicinemaLocalStorageSyncReducer, loggerMetaReducer, + animeRatesLocalStorageSyncReducer, recentAnimesLocalStorageSyncReducer, shikimoriLocalStorageSyncReducer, ], diff --git a/src/app/store/settings/reducers/settings.reducer.ts b/src/app/store/settings/reducers/settings.reducer.ts index 383888f9..3499b6d8 100644 --- a/src/app/store/settings/reducers/settings.reducer.ts +++ b/src/app/store/settings/reducers/settings.reducer.ts @@ -21,7 +21,6 @@ const initialState: SettingsStoreInterface = { playerMode: 'auto', playerKindDisplayMode: 'special-only', availableLangs: defaultAvailableLangs, - animePaginationSize: 100, authorPreferences: {}, kindPreferences: {}, domainPreferences: {}, diff --git a/src/app/store/settings/selectors/settings.selectors.ts b/src/app/store/settings/selectors/settings.selectors.ts index fdcb1d98..b9775767 100644 --- a/src/app/store/settings/selectors/settings.selectors.ts +++ b/src/app/store/settings/selectors/settings.selectors.ts @@ -18,11 +18,6 @@ export const selectAvailableLanguages = createSelector( (state) => state.availableLangs, ); -export const selectAnimePaginationSize = createSelector( - selectSettings, - (state) => state.animePaginationSize, -); - export const selectAuthorPreferences = createSelector( selectSettings, (state) => state.authorPreferences, diff --git a/src/app/store/settings/types/settings-store.interface.ts b/src/app/store/settings/types/settings-store.interface.ts index d0abca37..8130508f 100644 --- a/src/app/store/settings/types/settings-store.interface.ts +++ b/src/app/store/settings/types/settings-store.interface.ts @@ -11,7 +11,6 @@ export interface SettingsStoreInterface { playerMode: PlayerModeType; playerKindDisplayMode: PlayerKindDisplayMode; availableLangs: string[]; - animePaginationSize: number; authorPreferences: PreferencesInterface; kindPreferences: PreferencesInterface; domainPreferences: PreferencesInterface; diff --git a/src/assets/1x1.avif b/src/assets/1x1.avif new file mode 100644 index 0000000000000000000000000000000000000000..29b33cd96a164dc1db16fb2d2f40278f4bbaac1b GIT binary patch literal 450 zcmYL_!AiqG5QZm7m8Ap`Z6TBv?5#&rijm8+acMO=2$2mJo^Ir0M+;K z1$-Q55~B;*`RDs**qH#pFtruwEvCRnqQNvYs5y7YBvuROGtQQI(MSNo*tWWjXN1<& ziNp5{^K~rQ{Vlj41f167JeWV#O4c9 zyyocZZMfKjFAY5`DG|X(rjYNPaEAgsr4)zX`k^4+qVXJY=6bVR2;LN{$WZcBUw2WA zF-2un+QgI**I{` nFQBUeI8`c}T}}M}x|8<