From cddec0d9d496fb9395cdf5a62c919384a27a4620 Mon Sep 17 00:00:00 2001 From: Geomitron <22552797+Geomitron@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:00:30 -0600 Subject: [PATCH] Update formatting --- package.json | 16 + pnpm-lock.yaml | 1576 ++++++++++++++++- src/app/app-routing.module.ts | 25 +- src/app/app.component.html | 8 +- src/app/app.component.ts | 19 +- src/app/app.module.ts | 58 +- .../components/browse/browse.component.html | 21 +- .../components/browse/browse.component.scss | 2 +- src/app/components/browse/browse.component.ts | 51 +- .../chart-sidebar.component.html | 90 +- .../chart-sidebar.component.scss | 2 +- .../chart-sidebar/chart-sidebar.component.ts | 498 +++--- .../result-table-row.component.html | 17 +- .../result-table-row.component.scss | 2 +- .../result-table-row.component.ts | 57 +- .../result-table/result-table.component.html | 57 +- .../result-table/result-table.component.ts | 130 +- .../search-bar/search-bar.component.html | 461 ++--- .../search-bar/search-bar.component.scss | 2 +- .../browse/search-bar/search-bar.component.ts | 123 +- .../downloads-modal.component.html | 52 +- .../downloads-modal.component.scss | 2 +- .../downloads-modal.component.ts | 81 +- .../status-bar/status-bar.component.html | 80 +- .../status-bar/status-bar.component.scss | 2 +- .../browse/status-bar/status-bar.component.ts | 191 +- .../settings/settings.component.html | 96 +- .../settings/settings.component.scss | 2 +- .../components/settings/settings.component.ts | 233 +-- .../components/toolbar/toolbar.component.html | 26 +- .../components/toolbar/toolbar.component.scss | 2 +- .../components/toolbar/toolbar.component.ts | 87 +- src/app/core/directives/checkbox.directive.ts | 58 +- .../core/directives/progress-bar.directive.ts | 33 +- src/app/core/services/album-art.service.ts | 31 +- src/app/core/services/download.service.ts | 143 +- src/app/core/services/electron.service.ts | 117 +- src/app/core/services/search.service.ts | 195 +- src/app/core/services/selection.service.ts | 127 +- src/app/core/services/settings.service.ts | 127 +- src/app/core/tab-persist.strategy.ts | 42 +- src/electron/ipc/CacheHandler.ipc.ts | 57 +- src/electron/ipc/OpenURLHandler.ipc.ts | 19 +- src/electron/ipc/SettingsHandler.ipc.ts | 145 +- src/electron/ipc/UpdateHandler.ipc.ts | 151 +- .../ipc/browse/AlbumArtHandler.ipc.ts | 20 +- .../ipc/browse/BatchSongDetailsHandler.ipc.ts | 20 +- src/electron/ipc/browse/SearchHandler.ipc.ts | 33 +- .../ipc/browse/SongDetailsHandler.ipc.ts | 20 +- src/electron/ipc/download/ChartDownload.ts | 470 ++--- src/electron/ipc/download/DownloadHandler.ts | 140 +- src/electron/ipc/download/DownloadQueue.ts | 73 +- src/electron/ipc/download/FileDownloader.ts | 574 +++--- src/electron/ipc/download/FileExtractor.ts | 263 +-- src/electron/ipc/download/FileTransfer.ts | 165 +- .../ipc/download/FilesystemChecker.ts | 191 +- src/electron/ipc/download/GoogleTimer.ts | 108 +- src/electron/main.ts | 166 +- src/electron/shared/ElectronUtilFunctions.ts | 11 +- src/electron/shared/IPCHandler.ts | 161 +- src/electron/shared/Paths.ts | 4 +- src/electron/shared/Settings.ts | 18 +- src/electron/shared/UtilFunctions.ts | 61 +- .../shared/interfaces/download.interface.ts | 36 +- .../shared/interfaces/search.interface.ts | 128 +- .../interfaces/songDetails.interface.ts | 146 +- .../shared/typeDefinitions/node-7z.d.ts | 1 - src/environments/environment.prod.ts | 4 +- src/environments/environment.ts | 4 +- src/index.html | 20 +- src/main.ts | 4 +- src/polyfills.ts | 5 +- src/styles.scss | 2 +- src/typings.d.ts | 11 +- tsconfig.json | 6 +- 75 files changed, 4900 insertions(+), 3279 deletions(-) delete mode 100644 src/electron/shared/typeDefinitions/node-7z.d.ts diff --git a/package.json b/package.json index 3fad109..b39fb3e 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,10 @@ "devDependencies": { "@angular-devkit/build-angular": "^17.0.3", "@angular-eslint/builder": "17.1.0", + "@angular-eslint/eslint-plugin": "^17.1.0", + "@angular-eslint/eslint-plugin-template": "^17.1.0", + "@angular-eslint/schematics": "^17.1.0", + "@angular-eslint/template-parser": "^17.1.0", "@angular/cli": "^17.0.3", "@angular/compiler-cli": "^17.0.4", "@angular/language-service": "^17.0.4", @@ -65,10 +69,22 @@ "@types/randombytes": "^2.0.0", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", + "concurrently": "^8.2.2", "electron": "^27.1.2", "electron-builder": "^23.6.0", "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jsdoc": "^46.9.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-prettier": "^5.0.1", "nodemon": "^3.0.1", + "postcss": "^8.4.31", + "prettier": "^3.1.0", + "prettier-eslint": "^16.1.2", + "source-map-support": "^0.5.21", + "tailwindcss": "^3.3.5", + "tsx": "^4.4.0", "typescript": "^5.2.2" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d0d412..8620cae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,10 +84,22 @@ dependencies: devDependencies: '@angular-devkit/build-angular': specifier: ^17.0.3 - version: 17.0.3(@angular/compiler-cli@17.0.4)(@types/node@18.16.0)(typescript@5.2.2) + version: 17.0.3(@angular/compiler-cli@17.0.4)(@types/node@18.16.0)(tailwindcss@3.3.5)(typescript@5.2.2) '@angular-eslint/builder': specifier: 17.1.0 version: 17.1.0(eslint@8.54.0)(typescript@5.2.2) + '@angular-eslint/eslint-plugin': + specifier: ^17.1.0 + version: 17.1.0(eslint@8.54.0)(typescript@5.2.2) + '@angular-eslint/eslint-plugin-template': + specifier: ^17.1.0 + version: 17.1.0(eslint@8.54.0)(typescript@5.2.2) + '@angular-eslint/schematics': + specifier: ^17.1.0 + version: 17.1.0(@angular/cli@17.0.3)(eslint@8.54.0)(typescript@5.2.2) + '@angular-eslint/template-parser': + specifier: ^17.1.0 + version: 17.1.0(eslint@8.54.0)(typescript@5.2.2) '@angular/cli': specifier: ^17.0.3 version: 17.0.3 @@ -118,6 +130,9 @@ devDependencies: '@typescript-eslint/parser': specifier: ^6.12.0 version: 6.12.0(eslint@8.54.0)(typescript@5.2.2) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 electron: specifier: ^27.1.2 version: 27.1.2 @@ -127,9 +142,42 @@ devDependencies: eslint: specifier: ^8.54.0 version: 8.54.0 + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.0.0(eslint@8.54.0) + eslint-plugin-import: + specifier: ^2.29.0 + version: 2.29.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0) + eslint-plugin-jsdoc: + specifier: ^46.9.0 + version: 46.9.0(eslint@8.54.0) + eslint-plugin-prefer-arrow: + specifier: ^1.2.3 + version: 1.2.3(eslint@8.54.0) + eslint-plugin-prettier: + specifier: ^5.0.1 + version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.1.0) nodemon: specifier: ^3.0.1 version: 3.0.1 + postcss: + specifier: ^8.4.31 + version: 8.4.31 + prettier: + specifier: ^3.1.0 + version: 3.1.0 + prettier-eslint: + specifier: ^16.1.2 + version: 16.1.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + tailwindcss: + specifier: ^3.3.5 + version: 3.3.5 + tsx: + specifier: ^4.4.0 + version: 4.4.0 typescript: specifier: ^5.2.2 version: 5.2.2 @@ -159,6 +207,11 @@ packages: undici: 5.27.2 dev: false + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -177,7 +230,7 @@ packages: - chokidar dev: true - /@angular-devkit/build-angular@17.0.3(@angular/compiler-cli@17.0.4)(@types/node@18.16.0)(typescript@5.2.2): + /@angular-devkit/build-angular@17.0.3(@angular/compiler-cli@17.0.4)(@types/node@18.16.0)(tailwindcss@3.3.5)(typescript@5.2.2): resolution: {integrity: sha512-1lx0mERC1eTHX4vf8q7kUHZNHS0jwZxbwYHAISOplwHjkzRociX0W6rx04yMXn2NCSNhK+w3xbWyAIgyYbP9nA==} engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: @@ -267,6 +320,7 @@ packages: semver: 7.5.4 source-map-loader: 4.0.1(webpack@5.89.0) source-map-support: 0.5.21 + tailwindcss: 3.3.5 terser: 5.24.0 text-table: 0.2.0 tree-kill: 1.2.2 @@ -362,6 +416,90 @@ packages: - debug dev: true + /@angular-eslint/bundled-angular-compiler@17.1.0: + resolution: {integrity: sha512-Y+CN/8nQZaYjsb2b2sXbkQr0LrgBWhCzyLZ+rLfnLE60B9k4GeDt5b7z/OdSObi1xozXfqiaAZ1eXo0iQMN3JA==} + dev: true + + /@angular-eslint/eslint-plugin-template@17.1.0(eslint@8.54.0)(typescript@5.2.2): + resolution: {integrity: sha512-nL9VhChwFQLIRQM4xbTY8Vo095Q4/D77hPtqt3ShYIrORjYTwaWa8+neexToAqXVMapce7oFmFa/OqtxvEerLg==} + peerDependencies: + eslint: ^7.20.0 || ^8.0.0 + typescript: '*' + dependencies: + '@angular-eslint/bundled-angular-compiler': 17.1.0 + '@angular-eslint/utils': 17.1.0(eslint@8.54.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2) + aria-query: 5.3.0 + axobject-query: 4.0.0 + eslint: 8.54.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@angular-eslint/eslint-plugin@17.1.0(eslint@8.54.0)(typescript@5.2.2): + resolution: {integrity: sha512-pQac5h+XwsquDzaasK/xs9tjdQ/f9eLq8e5An9eXJGHWy4KcrMmQ1XrpaMMMg503LF3rRG/dHKBskGsYgSN9oQ==} + peerDependencies: + eslint: ^7.20.0 || ^8.0.0 + typescript: '*' + dependencies: + '@angular-eslint/utils': 17.1.0(eslint@8.54.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2) + eslint: 8.54.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@angular-eslint/schematics@17.1.0(@angular/cli@17.0.3)(eslint@8.54.0)(typescript@5.2.2): + resolution: {integrity: sha512-74gW1E5P4z3PvxNXOTXGaF6li/MLcSeJO8z7XtcP7wcXWu0fihOKlMJGgqB3rIcBa8lRcTDLekQERF+kRZ15aQ==} + peerDependencies: + '@angular/cli': '>= 17.0.0 < 18.0.0' + dependencies: + '@angular-eslint/eslint-plugin': 17.1.0(eslint@8.54.0)(typescript@5.2.2) + '@angular-eslint/eslint-plugin-template': 17.1.0(eslint@8.54.0)(typescript@5.2.2) + '@angular/cli': 17.0.3 + '@nx/devkit': 17.1.2(nx@17.1.2) + ignore: 5.2.4 + nx: 17.1.2 + strip-json-comments: 3.1.1 + tmp: 0.2.1 + transitivePeerDependencies: + - '@swc-node/register' + - '@swc/core' + - debug + - eslint + - supports-color + - typescript + dev: true + + /@angular-eslint/template-parser@17.1.0(eslint@8.54.0)(typescript@5.2.2): + resolution: {integrity: sha512-CTxzB3stjynngTabdO8xTkiPc6Jvo15C2fxb1pYIlDIH2LgPJJxxCHi+IAt9oJpJOPa8QjLVF9VAXE3fLKAcpg==} + peerDependencies: + eslint: ^7.20.0 || ^8.0.0 + typescript: '*' + dependencies: + '@angular-eslint/bundled-angular-compiler': 17.1.0 + eslint: 8.54.0 + eslint-scope: 7.2.2 + typescript: 5.2.2 + dev: true + + /@angular-eslint/utils@17.1.0(eslint@8.54.0)(typescript@5.2.2): + resolution: {integrity: sha512-AmG0xpRtnBQwrbHObonSilmD3hiFEtZHwFY3LT28VWxznB6WIAHFE7SrKWrRsRsXlib8LaRo4uobR5+MO8aLpw==} + peerDependencies: + eslint: ^7.20.0 || ^8.0.0 + typescript: '*' + dependencies: + '@angular-eslint/bundled-angular-compiler': 17.1.0 + '@typescript-eslint/utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2) + eslint: 8.54.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@angular/animations@17.0.4(@angular/core@17.0.4): resolution: {integrity: sha512-XHkTBZAoYf1t4Hb06RkYa6cgtjEA5JGq1ArXu/DckOS6G/ZuY+dwWULEmaf9ejJem8O78ol223ZQ5d7sXqpixQ==} engines: {node: ^18.13.0 || >=20.9.0} @@ -1815,6 +1953,15 @@ packages: - supports-color dev: true + /@es-joy/jsdoccomment@0.41.0: + resolution: {integrity: sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==} + engines: {node: '>=16'} + dependencies: + comment-parser: 1.4.1 + esquery: 1.5.0 + jsdoc-type-pratt-parser: 4.0.0 + dev: true + /@esbuild/android-arm64@0.18.17: resolution: {integrity: sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==} engines: {node: '>=12'} @@ -1824,6 +1971,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.19.5: resolution: {integrity: sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==} engines: {node: '>=12'} @@ -1842,6 +1998,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.19.5: resolution: {integrity: sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==} engines: {node: '>=12'} @@ -1860,6 +2025,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.19.5: resolution: {integrity: sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==} engines: {node: '>=12'} @@ -1878,6 +2052,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.19.5: resolution: {integrity: sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==} engines: {node: '>=12'} @@ -1896,6 +2079,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.19.5: resolution: {integrity: sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==} engines: {node: '>=12'} @@ -1914,6 +2106,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.19.5: resolution: {integrity: sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==} engines: {node: '>=12'} @@ -1932,6 +2133,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.19.5: resolution: {integrity: sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==} engines: {node: '>=12'} @@ -1950,6 +2160,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.19.5: resolution: {integrity: sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==} engines: {node: '>=12'} @@ -1968,6 +2187,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.19.5: resolution: {integrity: sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==} engines: {node: '>=12'} @@ -1986,6 +2214,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.19.5: resolution: {integrity: sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==} engines: {node: '>=12'} @@ -2004,6 +2241,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.19.5: resolution: {integrity: sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==} engines: {node: '>=12'} @@ -2022,6 +2268,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.19.5: resolution: {integrity: sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==} engines: {node: '>=12'} @@ -2040,6 +2295,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.19.5: resolution: {integrity: sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==} engines: {node: '>=12'} @@ -2058,6 +2322,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.19.5: resolution: {integrity: sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==} engines: {node: '>=12'} @@ -2076,6 +2349,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.19.5: resolution: {integrity: sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==} engines: {node: '>=12'} @@ -2094,6 +2376,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.19.5: resolution: {integrity: sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==} engines: {node: '>=12'} @@ -2112,6 +2403,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.19.5: resolution: {integrity: sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==} engines: {node: '>=12'} @@ -2130,6 +2430,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.19.5: resolution: {integrity: sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==} engines: {node: '>=12'} @@ -2148,6 +2457,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.19.5: resolution: {integrity: sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==} engines: {node: '>=12'} @@ -2166,6 +2484,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.19.5: resolution: {integrity: sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==} engines: {node: '>=12'} @@ -2184,6 +2511,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.19.5: resolution: {integrity: sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==} engines: {node: '>=12'} @@ -2202,6 +2538,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.19.5: resolution: {integrity: sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==} engines: {node: '>=12'} @@ -2716,6 +3061,18 @@ packages: requiresBuild: true optional: true + /@pkgr/utils@2.4.2: + resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + fast-glob: 3.3.2 + is-glob: 4.0.3 + open: 9.1.0 + picocolors: 1.0.0 + tslib: 2.6.2 + dev: true + /@schematics/angular@17.0.3: resolution: {integrity: sha512-pFHAqHMNm2WLoquJD4osSA/OAgH+wsFayPuqQnKjDEzeVW/YfJSbUksJ2iFt+uSfrhc/VxPf6pmGBMzi+9d0ng==} engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -2922,6 +3279,10 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + /@types/jsonfile@6.1.4: resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} dependencies: @@ -3115,6 +3476,14 @@ packages: - supports-color dev: true + /@typescript-eslint/scope-manager@6.11.0: + resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 + dev: true + /@typescript-eslint/scope-manager@6.12.0: resolution: {integrity: sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -3123,6 +3492,26 @@ packages: '@typescript-eslint/visitor-keys': 6.12.0 dev: true + /@typescript-eslint/type-utils@6.11.0(eslint@8.54.0)(typescript@5.2.2): + resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2) + debug: 4.3.4 + eslint: 8.54.0 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/type-utils@6.12.0(eslint@8.54.0)(typescript@5.2.2): resolution: {integrity: sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==} engines: {node: ^16.0.0 || >=18.0.0} @@ -3143,11 +3532,37 @@ packages: - supports-color dev: true + /@typescript-eslint/types@6.11.0: + resolution: {integrity: sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/types@6.12.0: resolution: {integrity: sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==} engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.2.2): + resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@6.12.0(typescript@5.2.2): resolution: {integrity: sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -3169,6 +3584,25 @@ packages: - supports-color dev: true + /@typescript-eslint/utils@6.11.0(eslint@8.54.0)(typescript@5.2.2): + resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + eslint: 8.54.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@6.12.0(eslint@8.54.0)(typescript@5.2.2): resolution: {integrity: sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -3188,6 +3622,14 @@ packages: - typescript dev: true + /@typescript-eslint/visitor-keys@6.11.0: + resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.11.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@typescript-eslint/visitor-keys@6.12.0: resolution: {integrity: sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -3522,7 +3964,6 @@ packages: /ansi-regex@2.1.1: resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} engines: {node: '>=0.10.0'} - dev: false /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -3535,7 +3976,6 @@ packages: /ansi-styles@2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} engines: {node: '>=0.10.0'} - dev: false /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -3564,6 +4004,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + /any-shell-escape@0.1.1: resolution: {integrity: sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ==} dev: false @@ -3634,6 +4078,15 @@ packages: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} dev: false + /are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -3642,6 +4095,12 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + /arr-diff@1.1.0: resolution: {integrity: sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==} engines: {node: '>=0.10.0'} @@ -3684,6 +4143,13 @@ packages: engines: {node: '>=0.10.0'} dev: false + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + dev: true + /array-differ@1.0.0: resolution: {integrity: sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==} engines: {node: '>=0.10.0'} @@ -3702,6 +4168,17 @@ packages: resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} dev: true + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-string: 1.0.7 + dev: true + /array-initial@1.1.0: resolution: {integrity: sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==} engines: {node: '>=0.10.0'} @@ -3757,6 +4234,50 @@ packages: engines: {node: '>=0.10.0'} dev: false + /array.prototype.findlastindex@1.2.3: + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + get-intrinsic: 1.2.2 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + /asar@3.2.0: resolution: {integrity: sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==} engines: {node: '>=10.12.0'} @@ -3865,6 +4386,11 @@ packages: postcss: 8.4.31 postcss-value-parser: 4.2.0 + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + /axios@0.21.4(debug@4.3.2): resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: @@ -3883,6 +4409,12 @@ packages: - debug dev: true + /axobject-query@4.0.0: + resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} + dependencies: + dequal: 2.0.3 + dev: true + /babel-loader@9.1.3(@babel/core@7.23.2)(webpack@5.89.0): resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} engines: {node: '>= 14.15.0'} @@ -4004,6 +4536,11 @@ packages: cli-table: 0.3.11 dev: false + /big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + dev: true + /big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} dev: true @@ -4090,6 +4627,13 @@ packages: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} dev: false + /bplist-parser@0.2.0: + resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} + engines: {node: '>= 5.10.0'} + dependencies: + big-integer: 1.6.52 + dev: true + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -4286,12 +4830,24 @@ packages: - supports-color dev: true + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: semver: 7.5.4 dev: true + /bundle-name@3.0.0: + resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} + engines: {node: '>=12'} + dependencies: + run-applescript: 5.0.0 + dev: true + /bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -4363,6 +4919,11 @@ packages: engines: {node: '>=6'} dev: true + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + /camelcase@3.0.0: resolution: {integrity: sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==} engines: {node: '>=0.10.0'} @@ -4385,7 +4946,6 @@ packages: has-ansi: 2.0.0 strip-ansi: 3.0.1 supports-color: 2.0.0 - dev: false /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -4686,15 +5246,30 @@ packages: graceful-readlink: 1.0.1 dev: true + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + /commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} dev: true + /comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + dev: true + /common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} dev: true + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + /comparators@3.0.5: resolution: {integrity: sha512-xWeNJYdL57+ySi53YVnHFFKBI7SF/o7xMHDXGVD5bX7lb4ECPmKWXwrOr65lZRvowqgb9VHFt+aPr4TjqvyyiQ==} dev: false @@ -4749,6 +5324,22 @@ packages: source-map: 0.6.1 dev: false + /concurrently@8.2.2: + resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.1 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + dev: true + /config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} dependencies: @@ -4968,6 +5559,13 @@ packages: type: 1.2.0 dev: false + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.2 + dev: true + /dateformat@2.2.0: resolution: {integrity: sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==} dev: false @@ -5048,6 +5646,24 @@ packages: engines: {node: '>=0.10.0'} dev: false + /default-browser-id@3.0.0: + resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} + engines: {node: '>=12'} + dependencies: + bplist-parser: 0.2.0 + untildify: 4.0.0 + dev: true + + /default-browser@4.0.0: + resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} + engines: {node: '>=14.16'} + dependencies: + bundle-name: 3.0.0 + default-browser-id: 3.0.0 + execa: 7.2.0 + titleize: 3.0.0 + dev: true + /default-compare@1.0.0: resolution: {integrity: sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==} engines: {node: '>=0.10.0'} @@ -5089,6 +5705,11 @@ packages: engines: {node: '>=8'} dev: true + /define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dev: true + /define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -5152,6 +5773,11 @@ packages: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} dev: false + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + /destroy@1.0.4: resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} dev: true @@ -5180,6 +5806,10 @@ packages: hasBin: true dev: true + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5206,6 +5836,10 @@ packages: dependencies: path-type: 4.0.0 + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + /dmg-builder@23.6.0: resolution: {integrity: sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==} dependencies: @@ -5250,6 +5884,13 @@ packages: '@leichtgewicht/ip-codec': 2.0.4 dev: true + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -5586,10 +6227,79 @@ packages: dependencies: is-arrayish: 0.2.1 + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + internal-slot: 1.0.6 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: true + /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} dev: true + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + has-tostringtag: 1.0.0 + hasown: 2.0.0 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.0 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + /es5-ext@0.10.62: resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==} engines: {node: '>=0.10'} @@ -5665,6 +6375,36 @@ packages: '@esbuild/win32-x64': 0.18.17 dev: true + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + /esbuild@0.19.5: resolution: {integrity: sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==} engines: {node: '>=12'} @@ -5717,6 +6457,138 @@ packages: engines: {node: '>=12'} dev: true + /eslint-config-prettier@9.0.0(eslint@8.54.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.54.0 + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7(supports-color@5.5.0) + is-core-module: 2.13.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.12.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.12.0(eslint@8.54.0)(typescript@5.2.2) + debug: 3.2.7(supports-color@5.5.0) + eslint: 8.54.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.12.0)(eslint@8.54.0): + resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.12.0(eslint@8.54.0)(typescript@5.2.2) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@5.5.0) + doctrine: 2.1.0 + eslint: 8.54.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.12.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-jsdoc@46.9.0(eslint@8.54.0): + resolution: {integrity: sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q==} + engines: {node: '>=16'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@es-joy/jsdoccomment': 0.41.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.3.4 + escape-string-regexp: 4.0.0 + eslint: 8.54.0 + esquery: 1.5.0 + is-builtin-module: 3.2.1 + semver: 7.5.4 + spdx-expression-parse: 3.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-prefer-arrow@1.2.3(eslint@8.54.0): + resolution: {integrity: sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==} + peerDependencies: + eslint: '>=2.0.0' + dependencies: + eslint: 8.54.0 + dev: true + + /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.1.0): + resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.54.0 + eslint-config-prettier: 9.0.0(eslint@8.54.0) + prettier: 3.1.0 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.5 + dev: true + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -5869,6 +6741,21 @@ packages: strip-final-newline: 2.0.0 dev: true + /execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + /expand-brackets@2.1.4: resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} engines: {node: '>=0.10.0'} @@ -6024,6 +6911,10 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -6326,6 +7217,12 @@ packages: - supports-color dev: false + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} engines: {node: '>=0.10.0'} @@ -6477,6 +7374,20 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6523,6 +7434,20 @@ packages: engines: {node: '>=10'} dev: true + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: true + + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -6625,6 +7550,17 @@ packages: path-is-absolute: 1.0.1 dev: true + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -6686,7 +7622,6 @@ packages: requiresBuild: true dependencies: define-properties: 1.2.1 - optional: true /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -7070,7 +8005,10 @@ packages: engines: {node: '>=0.10.0'} dependencies: ansi-regex: 2.1.1 - dev: false + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} @@ -7100,6 +8038,13 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /has-value@0.3.1: resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} engines: {node: '>=0.10.0'} @@ -7311,6 +8256,11 @@ packages: engines: {node: '>=10.17.0'} dev: true + /human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + dev: true + /iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -7357,6 +8307,11 @@ packages: minimatch: 9.0.3 dev: true + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + /ignore@5.3.0: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} engines: {node: '>= 4'} @@ -7463,6 +8418,15 @@ packages: wrap-ansi: 6.2.0 dev: true + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + hasown: 2.0.0 + side-channel: 1.0.4 + dev: true + /interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} @@ -7507,9 +8471,23 @@ packages: hasown: 2.0.0 dev: false + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + /is-binary-path@1.0.1: resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} engines: {node: '>=0.10.0'} @@ -7524,10 +8502,30 @@ packages: binary-extensions: 2.2.0 dev: true + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + /is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: false + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + /is-ci@3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} hasBin: true @@ -7547,6 +8545,13 @@ packages: hasown: 2.0.0 dev: false + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-descriptor@0.1.7: resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} engines: {node: '>= 0.4'} @@ -7569,6 +8574,12 @@ packages: hasBin: true dev: true + /is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + dev: true + /is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -7609,6 +8620,14 @@ packages: dependencies: is-extglob: 2.1.1 + /is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + dependencies: + is-docker: 3.0.0 + dev: true + /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -7622,12 +8641,24 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + /is-number-like@1.0.8: resolution: {integrity: sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==} dependencies: lodash.isfinite: 3.3.2 dev: true + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-number@3.0.0: resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} engines: {node: '>=0.10.0'} @@ -7678,6 +8709,14 @@ packages: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: false + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + /is-relative@1.0.0: resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} engines: {node: '>=0.10.0'} @@ -7685,11 +8724,43 @@ packages: is-unc-path: 1.0.0 dev: false + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.5 + dev: true + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} dev: true + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.13 + dev: true + /is-unc-path@1.0.0: resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} engines: {node: '>=0.10.0'} @@ -7715,6 +8786,12 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.5 + dev: true + /is-what@3.14.1: resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} @@ -7742,6 +8819,10 @@ packages: /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isbinaryfile@3.0.3: resolution: {integrity: sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==} engines: {node: '>=0.6.0'} @@ -7880,6 +8961,11 @@ packages: dependencies: argparse: 2.0.1 + /jsdoc-type-pratt-parser@4.0.0: + resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} + engines: {node: '>=12.0.0'} + dev: true + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -7919,6 +9005,13 @@ packages: requiresBuild: true optional: true + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -8108,6 +9201,16 @@ packages: - supports-color dev: false + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: true + /limiter@1.1.5: resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} dev: true @@ -8378,6 +9481,18 @@ packages: chalk: 4.1.2 is-unicode-supported: 0.1.0 + /loglevel-colored-level-prefix@1.0.0: + resolution: {integrity: sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==} + dependencies: + chalk: 1.1.3 + loglevel: 1.8.1 + dev: true + + /loglevel@1.8.1: + resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} + engines: {node: '>= 0.6.0'} + dev: true + /lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -8603,6 +9718,11 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + /mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -8810,6 +9930,14 @@ packages: rimraf: 2.4.5 dev: false + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + /nan@2.18.0: resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} requiresBuild: true @@ -9091,6 +10219,13 @@ packages: path-key: 3.1.1 dev: true + /npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + /nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} dependencies: @@ -9183,6 +10318,11 @@ packages: kind-of: 3.2.2 dev: false + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: true @@ -9206,7 +10346,6 @@ packages: define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 - dev: false /object.defaults@1.1.0: resolution: {integrity: sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==} @@ -9218,6 +10357,24 @@ packages: isobject: 3.0.1 dev: false + /object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.groupby@1.0.1: + resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + dev: true + /object.map@1.0.1: resolution: {integrity: sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==} engines: {node: '>=0.10.0'} @@ -9241,6 +10398,15 @@ packages: make-iterator: 1.0.1 dev: false + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + /obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} dev: true @@ -9275,6 +10441,13 @@ packages: dependencies: mimic-fn: 2.1.0 + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + /open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} @@ -9284,6 +10457,16 @@ packages: is-wsl: 2.2.0 dev: true + /open@9.1.0: + resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} + engines: {node: '>=14.16'} + dependencies: + default-browser: 4.0.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 2.2.0 + dev: true + /openurl@1.1.1: resolution: {integrity: sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==} dev: true @@ -9540,6 +10723,11 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -9597,7 +10785,6 @@ packages: /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - dev: false /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -9617,6 +10804,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + /piscina@4.1.0: resolution: {integrity: sha512-sjbLMi3sokkie+qmtZpkfMCUJTpbxJm/wvaPzU28vmYSsTSW8xk9JcFUsbqGJdtPpIQ9tuj+iDcTtgZjwnOSig==} dependencies: @@ -9684,6 +10876,45 @@ packages: engines: {node: '>=0.10.0'} dev: false + /postcss-import@15.1.0(postcss@8.4.31): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.31 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.31): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.31 + dev: true + + /postcss-load-config@4.0.2(postcss@8.4.31): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.0.0 + postcss: 8.4.31 + yaml: 2.3.4 + dev: true + /postcss-loader@7.3.3(postcss@8.4.31)(typescript@5.2.2)(webpack@5.89.0): resolution: {integrity: sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==} engines: {node: '>= 14.15.0'} @@ -9741,6 +10972,16 @@ packages: postcss: 8.4.31 dev: true + /postcss-nested@6.0.1(postcss@8.4.31): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.31 + postcss-selector-parser: 6.0.13 + dev: true + /postcss-selector-parser@6.0.13: resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} engines: {node: '>=4'} @@ -9765,6 +11006,39 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /prettier-eslint@16.1.2: + resolution: {integrity: sha512-mGFGZQbAh11FSnwW3H1zngzQYR2QMmHO8vdfgnAuzOFhnDeUZHYtwpqQvOMOMT0k818Dr1X+J4a/sVE0r34RKQ==} + engines: {node: '>=16.10.0'} + dependencies: + '@typescript-eslint/parser': 6.12.0(eslint@8.54.0)(typescript@5.2.2) + common-tags: 1.8.2 + dlv: 1.1.3 + eslint: 8.54.0 + indent-string: 4.0.0 + lodash.merge: 4.6.2 + loglevel-colored-level-prefix: 1.0.0 + prettier: 3.1.0 + pretty-format: 29.7.0 + require-relative: 0.8.7 + typescript: 5.2.2 + vue-eslint-parser: 9.3.2(eslint@8.54.0) + transitivePeerDependencies: + - supports-color + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@3.1.0: + resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + engines: {node: '>=14'} + hasBin: true + dev: true + /pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} @@ -9902,6 +11176,12 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + /read-config-file@6.2.0: resolution: {integrity: sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==} engines: {node: '>=12.0.0'} @@ -10038,6 +11318,15 @@ packages: resolution: {integrity: sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==} dev: true + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + /regexpu-core@5.3.2: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} engines: {node: '>=4'} @@ -10141,6 +11430,10 @@ packages: resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} dev: false + /require-relative@0.8.7: + resolution: {integrity: sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==} + dev: true + /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true @@ -10173,6 +11466,10 @@ packages: value-or-function: 3.0.0 dev: false + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve-url-loader@5.0.0: resolution: {integrity: sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==} engines: {node: '>=12'} @@ -10323,6 +11620,13 @@ packages: strip-json-comments: 3.1.1 dev: false + /run-applescript@5.0.0: + resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} + engines: {node: '>=12'} + dependencies: + execa: 5.1.1 + dev: true + /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -10347,12 +11651,30 @@ packages: dependencies: tslib: 2.6.2 + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-regex: 1.1.4 + dev: true + /safe-regex@1.1.0: resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} dependencies: @@ -10594,6 +11916,15 @@ packages: gopd: 1.0.1 has-property-descriptors: 1.0.1 + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.1 + dev: true + /set-value@2.0.1: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} @@ -10866,6 +12197,10 @@ packages: engines: {node: '>= 0.10'} dev: false + /spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + dev: true + /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: @@ -11015,6 +12350,31 @@ packages: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + /string_decoder@0.10.31: resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} dev: false @@ -11034,7 +12394,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: ansi-regex: 2.1.1 - dev: false /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -11080,6 +12439,11 @@ packages: engines: {node: '>=6'} dev: true + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -11094,6 +12458,20 @@ packages: through: 2.3.8 dev: true + /sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + /sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} @@ -11105,7 +12483,6 @@ packages: /supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} engines: {node: '>=0.8.0'} - dev: false /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -11142,6 +12519,45 @@ packages: engines: {node: '>=0.10'} dev: true + /synckit@0.8.5: + resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/utils': 2.4.2 + tslib: 2.6.2 + dev: true + + /tailwindcss@3.3.5: + resolution: {integrity: sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.31 + postcss-import: 15.1.0(postcss@8.4.31) + postcss-js: 4.0.1(postcss@8.4.31) + postcss-load-config: 4.0.2(postcss@8.4.31) + postcss-nested: 6.0.1(postcss@8.4.31) + postcss-selector-parser: 6.0.13 + resolve: 1.22.8 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + dev: true + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -11241,6 +12657,19 @@ packages: engines: {node: '>=8'} dev: false + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + /through2-filter@3.0.0: resolution: {integrity: sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==} dependencies: @@ -11293,6 +12722,11 @@ packages: next-tick: 1.1.0 dev: false + /titleize@3.0.0: + resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} + engines: {node: '>=12'} + dev: true + /tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} dependencies: @@ -11398,6 +12832,19 @@ packages: typescript: 5.2.2 dev: true + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + /tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -11410,6 +12857,17 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tsx@4.4.0: + resolution: {integrity: sha512-4fwcEjRUxW20ciSaMB8zkpGwCPxuRGnadDuj/pBk5S9uT29zvWz15PK36GrKJo45mSJomDxVejZ73c6lr3811Q==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.18.20 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /tuf-js@2.1.0: resolution: {integrity: sha512-eD7YPPjVlMzdggrOeE8zwoegUaG/rt6Bt3jwoQPunRiNVzgcCE009UDFJKJjG+Gk9wFu6W/Vi+P5d/5QpdD9jA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -11463,6 +12921,44 @@ packages: resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} dev: false + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + /typed-assert@1.0.9: resolution: {integrity: sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==} dev: true @@ -11487,6 +12983,15 @@ packages: hasBin: true dev: false + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.5 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + /unc-path-regex@0.1.2: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} @@ -11602,6 +13107,11 @@ packages: isobject: 3.0.1 dev: false + /untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + dev: true + /upath@1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} @@ -11802,6 +13312,24 @@ packages: fsevents: 2.3.3 dev: true + /vue-eslint-parser@9.3.2(eslint@8.54.0): + resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.4 + eslint: 8.54.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + lodash: 4.17.21 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} @@ -11996,10 +13524,31 @@ packages: webidl-conversions: 3.0.1 dev: false + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + /which-module@1.0.0: resolution: {integrity: sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==} dev: false + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -12124,6 +13673,11 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true + /yamljs@0.3.0: resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==} hasBin: true diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 1380bc3..4aae81e 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,23 +1,24 @@ import { NgModule } from '@angular/core' -import { Routes, RouterModule, RouteReuseStrategy } from '@angular/router' +import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' + import { BrowseComponent } from './components/browse/browse.component' import { SettingsComponent } from './components/settings/settings.component' import { TabPersistStrategy } from './core/tab-persist.strategy' // TODO: replace these with the correct components const routes: Routes = [ - { path: 'browse', component: BrowseComponent, data: { shouldReuse: true } }, - { path: 'library', redirectTo: '/browse' }, - { path: 'settings', component: SettingsComponent, data: { shouldReuse: true } }, - { path: 'about', redirectTo: '/browse' }, - { path: '**', redirectTo: '/browse' } + { path: 'browse', component: BrowseComponent, data: { shouldReuse: true } }, + { path: 'library', redirectTo: '/browse' }, + { path: 'settings', component: SettingsComponent, data: { shouldReuse: true } }, + { path: 'about', redirectTo: '/browse' }, + { path: '**', redirectTo: '/browse' }, ] @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule], - providers: [ - { provide: RouteReuseStrategy, useClass: TabPersistStrategy }, - ] + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], + providers: [ + { provide: RouteReuseStrategy, useClass: TabPersistStrategy }, + ], }) -export class AppRoutingModule { } \ No newline at end of file +export class AppRoutingModule { } diff --git a/src/app/app.component.html b/src/app/app.component.html index 69cc8fd..b77d2fd 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,4 @@ -
- - -
\ No newline at end of file +
+ + +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e8c299e..dd5b534 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,17 +1,18 @@ import { Component } from '@angular/core' + import { SettingsService } from './core/services/settings.service' @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styles: [] + selector: 'app-root', + templateUrl: './app.component.html', + styles: [], }) export class AppComponent { - settingsLoaded = false + settingsLoaded = false - constructor(private settingsService: SettingsService) { - // Ensure settings are loaded before rendering the application - settingsService.loadSettings().then(() => this.settingsLoaded = true) - } -} \ No newline at end of file + constructor(private settingsService: SettingsService) { + // Ensure settings are loaded before rendering the application + settingsService.loadSettings().then(() => this.settingsLoaded = true) + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3ecd56b..8f2187a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,41 +1,41 @@ -import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { BrowserModule } from '@angular/platform-browser' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './app.component' -import { ToolbarComponent } from './components/toolbar/toolbar.component' import { BrowseComponent } from './components/browse/browse.component' -import { SearchBarComponent } from './components/browse/search-bar/search-bar.component' -import { StatusBarComponent } from './components/browse/status-bar/status-bar.component' -import { ResultTableComponent } from './components/browse/result-table/result-table.component' import { ChartSidebarComponent } from './components/browse/chart-sidebar/chart-sidebar.component' import { ResultTableRowComponent } from './components/browse/result-table/result-table-row/result-table-row.component' +import { ResultTableComponent } from './components/browse/result-table/result-table.component' +import { SearchBarComponent } from './components/browse/search-bar/search-bar.component' import { DownloadsModalComponent } from './components/browse/status-bar/downloads-modal/downloads-modal.component' -import { ProgressBarDirective } from './core/directives/progress-bar.directive' -import { CheckboxDirective } from './core/directives/checkbox.directive' +import { StatusBarComponent } from './components/browse/status-bar/status-bar.component' import { SettingsComponent } from './components/settings/settings.component' -import { FormsModule } from '@angular/forms' +import { ToolbarComponent } from './components/toolbar/toolbar.component' +import { CheckboxDirective } from './core/directives/checkbox.directive' +import { ProgressBarDirective } from './core/directives/progress-bar.directive' @NgModule({ - declarations: [ - AppComponent, - ToolbarComponent, - BrowseComponent, - SearchBarComponent, - StatusBarComponent, - ResultTableComponent, - ChartSidebarComponent, - ResultTableRowComponent, - DownloadsModalComponent, - ProgressBarDirective, - CheckboxDirective, - SettingsComponent - ], - imports: [ - BrowserModule, - AppRoutingModule, - FormsModule - ], - bootstrap: [AppComponent] + declarations: [ + AppComponent, + ToolbarComponent, + BrowseComponent, + SearchBarComponent, + StatusBarComponent, + ResultTableComponent, + ChartSidebarComponent, + ResultTableRowComponent, + DownloadsModalComponent, + ProgressBarDirective, + CheckboxDirective, + SettingsComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + FormsModule, + ], + bootstrap: [AppComponent], }) -export class AppModule { } \ No newline at end of file +export class AppModule { } diff --git a/src/app/components/browse/browse.component.html b/src/app/components/browse/browse.component.html index bd722a8..c59bc87 100644 --- a/src/app/components/browse/browse.component.html +++ b/src/app/components/browse/browse.component.html @@ -1,15 +1,12 @@
-
-
- -
- -
+
+
+ +
+ +
- \ No newline at end of file + diff --git a/src/app/components/browse/browse.component.scss b/src/app/components/browse/browse.component.scss index b57ee60..e13ba8f 100644 --- a/src/app/components/browse/browse.component.scss +++ b/src/app/components/browse/browse.component.scss @@ -24,4 +24,4 @@ min-width: 175px; overflow: hidden; padding: 0; -} \ No newline at end of file +} diff --git a/src/app/components/browse/browse.component.ts b/src/app/components/browse/browse.component.ts index cf0c321..1cee053 100644 --- a/src/app/components/browse/browse.component.ts +++ b/src/app/components/browse/browse.component.ts @@ -1,34 +1,35 @@ -import { Component, ViewChild, AfterViewInit } from '@angular/core' -import { ChartSidebarComponent } from './chart-sidebar/chart-sidebar.component' -import { StatusBarComponent } from './status-bar/status-bar.component' -import { ResultTableComponent } from './result-table/result-table.component' +import { AfterViewInit, Component, ViewChild } from '@angular/core' + import { SearchService } from 'src/app/core/services/search.service' +import { ChartSidebarComponent } from './chart-sidebar/chart-sidebar.component' +import { ResultTableComponent } from './result-table/result-table.component' +import { StatusBarComponent } from './status-bar/status-bar.component' @Component({ - selector: 'app-browse', - templateUrl: './browse.component.html', - styleUrls: ['./browse.component.scss'] + selector: 'app-browse', + templateUrl: './browse.component.html', + styleUrls: ['./browse.component.scss'], }) export class BrowseComponent implements AfterViewInit { - @ViewChild('resultTable', { static: true }) resultTable: ResultTableComponent - @ViewChild('chartSidebar', { static: true }) chartSidebar: ChartSidebarComponent - @ViewChild('statusBar', { static: true }) statusBar: StatusBarComponent + @ViewChild('resultTable', { static: true }) resultTable: ResultTableComponent + @ViewChild('chartSidebar', { static: true }) chartSidebar: ChartSidebarComponent + @ViewChild('statusBar', { static: true }) statusBar: StatusBarComponent - constructor(private searchService: SearchService) { } + constructor(private searchService: SearchService) { } - ngAfterViewInit() { - const $tableColumn = $('#table-column') - $tableColumn.on('scroll', () => { - const pos = $tableColumn[0].scrollTop + $tableColumn[0].offsetHeight - const max = $tableColumn[0].scrollHeight - if (pos >= max - 5) { - this.searchService.updateScroll() - } - }) + ngAfterViewInit() { + const $tableColumn = $('#table-column') + $tableColumn.on('scroll', () => { + const pos = $tableColumn[0].scrollTop + $tableColumn[0].offsetHeight + const max = $tableColumn[0].scrollHeight + if (pos >= max - 5) { + this.searchService.updateScroll() + } + }) - this.searchService.onNewSearch(() => { - $tableColumn.scrollTop(0) - }) - } -} \ No newline at end of file + this.searchService.onNewSearch(() => { + $tableColumn.scrollTop(0) + }) + } +} diff --git a/src/app/components/browse/chart-sidebar/chart-sidebar.component.html b/src/app/components/browse/chart-sidebar/chart-sidebar.component.html index 7c19a2b..27831bb 100644 --- a/src/app/components/browse/chart-sidebar/chart-sidebar.component.html +++ b/src/app/components/browse/chart-sidebar/chart-sidebar.component.html @@ -1,46 +1,46 @@
-
- -
- -
- {{selectedVersion.chartName}} -
-
Album: {{selectedVersion.album}} ({{selectedVersion.year}})
-
Year: {{selectedVersion.year}}
-
Genre: {{selectedVersion.genre}}
-
{{charterPlural}} {{selectedVersion.charters}}
-
Tags: {{selectedVersion.tags}}
-
Audio Length: {{songLength}}
-
-
-
- -
-
Diff: {{difficulty.diffNumber}}
- {{difficulty.chartedDifficulties}} -
-
-
- -
- -
-
-
{{downloadButtonText}}
- -
-
\ No newline at end of file +
+ +
+ +
+ {{ selectedVersion.chartName }} +
+
Album: {{ selectedVersion.album }} ({{ selectedVersion.year }})
+
Year: {{ selectedVersion.year }}
+
Genre: {{ selectedVersion.genre }}
+
+ {{ charterPlural }} {{ selectedVersion.charters }} +
+
Tags: {{ selectedVersion.tags }}
+
Audio Length: {{ songLength }}
+
+
+
+ +
+
Diff: {{ difficulty.diffNumber }}
+ {{ difficulty.chartedDifficulties }} +
+
+
+ +
+
+
+
{{ downloadButtonText }}
+ +
+ diff --git a/src/app/components/browse/chart-sidebar/chart-sidebar.component.scss b/src/app/components/browse/chart-sidebar/chart-sidebar.component.scss index 8e340a5..3497b0f 100644 --- a/src/app/components/browse/chart-sidebar/chart-sidebar.component.scss +++ b/src/app/components/browse/chart-sidebar/chart-sidebar.component.scss @@ -61,4 +61,4 @@ #versionDropdown { margin: 0px 1px 1px -3px; -} \ No newline at end of file +} diff --git a/src/app/components/browse/chart-sidebar/chart-sidebar.component.ts b/src/app/components/browse/chart-sidebar/chart-sidebar.component.ts index 57a894a..d0e45fc 100644 --- a/src/app/components/browse/chart-sidebar/chart-sidebar.component.ts +++ b/src/app/components/browse/chart-sidebar/chart-sidebar.component.ts @@ -1,286 +1,288 @@ import { Component, OnInit } from '@angular/core' +import { DomSanitizer, SafeUrl } from '@angular/platform-browser' + +import { SearchService } from 'src/app/core/services/search.service' +import { SettingsService } from 'src/app/core/services/settings.service' +import { groupBy } from 'src/electron/shared/UtilFunctions' import { SongResult } from '../../../../electron/shared/interfaces/search.interface' -import { ElectronService } from '../../../core/services/electron.service' -import { VersionResult, getInstrumentIcon, Instrument, ChartedDifficulty } from '../../../../electron/shared/interfaces/songDetails.interface' +import { ChartedDifficulty, getInstrumentIcon, Instrument, VersionResult } from '../../../../electron/shared/interfaces/songDetails.interface' import { AlbumArtService } from '../../../core/services/album-art.service' import { DownloadService } from '../../../core/services/download.service' -import { groupBy } from 'src/electron/shared/UtilFunctions' -import { SearchService } from 'src/app/core/services/search.service' -import { DomSanitizer, SafeUrl } from '@angular/platform-browser' -import { SettingsService } from 'src/app/core/services/settings.service' +import { ElectronService } from '../../../core/services/electron.service' interface Difficulty { - instrument: string - diffNumber: string - chartedDifficulties: string + instrument: string + diffNumber: string + chartedDifficulties: string } @Component({ - selector: 'app-chart-sidebar', - templateUrl: './chart-sidebar.component.html', - styleUrls: ['./chart-sidebar.component.scss'] + selector: 'app-chart-sidebar', + templateUrl: './chart-sidebar.component.html', + styleUrls: ['./chart-sidebar.component.scss'], }) export class ChartSidebarComponent implements OnInit { - songResult: SongResult - selectedVersion: VersionResult - charts: VersionResult[][] + songResult: SongResult + selectedVersion: VersionResult + charts: VersionResult[][] - constructor( - private electronService: ElectronService, - private albumArtService: AlbumArtService, - private downloadService: DownloadService, - private searchService: SearchService, - private sanitizer: DomSanitizer, - public settingsService: SettingsService - ) { } + albumArtSrc: SafeUrl = '' + charterPlural: string + songLength: string + difficultiesList: Difficulty[] + downloadButtonText: string - ngOnInit() { - this.searchService.onNewSearch(() => { - this.selectVersion(undefined) - this.songResult = undefined - }) - } + constructor( + private electronService: ElectronService, + private albumArtService: AlbumArtService, + private downloadService: DownloadService, + private searchService: SearchService, + private sanitizer: DomSanitizer, + public settingsService: SettingsService + ) { } - /** - * Displays the information for the selected song. - */ - async onRowClicked(result: SongResult) { - if (this.songResult == undefined || result.id != this.songResult.id) { // Clicking the same row again will not reload - this.songResult = result - const albumArt = this.albumArtService.getImage(result.id) - const results = await this.electronService.invoke('song-details', result.id) - this.charts = groupBy(results, 'chartID').sort((v1, v2) => v1[0].chartName.length - v2[0].chartName.length) - this.sortCharts() - await this.selectChart(this.charts[0][0].chartID) - this.initChartDropdown() + ngOnInit() { + this.searchService.onNewSearch(() => { + this.selectVersion(undefined) + this.songResult = undefined + }) + } - this.updateAlbumArtSrc(await albumArt) - } - } + /** + * Displays the information for the selected song. + */ + async onRowClicked(result: SongResult) { + if (this.songResult == undefined || result.id != this.songResult.id) { // Clicking the same row again will not reload + this.songResult = result + const albumArt = this.albumArtService.getImage(result.id) + const results = await this.electronService.invoke('song-details', result.id) + this.charts = groupBy(results, 'chartID').sort((v1, v2) => v1[0].chartName.length - v2[0].chartName.length) + this.sortCharts() + await this.selectChart(this.charts[0][0].chartID) + this.initChartDropdown() - /** - * Sorts `this.charts` and its subarrays in the correct order. - * The chart dropdown should display in a random order, but verified charters are prioritized. - * The version dropdown should be ordered by lastModified date. - * (but prefer the non-pack version if it's only a few days older) - */ - private sortCharts() { - for (const chart of this.charts) { - // TODO: sort by verified charter - this.searchService.sortChart(chart) - } - } + this.updateAlbumArtSrc(await albumArt) + } + } - albumArtSrc: SafeUrl = '' - /** - * Updates the sidebar to display the album art. - */ - updateAlbumArtSrc(albumArtBase64String?: string) { - if (albumArtBase64String) { - this.albumArtSrc = this.sanitizer.bypassSecurityTrustUrl('data:image/jpg;base64,' + albumArtBase64String) - } else { - this.albumArtSrc = null - } - } + /** + * Sorts `this.charts` and its subarrays in the correct order. + * The chart dropdown should display in a random order, but verified charters are prioritized. + * The version dropdown should be ordered by lastModified date. + * (but prefer the non-pack version if it's only a few days older) + */ + private sortCharts() { + for (const chart of this.charts) { + // TODO: sort by verified charter + this.searchService.sortChart(chart) + } + } - /** - * Initializes the chart dropdown from `this.charts` (or removes it if there's only one chart). - */ - private initChartDropdown() { - const values = this.charts.map(chart => { - const version = chart[0] - return { - value: version.chartID, - text: version.chartName, - name: `${version.chartName} [${version.charters}]` - } - }) - const $chartDropdown = $('#chartDropdown') - $chartDropdown.dropdown('setup menu', { values }) - $chartDropdown.dropdown('setting', 'onChange', (chartID: number) => this.selectChart(chartID)) - $chartDropdown.dropdown('set selected', values[0].value) - } + /** + * Updates the sidebar to display the album art. + */ + updateAlbumArtSrc(albumArtBase64String?: string) { + if (albumArtBase64String) { + this.albumArtSrc = this.sanitizer.bypassSecurityTrustUrl('data:image/jpg;base64,' + albumArtBase64String) + } else { + this.albumArtSrc = null + } + } - private async selectChart(chartID: number) { - const chart = this.charts.find(chart => chart[0].chartID == chartID) - await this.selectVersion(chart[0]) - this.initVersionDropdown() - } + /** + * Initializes the chart dropdown from `this.charts` (or removes it if there's only one chart). + */ + private initChartDropdown() { + const values = this.charts.map(chart => { + const version = chart[0] + return { + value: version.chartID, + text: version.chartName, + name: `${version.chartName} [${version.charters}]`, + } + }) + const $chartDropdown = $('#chartDropdown') + $chartDropdown.dropdown('setup menu', { values }) + $chartDropdown.dropdown('setting', 'onChange', (chartID: number) => this.selectChart(chartID)) + $chartDropdown.dropdown('set selected', values[0].value) + } - /** - * Updates the sidebar to display the metadata for `selectedVersion`. - */ - async selectVersion(selectedVersion: VersionResult) { - this.selectedVersion = selectedVersion - await new Promise(resolve => setTimeout(() => resolve(), 0)) // Wait for *ngIf to update DOM + private async selectChart(chartID: number) { + const chart = this.charts.find(chart => chart[0].chartID == chartID) + await this.selectVersion(chart[0]) + this.initVersionDropdown() + } - if (this.selectedVersion != undefined) { - this.updateCharterPlural() - this.updateSongLength() - this.updateDifficultiesList() - this.updateDownloadButtonText() - } - } + /** + * Updates the sidebar to display the metadata for `selectedVersion`. + */ + async selectVersion(selectedVersion: VersionResult) { + this.selectedVersion = selectedVersion + await new Promise(resolve => setTimeout(() => resolve(), 0)) // Wait for *ngIf to update DOM - charterPlural: string - /** - * Chooses to display 'Charter:' or 'Charters:'. - */ - private updateCharterPlural() { - this.charterPlural = this.selectedVersion.charterIDs.split('&').length == 1 ? 'Charter:' : 'Charters:' - } + if (this.selectedVersion != undefined) { + this.updateCharterPlural() + this.updateSongLength() + this.updateDifficultiesList() + this.updateDownloadButtonText() + } + } - songLength: string - /** - * Converts `this.selectedVersion.chartMetadata.length` into a readable duration. - */ - private updateSongLength() { - let seconds = this.selectedVersion.songLength - if (seconds < 60) { this.songLength = `${seconds} second${seconds == 1 ? '' : 's'}`; return } - let minutes = Math.floor(seconds / 60) - let hours = 0 - while (minutes > 59) { - hours++ - minutes -= 60 - } - seconds = Math.floor(seconds % 60) - this.songLength = `${hours == 0 ? '' : hours + ':'}${minutes == 0 ? '' : minutes + ':'}${seconds < 10 ? '0' + seconds : seconds}` - } + /** + * Chooses to display 'Charter:' or 'Charters:'. + */ + private updateCharterPlural() { + this.charterPlural = this.selectedVersion.charterIDs.split('&').length == 1 ? 'Charter:' : 'Charters:' + } - difficultiesList: Difficulty[] - /** - * Updates `dfficultiesList` with the difficulty information for the selected version. - */ - private updateDifficultiesList() { - const instruments = Object.keys(this.selectedVersion.chartData.noteCounts) as Instrument[] - this.difficultiesList = [] - for (const instrument of instruments) { - if (instrument != 'undefined') { - this.difficultiesList.push({ - instrument: getInstrumentIcon(instrument), - diffNumber: this.getDiffNumber(instrument), - chartedDifficulties: this.getChartedDifficultiesText(instrument) - }) - } - } - } + /** + * Converts `this.selectedVersion.chartMetadata.length` into a readable duration. + */ + private updateSongLength() { + let seconds = this.selectedVersion.songLength + if (seconds < 60) { this.songLength = `${seconds} second${seconds == 1 ? '' : 's'}`; return } + let minutes = Math.floor(seconds / 60) + let hours = 0 + while (minutes > 59) { + hours++ + minutes -= 60 + } + seconds = Math.floor(seconds % 60) + this.songLength = `${hours == 0 ? '' : hours + ':'}${minutes == 0 ? '' : minutes + ':'}${seconds < 10 ? '0' + seconds : seconds}` + } - /** - * @returns a string describing the difficulty number in the selected version. - */ - private getDiffNumber(instrument: Instrument) { - const diffNumber: number = this.selectedVersion[`diff_${instrument}`] - return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber) - } + /** + * Updates `dfficultiesList` with the difficulty information for the selected version. + */ + private updateDifficultiesList() { + const instruments = Object.keys(this.selectedVersion.chartData.noteCounts) as Instrument[] + this.difficultiesList = [] + for (const instrument of instruments) { + if (instrument != 'undefined') { + this.difficultiesList.push({ + instrument: getInstrumentIcon(instrument), + diffNumber: this.getDiffNumber(instrument), + chartedDifficulties: this.getChartedDifficultiesText(instrument), + }) + } + } + } - /** - * @returns a string describing the list of charted difficulties in the selected version. - */ - private getChartedDifficultiesText(instrument: Instrument) { - const difficulties = Object.keys(this.selectedVersion.chartData.noteCounts[instrument]) as ChartedDifficulty[] - if (difficulties.length == 4) { return 'Full Difficulty' } - const difficultyNames = [] - if (difficulties.includes('x')) { difficultyNames.push('Expert') } - if (difficulties.includes('h')) { difficultyNames.push('Hard') } - if (difficulties.includes('m')) { difficultyNames.push('Medium') } - if (difficulties.includes('e')) { difficultyNames.push('Easy') } + /** + * @returns a string describing the difficulty number in the selected version. + */ + private getDiffNumber(instrument: Instrument) { + const diffNumber: number = this.selectedVersion[`diff_${instrument}`] + return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber) + } - return difficultyNames.join(', ') - } + /** + * @returns a string describing the list of charted difficulties in the selected version. + */ + private getChartedDifficultiesText(instrument: Instrument) { + const difficulties = Object.keys(this.selectedVersion.chartData.noteCounts[instrument]) as ChartedDifficulty[] + if (difficulties.length == 4) { return 'Full Difficulty' } + const difficultyNames = [] + if (difficulties.includes('x')) { difficultyNames.push('Expert') } + if (difficulties.includes('h')) { difficultyNames.push('Hard') } + if (difficulties.includes('m')) { difficultyNames.push('Medium') } + if (difficulties.includes('e')) { difficultyNames.push('Easy') } - downloadButtonText: string - /** - * Chooses the text to display on the download button. - */ - private updateDownloadButtonText() { - this.downloadButtonText = 'Download' - if (this.selectedVersion.driveData.inChartPack) { - this.downloadButtonText += ' Chart Pack' - } else { - this.downloadButtonText += (this.selectedVersion.driveData.isArchive ? ' Archive' : ' Files') - } + return difficultyNames.join(', ') + } - if (this.getSelectedChartVersions().length > 1) { - if (this.selectedVersion.versionID == this.selectedVersion.latestVersionID) { - this.downloadButtonText += ' (Latest)' - } else { - this.downloadButtonText += ` (${this.getLastModifiedText(this.selectedVersion.lastModified)})` - } - } - } + /** + * Chooses the text to display on the download button. + */ + private updateDownloadButtonText() { + this.downloadButtonText = 'Download' + if (this.selectedVersion.driveData.inChartPack) { + this.downloadButtonText += ' Chart Pack' + } else { + this.downloadButtonText += (this.selectedVersion.driveData.isArchive ? ' Archive' : ' Files') + } - /** - * Initializes the version dropdown from `this.selectedVersion` (or removes it if there's only one version). - */ - private initVersionDropdown() { - const $versionDropdown = $('#versionDropdown') - const versions = this.getSelectedChartVersions() - const values = versions.map(version => ({ - value: version.versionID, - text: 'Uploaded ' + this.getLastModifiedText(version.lastModified), - name: 'Uploaded ' + this.getLastModifiedText(version.lastModified) - })) + if (this.getSelectedChartVersions().length > 1) { + if (this.selectedVersion.versionID == this.selectedVersion.latestVersionID) { + this.downloadButtonText += ' (Latest)' + } else { + this.downloadButtonText += ` (${this.getLastModifiedText(this.selectedVersion.lastModified)})` + } + } + } - $versionDropdown.dropdown('setup menu', { values }) - $versionDropdown.dropdown('setting', 'onChange', (versionID: number) => { - this.selectVersion(versions.find(version => version.versionID == versionID)) - }) - $versionDropdown.dropdown('set selected', values[0].value) - } + /** + * Initializes the version dropdown from `this.selectedVersion` (or removes it if there's only one version). + */ + private initVersionDropdown() { + const $versionDropdown = $('#versionDropdown') + const versions = this.getSelectedChartVersions() + const values = versions.map(version => ({ + value: version.versionID, + text: 'Uploaded ' + this.getLastModifiedText(version.lastModified), + name: 'Uploaded ' + this.getLastModifiedText(version.lastModified), + })) - /** - * Returns the list of versions for the selected chart, sorted by `lastModified`. - */ - getSelectedChartVersions() { - return this.charts.find(chart => chart[0].chartID == this.selectedVersion.chartID) - } + $versionDropdown.dropdown('setup menu', { values }) + $versionDropdown.dropdown('setting', 'onChange', (versionID: number) => { + this.selectVersion(versions.find(version => version.versionID == versionID)) + }) + $versionDropdown.dropdown('set selected', values[0].value) + } - /** - * Converts the value to a user-readable format. - * @param lastModified The UNIX timestamp for the lastModified date. - */ - private getLastModifiedText(lastModified: string) { - const date = new Date(lastModified) - const day = date.getDate().toString().padStart(2, '0') - const month = (date.getMonth() + 1).toString().padStart(2, '0') - const year = date.getFullYear().toString().substr(-2) - return `${month}/${day}/${year}` - } + /** + * Returns the list of versions for the selected chart, sorted by `lastModified`. + */ + getSelectedChartVersions() { + return this.charts.find(chart => chart[0].chartID == this.selectedVersion.chartID) + } - /** - * Opens the proxy link or source folder in the default browser. - */ - onSourceLinkClicked() { - const source = this.selectedVersion.driveData.source - this.electronService.sendIPC('open-url', source.proxyLink ?? `https://drive.google.com/drive/folders/${source.sourceDriveID}`) - } + /** + * Converts the value to a user-readable format. + * @param lastModified The UNIX timestamp for the lastModified date. + */ + private getLastModifiedText(lastModified: string) { + const date = new Date(lastModified) + const day = date.getDate().toString().padStart(2, '0') + const month = (date.getMonth() + 1).toString().padStart(2, '0') + const year = date.getFullYear().toString().substr(-2) + return `${month}/${day}/${year}` + } - /** - * @returns `true` if the source folder button should be shown. - */ - shownFolderButton() { - const driveData = this.selectedVersion.driveData - return driveData.source.proxyLink || driveData.source.sourceDriveID != driveData.folderID - } + /** + * Opens the proxy link or source folder in the default browser. + */ + onSourceLinkClicked() { + const source = this.selectedVersion.driveData.source + this.electronService.sendIPC('open-url', source.proxyLink ?? `https://drive.google.com/drive/folders/${source.sourceDriveID}`) + } - /** - * Opens the chart folder in the default browser. - */ - onFolderButtonClicked() { - this.electronService.sendIPC('open-url', `https://drive.google.com/drive/folders/${this.selectedVersion.driveData.folderID}`) - } + /** + * @returns `true` if the source folder button should be shown. + */ + shownFolderButton() { + const driveData = this.selectedVersion.driveData + return driveData.source.proxyLink || driveData.source.sourceDriveID != driveData.folderID + } - /** - * Adds the selected version to the download queue. - */ - onDownloadClicked() { - this.downloadService.addDownload( - this.selectedVersion.versionID, { - chartName: this.selectedVersion.chartName, - artist: this.songResult.artist, - charter: this.selectedVersion.charters, - driveData: this.selectedVersion.driveData - }) - } -} \ No newline at end of file + /** + * Opens the chart folder in the default browser. + */ + onFolderButtonClicked() { + this.electronService.sendIPC('open-url', `https://drive.google.com/drive/folders/${this.selectedVersion.driveData.folderID}`) + } + + /** + * Adds the selected version to the download queue. + */ + onDownloadClicked() { + this.downloadService.addDownload( + this.selectedVersion.versionID, { + chartName: this.selectedVersion.chartName, + artist: this.songResult.artist, + charter: this.selectedVersion.charters, + driveData: this.selectedVersion.driveData, + }) + } +} diff --git a/src/app/components/browse/result-table/result-table-row/result-table-row.component.html b/src/app/components/browse/result-table/result-table-row/result-table-row.component.html index d048624..eaf63ea 100644 --- a/src/app/components/browse/result-table/result-table-row/result-table-row.component.html +++ b/src/app/components/browse/result-table/result-table-row/result-table-row.component.html @@ -1,9 +1,12 @@ -
- -
+
+ +
-{{result.chartCount}}{{result.name}} -{{result.artist}} -{{result.album || 'Various'}} -{{result.genre || 'Various'}} \ No newline at end of file + + {{ result.chartCount }}{{ result.name }} + +{{ result.artist }} +{{ result.album || 'Various' }} +{{ result.genre || 'Various' }} diff --git a/src/app/components/browse/result-table/result-table-row/result-table-row.component.scss b/src/app/components/browse/result-table/result-table-row/result-table-row.component.scss index 4e2a229..b11b505 100644 --- a/src/app/components/browse/result-table/result-table-row/result-table-row.component.scss +++ b/src/app/components/browse/result-table/result-table-row/result-table-row.component.scss @@ -7,4 +7,4 @@ border-radius: 3px; padding: 1px 4px 2px 4px; margin-right: 3px; -} \ No newline at end of file +} diff --git a/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts b/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts index 5322afa..c6ae55d 100644 --- a/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts +++ b/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts @@ -1,39 +1,40 @@ -import { Component, AfterViewInit, Input, ViewChild, ElementRef } from '@angular/core' +import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core' + import { SongResult } from '../../../../../electron/shared/interfaces/search.interface' import { SelectionService } from '../../../../core/services/selection.service' @Component({ - selector: 'tr[app-result-table-row]', - templateUrl: './result-table-row.component.html', - styleUrls: ['./result-table-row.component.scss'] + selector: 'tr[app-result-table-row]', + templateUrl: './result-table-row.component.html', + styleUrls: ['./result-table-row.component.scss'], }) export class ResultTableRowComponent implements AfterViewInit { - @Input() result: SongResult + @Input() result: SongResult - @ViewChild('checkbox', { static: true }) checkbox: ElementRef + @ViewChild('checkbox', { static: true }) checkbox: ElementRef - constructor(private selectionService: SelectionService) { } + constructor(private selectionService: SelectionService) { } - get songID() { - return this.result.id - } + get songID() { + return this.result.id + } - ngAfterViewInit() { - this.selectionService.onSelectionChanged(this.songID, (isChecked) => { - if (isChecked) { - $(this.checkbox.nativeElement).checkbox('check') - } else { - $(this.checkbox.nativeElement).checkbox('uncheck') - } - }) + ngAfterViewInit() { + this.selectionService.onSelectionChanged(this.songID, isChecked => { + if (isChecked) { + $(this.checkbox.nativeElement).checkbox('check') + } else { + $(this.checkbox.nativeElement).checkbox('uncheck') + } + }) - $(this.checkbox.nativeElement).checkbox({ - onChecked: () => { - this.selectionService.selectSong(this.songID) - }, - onUnchecked: () => { - this.selectionService.deselectSong(this.songID) - } - }) - } -} \ No newline at end of file + $(this.checkbox.nativeElement).checkbox({ + onChecked: () => { + this.selectionService.selectSong(this.songID) + }, + onUnchecked: () => { + this.selectionService.deselectSong(this.songID) + }, + }) + } +} diff --git a/src/app/components/browse/result-table/result-table.component.html b/src/app/components/browse/result-table/result-table.component.html index c10b968..6700527 100644 --- a/src/app/components/browse/result-table/result-table.component.html +++ b/src/app/components/browse/result-table/result-table.component.html @@ -1,27 +1,30 @@ - - - - - - - - - - - - - - - - -
-
- -
-
NameArtistAlbumGenre
\ No newline at end of file + + + + + + + + + + + + + + + + +
+
+ +
+
NameArtistAlbumGenre
diff --git a/src/app/components/browse/result-table/result-table.component.ts b/src/app/components/browse/result-table/result-table.component.ts index 4e1aa8b..afc50fe 100644 --- a/src/app/components/browse/result-table/result-table.component.ts +++ b/src/app/components/browse/result-table/result-table.component.ts @@ -1,83 +1,85 @@ -import { Component, Output, EventEmitter, ViewChildren, QueryList, ViewChild, OnInit } from '@angular/core' +import { Component, EventEmitter, OnInit, Output, QueryList, ViewChild, ViewChildren } from '@angular/core' + +import Comparators from 'comparators' + +import { SettingsService } from 'src/app/core/services/settings.service' import { SongResult } from '../../../../electron/shared/interfaces/search.interface' -import { ResultTableRowComponent } from './result-table-row/result-table-row.component' import { CheckboxDirective } from '../../../core/directives/checkbox.directive' import { SearchService } from '../../../core/services/search.service' import { SelectionService } from '../../../core/services/selection.service' -import { SettingsService } from 'src/app/core/services/settings.service' -import Comparators from 'comparators' +import { ResultTableRowComponent } from './result-table-row/result-table-row.component' @Component({ - selector: 'app-result-table', - templateUrl: './result-table.component.html', - styleUrls: ['./result-table.component.scss'] + selector: 'app-result-table', + templateUrl: './result-table.component.html', + styleUrls: ['./result-table.component.scss'], }) export class ResultTableComponent implements OnInit { - @Output() rowClicked = new EventEmitter() + @Output() rowClicked = new EventEmitter() - @ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective - @ViewChildren('tableRow') tableRows: QueryList + @ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective + @ViewChildren('tableRow') tableRows: QueryList - results: SongResult[] = [] - activeRowID: number = null - sortDirection: 'ascending' | 'descending' = 'descending' - sortColumn: 'name' | 'artist' | 'album' | 'genre' | null = null + results: SongResult[] = [] + activeRowID: number = null + sortDirection: 'ascending' | 'descending' = 'descending' + sortColumn: 'name' | 'artist' | 'album' | 'genre' | null = null - constructor( - private searchService: SearchService, - private selectionService: SelectionService, - public settingsService: SettingsService - ) { } + constructor( + private searchService: SearchService, + private selectionService: SelectionService, + public settingsService: SettingsService + ) { } - ngOnInit() { - this.selectionService.onSelectAllChanged((selected) => { - this.checkboxColumn.check(selected) - }) + ngOnInit() { + this.selectionService.onSelectAllChanged(selected => { + this.checkboxColumn.check(selected) + }) - this.searchService.onSearchChanged(results => { - this.activeRowID = null - this.results = results - this.updateSort() - }) + this.searchService.onSearchChanged(results => { + this.activeRowID = null + this.results = results + this.updateSort() + }) - this.searchService.onNewSearch(() => { - this.sortColumn = null - }) - } + this.searchService.onNewSearch(() => { + this.sortColumn = null + }) + } - onRowClicked(result: SongResult) { - this.activeRowID = result.id - this.rowClicked.emit(result) - } + onRowClicked(result: SongResult) { + this.activeRowID = result.id + this.rowClicked.emit(result) + } - onColClicked(column: 'name' | 'artist' | 'album' | 'genre') { - if (this.results.length == 0) { return } - if (this.sortColumn != column) { - this.sortColumn = column - this.sortDirection = 'descending' - } else if (this.sortDirection == 'descending') { - this.sortDirection = 'ascending' - } else { - this.sortDirection = 'descending' - } - this.updateSort() - } + onColClicked(column: 'name' | 'artist' | 'album' | 'genre') { + if (this.results.length == 0) { return } + if (this.sortColumn != column) { + this.sortColumn = column + this.sortDirection = 'descending' + } else if (this.sortDirection == 'descending') { + this.sortDirection = 'ascending' + } else { + this.sortDirection = 'descending' + } + this.updateSort() + } - private updateSort() { - if (this.sortColumn != null) { - this.results.sort(Comparators.comparing(this.sortColumn, { reversed: this.sortDirection == 'ascending' })) - } - } + private updateSort() { + if (this.sortColumn != null) { + this.results.sort(Comparators.comparing(this.sortColumn, { reversed: this.sortDirection == 'ascending' })) + } + } - /** - * Called when the user checks the `checkboxColumn`. - */ - checkAll(isChecked: boolean) { - if (isChecked) { - this.selectionService.selectAll() - } else { - this.selectionService.deselectAll() - } - } -} \ No newline at end of file + /** + * Called when the user checks the `checkboxColumn`. + */ + checkAll(isChecked: boolean) { + if (isChecked) { + this.selectionService.selectAll() + } else { + this.selectionService.deselectAll() + } + } +} diff --git a/src/app/components/browse/search-bar/search-bar.component.html b/src/app/components/browse/search-bar/search-bar.component.html index a047280..80118f6 100644 --- a/src/app/components/browse/search-bar/search-bar.component.html +++ b/src/app/components/browse/search-bar/search-bar.component.html @@ -1,235 +1,240 @@
-
-
-
Search for
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
-
Must include
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
-
Instruments
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
Charted difficulties
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
Difficulty range
-
-
-
-
-
-
\ No newline at end of file +
+
+
Search for
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
Must include
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
Instruments
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
Charted difficulties
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
Difficulty range
+
+
+
+
+
+ diff --git a/src/app/components/browse/search-bar/search-bar.component.scss b/src/app/components/browse/search-bar/search-bar.component.scss index 6ebad67..7adc781 100644 --- a/src/app/components/browse/search-bar/search-bar.component.scss +++ b/src/app/components/browse/search-bar/search-bar.component.scss @@ -48,4 +48,4 @@ visibility 0s linear 350ms, max-width 0s linear 350ms, max-height 0s linear 350ms !important; -} \ No newline at end of file +} diff --git a/src/app/components/browse/search-bar/search-bar.component.ts b/src/app/components/browse/search-bar/search-bar.component.ts index 9c84385..38c4079 100644 --- a/src/app/components/browse/search-bar/search-bar.component.ts +++ b/src/app/components/browse/search-bar/search-bar.component.ts @@ -1,74 +1,75 @@ -import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core' +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core' + import { SearchService } from 'src/app/core/services/search.service' -import { getDefaultSearch, SongSearch } from 'src/electron/shared/interfaces/search.interface' +import { getDefaultSearch } from 'src/electron/shared/interfaces/search.interface' @Component({ - selector: 'app-search-bar', - templateUrl: './search-bar.component.html', - styleUrls: ['./search-bar.component.scss'] + selector: 'app-search-bar', + templateUrl: './search-bar.component.html', + styleUrls: ['./search-bar.component.scss'], }) export class SearchBarComponent implements AfterViewInit { - @ViewChild('searchIcon', { static: true }) searchIcon: ElementRef - @ViewChild('quantityDropdown', { static: true }) quantityDropdown: ElementRef - @ViewChild('similarityDropdown', { static: true }) similarityDropdown: ElementRef - @ViewChild('diffSlider', { static: true }) diffSlider: ElementRef + @ViewChild('searchIcon', { static: true }) searchIcon: ElementRef + @ViewChild('quantityDropdown', { static: true }) quantityDropdown: ElementRef + @ViewChild('similarityDropdown', { static: true }) similarityDropdown: ElementRef + @ViewChild('diffSlider', { static: true }) diffSlider: ElementRef - isError = false - showAdvanced = false - searchSettings = getDefaultSearch() - private sliderInitialized = false + isError = false + showAdvanced = false + searchSettings = getDefaultSearch() + private sliderInitialized = false - constructor(public searchService: SearchService) { } + constructor(public searchService: SearchService) { } - ngAfterViewInit() { - $(this.searchIcon.nativeElement).popup({ - onShow: () => this.isError // Only show the popup if there is an error - }) - this.searchService.onSearchErrorStateUpdate((isError) => { - this.isError = isError - }) - $(this.quantityDropdown.nativeElement).dropdown({ - onChange: (value: string) => { - this.searchSettings.quantity = value as 'all' | 'any' - } - }) - $(this.similarityDropdown.nativeElement).dropdown({ - onChange: (value: string) => { - this.searchSettings.similarity = value as 'similar' | 'exact' - } - }) - } + ngAfterViewInit() { + $(this.searchIcon.nativeElement).popup({ + onShow: () => this.isError, // Only show the popup if there is an error + }) + this.searchService.onSearchErrorStateUpdate(isError => { + this.isError = isError + }) + $(this.quantityDropdown.nativeElement).dropdown({ + onChange: (value: string) => { + this.searchSettings.quantity = value as 'all' | 'any' + }, + }) + $(this.similarityDropdown.nativeElement).dropdown({ + onChange: (value: string) => { + this.searchSettings.similarity = value as 'similar' | 'exact' + }, + }) + } - onSearch(query: string) { - this.searchSettings.query = query - this.searchSettings.limit = 50 + 1 - this.searchSettings.offset = 0 - this.searchService.newSearch(this.searchSettings) - } + onSearch(query: string) { + this.searchSettings.query = query + this.searchSettings.limit = 50 + 1 + this.searchSettings.offset = 0 + this.searchService.newSearch(this.searchSettings) + } - onAdvancedSearchClick() { - this.showAdvanced = !this.showAdvanced + onAdvancedSearchClick() { + this.showAdvanced = !this.showAdvanced - if (!this.sliderInitialized) { - setTimeout(() => { // Initialization requires this element to not be collapsed - $(this.diffSlider.nativeElement).slider({ - min: 0, - max: 6, - start: 0, - end: 6, - step: 1, - onChange: (_length: number, min: number, max: number) => { - this.searchSettings.minDiff = min - this.searchSettings.maxDiff = max - } - }) - }, 50) - this.sliderInitialized = true - } - } + if (!this.sliderInitialized) { + setTimeout(() => { // Initialization requires this element to not be collapsed + $(this.diffSlider.nativeElement).slider({ + min: 0, + max: 6, + start: 0, + end: 6, + step: 1, + onChange: (_length: number, min: number, max: number) => { + this.searchSettings.minDiff = min + this.searchSettings.maxDiff = max + }, + }) + }, 50) + this.sliderInitialized = true + } + } - isLoading() { - return this.searchService.isLoading() - } -} \ No newline at end of file + isLoading() { + return this.searchService.isLoading() + } +} diff --git a/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.html b/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.html index 5980ed1..139a2b3 100644 --- a/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.html +++ b/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.html @@ -1,30 +1,28 @@
-
+
+

+ {{ download.title }} + +

-

- {{download.title}} - -

+
+ {{ download.header }} + {{ download.description }} + + {{ download.description }} + +
-
- {{download.header}} - {{download.description}} - - {{download.description}} - -
- -
-
-
-
-
-
- -
-
-
\ No newline at end of file +
+
+
+
+
+
+ +
+
+ diff --git a/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.scss b/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.scss index 2e8f46d..d4ee7b5 100644 --- a/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.scss +++ b/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.scss @@ -23,4 +23,4 @@ i.close.icon, a { cursor: pointer; -} \ No newline at end of file +} diff --git a/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.ts b/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.ts index a228a07..5cdbe9c 100644 --- a/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.ts +++ b/src/app/components/browse/status-bar/downloads-modal/downloads-modal.component.ts @@ -1,55 +1,56 @@ -import { Component, ChangeDetectorRef } from '@angular/core' +import { ChangeDetectorRef, Component } from '@angular/core' + import { DownloadProgress } from '../../../../../electron/shared/interfaces/download.interface' import { DownloadService } from '../../../../core/services/download.service' import { ElectronService } from '../../../../core/services/electron.service' @Component({ - selector: 'app-downloads-modal', - templateUrl: './downloads-modal.component.html', - styleUrls: ['./downloads-modal.component.scss'] + selector: 'app-downloads-modal', + templateUrl: './downloads-modal.component.html', + styleUrls: ['./downloads-modal.component.scss'], }) export class DownloadsModalComponent { - downloads: DownloadProgress[] = [] + downloads: DownloadProgress[] = [] - constructor(private electronService: ElectronService, private downloadService: DownloadService, ref: ChangeDetectorRef) { - electronService.receiveIPC('queue-updated', (order) => { - this.downloads.sort((a, b) => order.indexOf(a.versionID) - order.indexOf(b.versionID)) - }) + constructor(private electronService: ElectronService, private downloadService: DownloadService, ref: ChangeDetectorRef) { + electronService.receiveIPC('queue-updated', order => { + this.downloads.sort((a, b) => order.indexOf(a.versionID) - order.indexOf(b.versionID)) + }) - downloadService.onDownloadUpdated(download => { - const index = this.downloads.findIndex(thisDownload => thisDownload.versionID == download.versionID) - if (download.type == 'cancel') { - this.downloads = this.downloads.filter(thisDownload => thisDownload.versionID != download.versionID) - } else if (index == -1) { - this.downloads.push(download) - } else { - this.downloads[index] = download - } - ref.detectChanges() - }) - } + downloadService.onDownloadUpdated(download => { + const index = this.downloads.findIndex(thisDownload => thisDownload.versionID == download.versionID) + if (download.type == 'cancel') { + this.downloads = this.downloads.filter(thisDownload => thisDownload.versionID != download.versionID) + } else if (index == -1) { + this.downloads.push(download) + } else { + this.downloads[index] = download + } + ref.detectChanges() + }) + } - trackByVersionID(_index: number, item: DownloadProgress) { - return item.versionID - } + trackByVersionID(_index: number, item: DownloadProgress) { + return item.versionID + } - cancelDownload(versionID: number) { - this.downloadService.cancelDownload(versionID) - } + cancelDownload(versionID: number) { + this.downloadService.cancelDownload(versionID) + } - retryDownload(versionID: number) { - this.downloadService.retryDownload(versionID) - } + retryDownload(versionID: number) { + this.downloadService.retryDownload(versionID) + } - getBackgroundColor(download: DownloadProgress) { - switch (download.type) { - case 'error': return '#a63a3a' - default: return undefined - } - } + getBackgroundColor(download: DownloadProgress) { + switch (download.type) { + case 'error': return '#a63a3a' + default: return undefined + } + } - openFolder(filepath: string) { - this.electronService.showFolder(filepath) - } -} \ No newline at end of file + openFolder(filepath: string) { + this.electronService.showFolder(filepath) + } +} diff --git a/src/app/components/browse/status-bar/status-bar.component.html b/src/app/components/browse/status-bar/status-bar.component.html index bd490bf..31aa53c 100644 --- a/src/app/components/browse/status-bar/status-bar.component.html +++ b/src/app/components/browse/status-bar/status-bar.component.html @@ -1,42 +1,44 @@ \ No newline at end of file + + diff --git a/src/app/components/browse/status-bar/status-bar.component.scss b/src/app/components/browse/status-bar/status-bar.component.scss index 9f58dcf..0e6c7fd 100644 --- a/src/app/components/browse/status-bar/status-bar.component.scss +++ b/src/app/components/browse/status-bar/status-bar.component.scss @@ -20,4 +20,4 @@ margin-right: 30px; } } -} \ No newline at end of file +} diff --git a/src/app/components/browse/status-bar/status-bar.component.ts b/src/app/components/browse/status-bar/status-bar.component.ts index 260f685..874d981 100644 --- a/src/app/components/browse/status-bar/status-bar.component.ts +++ b/src/app/components/browse/status-bar/status-bar.component.ts @@ -1,114 +1,115 @@ -import { Component, ChangeDetectorRef } from '@angular/core' +import { ChangeDetectorRef, Component } from '@angular/core' + +import { VersionResult } from '../../../../electron/shared/interfaces/songDetails.interface' +import { groupBy } from '../../../../electron/shared/UtilFunctions' import { DownloadService } from '../../../core/services/download.service' import { ElectronService } from '../../../core/services/electron.service' -import { groupBy } from '../../../../electron/shared/UtilFunctions' -import { VersionResult } from '../../../../electron/shared/interfaces/songDetails.interface' import { SearchService } from '../../../core/services/search.service' import { SelectionService } from '../../../core/services/selection.service' @Component({ - selector: 'app-status-bar', - templateUrl: './status-bar.component.html', - styleUrls: ['./status-bar.component.scss'] + selector: 'app-status-bar', + templateUrl: './status-bar.component.html', + styleUrls: ['./status-bar.component.scss'], }) export class StatusBarComponent { - resultCount = 0 - multipleCompleted = false - downloading = false - error = false - percent = 0 - batchResults: VersionResult[] - chartGroups: VersionResult[][] + resultCount = 0 + multipleCompleted = false + downloading = false + error = false + percent = 0 + batchResults: VersionResult[] + chartGroups: VersionResult[][] - constructor( - private electronService: ElectronService, - private downloadService: DownloadService, - private searchService: SearchService, - private selectionService: SelectionService, - ref: ChangeDetectorRef - ) { - downloadService.onDownloadUpdated(() => { - setTimeout(() => { // Make sure this is the last callback executed to get the accurate downloadCount - this.downloading = downloadService.downloadCount > 0 - this.multipleCompleted = downloadService.completedCount > 1 - this.percent = downloadService.totalDownloadingPercent - this.error = downloadService.anyErrorsExist - ref.detectChanges() - }, 0) - }) + constructor( + private electronService: ElectronService, + private downloadService: DownloadService, + private searchService: SearchService, + private selectionService: SelectionService, + ref: ChangeDetectorRef + ) { + downloadService.onDownloadUpdated(() => { + setTimeout(() => { // Make sure this is the last callback executed to get the accurate downloadCount + this.downloading = downloadService.downloadCount > 0 + this.multipleCompleted = downloadService.completedCount > 1 + this.percent = downloadService.totalDownloadingPercent + this.error = downloadService.anyErrorsExist + ref.detectChanges() + }, 0) + }) - searchService.onSearchChanged(() => { - this.resultCount = searchService.resultCount - }) - } + searchService.onSearchChanged(() => { + this.resultCount = searchService.resultCount + }) + } - get allResultsVisible() { - return this.searchService.allResultsVisible - } + get allResultsVisible() { + return this.searchService.allResultsVisible + } - get selectedResults() { - return this.selectionService.getSelectedResults() - } + get selectedResults() { + return this.selectionService.getSelectedResults() + } - showDownloads() { - $('#downloadsModal').modal('show') - } + showDownloads() { + $('#downloadsModal').modal('show') + } - async downloadSelected() { - this.chartGroups = [] - this.batchResults = await this.electronService.invoke('batch-song-details', this.selectedResults.map(result => result.id)) - const versionGroups = groupBy(this.batchResults, 'songID') - for (const versionGroup of versionGroups) { - if (versionGroup.findIndex(version => version.chartID != versionGroup[0].chartID) != -1) { - // Must have multiple charts of this song - this.chartGroups.push(versionGroup.filter(version => version.versionID == version.latestVersionID)) - } - } + async downloadSelected() { + this.chartGroups = [] + this.batchResults = await this.electronService.invoke('batch-song-details', this.selectedResults.map(result => result.id)) + const versionGroups = groupBy(this.batchResults, 'songID') + for (const versionGroup of versionGroups) { + if (versionGroup.findIndex(version => version.chartID != versionGroup[0].chartID) != -1) { + // Must have multiple charts of this song + this.chartGroups.push(versionGroup.filter(version => version.versionID == version.latestVersionID)) + } + } - if (this.chartGroups.length == 0) { - for (const versions of versionGroups) { - this.searchService.sortChart(versions) - const downloadVersion = versions[0] - const downloadSong = this.selectedResults.find(song => song.id == downloadVersion.songID) - this.downloadService.addDownload( - downloadVersion.versionID, { - chartName: downloadVersion.chartName, - artist: downloadSong.artist, - charter: downloadVersion.charters, - driveData: downloadVersion.driveData - }) - } - } else { - $('#selectedModal').modal('show') - // [download all charts for each song] [deselect these songs] [X] - } - } + if (this.chartGroups.length == 0) { + for (const versions of versionGroups) { + this.searchService.sortChart(versions) + const downloadVersion = versions[0] + const downloadSong = this.selectedResults.find(song => song.id == downloadVersion.songID) + this.downloadService.addDownload( + downloadVersion.versionID, { + chartName: downloadVersion.chartName, + artist: downloadSong.artist, + charter: downloadVersion.charters, + driveData: downloadVersion.driveData, + }) + } + } else { + $('#selectedModal').modal('show') + // [download all charts for each song] [deselect these songs] [X] + } + } - downloadAllCharts() { - const songChartGroups = groupBy(this.batchResults, 'songID', 'chartID') - for (const chart of songChartGroups) { - this.searchService.sortChart(chart) - const downloadVersion = chart[0] - const downloadSong = this.selectedResults.find(song => song.id == downloadVersion.songID) - this.downloadService.addDownload( - downloadVersion.versionID, { - chartName: downloadVersion.chartName, - artist: downloadSong.artist, - charter: downloadVersion.charters, - driveData: downloadVersion.driveData - } - ) - } - } + downloadAllCharts() { + const songChartGroups = groupBy(this.batchResults, 'songID', 'chartID') + for (const chart of songChartGroups) { + this.searchService.sortChart(chart) + const downloadVersion = chart[0] + const downloadSong = this.selectedResults.find(song => song.id == downloadVersion.songID) + this.downloadService.addDownload( + downloadVersion.versionID, { + chartName: downloadVersion.chartName, + artist: downloadSong.artist, + charter: downloadVersion.charters, + driveData: downloadVersion.driveData, + } + ) + } + } - deselectSongsWithMultipleCharts() { - for (const chartGroup of this.chartGroups) { - this.selectionService.deselectSong(chartGroup[0].songID) - } - } + deselectSongsWithMultipleCharts() { + for (const chartGroup of this.chartGroups) { + this.selectionService.deselectSong(chartGroup[0].songID) + } + } - clearCompleted() { - this.downloadService.cancelCompleted() - } -} \ No newline at end of file + clearCompleted() { + this.downloadService.cancelCompleted() + } +} diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index 8993aee..8060e18 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -1,67 +1,71 @@

Paths

-
- -
- - - -
-
+
+ +
+ + + +
+

Cache

-
Current Cache Size:
{{cacheSize}}
+
+ Current Cache Size: +
{{ cacheSize }}
- +

Downloads

-
-
- - -
-
+
+
+ + +
+
- -
- -
- sec -
-
+ +
+ +
sec
+
- - Warning: downloading files from Google with a delay less than about 30 seconds will eventually cause Google to - refuse download requests from this program for a few hours. This limitation will be removed in a future update. + + Warning: downloading files from Google with a delay less than about 30 seconds will eventually cause Google to refuse download requests from + this program for a few hours. This limitation will be removed in a future update.

Theme

-
- - - -
+
+ + + +
- -
\ No newline at end of file + +
diff --git a/src/app/components/settings/settings.component.scss b/src/app/components/settings/settings.component.scss index ed3c076..13b8049 100644 --- a/src/app/components/settings/settings.component.scss +++ b/src/app/components/settings/settings.component.scss @@ -21,4 +21,4 @@ #versionNumberButton { margin-right: 1em; -} \ No newline at end of file +} diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts index 5e4f41e..f4d9cef 100644 --- a/src/app/components/settings/settings.component.ts +++ b/src/app/components/settings/settings.component.ts @@ -1,139 +1,140 @@ -import { Component, OnInit, AfterViewInit, ChangeDetectorRef, ViewChild, ElementRef } from '@angular/core' +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core' + import { CheckboxDirective } from 'src/app/core/directives/checkbox.directive' import { ElectronService } from 'src/app/core/services/electron.service' import { SettingsService } from 'src/app/core/services/settings.service' @Component({ - selector: 'app-settings', - templateUrl: './settings.component.html', - styleUrls: ['./settings.component.scss'] + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrls: ['./settings.component.scss'], }) export class SettingsComponent implements OnInit, AfterViewInit { - @ViewChild('themeDropdown', { static: true }) themeDropdown: ElementRef - @ViewChild(CheckboxDirective, { static: true }) videoCheckbox: CheckboxDirective + @ViewChild('themeDropdown', { static: true }) themeDropdown: ElementRef + @ViewChild(CheckboxDirective, { static: true }) videoCheckbox: CheckboxDirective - cacheSize = 'Calculating...' - updateAvailable = false - loginClicked = false - downloadUpdateText = 'Update available' - retryUpdateText = 'Failed to check for update' - updateDownloading = false - updateDownloaded = false - updateRetrying = false - currentVersion = '' + cacheSize = 'Calculating...' + updateAvailable = false + loginClicked = false + downloadUpdateText = 'Update available' + retryUpdateText = 'Failed to check for update' + updateDownloading = false + updateDownloaded = false + updateRetrying = false + currentVersion = '' - constructor( - public settingsService: SettingsService, - private electronService: ElectronService, - private ref: ChangeDetectorRef - ) { } + constructor( + public settingsService: SettingsService, + private electronService: ElectronService, + private ref: ChangeDetectorRef + ) { } - async ngOnInit() { - this.electronService.receiveIPC('update-available', (result) => { - this.updateAvailable = result != null - this.updateRetrying = false - if (this.updateAvailable) { - this.downloadUpdateText = `Update available (${result.version})` - } - this.ref.detectChanges() - }) - this.electronService.receiveIPC('update-error', (err: Error) => { - console.log(err) - this.updateAvailable = null - this.updateRetrying = false - this.retryUpdateText = `Failed to check for update: ${err.message}` - this.ref.detectChanges() - }) - this.electronService.invoke('get-current-version', undefined).then(version => { - this.currentVersion = `v${version}` - this.ref.detectChanges() - }) - this.electronService.invoke('get-update-available', undefined).then(isAvailable => { - this.updateAvailable = isAvailable - this.ref.detectChanges() - }) + async ngOnInit() { + this.electronService.receiveIPC('update-available', result => { + this.updateAvailable = result != null + this.updateRetrying = false + if (this.updateAvailable) { + this.downloadUpdateText = `Update available (${result.version})` + } + this.ref.detectChanges() + }) + this.electronService.receiveIPC('update-error', (err: Error) => { + console.log(err) + this.updateAvailable = null + this.updateRetrying = false + this.retryUpdateText = `Failed to check for update: ${err.message}` + this.ref.detectChanges() + }) + this.electronService.invoke('get-current-version', undefined).then(version => { + this.currentVersion = `v${version}` + this.ref.detectChanges() + }) + this.electronService.invoke('get-update-available', undefined).then(isAvailable => { + this.updateAvailable = isAvailable + this.ref.detectChanges() + }) - const cacheSize = await this.settingsService.getCacheSize() - this.cacheSize = Math.round(cacheSize / 1000000) + ' MB' - } + const cacheSize = await this.settingsService.getCacheSize() + this.cacheSize = Math.round(cacheSize / 1000000) + ' MB' + } - ngAfterViewInit() { - $(this.themeDropdown.nativeElement).dropdown({ - onChange: (_value: string, text: string) => { - this.settingsService.theme = text - } - }) + ngAfterViewInit() { + $(this.themeDropdown.nativeElement).dropdown({ + onChange: (_value: string, text: string) => { + this.settingsService.theme = text + }, + }) - this.videoCheckbox.check(this.settingsService.downloadVideos) - } + this.videoCheckbox.check(this.settingsService.downloadVideos) + } - async clearCache() { - this.cacheSize = 'Please wait...' - await this.settingsService.clearCache() - this.cacheSize = 'Cleared!' - } + async clearCache() { + this.cacheSize = 'Please wait...' + await this.settingsService.clearCache() + this.cacheSize = 'Cleared!' + } - async downloadVideos(isChecked: boolean) { - this.settingsService.downloadVideos = isChecked - } + async downloadVideos(isChecked: boolean) { + this.settingsService.downloadVideos = isChecked + } - async getLibraryDirectory() { - const result = await this.electronService.showOpenDialog({ - title: 'Choose library folder', - buttonLabel: 'This is where my charts are!', - defaultPath: this.settingsService.libraryDirectory || '', - properties: ['openDirectory'] - }) + async getLibraryDirectory() { + const result = await this.electronService.showOpenDialog({ + title: 'Choose library folder', + buttonLabel: 'This is where my charts are!', + defaultPath: this.settingsService.libraryDirectory || '', + properties: ['openDirectory'], + }) - if (result.canceled == false) { - this.settingsService.libraryDirectory = result.filePaths[0] - } - } + if (result.canceled == false) { + this.settingsService.libraryDirectory = result.filePaths[0] + } + } - openLibraryDirectory() { - this.electronService.openFolder(this.settingsService.libraryDirectory) - } + openLibraryDirectory() { + this.electronService.openFolder(this.settingsService.libraryDirectory) + } - changeRateLimit(event: Event) { - const inputElement = event.srcElement as HTMLInputElement - this.settingsService.rateLimitDelay = Number(inputElement.value) - } + changeRateLimit(event: Event) { + const inputElement = event.srcElement as HTMLInputElement + this.settingsService.rateLimitDelay = Number(inputElement.value) + } - downloadUpdate() { - if (this.updateDownloaded) { - this.electronService.sendIPC('quit-and-install', undefined) - } else if (!this.updateDownloading) { - this.updateDownloading = true - this.electronService.sendIPC('download-update', undefined) - this.downloadUpdateText = 'Downloading... (0%)' - this.electronService.receiveIPC('update-progress', (result) => { - this.downloadUpdateText = `Downloading... (${result.percent.toFixed(0)}%)` - this.ref.detectChanges() - }) - this.electronService.receiveIPC('update-downloaded', () => { - this.downloadUpdateText = 'Quit and install update' - this.updateDownloaded = true - this.ref.detectChanges() - }) - } - } + downloadUpdate() { + if (this.updateDownloaded) { + this.electronService.sendIPC('quit-and-install', undefined) + } else if (!this.updateDownloading) { + this.updateDownloading = true + this.electronService.sendIPC('download-update', undefined) + this.downloadUpdateText = 'Downloading... (0%)' + this.electronService.receiveIPC('update-progress', result => { + this.downloadUpdateText = `Downloading... (${result.percent.toFixed(0)}%)` + this.ref.detectChanges() + }) + this.electronService.receiveIPC('update-downloaded', () => { + this.downloadUpdateText = 'Quit and install update' + this.updateDownloaded = true + this.ref.detectChanges() + }) + } + } - retryUpdate() { - if (this.updateRetrying == false) { - this.updateRetrying = true - this.retryUpdateText = 'Retrying...' - this.ref.detectChanges() - this.electronService.sendIPC('retry-update', undefined) - } - } + retryUpdate() { + if (this.updateRetrying == false) { + this.updateRetrying = true + this.retryUpdateText = 'Retrying...' + this.ref.detectChanges() + this.electronService.sendIPC('retry-update', undefined) + } + } - toggleDevTools() { - const toolsOpened = this.electronService.currentWindow.webContents.isDevToolsOpened() + toggleDevTools() { + const toolsOpened = this.electronService.currentWindow.webContents.isDevToolsOpened() - if (toolsOpened) { - this.electronService.currentWindow.webContents.closeDevTools() - } else { - this.electronService.currentWindow.webContents.openDevTools() - } - } -} \ No newline at end of file + if (toolsOpened) { + this.electronService.currentWindow.webContents.closeDevTools() + } else { + this.electronService.currentWindow.webContents.openDevTools() + } + } +} diff --git a/src/app/components/toolbar/toolbar.component.html b/src/app/components/toolbar/toolbar.component.html index 036b84d..540a607 100644 --- a/src/app/components/toolbar/toolbar.component.html +++ b/src/app/components/toolbar/toolbar.component.html @@ -1,15 +1,15 @@ \ No newline at end of file + + diff --git a/src/app/components/toolbar/toolbar.component.scss b/src/app/components/toolbar/toolbar.component.scss index e20917a..fb67b91 100644 --- a/src/app/components/toolbar/toolbar.component.scss +++ b/src/app/components/toolbar/toolbar.component.scss @@ -24,4 +24,4 @@ .close:hover { background: rgba(255, 0, 0, .15) !important; -} \ No newline at end of file +} diff --git a/src/app/components/toolbar/toolbar.component.ts b/src/app/components/toolbar/toolbar.component.ts index 2c92d31..c53529d 100644 --- a/src/app/components/toolbar/toolbar.component.ts +++ b/src/app/components/toolbar/toolbar.component.ts @@ -1,55 +1,56 @@ -import { Component, OnInit, ChangeDetectorRef } from '@angular/core' +import { ChangeDetectorRef, Component, OnInit } from '@angular/core' + import { ElectronService } from '../../core/services/electron.service' @Component({ - selector: 'app-toolbar', - templateUrl: './toolbar.component.html', - styleUrls: ['./toolbar.component.scss'] + selector: 'app-toolbar', + templateUrl: './toolbar.component.html', + styleUrls: ['./toolbar.component.scss'], }) export class ToolbarComponent implements OnInit { - isMaximized: boolean - updateAvailable = false + isMaximized: boolean + updateAvailable = false - constructor(private electronService: ElectronService, private ref: ChangeDetectorRef) { } + constructor(private electronService: ElectronService, private ref: ChangeDetectorRef) { } - async ngOnInit() { - this.isMaximized = this.electronService.currentWindow.isMaximized() - this.electronService.currentWindow.on('unmaximize', () => { - this.isMaximized = false - this.ref.detectChanges() - }) - this.electronService.currentWindow.on('maximize', () => { - this.isMaximized = true - this.ref.detectChanges() - }) + async ngOnInit() { + this.isMaximized = this.electronService.currentWindow.isMaximized() + this.electronService.currentWindow.on('unmaximize', () => { + this.isMaximized = false + this.ref.detectChanges() + }) + this.electronService.currentWindow.on('maximize', () => { + this.isMaximized = true + this.ref.detectChanges() + }) - this.electronService.receiveIPC('update-available', (result) => { - this.updateAvailable = result != null - this.ref.detectChanges() - }) - this.electronService.receiveIPC('update-error', () => { - this.updateAvailable = null - this.ref.detectChanges() - }) - this.updateAvailable = await this.electronService.invoke('get-update-available', undefined) - this.ref.detectChanges() - } + this.electronService.receiveIPC('update-available', result => { + this.updateAvailable = result != null + this.ref.detectChanges() + }) + this.electronService.receiveIPC('update-error', () => { + this.updateAvailable = null + this.ref.detectChanges() + }) + this.updateAvailable = await this.electronService.invoke('get-update-available', undefined) + this.ref.detectChanges() + } - minimize() { - this.electronService.currentWindow.minimize() - } + minimize() { + this.electronService.currentWindow.minimize() + } - toggleMaximized() { - if (this.isMaximized) { - this.electronService.currentWindow.restore() - } else { - this.electronService.currentWindow.maximize() - } - this.isMaximized = !this.isMaximized - } + toggleMaximized() { + if (this.isMaximized) { + this.electronService.currentWindow.restore() + } else { + this.electronService.currentWindow.maximize() + } + this.isMaximized = !this.isMaximized + } - close() { - this.electronService.quit() - } -} \ No newline at end of file + close() { + this.electronService.quit() + } +} diff --git a/src/app/core/directives/checkbox.directive.ts b/src/app/core/directives/checkbox.directive.ts index c8f027a..c61deaf 100644 --- a/src/app/core/directives/checkbox.directive.ts +++ b/src/app/core/directives/checkbox.directive.ts @@ -1,38 +1,38 @@ -import { Directive, ElementRef, Output, EventEmitter, AfterViewInit } from '@angular/core' +import { AfterViewInit, Directive, ElementRef, EventEmitter, Output } from '@angular/core' @Directive({ - selector: '[appCheckbox]' + selector: '[appCheckbox]', }) export class CheckboxDirective implements AfterViewInit { - @Output() checked = new EventEmitter() + @Output() checked = new EventEmitter() - _isChecked = false + _isChecked = false - constructor(private checkbox: ElementRef) { } + constructor(private checkbox: ElementRef) { } - ngAfterViewInit() { - $(this.checkbox.nativeElement).checkbox({ - onChecked: () => { - this.checked.emit(true) - this._isChecked = true - }, - onUnchecked: () => { - this.checked.emit(false) - this._isChecked = false - } - }) - } + ngAfterViewInit() { + $(this.checkbox.nativeElement).checkbox({ + onChecked: () => { + this.checked.emit(true) + this._isChecked = true + }, + onUnchecked: () => { + this.checked.emit(false) + this._isChecked = false + }, + }) + } - check(isChecked: boolean) { - this._isChecked = isChecked - if (isChecked) { - $(this.checkbox.nativeElement).checkbox('check') - } else { - $(this.checkbox.nativeElement).checkbox('uncheck') - } - } + check(isChecked: boolean) { + this._isChecked = isChecked + if (isChecked) { + $(this.checkbox.nativeElement).checkbox('check') + } else { + $(this.checkbox.nativeElement).checkbox('uncheck') + } + } - get isChecked() { - return this._isChecked - } -} \ No newline at end of file + get isChecked() { + return this._isChecked + } +} diff --git a/src/app/core/directives/progress-bar.directive.ts b/src/app/core/directives/progress-bar.directive.ts index 6080f89..a8ec780 100644 --- a/src/app/core/directives/progress-bar.directive.ts +++ b/src/app/core/directives/progress-bar.directive.ts @@ -1,26 +1,27 @@ import { Directive, ElementRef, Input } from '@angular/core' + import * as _ from 'underscore' @Directive({ - selector: '[appProgressBar]' + selector: '[appProgressBar]', }) export class ProgressBarDirective { - progress: (percent: number) => void + progress: (percent: number) => void - @Input() set percent(percent: number) { - this.progress(percent) - } + @Input() set percent(percent: number) { + this.progress(percent) + } - constructor(private element: ElementRef) { - this.progress = _.throttle((percent: number) => this.$progressBar.progress('set').percent(percent), 100) - } + constructor(private element: ElementRef) { + this.progress = _.throttle((percent: number) => this.$progressBar.progress('set').percent(percent), 100) + } - private get $progressBar() { - if (!this._$progressBar) { - this._$progressBar = $(this.element.nativeElement) - } - return this._$progressBar - } - private _$progressBar: any -} \ No newline at end of file + private get $progressBar() { + if (!this._$progressBar) { + this._$progressBar = $(this.element.nativeElement) + } + return this._$progressBar + } + private _$progressBar: any +} diff --git a/src/app/core/services/album-art.service.ts b/src/app/core/services/album-art.service.ts index 7852d1c..df08b0f 100644 --- a/src/app/core/services/album-art.service.ts +++ b/src/app/core/services/album-art.service.ts @@ -1,25 +1,26 @@ import { Injectable } from '@angular/core' + import { ElectronService } from './electron.service' @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AlbumArtService { - constructor(private electronService: ElectronService) { } + constructor(private electronService: ElectronService) { } - private imageCache: { [songID: number]: string } = {} + private imageCache: { [songID: number]: string } = {} - async getImage(songID: number): Promise { - if (this.imageCache[songID] == undefined) { - const albumArtResult = await this.electronService.invoke('album-art', songID) - if (albumArtResult) { - this.imageCache[songID] = albumArtResult.base64Art - } else { - this.imageCache[songID] = null - } - } + async getImage(songID: number): Promise { + if (this.imageCache[songID] == undefined) { + const albumArtResult = await this.electronService.invoke('album-art', songID) + if (albumArtResult) { + this.imageCache[songID] = albumArtResult.base64Art + } else { + this.imageCache[songID] = null + } + } - return this.imageCache[songID] - } -} \ No newline at end of file + return this.imageCache[songID] + } +} diff --git a/src/app/core/services/download.service.ts b/src/app/core/services/download.service.ts index 0857d13..7c3215d 100644 --- a/src/app/core/services/download.service.ts +++ b/src/app/core/services/download.service.ts @@ -1,88 +1,89 @@ -import { Injectable, EventEmitter } from '@angular/core' +import { EventEmitter, Injectable } from '@angular/core' + +import { DownloadProgress, NewDownload } from '../../../electron/shared/interfaces/download.interface' import { ElectronService } from './electron.service' -import { NewDownload, DownloadProgress } from '../../../electron/shared/interfaces/download.interface' @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DownloadService { - private downloadUpdatedEmitter = new EventEmitter() - private downloads: DownloadProgress[] = [] + private downloadUpdatedEmitter = new EventEmitter() + private downloads: DownloadProgress[] = [] - constructor(private electronService: ElectronService) { - this.electronService.receiveIPC('download-updated', result => { - // Update with result - const thisDownloadIndex = this.downloads.findIndex(download => download.versionID == result.versionID) - if (result.type == 'cancel') { - this.downloads = this.downloads.filter(download => download.versionID != result.versionID) - } else if (thisDownloadIndex == -1) { - this.downloads.push(result) - } else { - this.downloads[thisDownloadIndex] = result - } + constructor(private electronService: ElectronService) { + this.electronService.receiveIPC('download-updated', result => { + // Update with result + const thisDownloadIndex = this.downloads.findIndex(download => download.versionID == result.versionID) + if (result.type == 'cancel') { + this.downloads = this.downloads.filter(download => download.versionID != result.versionID) + } else if (thisDownloadIndex == -1) { + this.downloads.push(result) + } else { + this.downloads[thisDownloadIndex] = result + } - this.downloadUpdatedEmitter.emit(result) - }) - } + this.downloadUpdatedEmitter.emit(result) + }) + } - get downloadCount() { - return this.downloads.length - } + get downloadCount() { + return this.downloads.length + } - get completedCount() { - return this.downloads.filter(download => download.type == 'done').length - } + get completedCount() { + return this.downloads.filter(download => download.type == 'done').length + } - get totalDownloadingPercent() { - let total = 0 - let count = 0 - for (const download of this.downloads) { - if (!download.stale) { - total += download.percent - count++ - } - } - return total / count - } + get totalDownloadingPercent() { + let total = 0 + let count = 0 + for (const download of this.downloads) { + if (!download.stale) { + total += download.percent + count++ + } + } + return total / count + } - get anyErrorsExist() { - return this.downloads.find(download => download.type == 'error') ? true : false - } + get anyErrorsExist() { + return this.downloads.find(download => download.type == 'error') ? true : false + } - addDownload(versionID: number, newDownload: NewDownload) { - if (!this.downloads.find(download => download.versionID == versionID)) { // Don't download something twice - if (this.downloads.every(download => download.type == 'done')) { // Reset overall progress bar if it finished - this.downloads.forEach(download => download.stale = true) - } - this.electronService.sendIPC('download', { action: 'add', versionID, data: newDownload }) - } - } + addDownload(versionID: number, newDownload: NewDownload) { + if (!this.downloads.find(download => download.versionID == versionID)) { // Don't download something twice + if (this.downloads.every(download => download.type == 'done')) { // Reset overall progress bar if it finished + this.downloads.forEach(download => download.stale = true) + } + this.electronService.sendIPC('download', { action: 'add', versionID, data: newDownload }) + } + } - onDownloadUpdated(callback: (download: DownloadProgress) => void) { - this.downloadUpdatedEmitter.subscribe(callback) - } + onDownloadUpdated(callback: (download: DownloadProgress) => void) { + this.downloadUpdatedEmitter.subscribe(callback) + } - cancelDownload(versionID: number) { - const removedDownload = this.downloads.find(download => download.versionID == versionID) - if (['error', 'done'].includes(removedDownload.type)) { - this.downloads = this.downloads.filter(download => download.versionID != versionID) - removedDownload.type = 'cancel' - this.downloadUpdatedEmitter.emit(removedDownload) - } else { - this.electronService.sendIPC('download', { action: 'cancel', versionID }) - } - } + cancelDownload(versionID: number) { + const removedDownload = this.downloads.find(download => download.versionID == versionID) + if (['error', 'done'].includes(removedDownload.type)) { + this.downloads = this.downloads.filter(download => download.versionID != versionID) + removedDownload.type = 'cancel' + this.downloadUpdatedEmitter.emit(removedDownload) + } else { + this.electronService.sendIPC('download', { action: 'cancel', versionID }) + } + } - cancelCompleted() { - for (const download of this.downloads) { - if (download.type == 'done') { - this.cancelDownload(download.versionID) - } - } - } + cancelCompleted() { + for (const download of this.downloads) { + if (download.type == 'done') { + this.cancelDownload(download.versionID) + } + } + } - retryDownload(versionID: number) { - this.electronService.sendIPC('download', { action: 'retry', versionID }) - } -} \ No newline at end of file + retryDownload(versionID: number) { + this.electronService.sendIPC('download', { action: 'retry', versionID }) + } +} diff --git a/src/app/core/services/electron.service.ts b/src/app/core/services/electron.service.ts index 7c073f2..0652e77 100644 --- a/src/app/core/services/electron.service.ts +++ b/src/app/core/services/electron.service.ts @@ -3,78 +3,79 @@ import { Injectable } from '@angular/core' // If you import a module but never use any of the imported values other than as TypeScript types, // the resulting javascript file will look as if you never imported the module at all. import * as electron from 'electron' -import { IPCInvokeEvents, IPCEmitEvents } from '../../../electron/shared/IPCHandler' + +import { IPCEmitEvents, IPCInvokeEvents } from '../../../electron/shared/IPCHandler' const { app, getCurrentWindow, dialog, session } = window.require('@electron/remote') @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ElectronService { - electron: typeof electron + electron: typeof electron - get isElectron() { - return !!(window && window.process && window.process.type) - } + get isElectron() { + return !!(window && window.process && window.process.type) + } - constructor() { - if (this.isElectron) { - this.electron = window.require('electron') - this.receiveIPC('log', results => results.forEach(result => console.log(result))) - } - } + constructor() { + if (this.isElectron) { + this.electron = window.require('electron') + this.receiveIPC('log', results => results.forEach(result => console.log(result))) + } + } - get currentWindow() { - return getCurrentWindow() - } + get currentWindow() { + return getCurrentWindow() + } - /** - * Calls an async function in the main process. - * @param event The name of the IPC event to invoke. - * @param data The data object to send across IPC. - * @returns A promise that resolves to the output data. - */ - async invoke(event: E, data: IPCInvokeEvents[E]['input']) { - return this.electron.ipcRenderer.invoke(event, data) as Promise - } + /** + * Calls an async function in the main process. + * @param event The name of the IPC event to invoke. + * @param data The data object to send across IPC. + * @returns A promise that resolves to the output data. + */ + async invoke(event: E, data: IPCInvokeEvents[E]['input']) { + return this.electron.ipcRenderer.invoke(event, data) as Promise + } - /** - * Sends an IPC message to the main process. - * @param event The name of the IPC event to send. - * @param data The data object to send across IPC. - */ - sendIPC(event: E, data: IPCEmitEvents[E]) { - this.electron.ipcRenderer.send(event, data) - } + /** + * Sends an IPC message to the main process. + * @param event The name of the IPC event to send. + * @param data The data object to send across IPC. + */ + sendIPC(event: E, data: IPCEmitEvents[E]) { + this.electron.ipcRenderer.send(event, data) + } - /** - * Receives an IPC message from the main process. - * @param event The name of the IPC event to receive. - * @param callback The data object to receive across IPC. - */ - receiveIPC(event: E, callback: (result: IPCEmitEvents[E]) => void) { - this.electron.ipcRenderer.on(event, (_event, ...results) => { - callback(results[0]) - }) - } + /** + * Receives an IPC message from the main process. + * @param event The name of the IPC event to receive. + * @param callback The data object to receive across IPC. + */ + receiveIPC(event: E, callback: (result: IPCEmitEvents[E]) => void) { + this.electron.ipcRenderer.on(event, (_event, ...results) => { + callback(results[0]) + }) + } - quit() { - app.exit() - } + quit() { + app.exit() + } - openFolder(filepath: string) { - this.electron.shell.openPath(filepath) - } + openFolder(filepath: string) { + this.electron.shell.openPath(filepath) + } - showFolder(filepath: string) { - this.electron.shell.showItemInFolder(filepath) - } + showFolder(filepath: string) { + this.electron.shell.showItemInFolder(filepath) + } - showOpenDialog(options: Electron.OpenDialogOptions) { - return dialog.showOpenDialog(this.currentWindow, options) - } + showOpenDialog(options: Electron.OpenDialogOptions) { + return dialog.showOpenDialog(this.currentWindow, options) + } - get defaultSession() { - return session.defaultSession - } -} \ No newline at end of file + get defaultSession() { + return session.defaultSession + } +} diff --git a/src/app/core/services/search.service.ts b/src/app/core/services/search.service.ts index 8c64774..a25309e 100644 --- a/src/app/core/services/search.service.ts +++ b/src/app/core/services/search.service.ts @@ -1,118 +1,119 @@ -import { Injectable, EventEmitter } from '@angular/core' -import { ElectronService } from './electron.service' +import { EventEmitter, Injectable } from '@angular/core' + import { SongResult, SongSearch } from 'src/electron/shared/interfaces/search.interface' import { VersionResult } from 'src/electron/shared/interfaces/songDetails.interface' +import { ElectronService } from './electron.service' @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SearchService { - private resultsChangedEmitter = new EventEmitter() // For when any results change - private newResultsEmitter = new EventEmitter() // For when a new search happens - private errorStateEmitter = new EventEmitter() // To indicate the search's error state - private results: SongResult[] = [] - private awaitingResults = false - private currentQuery: SongSearch - private _allResultsVisible = true + private resultsChangedEmitter = new EventEmitter() // For when any results change + private newResultsEmitter = new EventEmitter() // For when a new search happens + private errorStateEmitter = new EventEmitter() // To indicate the search's error state + private results: SongResult[] = [] + private awaitingResults = false + private currentQuery: SongSearch + private _allResultsVisible = true - constructor(private electronService: ElectronService) { } + constructor(private electronService: ElectronService) { } - async newSearch(query: SongSearch) { - if (this.awaitingResults) { return } - this.awaitingResults = true - this.currentQuery = query - try { - this.results = this.trimLastChart(await this.electronService.invoke('song-search', this.currentQuery)) - this.errorStateEmitter.emit(false) - } catch (err) { - this.results = [] - console.log(err.message) - this.errorStateEmitter.emit(true) - } - this.awaitingResults = false + async newSearch(query: SongSearch) { + if (this.awaitingResults) { return } + this.awaitingResults = true + this.currentQuery = query + try { + this.results = this.trimLastChart(await this.electronService.invoke('song-search', this.currentQuery)) + this.errorStateEmitter.emit(false) + } catch (err) { + this.results = [] + console.log(err.message) + this.errorStateEmitter.emit(true) + } + this.awaitingResults = false - this.newResultsEmitter.emit(this.results) - this.resultsChangedEmitter.emit(this.results) - } + this.newResultsEmitter.emit(this.results) + this.resultsChangedEmitter.emit(this.results) + } - isLoading() { - return this.awaitingResults - } + isLoading() { + return this.awaitingResults + } - /** - * Event emitted when new search results are returned - * or when more results are added to an existing search. - * (emitted after `onNewSearch`) - */ - onSearchChanged(callback: (results: SongResult[]) => void) { - this.resultsChangedEmitter.subscribe(callback) - } + /** + * Event emitted when new search results are returned + * or when more results are added to an existing search. + * (emitted after `onNewSearch`) + */ + onSearchChanged(callback: (results: SongResult[]) => void) { + this.resultsChangedEmitter.subscribe(callback) + } - /** - * Event emitted when a new search query is typed in. - * (emitted before `onSearchChanged`) - */ - onNewSearch(callback: (results: SongResult[]) => void) { - this.newResultsEmitter.subscribe(callback) - } + /** + * Event emitted when a new search query is typed in. + * (emitted before `onSearchChanged`) + */ + onNewSearch(callback: (results: SongResult[]) => void) { + this.newResultsEmitter.subscribe(callback) + } - /** - * Event emitted when the error state of the search changes. - * (emitted before `onSearchChanged`) - */ - onSearchErrorStateUpdate(callback: (isError: boolean) => void) { - this.errorStateEmitter.subscribe(callback) - } + /** + * Event emitted when the error state of the search changes. + * (emitted before `onSearchChanged`) + */ + onSearchErrorStateUpdate(callback: (isError: boolean) => void) { + this.errorStateEmitter.subscribe(callback) + } - get resultCount() { - return this.results.length - } + get resultCount() { + return this.results.length + } - async updateScroll() { - if (!this.awaitingResults && !this._allResultsVisible) { - this.awaitingResults = true - this.currentQuery.offset += 50 - this.results.push(...this.trimLastChart(await this.electronService.invoke('song-search', this.currentQuery))) - this.awaitingResults = false + async updateScroll() { + if (!this.awaitingResults && !this._allResultsVisible) { + this.awaitingResults = true + this.currentQuery.offset += 50 + this.results.push(...this.trimLastChart(await this.electronService.invoke('song-search', this.currentQuery))) + this.awaitingResults = false - this.resultsChangedEmitter.emit(this.results) - } - } + this.resultsChangedEmitter.emit(this.results) + } + } - trimLastChart(results: SongResult[]) { - if (results.length > 50) { - results.splice(50, 1) - this._allResultsVisible = false - } else { - this._allResultsVisible = true - } + trimLastChart(results: SongResult[]) { + if (results.length > 50) { + results.splice(50, 1) + this._allResultsVisible = false + } else { + this._allResultsVisible = true + } - return results - } + return results + } - get allResultsVisible() { - return this._allResultsVisible - } + get allResultsVisible() { + return this._allResultsVisible + } - /** - * Orders `versionResults` by lastModified date, but prefer the - * non-pack version if it's only a few days older. - */ - sortChart(versionResults: VersionResult[]) { - const dates: { [versionID: number]: number } = {} - versionResults.forEach(version => dates[version.versionID] = new Date(version.lastModified).getTime()) - versionResults.sort((v1, v2) => { - const diff = dates[v2.versionID] - dates[v1.versionID] - if (Math.abs(diff) < 6.048e+8 && v1.driveData.inChartPack != v2.driveData.inChartPack) { - if (v1.driveData.inChartPack) { - return 1 // prioritize v2 - } else { - return -1 // prioritize v1 - } - } else { - return diff - } - }) - } -} \ No newline at end of file + /** + * Orders `versionResults` by lastModified date, but prefer the + * non-pack version if it's only a few days older. + */ + sortChart(versionResults: VersionResult[]) { + const dates: { [versionID: number]: number } = {} + versionResults.forEach(version => dates[version.versionID] = new Date(version.lastModified).getTime()) + versionResults.sort((v1, v2) => { + const diff = dates[v2.versionID] - dates[v1.versionID] + if (Math.abs(diff) < 6.048e+8 && v1.driveData.inChartPack != v2.driveData.inChartPack) { + if (v1.driveData.inChartPack) { + return 1 // prioritize v2 + } else { + return -1 // prioritize v1 + } + } else { + return diff + } + }) + } +} diff --git a/src/app/core/services/selection.service.ts b/src/app/core/services/selection.service.ts index 8a284c4..825dc98 100644 --- a/src/app/core/services/selection.service.ts +++ b/src/app/core/services/selection.service.ts @@ -1,89 +1,90 @@ -import { Injectable, EventEmitter } from '@angular/core' +import { EventEmitter, Injectable } from '@angular/core' + import { SongResult } from '../../../electron/shared/interfaces/search.interface' import { SearchService } from './search.service' // Note: this class prevents event cycles by only emitting events if the checkbox changes interface SelectionEvent { - songID: number - selected: boolean + songID: number + selected: boolean } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SelectionService { - private searchResults: SongResult[] = [] + private searchResults: SongResult[] = [] - private selectAllChangedEmitter = new EventEmitter() - private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {} + private selectAllChangedEmitter = new EventEmitter() + private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {} - private allSelected = false - private selections: { [songID: number]: boolean | undefined } = {} + private allSelected = false + private selections: { [songID: number]: boolean | undefined } = {} - constructor(searchService: SearchService) { - searchService.onSearchChanged((results) => { - this.searchResults = results - if (this.allSelected) { - this.selectAll() // Select newly added rows if allSelected - } - }) + constructor(searchService: SearchService) { + searchService.onSearchChanged(results => { + this.searchResults = results + if (this.allSelected) { + this.selectAll() // Select newly added rows if allSelected + } + }) - searchService.onNewSearch((results) => { - this.searchResults = results - this.selectionChangedCallbacks = {} - this.selections = {} - this.selectAllChangedEmitter.emit(false) - }) - } + searchService.onNewSearch(results => { + this.searchResults = results + this.selectionChangedCallbacks = {} + this.selections = {} + this.selectAllChangedEmitter.emit(false) + }) + } - getSelectedResults() { - return this.searchResults.filter(result => this.selections[result.id] == true) - } + getSelectedResults() { + return this.searchResults.filter(result => this.selections[result.id] == true) + } - onSelectAllChanged(callback: (selected: boolean) => void) { - this.selectAllChangedEmitter.subscribe(callback) - } + onSelectAllChanged(callback: (selected: boolean) => void) { + this.selectAllChangedEmitter.subscribe(callback) + } - /** - * Emits an event when the selection for `songID` needs to change. - * (note: only one emitter can be registered per `songID`) - */ - onSelectionChanged(songID: number, callback: (selection: boolean) => void) { - this.selectionChangedCallbacks[songID] = callback - } + /** + * Emits an event when the selection for `songID` needs to change. + * (note: only one emitter can be registered per `songID`) + */ + onSelectionChanged(songID: number, callback: (selection: boolean) => void) { + this.selectionChangedCallbacks[songID] = callback + } - deselectAll() { - if (this.allSelected) { - this.allSelected = false - this.selectAllChangedEmitter.emit(false) - } + deselectAll() { + if (this.allSelected) { + this.allSelected = false + this.selectAllChangedEmitter.emit(false) + } - setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0) - } + setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0) + } - selectAll() { - if (!this.allSelected) { - this.allSelected = true - this.selectAllChangedEmitter.emit(true) - } + selectAll() { + if (!this.allSelected) { + this.allSelected = true + this.selectAllChangedEmitter.emit(true) + } - setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0) - } + setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0) + } - deselectSong(songID: number) { - if (this.selections[songID]) { - this.selections[songID] = false - this.selectionChangedCallbacks[songID](false) - } - } + deselectSong(songID: number) { + if (this.selections[songID]) { + this.selections[songID] = false + this.selectionChangedCallbacks[songID](false) + } + } - selectSong(songID: number) { - if (!this.selections[songID]) { - this.selections[songID] = true - this.selectionChangedCallbacks[songID](true) - } - } -} \ No newline at end of file + selectSong(songID: number) { + if (!this.selections[songID]) { + this.selections[songID] = true + this.selectionChangedCallbacks[songID](true) + } + } +} diff --git a/src/app/core/services/settings.service.ts b/src/app/core/services/settings.service.ts index b0f2252..1af9b88 100644 --- a/src/app/core/services/settings.service.ts +++ b/src/app/core/services/settings.service.ts @@ -1,81 +1,82 @@ import { Injectable } from '@angular/core' -import { ElectronService } from './electron.service' + import { Settings } from 'src/electron/shared/Settings' +import { ElectronService } from './electron.service' @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SettingsService { - readonly builtinThemes = ['Default', 'Dark'] + readonly builtinThemes = ['Default', 'Dark'] - private settings: Settings - private currentThemeLink: HTMLLinkElement + private settings: Settings + private currentThemeLink: HTMLLinkElement - constructor(private electronService: ElectronService) { } + constructor(private electronService: ElectronService) { } - async loadSettings() { - this.settings = await this.electronService.invoke('get-settings', undefined) - if (this.settings.theme != this.builtinThemes[0]) { - this.changeTheme(this.settings.theme) - } - } + async loadSettings() { + this.settings = await this.electronService.invoke('get-settings', undefined) + if (this.settings.theme != this.builtinThemes[0]) { + this.changeTheme(this.settings.theme) + } + } - saveSettings() { - this.electronService.sendIPC('set-settings', this.settings) - } + saveSettings() { + this.electronService.sendIPC('set-settings', this.settings) + } - changeTheme(theme: string) { - if (this.currentThemeLink != undefined) this.currentThemeLink.remove() - if (theme == 'Default') { return } + changeTheme(theme: string) { + if (this.currentThemeLink != undefined) this.currentThemeLink.remove() + if (theme == 'Default') { return } - const link = document.createElement('link') - link.type = 'text/css' - link.rel = 'stylesheet' - link.href = `./assets/themes/${theme.toLowerCase()}.css` - this.currentThemeLink = document.head.appendChild(link) - } + const link = document.createElement('link') + link.type = 'text/css' + link.rel = 'stylesheet' + link.href = `./assets/themes/${theme.toLowerCase()}.css` + this.currentThemeLink = document.head.appendChild(link) + } - async getCacheSize() { - return this.electronService.defaultSession.getCacheSize() - } + async getCacheSize() { + return this.electronService.defaultSession.getCacheSize() + } - async clearCache() { - this.saveSettings() - await this.electronService.defaultSession.clearCache() - await this.electronService.invoke('clear-cache', undefined) - } + async clearCache() { + this.saveSettings() + await this.electronService.defaultSession.clearCache() + await this.electronService.invoke('clear-cache', undefined) + } - // Individual getters/setters - get libraryDirectory() { - return this.settings.libraryPath - } - set libraryDirectory(newValue: string) { - this.settings.libraryPath = newValue - this.saveSettings() - } + // Individual getters/setters + get libraryDirectory() { + return this.settings.libraryPath + } + set libraryDirectory(newValue: string) { + this.settings.libraryPath = newValue + this.saveSettings() + } - get downloadVideos() { - return this.settings.downloadVideos - } - set downloadVideos(isChecked) { - this.settings.downloadVideos = isChecked - this.saveSettings() - } + get downloadVideos() { + return this.settings.downloadVideos + } + set downloadVideos(isChecked) { + this.settings.downloadVideos = isChecked + this.saveSettings() + } - get theme() { - return this.settings.theme - } - set theme(newValue: string) { - this.settings.theme = newValue - this.changeTheme(newValue) - this.saveSettings() - } + get theme() { + return this.settings.theme + } + set theme(newValue: string) { + this.settings.theme = newValue + this.changeTheme(newValue) + this.saveSettings() + } - get rateLimitDelay() { - return this.settings.rateLimitDelay - } - set rateLimitDelay(delay: number) { - this.settings.rateLimitDelay = delay - this.saveSettings() - } -} \ No newline at end of file + get rateLimitDelay() { + return this.settings.rateLimitDelay + } + set rateLimitDelay(delay: number) { + this.settings.rateLimitDelay = delay + this.saveSettings() + } +} diff --git a/src/app/core/tab-persist.strategy.ts b/src/app/core/tab-persist.strategy.ts index 4b667fd..d4512e4 100644 --- a/src/app/core/tab-persist.strategy.ts +++ b/src/app/core/tab-persist.strategy.ts @@ -1,29 +1,29 @@ -import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router' import { Injectable } from '@angular/core' +import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router' /** * This makes each route with the 'reuse' data flag persist when not in focus. */ @Injectable() export class TabPersistStrategy extends RouteReuseStrategy { - private handles: { [path: string]: DetachedRouteHandle } = {} + private handles: { [path: string]: DetachedRouteHandle } = {} - shouldDetach(route: ActivatedRouteSnapshot) { - return route.data.shouldReuse || false - } - store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle) { - if (route.data.shouldReuse) { - this.handles[route.routeConfig.path] = handle - } - } - shouldAttach(route: ActivatedRouteSnapshot) { - return !!route.routeConfig && !!this.handles[route.routeConfig.path] - } - retrieve(route: ActivatedRouteSnapshot) { - if (!route.routeConfig) return null - return this.handles[route.routeConfig.path] - } - shouldReuseRoute(future: ActivatedRouteSnapshot) { - return future.data.shouldReuse || false - } -} \ No newline at end of file + shouldDetach(route: ActivatedRouteSnapshot) { + return route.data.shouldReuse || false + } + store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle) { + if (route.data.shouldReuse) { + this.handles[route.routeConfig.path] = handle + } + } + shouldAttach(route: ActivatedRouteSnapshot) { + return !!route.routeConfig && !!this.handles[route.routeConfig.path] + } + retrieve(route: ActivatedRouteSnapshot) { + if (!route.routeConfig) return null + return this.handles[route.routeConfig.path] + } + shouldReuseRoute(future: ActivatedRouteSnapshot) { + return future.data.shouldReuse || false + } +} diff --git a/src/electron/ipc/CacheHandler.ipc.ts b/src/electron/ipc/CacheHandler.ipc.ts index 0d4e625..131f4dd 100644 --- a/src/electron/ipc/CacheHandler.ipc.ts +++ b/src/electron/ipc/CacheHandler.ipc.ts @@ -1,10 +1,11 @@ +import { Dirent, readdir as _readdir } from 'fs' +import { join } from 'path' +import { rimraf } from 'rimraf' +import { inspect, promisify } from 'util' + +import { devLog } from '../shared/ElectronUtilFunctions' import { IPCInvokeHandler } from '../shared/IPCHandler' import { tempPath } from '../shared/Paths' -import { rimraf } from 'rimraf' -import { Dirent, readdir as _readdir } from 'fs' -import { inspect, promisify } from 'util' -import { join } from 'path' -import { devLog } from '../shared/ElectronUtilFunctions' const readdir = promisify(_readdir) @@ -12,30 +13,30 @@ const readdir = promisify(_readdir) * Handles the 'clear-cache' event. */ class ClearCacheHandler implements IPCInvokeHandler<'clear-cache'> { - event: 'clear-cache' = 'clear-cache' + event = 'clear-cache' as const - /** - * Deletes all the files under `tempPath` - */ - async handler() { - let files: Dirent[] - try { - files = await readdir(tempPath, { withFileTypes: true }) - } catch (err) { - devLog('Failed to read cache: ', err) - return - } + /** + * Deletes all the files under `tempPath` + */ + async handler() { + let files: Dirent[] + try { + files = await readdir(tempPath, { withFileTypes: true }) + } catch (err) { + devLog('Failed to read cache: ', err) + return + } - for (const file of files) { - try { - devLog(`Deleting ${file.isFile() ? 'file' : 'folder'}: ${join(tempPath, file.name)}`) - await rimraf(join(tempPath, file.name)) - } catch (err) { - devLog(`Failed to delete ${file.isFile() ? 'file' : 'folder'}: `, inspect(err)) - return - } - } - } + for (const file of files) { + try { + devLog(`Deleting ${file.isFile() ? 'file' : 'folder'}: ${join(tempPath, file.name)}`) + await rimraf(join(tempPath, file.name)) + } catch (err) { + devLog(`Failed to delete ${file.isFile() ? 'file' : 'folder'}: `, inspect(err)) + return + } + } + } } -export const clearCacheHandler = new ClearCacheHandler() \ No newline at end of file +export const clearCacheHandler = new ClearCacheHandler() diff --git a/src/electron/ipc/OpenURLHandler.ipc.ts b/src/electron/ipc/OpenURLHandler.ipc.ts index 54f3139..bdd5be5 100644 --- a/src/electron/ipc/OpenURLHandler.ipc.ts +++ b/src/electron/ipc/OpenURLHandler.ipc.ts @@ -1,18 +1,19 @@ -import { IPCEmitHandler } from '../shared/IPCHandler' import { shell } from 'electron' +import { IPCEmitHandler } from '../shared/IPCHandler' + /** * Handles the 'open-url' event. */ class OpenURLHandler implements IPCEmitHandler<'open-url'> { - event: 'open-url' = 'open-url' + event = 'open-url' as const - /** - * Opens `url` in the default browser. - */ - handler(url: string) { - shell.openExternal(url) - } + /** + * Opens `url` in the default browser. + */ + handler(url: string) { + shell.openExternal(url) + } } -export const openURLHandler = new OpenURLHandler() \ No newline at end of file +export const openURLHandler = new OpenURLHandler() diff --git a/src/electron/ipc/SettingsHandler.ipc.ts b/src/electron/ipc/SettingsHandler.ipc.ts index 9125e7a..bfda4b3 100644 --- a/src/electron/ipc/SettingsHandler.ipc.ts +++ b/src/electron/ipc/SettingsHandler.ipc.ts @@ -1,7 +1,8 @@ import * as fs from 'fs' -import { dataPath, tempPath, themesPath, settingsPath } from '../shared/Paths' import { promisify } from 'util' -import { IPCInvokeHandler, IPCEmitHandler } from '../shared/IPCHandler' + +import { IPCEmitHandler, IPCInvokeHandler } from '../shared/IPCHandler' +import { dataPath, settingsPath, tempPath, themesPath } from '../shared/Paths' import { defaultSettings, Settings } from '../shared/Settings' const exists = promisify(fs.exists) @@ -11,83 +12,83 @@ const writeFile = promisify(fs.writeFile) let settings: Settings -/** - * Handles the 'get-settings' event. - */ -class GetSettingsHandler implements IPCInvokeHandler<'get-settings'> { - event: 'get-settings' = 'get-settings' - - /** - * @returns the current settings oject, or default settings if they couldn't be loaded. - */ - handler() { - return this.getSettings() - } - - /** - * @returns the current settings oject, or default settings if they couldn't be loaded. - */ - getSettings() { - if (settings == undefined) { - return defaultSettings - } else { - return settings - } - } - - /** - * If data directories don't exist, creates them and saves the default settings. - * Otherwise, loads user settings from data directories. - * If this process fails, default settings are used. - */ - async initSettings() { - try { - // Create data directories if they don't exists - for (const path of [dataPath, tempPath, themesPath]) { - if (!await exists(path)) { - await mkdir(path) - } - } - - // Read/create settings - if (await exists(settingsPath)) { - settings = JSON.parse(await readFile(settingsPath, 'utf8')) - settings = Object.assign(JSON.parse(JSON.stringify(defaultSettings)), settings) - } else { - await SetSettingsHandler.saveSettings(defaultSettings) - settings = defaultSettings - } - } catch (e) { - console.error('Failed to initialize settings! Default settings will be used.') - console.error(e) - settings = defaultSettings - } - } -} - /** * Handles the 'set-settings' event. */ class SetSettingsHandler implements IPCEmitHandler<'set-settings'> { - event: 'set-settings' = 'set-settings' + event = 'set-settings' as const - /** - * Updates Bridge's settings object to `newSettings` and saves them to Bridge's data directories. - */ - handler(newSettings: Settings) { - settings = newSettings - SetSettingsHandler.saveSettings(settings) - } + /** + * Updates Bridge's settings object to `newSettings` and saves them to Bridge's data directories. + */ + handler(newSettings: Settings) { + settings = newSettings + SetSettingsHandler.saveSettings(settings) + } - /** - * Saves `settings` to Bridge's data directories. - */ - static async saveSettings(settings: Settings) { - const settingsJSON = JSON.stringify(settings, undefined, 2) - await writeFile(settingsPath, settingsJSON, 'utf8') - } + /** + * Saves `settings` to Bridge's data directories. + */ + static async saveSettings(settings: Settings) { + const settingsJSON = JSON.stringify(settings, undefined, 2) + await writeFile(settingsPath, settingsJSON, 'utf8') + } +} + +/** + * Handles the 'get-settings' event. + */ +class GetSettingsHandler implements IPCInvokeHandler<'get-settings'> { + event = 'get-settings' as const + + /** + * @returns the current settings oject, or default settings if they couldn't be loaded. + */ + handler() { + return this.getSettings() + } + + /** + * @returns the current settings oject, or default settings if they couldn't be loaded. + */ + getSettings() { + if (settings == undefined) { + return defaultSettings + } else { + return settings + } + } + + /** + * If data directories don't exist, creates them and saves the default settings. + * Otherwise, loads user settings from data directories. + * If this process fails, default settings are used. + */ + async initSettings() { + try { + // Create data directories if they don't exists + for (const path of [dataPath, tempPath, themesPath]) { + if (!await exists(path)) { + await mkdir(path) + } + } + + // Read/create settings + if (await exists(settingsPath)) { + settings = JSON.parse(await readFile(settingsPath, 'utf8')) + settings = Object.assign(JSON.parse(JSON.stringify(defaultSettings)), settings) + } else { + await SetSettingsHandler.saveSettings(defaultSettings) + settings = defaultSettings + } + } catch (e) { + console.error('Failed to initialize settings! Default settings will be used.') + console.error(e) + settings = defaultSettings + } + } } export const getSettingsHandler = new GetSettingsHandler() export const setSettingsHandler = new SetSettingsHandler() -export function getSettings() { return getSettingsHandler.getSettings() } \ No newline at end of file +export function getSettings() { return getSettingsHandler.getSettings() } diff --git a/src/electron/ipc/UpdateHandler.ipc.ts b/src/electron/ipc/UpdateHandler.ipc.ts index e68b537..3ac6a66 100644 --- a/src/electron/ipc/UpdateHandler.ipc.ts +++ b/src/electron/ipc/UpdateHandler.ipc.ts @@ -1,12 +1,13 @@ -import { IPCEmitHandler, IPCInvokeHandler } from '../shared/IPCHandler' import { autoUpdater, UpdateInfo } from 'electron-updater' + import { emitIPCEvent } from '../main' +import { IPCEmitHandler, IPCInvokeHandler } from '../shared/IPCHandler' export interface UpdateProgress { - bytesPerSecond: number - percent: number - transferred: number - total: number + bytesPerSecond: number + percent: number + transferred: number + total: number } let updateAvailable = false @@ -15,44 +16,44 @@ let updateAvailable = false * Checks for updates when the program is launched. */ class UpdateChecker implements IPCEmitHandler<'retry-update'> { - event: 'retry-update' = 'retry-update' + event = 'retry-update' as const - /** - * Check for an update. - */ - handler() { - this.checkForUpdates() - } + constructor() { + autoUpdater.autoDownload = false + autoUpdater.logger = null + this.registerUpdaterListeners() + } - constructor() { - autoUpdater.autoDownload = false - autoUpdater.logger = null - this.registerUpdaterListeners() - } + /** + * Check for an update. + */ + handler() { + this.checkForUpdates() + } - checkForUpdates() { - autoUpdater.checkForUpdates().catch(reason => { - updateAvailable = null - emitIPCEvent('update-error', reason) - }) - } + checkForUpdates() { + autoUpdater.checkForUpdates().catch(reason => { + updateAvailable = null + emitIPCEvent('update-error', reason) + }) + } - private registerUpdaterListeners() { - autoUpdater.on('error', (err: Error) => { - updateAvailable = null - emitIPCEvent('update-error', err) - }) + private registerUpdaterListeners() { + autoUpdater.on('error', (err: Error) => { + updateAvailable = null + emitIPCEvent('update-error', err) + }) - autoUpdater.on('update-available', (info: UpdateInfo) => { - updateAvailable = true - emitIPCEvent('update-available', info) - }) + autoUpdater.on('update-available', (info: UpdateInfo) => { + updateAvailable = true + emitIPCEvent('update-available', info) + }) - autoUpdater.on('update-not-available', (info: UpdateInfo) => { - updateAvailable = false - emitIPCEvent('update-available', null) - }) - } + autoUpdater.on('update-not-available', (info: UpdateInfo) => { + updateAvailable = false + emitIPCEvent('update-available', null) + }) + } } export const updateChecker = new UpdateChecker() @@ -61,14 +62,14 @@ export const updateChecker = new UpdateChecker() * Handles the 'get-update-available' event. */ class GetUpdateAvailableHandler implements IPCInvokeHandler<'get-update-available'> { - event: 'get-update-available' = 'get-update-available' + event = 'get-update-available' as const - /** - * @returns `true` if an update is available. - */ - handler() { - return updateAvailable - } + /** + * @returns `true` if an update is available. + */ + handler() { + return updateAvailable + } } export const getUpdateAvailableHandler = new GetUpdateAvailableHandler() @@ -77,14 +78,14 @@ export const getUpdateAvailableHandler = new GetUpdateAvailableHandler() * Handles the 'get-current-version' event. */ class GetCurrentVersionHandler implements IPCInvokeHandler<'get-current-version'> { - event: 'get-current-version' = 'get-current-version' + event = 'get-current-version' as const - /** - * @returns the current version of Bridge. - */ - handler() { - return autoUpdater.currentVersion.raw - } + /** + * @returns the current version of Bridge. + */ + handler() { + return autoUpdater.currentVersion.raw + } } export const getCurrentVersionHandler = new GetCurrentVersionHandler() @@ -93,26 +94,26 @@ export const getCurrentVersionHandler = new GetCurrentVersionHandler() * Handles the 'download-update' event. */ class DownloadUpdateHandler implements IPCEmitHandler<'download-update'> { - event: 'download-update' = 'download-update' - downloading = false + event = 'download-update' as const + downloading = false - /** - * Begins the process of downloading the latest update. - */ - handler() { - if (this.downloading) { return } - this.downloading = true + /** + * Begins the process of downloading the latest update. + */ + handler() { + if (this.downloading) { return } + this.downloading = true - autoUpdater.on('download-progress', (updateProgress: UpdateProgress) => { - emitIPCEvent('update-progress', updateProgress) - }) + autoUpdater.on('download-progress', (updateProgress: UpdateProgress) => { + emitIPCEvent('update-progress', updateProgress) + }) - autoUpdater.on('update-downloaded', () => { - emitIPCEvent('update-downloaded', undefined) - }) + autoUpdater.on('update-downloaded', () => { + emitIPCEvent('update-downloaded', undefined) + }) - autoUpdater.downloadUpdate() - } + autoUpdater.downloadUpdate() + } } export const downloadUpdateHandler = new DownloadUpdateHandler() @@ -121,14 +122,14 @@ export const downloadUpdateHandler = new DownloadUpdateHandler() * Handles the 'quit-and-install' event. */ class QuitAndInstallHandler implements IPCEmitHandler<'quit-and-install'> { - event: 'quit-and-install' = 'quit-and-install' + event = 'quit-and-install' as const - /** - * Immediately closes the application and installs the update. - */ - handler() { - autoUpdater.quitAndInstall() // autoUpdater installs a downloaded update on the next program restart by default - } + /** + * Immediately closes the application and installs the update. + */ + handler() { + autoUpdater.quitAndInstall() // autoUpdater installs a downloaded update on the next program restart by default + } } -export const quitAndInstallHandler = new QuitAndInstallHandler() \ No newline at end of file +export const quitAndInstallHandler = new QuitAndInstallHandler() diff --git a/src/electron/ipc/browse/AlbumArtHandler.ipc.ts b/src/electron/ipc/browse/AlbumArtHandler.ipc.ts index 8c3f1db..1f6138b 100644 --- a/src/electron/ipc/browse/AlbumArtHandler.ipc.ts +++ b/src/electron/ipc/browse/AlbumArtHandler.ipc.ts @@ -1,20 +1,20 @@ -import { IPCInvokeHandler } from '../../shared/IPCHandler' import { AlbumArtResult } from '../../shared/interfaces/songDetails.interface' +import { IPCInvokeHandler } from '../../shared/IPCHandler' import { serverURL } from '../../shared/Paths' /** * Handles the 'album-art' event. */ class AlbumArtHandler implements IPCInvokeHandler<'album-art'> { - event: 'album-art' = 'album-art' + event = 'album-art' as const - /** - * @returns an `AlbumArtResult` object containing the album art for the song with `songID`. - */ - async handler(songID: number): Promise { - const response = await fetch(`https://${serverURL}/api/data/album-art/${songID}`) - return await response.json() - } + /** + * @returns an `AlbumArtResult` object containing the album art for the song with `songID`. + */ + async handler(songID: number): Promise { + const response = await fetch(`https://${serverURL}/api/data/album-art/${songID}`) + return await response.json() + } } -export const albumArtHandler = new AlbumArtHandler() \ No newline at end of file +export const albumArtHandler = new AlbumArtHandler() diff --git a/src/electron/ipc/browse/BatchSongDetailsHandler.ipc.ts b/src/electron/ipc/browse/BatchSongDetailsHandler.ipc.ts index 0182db3..6a171c2 100644 --- a/src/electron/ipc/browse/BatchSongDetailsHandler.ipc.ts +++ b/src/electron/ipc/browse/BatchSongDetailsHandler.ipc.ts @@ -1,20 +1,20 @@ -import { IPCInvokeHandler } from '../../shared/IPCHandler' import { VersionResult } from '../../shared/interfaces/songDetails.interface' +import { IPCInvokeHandler } from '../../shared/IPCHandler' import { serverURL } from '../../shared/Paths' /** * Handles the 'batch-song-details' event. */ class BatchSongDetailsHandler implements IPCInvokeHandler<'batch-song-details'> { - event: 'batch-song-details' = 'batch-song-details' + event = 'batch-song-details' as const - /** - * @returns an array of all the chart versions with a songID found in `songIDs`. - */ - async handler(songIDs: number[]): Promise { - const response = await fetch(`https://${serverURL}/api/data/song-versions/${songIDs.join(',')}`) - return await response.json() - } + /** + * @returns an array of all the chart versions with a songID found in `songIDs`. + */ + async handler(songIDs: number[]): Promise { + const response = await fetch(`https://${serverURL}/api/data/song-versions/${songIDs.join(',')}`) + return await response.json() + } } -export const batchSongDetailsHandler = new BatchSongDetailsHandler() \ No newline at end of file +export const batchSongDetailsHandler = new BatchSongDetailsHandler() diff --git a/src/electron/ipc/browse/SearchHandler.ipc.ts b/src/electron/ipc/browse/SearchHandler.ipc.ts index 838f604..fab7dfc 100644 --- a/src/electron/ipc/browse/SearchHandler.ipc.ts +++ b/src/electron/ipc/browse/SearchHandler.ipc.ts @@ -1,27 +1,28 @@ -import { IPCInvokeHandler } from '../../shared/IPCHandler' import { SongResult, SongSearch } from '../../shared/interfaces/search.interface' +import { IPCInvokeHandler } from '../../shared/IPCHandler' import { serverURL } from '../../shared/Paths' /** * Handles the 'song-search' event. */ class SearchHandler implements IPCInvokeHandler<'song-search'> { - event: 'song-search' = 'song-search' + event = 'song-search' as const - /** - * @returns the top 50 songs that match `search`. - */ - async handler(search: SongSearch): Promise { - const response = await fetch(`https://${serverURL}/api/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(search) - }) + /** + * @returns the top 50 songs that match `search`. + */ + async handler(search: SongSearch): Promise { + const response = await fetch(`https://${serverURL}/api/search`, { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + }, + body: JSON.stringify(search), + }) - return await response.json() - } + return await response.json() + } } -export const searchHandler = new SearchHandler() \ No newline at end of file +export const searchHandler = new SearchHandler() diff --git a/src/electron/ipc/browse/SongDetailsHandler.ipc.ts b/src/electron/ipc/browse/SongDetailsHandler.ipc.ts index 8f257fa..bf66222 100644 --- a/src/electron/ipc/browse/SongDetailsHandler.ipc.ts +++ b/src/electron/ipc/browse/SongDetailsHandler.ipc.ts @@ -1,20 +1,20 @@ -import { IPCInvokeHandler } from '../../shared/IPCHandler' import { VersionResult } from '../../shared/interfaces/songDetails.interface' +import { IPCInvokeHandler } from '../../shared/IPCHandler' import { serverURL } from '../../shared/Paths' /** * Handles the 'song-details' event. */ class SongDetailsHandler implements IPCInvokeHandler<'song-details'> { - event: 'song-details' = 'song-details' + event = 'song-details' as const - /** - * @returns the chart versions with `songID`. - */ - async handler(songID: number): Promise { - const response = await fetch(`https://${serverURL}/api/data/song-versions/${songID}`) - return await response.json() - } + /** + * @returns the chart versions with `songID`. + */ + async handler(songID: number): Promise { + const response = await fetch(`https://${serverURL}/api/data/song-versions/${songID}`) + return await response.json() + } } -export const songDetailsHandler = new SongDetailsHandler() \ No newline at end of file +export const songDetailsHandler = new SongDetailsHandler() diff --git a/src/electron/ipc/download/ChartDownload.ts b/src/electron/ipc/download/ChartDownload.ts index ccf8de3..500adfe 100644 --- a/src/electron/ipc/download/ChartDownload.ts +++ b/src/electron/ipc/download/ChartDownload.ts @@ -1,282 +1,284 @@ -import { FileDownloader, getDownloader } from './FileDownloader' import { join, parse } from 'path' -import { FileExtractor } from './FileExtractor' -import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions' -import { emitIPCEvent } from '../../main' -import { ProgressType, NewDownload } from 'src/electron/shared/interfaces/download.interface' -import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface' -import { FileTransfer } from './FileTransfer' import { rimraf } from 'rimraf' -import { FilesystemChecker } from './FilesystemChecker' -import { getSettings } from '../SettingsHandler.ipc' -import { hasVideoExtension } from '../../shared/ElectronUtilFunctions' -type EventCallback = { - /** Note: this will not be the last event if `retry()` is called. */ - 'error': () => void - 'complete': () => void +import { NewDownload, ProgressType } from 'src/electron/shared/interfaces/download.interface' +import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface' +import { emitIPCEvent } from '../../main' +import { hasVideoExtension } from '../../shared/ElectronUtilFunctions' +import { interpolate, sanitizeFilename } from '../../shared/UtilFunctions' +import { getSettings } from '../SettingsHandler.ipc' +import { FileDownloader, getDownloader } from './FileDownloader' +import { FileExtractor } from './FileExtractor' +import { FilesystemChecker } from './FilesystemChecker' +import { FileTransfer } from './FileTransfer' + +interface EventCallback { + /** Note: this will not be the last event if `retry()` is called. */ + 'error': () => void + 'complete': () => void } type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } -export type DownloadError = { header: string; body: string; isLink?: boolean } +export interface DownloadError { header: string; body: string; isLink?: boolean } export class ChartDownload { - private retryFn: () => void | Promise - private cancelFn: () => void + private retryFn: () => void | Promise + private cancelFn: () => void - private callbacks = {} as Callbacks - private files: DriveFile[] - private percent = 0 // Needs to be stored here because errors won't know the exact percent - private tempPath: string - private wasCanceled = false + private callbacks = {} as Callbacks + private files: DriveFile[] + private percent = 0 // Needs to be stored here because errors won't know the exact percent + private tempPath: string + private wasCanceled = false - private readonly individualFileProgressPortion: number - private readonly destinationFolderName: string + private readonly individualFileProgressPortion: number + private readonly destinationFolderName: string - private _allFilesProgress = 0 - get allFilesProgress() { return this._allFilesProgress } - private _hasFailed = false - /** If this chart download needs to be retried */ - get hasFailed() { return this._hasFailed } - get isArchive() { return this.data.driveData.isArchive } - get hash() { return this.data.driveData.filesHash } + private _allFilesProgress = 0 + get allFilesProgress() { return this._allFilesProgress } + private _hasFailed = false + /** If this chart download needs to be retried */ + get hasFailed() { return this._hasFailed } + get isArchive() { return this.data.driveData.isArchive } + get hash() { return this.data.driveData.filesHash } - constructor(public versionID: number, private data: NewDownload) { - this.updateGUI('', 'Waiting for other downloads to finish...', 'good') - this.files = this.filterDownloadFiles(data.driveData.files) - this.individualFileProgressPortion = 80 / this.files.length - if (data.driveData.inChartPack) { - this.destinationFolderName = sanitizeFilename(parse(data.driveData.files[0].name).name) - } else { - this.destinationFolderName = sanitizeFilename(`${this.data.artist} - ${this.data.chartName} (${this.data.charter})`) - } - } + constructor(public versionID: number, private data: NewDownload) { + this.updateGUI('', 'Waiting for other downloads to finish...', 'good') + this.files = this.filterDownloadFiles(data.driveData.files) + this.individualFileProgressPortion = 80 / this.files.length + if (data.driveData.inChartPack) { + this.destinationFolderName = sanitizeFilename(parse(data.driveData.files[0].name).name) + } else { + this.destinationFolderName = sanitizeFilename(`${this.data.artist} - ${this.data.chartName} (${this.data.charter})`) + } + } - /** - * Calls `callback` when `event` fires. (no events will be fired after `this.cancel()` is called) - */ - on(event: E, callback: EventCallback[E]) { - this.callbacks[event] = callback - } + /** + * Calls `callback` when `event` fires. (no events will be fired after `this.cancel()` is called) + */ + on(event: E, callback: EventCallback[E]) { + this.callbacks[event] = callback + } - filterDownloadFiles(files: DriveFile[]) { - return files.filter(file => { - return (file.name != 'ch.dat') && (getSettings().downloadVideos || !hasVideoExtension(file.name)) - }) - } + filterDownloadFiles(files: DriveFile[]) { + return files.filter(file => { + return (file.name != 'ch.dat') && (getSettings().downloadVideos || !hasVideoExtension(file.name)) + }) + } - /** - * Retries the last failed step if it is running. - */ - retry() { // Only allow it to be called once - if (this.retryFn != undefined) { - this._hasFailed = false - const retryFn = this.retryFn - this.retryFn = undefined - retryFn() - } - } + /** + * Retries the last failed step if it is running. + */ + retry() { // Only allow it to be called once + if (this.retryFn != undefined) { + this._hasFailed = false + const retryFn = this.retryFn + this.retryFn = undefined + retryFn() + } + } - /** - * Updates the GUI to indicate that a retry will be attempted. - */ - displayRetrying() { - this.updateGUI('', 'Waiting for other downloads to finish to retry...', 'good') - } + /** + * Updates the GUI to indicate that a retry will be attempted. + */ + displayRetrying() { + this.updateGUI('', 'Waiting for other downloads to finish to retry...', 'good') + } - /** - * Cancels the download if it is running. - */ - cancel() { // Only allow it to be called once - if (this.cancelFn != undefined) { - const cancelFn = this.cancelFn - this.cancelFn = undefined - cancelFn() - rimraf(this.tempPath).catch(() => { /** Do nothing */ }) // Delete temp folder - } - this.updateGUI('', '', 'cancel') - this.wasCanceled = true - } + /** + * Cancels the download if it is running. + */ + cancel() { // Only allow it to be called once + if (this.cancelFn != undefined) { + const cancelFn = this.cancelFn + this.cancelFn = undefined + cancelFn() + rimraf(this.tempPath).catch(() => { /** Do nothing */ }) // Delete temp folder + } + this.updateGUI('', '', 'cancel') + this.wasCanceled = true + } - /** - * Updates the GUI with new information about this chart download. - */ - private updateGUI(header: string, description: string, type: ProgressType, isLink = false) { - if (this.wasCanceled) { return } + /** + * Updates the GUI with new information about this chart download. + */ + private updateGUI(header: string, description: string, type: ProgressType, isLink = false) { + if (this.wasCanceled) { return } - emitIPCEvent('download-updated', { - versionID: this.versionID, - title: `${this.data.chartName} - ${this.data.artist}`, - header: header, - description: description, - percent: this.percent, - type: type, - isLink - }) - } + emitIPCEvent('download-updated', { + versionID: this.versionID, + title: `${this.data.chartName} - ${this.data.artist}`, + header: header, + description: description, + percent: this.percent, + type: type, + isLink, + }) + } - /** - * Save the retry function, update the GUI, and call the `error` callback. - */ - private handleError(err: DownloadError, retry: () => void) { - this._hasFailed = true - this.retryFn = retry - this.updateGUI(err.header, err.body, 'error', err.isLink == true) - this.callbacks.error() - } + /** + * Save the retry function, update the GUI, and call the `error` callback. + */ + private handleError(err: DownloadError, retry: () => void) { + this._hasFailed = true + this.retryFn = retry + this.updateGUI(err.header, err.body, 'error', err.isLink == true) + this.callbacks.error() + } - /** - * Starts the download process. - */ - async beginDownload() { - // CHECK FILESYSTEM ACCESS - const checker = new FilesystemChecker(this.destinationFolderName) - this.cancelFn = () => checker.cancelCheck() + /** + * Starts the download process. + */ + async beginDownload() { + // CHECK FILESYSTEM ACCESS + const checker = new FilesystemChecker(this.destinationFolderName) + this.cancelFn = () => checker.cancelCheck() - const checkerComplete = this.addFilesystemCheckerEventListeners(checker) - checker.beginCheck() - await checkerComplete + const checkerComplete = this.addFilesystemCheckerEventListeners(checker) + checker.beginCheck() + await checkerComplete - // DOWNLOAD FILES - for (let i = 0; i < this.files.length; i++) { - let wasCanceled = false - this.cancelFn = () => { wasCanceled = true } - const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name)) - if (wasCanceled) { return } - this.cancelFn = () => downloader.cancelDownload() + // DOWNLOAD FILES + for (let i = 0; i < this.files.length; i++) { + let wasCanceled = false + this.cancelFn = () => { wasCanceled = true } + const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name)) + if (wasCanceled) { return } + this.cancelFn = () => downloader.cancelDownload() - const downloadComplete = this.addDownloadEventListeners(downloader, i) - downloader.beginDownload() - await downloadComplete - } + const downloadComplete = this.addDownloadEventListeners(downloader, i) + downloader.beginDownload() + await downloadComplete + } - // EXTRACT FILES - if (this.isArchive) { - const extractor = new FileExtractor(this.tempPath) - this.cancelFn = () => extractor.cancelExtract() + // EXTRACT FILES + if (this.isArchive) { + const extractor = new FileExtractor(this.tempPath) + this.cancelFn = () => extractor.cancelExtract() - const extractComplete = this.addExtractorEventListeners(extractor) - extractor.beginExtract() - await extractComplete - } + const extractComplete = this.addExtractorEventListeners(extractor) + extractor.beginExtract() + await extractComplete + } - // TRANSFER FILES - const transfer = new FileTransfer(this.tempPath, this.destinationFolderName) - this.cancelFn = () => transfer.cancelTransfer() + // TRANSFER FILES + const transfer = new FileTransfer(this.tempPath, this.destinationFolderName) + this.cancelFn = () => transfer.cancelTransfer() - const transferComplete = this.addTransferEventListeners(transfer) - transfer.beginTransfer() - await transferComplete + const transferComplete = this.addTransferEventListeners(transfer) + transfer.beginTransfer() + await transferComplete - this.callbacks.complete() - } + this.callbacks.complete() + } - /** - * Defines what happens in reponse to `FilesystemChecker` events. - * @returns a `Promise` that resolves when the filesystem has been checked. - */ - private addFilesystemCheckerEventListeners(checker: FilesystemChecker) { - checker.on('start', () => { - this.updateGUI('Checking filesystem...', '', 'good') - }) + /** + * Defines what happens in reponse to `FilesystemChecker` events. + * @returns a `Promise` that resolves when the filesystem has been checked. + */ + private addFilesystemCheckerEventListeners(checker: FilesystemChecker) { + checker.on('start', () => { + this.updateGUI('Checking filesystem...', '', 'good') + }) - checker.on('error', this.handleError.bind(this)) + checker.on('error', this.handleError.bind(this)) - return new Promise(resolve => { - checker.on('complete', (tempPath) => { - this.tempPath = tempPath - resolve() - }) - }) - } + return new Promise(resolve => { + checker.on('complete', tempPath => { + this.tempPath = tempPath + resolve() + }) + }) + } - /** - * Defines what happens in response to `FileDownloader` events. - * @returns a `Promise` that resolves when the download finishes. - */ - private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) { - let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})` - let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts - let fileProgress = 0 + /** + * Defines what happens in response to `FileDownloader` events. + * @returns a `Promise` that resolves when the download finishes. + */ + private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) { + let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})` + let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts + let fileProgress = 0 - downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => { - downloadStartPoint = this.individualFileProgressPortion / 2 - this.percent = this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2) - this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good') - }) + downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => { + downloadStartPoint = this.individualFileProgressPortion / 2 + this.percent = + this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2) + this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good') + }) - downloader.on('requestSent', () => { - fileProgress = downloadStartPoint - this.percent = this._allFilesProgress + fileProgress - this.updateGUI(downloadHeader, 'Sending request...', 'good') - }) + downloader.on('requestSent', () => { + fileProgress = downloadStartPoint + this.percent = this._allFilesProgress + fileProgress + this.updateGUI(downloadHeader, 'Sending request...', 'good') + }) - downloader.on('downloadProgress', (bytesDownloaded: number) => { - downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})` - const size = Number(this.files[fileIndex].size) - fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion) - this.percent = this._allFilesProgress + fileProgress - this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good') - }) + downloader.on('downloadProgress', (bytesDownloaded: number) => { + downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})` + const size = Number(this.files[fileIndex].size) + fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion) + this.percent = this._allFilesProgress + fileProgress + this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good') + }) - downloader.on('error', this.handleError.bind(this)) + downloader.on('error', this.handleError.bind(this)) - return new Promise(resolve => { - downloader.on('complete', () => { - this._allFilesProgress += this.individualFileProgressPortion - resolve() - }) - }) - } + return new Promise(resolve => { + downloader.on('complete', () => { + this._allFilesProgress += this.individualFileProgressPortion + resolve() + }) + }) + } - /** - * Defines what happens in response to `FileExtractor` events. - * @returns a `Promise` that resolves when the extraction finishes. - */ - private addExtractorEventListeners(extractor: FileExtractor) { - let archive = '' + /** + * Defines what happens in response to `FileExtractor` events. + * @returns a `Promise` that resolves when the extraction finishes. + */ + private addExtractorEventListeners(extractor: FileExtractor) { + let archive = '' - extractor.on('start', (filename) => { - archive = filename - this.updateGUI(`[${archive}]`, 'Extracting...', 'good') - }) + extractor.on('start', filename => { + archive = filename + this.updateGUI(`[${archive}]`, 'Extracting...', 'good') + }) - extractor.on('extractProgress', (percent, filecount) => { - this.percent = interpolate(percent, 0, 100, 80, 95) - this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good') - }) + extractor.on('extractProgress', (percent, filecount) => { + this.percent = interpolate(percent, 0, 100, 80, 95) + this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good') + }) - extractor.on('error', this.handleError.bind(this)) + extractor.on('error', this.handleError.bind(this)) - return new Promise(resolve => { - extractor.on('complete', () => { - this.percent = 95 - resolve() - }) - }) - } + return new Promise(resolve => { + extractor.on('complete', () => { + this.percent = 95 + resolve() + }) + }) + } - /** - * Defines what happens in response to `FileTransfer` events. - * @returns a `Promise` that resolves when the transfer finishes. - */ - private addTransferEventListeners(transfer: FileTransfer) { - let destinationFolder: string + /** + * Defines what happens in response to `FileTransfer` events. + * @returns a `Promise` that resolves when the transfer finishes. + */ + private addTransferEventListeners(transfer: FileTransfer) { + let destinationFolder: string - transfer.on('start', (_destinationFolder) => { - destinationFolder = _destinationFolder - this.updateGUI('Moving files to library folder...', destinationFolder, 'good', true) - }) + transfer.on('start', _destinationFolder => { + destinationFolder = _destinationFolder + this.updateGUI('Moving files to library folder...', destinationFolder, 'good', true) + }) - transfer.on('error', this.handleError.bind(this)) + transfer.on('error', this.handleError.bind(this)) - return new Promise(resolve => { - transfer.on('complete', () => { - this.percent = 100 - this.updateGUI('Download complete.', destinationFolder, 'done', true) - resolve() - }) - }) - } -} \ No newline at end of file + return new Promise(resolve => { + transfer.on('complete', () => { + this.percent = 100 + this.updateGUI('Download complete.', destinationFolder, 'done', true) + resolve() + }) + }) + } +} diff --git a/src/electron/ipc/download/DownloadHandler.ts b/src/electron/ipc/download/DownloadHandler.ts index a53b427..a840446 100644 --- a/src/electron/ipc/download/DownloadHandler.ts +++ b/src/electron/ipc/download/DownloadHandler.ts @@ -1,86 +1,86 @@ -import { IPCEmitHandler } from '../../shared/IPCHandler' import { Download } from '../../shared/interfaces/download.interface' +import { IPCEmitHandler } from '../../shared/IPCHandler' import { ChartDownload } from './ChartDownload' import { DownloadQueue } from './DownloadQueue' class DownloadHandler implements IPCEmitHandler<'download'> { - event: 'download' = 'download' + event = 'download' as const - downloadQueue: DownloadQueue = new DownloadQueue() - currentDownload: ChartDownload = undefined - retryWaiting: ChartDownload[] = [] + downloadQueue: DownloadQueue = new DownloadQueue() + currentDownload: ChartDownload = undefined + retryWaiting: ChartDownload[] = [] - handler(data: Download) { - switch (data.action) { - case 'add': this.addDownload(data); break - case 'retry': this.retryDownload(data); break - case 'cancel': this.cancelDownload(data); break - } - } + handler(data: Download) { + switch (data.action) { + case 'add': this.addDownload(data); break + case 'retry': this.retryDownload(data); break + case 'cancel': this.cancelDownload(data); break + } + } - private addDownload(data: Download) { - const filesHash = data.data.driveData.filesHash // Note: using versionID would cause chart packs to download multiple times - if (this.currentDownload?.hash == filesHash || this.downloadQueue.isDownloadingLink(filesHash)) { - return - } + private addDownload(data: Download) { + const filesHash = data.data.driveData.filesHash // Note: using versionID would cause chart packs to download multiple times + if (this.currentDownload?.hash == filesHash || this.downloadQueue.isDownloadingLink(filesHash)) { + return + } - const newDownload = new ChartDownload(data.versionID, data.data) - this.addDownloadEventListeners(newDownload) - if (this.currentDownload == undefined) { - this.currentDownload = newDownload - newDownload.beginDownload() - } else { - this.downloadQueue.push(newDownload) - } - } + const newDownload = new ChartDownload(data.versionID, data.data) + this.addDownloadEventListeners(newDownload) + if (this.currentDownload == undefined) { + this.currentDownload = newDownload + newDownload.beginDownload() + } else { + this.downloadQueue.push(newDownload) + } + } - private retryDownload(data: Download) { - const index = this.retryWaiting.findIndex(download => download.versionID == data.versionID) - if (index != -1) { - const retryDownload = this.retryWaiting.splice(index, 1)[0] - retryDownload.displayRetrying() - if (this.currentDownload == undefined) { - this.currentDownload = retryDownload - retryDownload.retry() - } else { - this.downloadQueue.push(retryDownload) - } - } - } + private retryDownload(data: Download) { + const index = this.retryWaiting.findIndex(download => download.versionID == data.versionID) + if (index != -1) { + const retryDownload = this.retryWaiting.splice(index, 1)[0] + retryDownload.displayRetrying() + if (this.currentDownload == undefined) { + this.currentDownload = retryDownload + retryDownload.retry() + } else { + this.downloadQueue.push(retryDownload) + } + } + } - private cancelDownload(data: Download) { - if (this.currentDownload?.versionID == data.versionID) { - this.currentDownload.cancel() - this.currentDownload = undefined - this.startNextDownload() - } else { - this.downloadQueue.remove(data.versionID) - } - } + private cancelDownload(data: Download) { + if (this.currentDownload?.versionID == data.versionID) { + this.currentDownload.cancel() + this.currentDownload = undefined + this.startNextDownload() + } else { + this.downloadQueue.remove(data.versionID) + } + } - private addDownloadEventListeners(download: ChartDownload) { - download.on('complete', () => { - this.currentDownload = undefined - this.startNextDownload() - }) + private addDownloadEventListeners(download: ChartDownload) { + download.on('complete', () => { + this.currentDownload = undefined + this.startNextDownload() + }) - download.on('error', () => { - this.retryWaiting.push(this.currentDownload) - this.currentDownload = undefined - this.startNextDownload() - }) - } + download.on('error', () => { + this.retryWaiting.push(this.currentDownload) + this.currentDownload = undefined + this.startNextDownload() + }) + } - private startNextDownload() { - if (!this.downloadQueue.isEmpty()) { - this.currentDownload = this.downloadQueue.shift() - if (this.currentDownload.hasFailed) { - this.currentDownload.retry() - } else { - this.currentDownload.beginDownload() - } - } - } + private startNextDownload() { + if (!this.downloadQueue.isEmpty()) { + this.currentDownload = this.downloadQueue.shift() + if (this.currentDownload.hasFailed) { + this.currentDownload.retry() + } else { + this.currentDownload.beginDownload() + } + } + } } -export const downloadHandler = new DownloadHandler() \ No newline at end of file +export const downloadHandler = new DownloadHandler() diff --git a/src/electron/ipc/download/DownloadQueue.ts b/src/electron/ipc/download/DownloadQueue.ts index fc2479f..55a021e 100644 --- a/src/electron/ipc/download/DownloadQueue.ts +++ b/src/electron/ipc/download/DownloadQueue.ts @@ -1,50 +1,51 @@ import Comparators from 'comparators' -import { ChartDownload } from './ChartDownload' + import { emitIPCEvent } from '../../main' +import { ChartDownload } from './ChartDownload' export class DownloadQueue { - private downloadQueue: ChartDownload[] = [] + private downloadQueue: ChartDownload[] = [] - isDownloadingLink(filesHash: string) { - return this.downloadQueue.some(download => download.hash == filesHash) - } + isDownloadingLink(filesHash: string) { + return this.downloadQueue.some(download => download.hash == filesHash) + } - isEmpty() { - return this.downloadQueue.length == 0 - } + isEmpty() { + return this.downloadQueue.length == 0 + } - push(chartDownload: ChartDownload) { - this.downloadQueue.push(chartDownload) - this.sort() - } + push(chartDownload: ChartDownload) { + this.downloadQueue.push(chartDownload) + this.sort() + } - shift() { - return this.downloadQueue.shift() - } + shift() { + return this.downloadQueue.shift() + } - get(versionID: number) { - return this.downloadQueue.find(download => download.versionID == versionID) - } + get(versionID: number) { + return this.downloadQueue.find(download => download.versionID == versionID) + } - remove(versionID: number) { - const index = this.downloadQueue.findIndex(download => download.versionID == versionID) - if (index != -1) { - this.downloadQueue[index].cancel() - this.downloadQueue.splice(index, 1) - emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID)) - } - } + remove(versionID: number) { + const index = this.downloadQueue.findIndex(download => download.versionID == versionID) + if (index != -1) { + this.downloadQueue[index].cancel() + this.downloadQueue.splice(index, 1) + emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID)) + } + } - private sort() { - let comparator = Comparators.comparing('allFilesProgress', { reversed: true }) + private sort() { + let comparator = Comparators.comparing('allFilesProgress', { reversed: true }) - const prioritizeArchives = true - if (prioritizeArchives) { - comparator = comparator.thenComparing('isArchive', { reversed: true }) - } + const prioritizeArchives = true + if (prioritizeArchives) { + comparator = comparator.thenComparing('isArchive', { reversed: true }) + } - this.downloadQueue.sort(comparator) - emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID)) - } -} \ No newline at end of file + this.downloadQueue.sort(comparator) + emitIPCEvent('queue-updated', this.downloadQueue.map(download => download.versionID)) + } +} diff --git a/src/electron/ipc/download/FileDownloader.ts b/src/electron/ipc/download/FileDownloader.ts index 7d194a9..71323eb 100644 --- a/src/electron/ipc/download/FileDownloader.ts +++ b/src/electron/ipc/download/FileDownloader.ts @@ -1,42 +1,44 @@ -import { AnyFunction } from '../../shared/UtilFunctions' -import { devLog } from '../../shared/ElectronUtilFunctions' +import Bottleneck from 'bottleneck' import { createWriteStream, writeFile as _writeFile } from 'fs' +import { google } from 'googleapis' import * as needle from 'needle' +import { join } from 'path' import { Readable } from 'stream' +import { inspect, promisify } from 'util' + +import { devLog } from '../../shared/ElectronUtilFunctions' +import { tempPath } from '../../shared/Paths' +import { AnyFunction } from '../../shared/UtilFunctions' +import { DownloadError } from './ChartDownload' // TODO: replace needle with got (for cancel() method) (if before-headers event is possible?) import { googleTimer } from './GoogleTimer' -import { DownloadError } from './ChartDownload' -import { google } from 'googleapis' -import Bottleneck from 'bottleneck' -import { inspect, promisify } from 'util' -import { join } from 'path' -import { tempPath } from '../../shared/Paths' + const drive = google.drive('v3') const limiter = new Bottleneck({ - minTime: 200 // Wait 200 ms between API requests + minTime: 200, // Wait 200 ms between API requests }) const RETRY_MAX = 2 const writeFile = promisify(_writeFile) -type EventCallback = { - 'waitProgress': (remainingSeconds: number, totalSeconds: number) => void - /** Note: this event can be called multiple times if the connection times out or a large file is downloaded */ - 'requestSent': () => void - 'downloadProgress': (bytesDownloaded: number) => void - /** Note: after calling retry, the event lifecycle restarts */ - 'error': (err: DownloadError, retry: () => void) => void - 'complete': () => void +interface EventCallback { + 'waitProgress': (remainingSeconds: number, totalSeconds: number) => void + /** Note: this event can be called multiple times if the connection times out or a large file is downloaded */ + 'requestSent': () => void + 'downloadProgress': (bytesDownloaded: number) => void + /** Note: after calling retry, the event lifecycle restarts */ + 'error': (err: DownloadError, retry: () => void) => void + 'complete': () => void } type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } export type FileDownloader = APIFileDownloader | SlowFileDownloader const downloadErrors = { - timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } }, - connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } }, - responseError: (statusCode: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } }, - htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } }, - linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } } + timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } }, + connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } }, + responseError: (statusCode: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } }, + htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } }, + linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } }, } /** @@ -48,7 +50,7 @@ const downloadErrors = { * @param fullPath The full path to where this file should be stored (including the filename). */ export function getDownloader(url: string, fullPath: string): FileDownloader { - return new SlowFileDownloader(url, fullPath) + return new SlowFileDownloader(url, fullPath) } /** @@ -56,141 +58,141 @@ export function getDownloader(url: string, fullPath: string): FileDownloader { * On error, provides the ability to retry. */ class APIFileDownloader { - private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u + private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u - private callbacks = {} as Callbacks - private retryCount: number - private wasCanceled = false - private fileID: string - private downloadStream: Readable + private callbacks = {} as Callbacks + private retryCount: number + private wasCanceled = false + private fileID: string + private downloadStream: Readable - /** - * @param url The download link. - * @param fullPath The full path to where this file should be stored (including the filename). - */ - constructor(private url: string, private fullPath: string) { - // url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download" - this.fileID = this.URL_REGEX.exec(url)[1] - } + /** + * @param url The download link. + * @param fullPath The full path to where this file should be stored (including the filename). + */ + constructor(private url: string, private fullPath: string) { + // url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download" + this.fileID = this.URL_REGEX.exec(url)[1] + } - /** - * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) - */ - on(event: E, callback: EventCallback[E]) { - this.callbacks[event] = callback - } + /** + * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) + */ + on(event: E, callback: EventCallback[E]) { + this.callbacks[event] = callback + } - /** - * Download the file after waiting for the google rate limit. - */ - beginDownload() { - if (this.fileID == undefined) { - this.failDownload(downloadErrors.linkError(this.url)) - } + /** + * Download the file after waiting for the google rate limit. + */ + beginDownload() { + if (this.fileID == undefined) { + this.failDownload(downloadErrors.linkError(this.url)) + } - this.startDownloadStream() - } + this.startDownloadStream() + } - /** - * Uses the Google Drive API to start a download stream for the file with `this.fileID`. - */ - private startDownloadStream() { - limiter.schedule(this.cancelable(async () => { - this.callbacks.requestSent() - try { - this.downloadStream = (await drive.files.get({ - fileId: this.fileID, - alt: 'media' - }, { - responseType: 'stream' - })).data + /** + * Uses the Google Drive API to start a download stream for the file with `this.fileID`. + */ + private startDownloadStream() { + limiter.schedule(this.cancelable(async () => { + this.callbacks.requestSent() + try { + this.downloadStream = (await drive.files.get({ + fileId: this.fileID, + alt: 'media', + }, { + responseType: 'stream', + })).data - if (this.wasCanceled) { return } + if (this.wasCanceled) { return } - this.handleDownloadResponse() - } catch (err) { - this.retryCount++ - if (this.retryCount <= RETRY_MAX) { - devLog(`Failed to get file: Retry attempt ${this.retryCount}...`) - if (this.wasCanceled) { return } - this.startDownloadStream() - } else { - devLog(inspect(err)) - if (err?.code && err?.response?.statusText) { - this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`)) - } else { - this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown')) - } - } - } - })) - } + this.handleDownloadResponse() + } catch (err) { + this.retryCount++ + if (this.retryCount <= RETRY_MAX) { + devLog(`Failed to get file: Retry attempt ${this.retryCount}...`) + if (this.wasCanceled) { return } + this.startDownloadStream() + } else { + devLog(inspect(err)) + if (err?.code && err?.response?.statusText) { + this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`)) + } else { + this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown')) + } + } + } + })) + } - /** - * Pipes the data from a download response to `this.fullPath`. - * @param req The download request. - */ - private handleDownloadResponse() { - this.callbacks.downloadProgress(0) - let downloadedSize = 0 - const writeStream = createWriteStream(this.fullPath) + /** + * Pipes the data from a download response to `this.fullPath`. + * @param req The download request. + */ + private handleDownloadResponse() { + this.callbacks.downloadProgress(0) + let downloadedSize = 0 + const writeStream = createWriteStream(this.fullPath) - try { - this.downloadStream.pipe(writeStream) - } catch (err) { - this.failDownload(downloadErrors.connectionError(err)) - } + try { + this.downloadStream.pipe(writeStream) + } catch (err) { + this.failDownload(downloadErrors.connectionError(err)) + } - this.downloadStream.on('data', this.cancelable((chunk: Buffer) => { - downloadedSize += chunk.length - })) + this.downloadStream.on('data', this.cancelable((chunk: Buffer) => { + downloadedSize += chunk.length + })) - const progressUpdater = setInterval(() => { - this.callbacks.downloadProgress(downloadedSize) - }, 100) + const progressUpdater = setInterval(() => { + this.callbacks.downloadProgress(downloadedSize) + }, 100) - this.downloadStream.on('error', this.cancelable((err: Error) => { - clearInterval(progressUpdater) - this.failDownload(downloadErrors.connectionError(err)) - })) + this.downloadStream.on('error', this.cancelable((err: Error) => { + clearInterval(progressUpdater) + this.failDownload(downloadErrors.connectionError(err)) + })) - this.downloadStream.on('end', this.cancelable(() => { - clearInterval(progressUpdater) - writeStream.end() - this.downloadStream.destroy() - this.downloadStream = null + this.downloadStream.on('end', this.cancelable(() => { + clearInterval(progressUpdater) + writeStream.end() + this.downloadStream.destroy() + this.downloadStream = null - this.callbacks.complete() - })) - } + this.callbacks.complete() + })) + } - /** - * Display an error message and provide a function to retry the download. - */ - private failDownload(error: DownloadError) { - this.callbacks.error(error, this.cancelable(() => this.beginDownload())) - } + /** + * Display an error message and provide a function to retry the download. + */ + private failDownload(error: DownloadError) { + this.callbacks.error(error, this.cancelable(() => this.beginDownload())) + } - /** - * Stop the process of downloading the file. (no more events will be fired after this is called) - */ - cancelDownload() { - this.wasCanceled = true - googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting - if (this.downloadStream) { - this.downloadStream.destroy() - } - } + /** + * Stop the process of downloading the file. (no more events will be fired after this is called) + */ + cancelDownload() { + this.wasCanceled = true + googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting + if (this.downloadStream) { + this.downloadStream.destroy() + } + } - /** - * Wraps a function that is able to be prevented if `this.cancelDownload()` was called. - */ - private cancelable(fn: F) { - return (...args: Parameters): ReturnType => { - if (this.wasCanceled) { return } - return fn(...Array.from(args)) - } - } + /** + * Wraps a function that is able to be prevented if `this.cancelDownload()` was called. + */ + private cancelable(fn: F) { + return (...args: Parameters): ReturnType => { + if (this.wasCanceled) { return } + return fn(...Array.from(args)) + } + } } /** @@ -201,166 +203,166 @@ class APIFileDownloader { */ class SlowFileDownloader { - private callbacks = {} as Callbacks - private retryCount: number - private wasCanceled = false - private req: NodeJS.ReadableStream + private callbacks = {} as Callbacks + private retryCount: number + private wasCanceled = false + private req: NodeJS.ReadableStream - /** - * @param url The download link. - * @param fullPath The full path to where this file should be stored (including the filename). - */ - constructor(private url: string, private fullPath: string) { } + /** + * @param url The download link. + * @param fullPath The full path to where this file should be stored (including the filename). + */ + constructor(private url: string, private fullPath: string) { } - /** - * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) - */ - on(event: E, callback: EventCallback[E]) { - this.callbacks[event] = callback - } + /** + * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) + */ + on(event: E, callback: EventCallback[E]) { + this.callbacks[event] = callback + } - /** - * Download the file after waiting for the google rate limit. - */ - beginDownload() { - googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => { - this.callbacks.waitProgress(remainingSeconds, totalSeconds) - })) + /** + * Download the file after waiting for the google rate limit. + */ + beginDownload() { + googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => { + this.callbacks.waitProgress(remainingSeconds, totalSeconds) + })) - googleTimer.on('complete', this.cancelable(() => { - this.requestDownload() - })) - } + googleTimer.on('complete', this.cancelable(() => { + this.requestDownload() + })) + } - /** - * Sends a request to download the file at `this.url`. - * @param cookieHeader the "cookie=" header to include this request. - */ - private requestDownload(cookieHeader?: string) { - this.callbacks.requestSent() - this.req = needle.get(this.url, { - 'follow_max': 10, - 'open_timeout': 5000, - 'headers': Object.assign({ - 'Referer': this.url, - 'Accept': '*/*' - }, - (cookieHeader ? { 'Cookie': cookieHeader } : undefined) - ) - }) + /** + * Sends a request to download the file at `this.url`. + * @param cookieHeader the "cookie=" header to include this request. + */ + private requestDownload(cookieHeader?: string) { + this.callbacks.requestSent() + this.req = needle.get(this.url, { + 'follow_max': 10, + 'open_timeout': 5000, + 'headers': Object.assign({ + 'Referer': this.url, + 'Accept': '*/*', + }, + (cookieHeader ? { 'Cookie': cookieHeader } : undefined) + ), + }) - this.req.on('timeout', this.cancelable((type: string) => { - this.retryCount++ - if (this.retryCount <= RETRY_MAX) { - devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`) - this.requestDownload(cookieHeader) - } else { - this.failDownload(downloadErrors.timeout(type)) - } - })) + this.req.on('timeout', this.cancelable((type: string) => { + this.retryCount++ + if (this.retryCount <= RETRY_MAX) { + devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`) + this.requestDownload(cookieHeader) + } else { + this.failDownload(downloadErrors.timeout(type)) + } + })) - this.req.on('err', this.cancelable((err: Error) => { - this.failDownload(downloadErrors.connectionError(err)) - })) + this.req.on('err', this.cancelable((err: Error) => { + this.failDownload(downloadErrors.connectionError(err)) + })) - this.req.on('header', this.cancelable((statusCode, headers: Headers) => { - if (statusCode != 200) { - this.failDownload(downloadErrors.responseError(statusCode)) - return - } + this.req.on('header', this.cancelable((statusCode, headers: Headers) => { + if (statusCode != 200) { + this.failDownload(downloadErrors.responseError(statusCode)) + return + } - if (headers['content-type'].startsWith('text/html')) { - this.handleHTMLResponse(headers['set-cookie']) - } else { - this.handleDownloadResponse() - } - })) - } + if (headers['content-type'].startsWith('text/html')) { + this.handleHTMLResponse(headers['set-cookie']) + } else { + this.handleDownloadResponse() + } + })) + } - /** - * A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning. - * This function sends the request that results from clicking "download anyway", or generates an error if it can't be found. - * @param cookieHeader The "cookie=" header of this request. - */ - private handleHTMLResponse(cookieHeader: string) { - let virusScanHTML = '' - this.req.on('data', this.cancelable(data => virusScanHTML += data)) - this.req.on('done', this.cancelable((err: Error) => { - if (err) { - this.failDownload(downloadErrors.connectionError(err)) - } else { - try { - const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g - const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML) - const confirmToken = confirmTokenResults[1] - const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length) - this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}` - const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1] - const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%') - const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}` - this.requestDownload(newHeader) - } catch(e) { - this.saveHTMLError(virusScanHTML).then((path) => { - this.failDownload(downloadErrors.htmlError(path)) - }) - } - } - })) - } + /** + * A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning. + * This function sends the request that results from clicking "download anyway", or generates an error if it can't be found. + * @param cookieHeader The "cookie=" header of this request. + */ + private handleHTMLResponse(cookieHeader: string) { + let virusScanHTML = '' + this.req.on('data', this.cancelable(data => virusScanHTML += data)) + this.req.on('done', this.cancelable((err: Error) => { + if (err) { + this.failDownload(downloadErrors.connectionError(err)) + } else { + try { + const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g + const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML) + const confirmToken = confirmTokenResults[1] + const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length) + this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}` + const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1] + const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%') + const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}` + this.requestDownload(newHeader) + } catch (e) { + this.saveHTMLError(virusScanHTML).then(path => { + this.failDownload(downloadErrors.htmlError(path)) + }) + } + } + })) + } - /** - * Pipes the data from a download response to `this.fullPath`. - * @param req The download request. - */ - private handleDownloadResponse() { - this.callbacks.downloadProgress(0) - let downloadedSize = 0 - this.req.pipe(createWriteStream(this.fullPath)) - this.req.on('data', this.cancelable((data) => { - downloadedSize += data.length - this.callbacks.downloadProgress(downloadedSize) - })) + /** + * Pipes the data from a download response to `this.fullPath`. + * @param req The download request. + */ + private handleDownloadResponse() { + this.callbacks.downloadProgress(0) + let downloadedSize = 0 + this.req.pipe(createWriteStream(this.fullPath)) + this.req.on('data', this.cancelable(data => { + downloadedSize += data.length + this.callbacks.downloadProgress(downloadedSize) + })) - this.req.on('err', this.cancelable((err: Error) => { - this.failDownload(downloadErrors.connectionError(err)) - })) + this.req.on('err', this.cancelable((err: Error) => { + this.failDownload(downloadErrors.connectionError(err)) + })) - this.req.on('end', this.cancelable(() => { - this.callbacks.complete() - })) - } + this.req.on('end', this.cancelable(() => { + this.callbacks.complete() + })) + } - private async saveHTMLError(text: string) { - const errorPath = join(tempPath, 'HTMLError.html') - await writeFile(errorPath, text) - return errorPath - } + private async saveHTMLError(text: string) { + const errorPath = join(tempPath, 'HTMLError.html') + await writeFile(errorPath, text) + return errorPath + } - /** - * Display an error message and provide a function to retry the download. - */ - private failDownload(error: DownloadError) { - this.callbacks.error(error, this.cancelable(() => this.beginDownload())) - } + /** + * Display an error message and provide a function to retry the download. + */ + private failDownload(error: DownloadError) { + this.callbacks.error(error, this.cancelable(() => this.beginDownload())) + } - /** - * Stop the process of downloading the file. (no more events will be fired after this is called) - */ - cancelDownload() { - this.wasCanceled = true - googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting - if (this.req) { - // TODO: destroy request - } - } + /** + * Stop the process of downloading the file. (no more events will be fired after this is called) + */ + cancelDownload() { + this.wasCanceled = true + googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting + if (this.req) { + // TODO: destroy request + } + } - /** - * Wraps a function that is able to be prevented if `this.cancelDownload()` was called. - */ - private cancelable(fn: F) { - return (...args: Parameters): ReturnType => { - if (this.wasCanceled) { return } - return fn(...Array.from(args)) - } - } -} \ No newline at end of file + /** + * Wraps a function that is able to be prevented if `this.cancelDownload()` was called. + */ + private cancelable(fn: F) { + return (...args: Parameters): ReturnType => { + if (this.wasCanceled) { return } + return fn(...Array.from(args)) + } + } +} diff --git a/src/electron/ipc/download/FileExtractor.ts b/src/electron/ipc/download/FileExtractor.ts index b7a130d..1235ca1 100644 --- a/src/electron/ipc/download/FileExtractor.ts +++ b/src/electron/ipc/download/FileExtractor.ts @@ -1,163 +1,164 @@ -import { readdir, unlink, mkdir as _mkdir } from 'fs' -import { promisify } from 'util' -import { join, extname } from 'path' -import { AnyFunction } from '../../shared/UtilFunctions' -import { devLog } from '../../shared/ElectronUtilFunctions' -import * as node7z from 'node-7z' import * as zipBin from '7zip-bin' +import { mkdir as _mkdir, readdir, unlink } from 'fs' +import * as node7z from 'node-7z' import * as unrarjs from 'node-unrar-js' // TODO find better rar library that has async extraction import { FailReason } from 'node-unrar-js/dist/js/extractor' +import { extname, join } from 'path' +import { promisify } from 'util' + +import { devLog } from '../../shared/ElectronUtilFunctions' +import { AnyFunction } from '../../shared/UtilFunctions' import { DownloadError } from './ChartDownload' const mkdir = promisify(_mkdir) -type EventCallback = { - 'start': (filename: string) => void - 'extractProgress': (percent: number, fileCount: number) => void - 'error': (err: DownloadError, retry: () => void | Promise) => void - 'complete': () => void +interface EventCallback { + 'start': (filename: string) => void + 'extractProgress': (percent: number, fileCount: number) => void + 'error': (err: DownloadError, retry: () => void | Promise) => void + 'complete': () => void } type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } const extractErrors = { - readError: (err: NodeJS.ErrnoException) => { return { header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` } }, - emptyError: () => { return { header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' } }, - rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => { - return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})`} - }, - rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => { - return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})`} - } + readError: (err: NodeJS.ErrnoException) => { return { header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` } }, + emptyError: () => { return { header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' } }, + rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => { + return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})` } + }, + rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => { + return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})` } + }, } export class FileExtractor { - private callbacks = {} as Callbacks - private wasCanceled = false - constructor(private sourceFolder: string) { } + private callbacks = {} as Callbacks + private wasCanceled = false + constructor(private sourceFolder: string) { } - /** - * Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called) - */ - on(event: E, callback: EventCallback[E]) { - this.callbacks[event] = callback - } + /** + * Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called) + */ + on(event: E, callback: EventCallback[E]) { + this.callbacks[event] = callback + } - /** - * Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder) - */ - beginExtract() { - setTimeout(this.cancelable(() => { - readdir(this.sourceFolder, (err, files) => { - if (err) { - this.callbacks.error(extractErrors.readError(err), () => this.beginExtract()) - } else if (files.length == 0) { - this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract()) - } else { - this.callbacks.start(files[0]) - this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar') - } - }) - }), 100) // Wait for filesystem to process downloaded file - } + /** + * Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder) + */ + beginExtract() { + setTimeout(this.cancelable(() => { + readdir(this.sourceFolder, (err, files) => { + if (err) { + this.callbacks.error(extractErrors.readError(err), () => this.beginExtract()) + } else if (files.length == 0) { + this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract()) + } else { + this.callbacks.start(files[0]) + this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar') + } + }) + }), 100) // Wait for filesystem to process downloaded file + } - /** - * Extracts the file at `fullPath` to `this.sourceFolder`. - */ - private async extract(fullPath: string, useRarExtractor: boolean) { - if (useRarExtractor) { - await this.extractRar(fullPath) // Use node-unrar-js to extract the archive - } else { - this.extract7z(fullPath) // Use node-7z to extract the archive - } - } + /** + * Extracts the file at `fullPath` to `this.sourceFolder`. + */ + private async extract(fullPath: string, useRarExtractor: boolean) { + if (useRarExtractor) { + await this.extractRar(fullPath) // Use node-unrar-js to extract the archive + } else { + this.extract7z(fullPath) // Use node-7z to extract the archive + } + } - /** - * Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`. - * @throws an `ExtractError` if this fails. - */ - private async extractRar(fullPath: string) { - const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder) + /** + * Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`. + * @throws an `ExtractError` if this fails. + */ + private async extractRar(fullPath: string) { + const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder) - const fileList = extractor.getFileList() + const fileList = extractor.getFileList() - if (fileList[0].state != 'FAIL') { + if (fileList[0].state != 'FAIL') { - // Create directories for nested archives (because unrarjs didn't feel like handling that automatically) - const headers = fileList[1].fileHeaders - for (const header of headers) { - if (header.flags.directory) { - try { - await mkdir(join(this.sourceFolder, header.name), { recursive: true }) - } catch (err) { - this.callbacks.error(extractErrors.rarmkdirError(err, fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar')) - return - } - } - } - } + // Create directories for nested archives (because unrarjs didn't feel like handling that automatically) + const headers = fileList[1].fileHeaders + for (const header of headers) { + if (header.flags.directory) { + try { + await mkdir(join(this.sourceFolder, header.name), { recursive: true }) + } catch (err) { + this.callbacks.error(extractErrors.rarmkdirError(err, fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar')) + return + } + } + } + } - const extractResult = extractor.extractAll() + const extractResult = extractor.extractAll() - if (extractResult[0].state == 'FAIL') { - this.callbacks.error(extractErrors.rarextractError(extractResult[0], fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar')) - } else { - this.deleteArchive(fullPath) - } - } + if (extractResult[0].state == 'FAIL') { + this.callbacks.error(extractErrors.rarextractError(extractResult[0], fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar')) + } else { + this.deleteArchive(fullPath) + } + } - /** - * Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`. - */ - private extract7z(fullPath: string) { - const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :) - const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath }) + /** + * Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`. + */ + private extract7z(fullPath: string) { + const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :) + const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath }) - stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => { - this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount) - })) + stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => { + this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount) + })) - let extractErrorOccured = false - stream.on('error', this.cancelable(() => { - extractErrorOccured = true - devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`) - this.extract(fullPath, true) - })) + let extractErrorOccured = false + stream.on('error', this.cancelable(() => { + extractErrorOccured = true + devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`) + this.extract(fullPath, true) + })) - stream.on('end', this.cancelable(() => { - if (!extractErrorOccured) { - this.deleteArchive(fullPath) - } - })) - } + stream.on('end', this.cancelable(() => { + if (!extractErrorOccured) { + this.deleteArchive(fullPath) + } + })) + } - /** - * Tries to delete the archive at `fullPath`. - */ - private deleteArchive(fullPath: string) { - unlink(fullPath, this.cancelable((err) => { - if (err && err.code != 'ENOENT') { - devLog(`Warning: failed to delete archive at [${fullPath}]`) - } + /** + * Tries to delete the archive at `fullPath`. + */ + private deleteArchive(fullPath: string) { + unlink(fullPath, this.cancelable(err => { + if (err && err.code != 'ENOENT') { + devLog(`Warning: failed to delete archive at [${fullPath}]`) + } - this.callbacks.complete() - })) - } + this.callbacks.complete() + })) + } - /** - * Stop the process of extracting the file. (no more events will be fired after this is called) - */ - cancelExtract() { - this.wasCanceled = true - } + /** + * Stop the process of extracting the file. (no more events will be fired after this is called) + */ + cancelExtract() { + this.wasCanceled = true + } - /** - * Wraps a function that is able to be prevented if `this.cancelExtract()` was called. - */ - private cancelable(fn: F) { - return (...args: Parameters): ReturnType => { - if (this.wasCanceled) { return } - return fn(...Array.from(args)) - } - } -} \ No newline at end of file + /** + * Wraps a function that is able to be prevented if `this.cancelExtract()` was called. + */ + private cancelable(fn: F) { + return (...args: Parameters): ReturnType => { + if (this.wasCanceled) { return } + return fn(...Array.from(args)) + } + } +} diff --git a/src/electron/ipc/download/FileTransfer.ts b/src/electron/ipc/download/FileTransfer.ts index 113dcf1..8953c7e 100644 --- a/src/electron/ipc/download/FileTransfer.ts +++ b/src/electron/ipc/download/FileTransfer.ts @@ -1,108 +1,109 @@ import { Dirent, readdir as _readdir } from 'fs' -import { promisify } from 'util' -import { getSettings } from '../SettingsHandler.ipc' import * as mv from 'mv' import { join } from 'path' import { rimraf } from 'rimraf' +import { promisify } from 'util' + +import { getSettings } from '../SettingsHandler.ipc' import { DownloadError } from './ChartDownload' const readdir = promisify(_readdir) -type EventCallback = { - 'start': (destinationFolder: string) => void - 'error': (err: DownloadError, retry: () => void | Promise) => void - 'complete': () => void +interface EventCallback { + 'start': (destinationFolder: string) => void + 'error': (err: DownloadError, retry: () => void | Promise) => void + 'complete': () => void } type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } const transferErrors = { - readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'), - deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'), - rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder.'), - mvError: (err: NodeJS.ErrnoException) => fsError(err, `Failed to move folder to library.${err.code == 'EPERM' ? ' (does the chart already exist?)' : ''}`) + readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'), + deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'), + rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder.'), + mvError: (err: NodeJS.ErrnoException) => fsError(err, `Failed to move folder to library.${err.code == 'EPERM' ? ' (does the chart already exist?)' : ''}`), } function fsError(err: NodeJS.ErrnoException, description: string) { - return { header: description, body: `${err.name}: ${err.message}` } + return { header: description, body: `${err.name}: ${err.message}` } } export class FileTransfer { - private callbacks = {} as Callbacks - private wasCanceled = false - private destinationFolder: string - private nestedSourceFolder: string // The top-level folder that is copied to the library folder - constructor(private sourceFolder: string, destinationFolderName: string) { - this.destinationFolder = join(getSettings().libraryPath, destinationFolderName) - this.nestedSourceFolder = sourceFolder - } + private callbacks = {} as Callbacks + private wasCanceled = false + private destinationFolder: string + private nestedSourceFolder: string // The top-level folder that is copied to the library folder + constructor(private sourceFolder: string, destinationFolderName: string) { + this.destinationFolder = join(getSettings().libraryPath, destinationFolderName) + this.nestedSourceFolder = sourceFolder + } - /** - * Calls `callback` when `event` fires. (no events will be fired after `this.cancelTransfer()` is called) - */ - on(event: E, callback: EventCallback[E]) { - this.callbacks[event] = callback - } + /** + * Calls `callback` when `event` fires. (no events will be fired after `this.cancelTransfer()` is called) + */ + on(event: E, callback: EventCallback[E]) { + this.callbacks[event] = callback + } - async beginTransfer() { - this.callbacks.start(this.destinationFolder) - await this.cleanFolder() - if (this.wasCanceled) { return } - this.moveFolder() - } + async beginTransfer() { + this.callbacks.start(this.destinationFolder) + await this.cleanFolder() + if (this.wasCanceled) { return } + this.moveFolder() + } - /** - * Fixes common problems with the download chart folder. - */ - private async cleanFolder() { - let files: Dirent[] - try { - files = await readdir(this.nestedSourceFolder, { withFileTypes: true }) - } catch (err) { - this.callbacks.error(transferErrors.readError(err), () => this.cleanFolder()) - return - } + /** + * Fixes common problems with the download chart folder. + */ + private async cleanFolder() { + let files: Dirent[] + try { + files = await readdir(this.nestedSourceFolder, { withFileTypes: true }) + } catch (err) { + this.callbacks.error(transferErrors.readError(err), () => this.cleanFolder()) + return + } - // Remove nested folders - if (files.length == 1 && !files[0].isFile()) { - this.nestedSourceFolder = join(this.nestedSourceFolder, files[0].name) - await this.cleanFolder() - return - } + // Remove nested folders + if (files.length == 1 && !files[0].isFile()) { + this.nestedSourceFolder = join(this.nestedSourceFolder, files[0].name) + await this.cleanFolder() + return + } - // Delete '__MACOSX' folder - for (const file of files) { - if (!file.isFile() && file.name == '__MACOSX') { - try { - await rimraf(join(this.nestedSourceFolder, file.name)) - } catch (err) { - this.callbacks.error(transferErrors.rimrafError(err), () => this.cleanFolder()) - return - } - } else { - // TODO: handle other common problems, like chart/audio files not named correctly - } - } - } + // Delete '__MACOSX' folder + for (const file of files) { + if (!file.isFile() && file.name == '__MACOSX') { + try { + await rimraf(join(this.nestedSourceFolder, file.name)) + } catch (err) { + this.callbacks.error(transferErrors.rimrafError(err), () => this.cleanFolder()) + return + } + } else { + // TODO: handle other common problems, like chart/audio files not named correctly + } + } + } - /** - * Moves the downloaded chart to the library path. - */ - private moveFolder() { - mv(this.nestedSourceFolder, this.destinationFolder, { mkdirp: true }, (err) => { - if (err) { - this.callbacks.error(transferErrors.mvError(err), () => this.moveFolder()) - } else { - rimraf(this.sourceFolder) // Delete temp folder - this.callbacks.complete() - } - }) - } + /** + * Moves the downloaded chart to the library path. + */ + private moveFolder() { + mv(this.nestedSourceFolder, this.destinationFolder, { mkdirp: true }, err => { + if (err) { + this.callbacks.error(transferErrors.mvError(err), () => this.moveFolder()) + } else { + rimraf(this.sourceFolder) // Delete temp folder + this.callbacks.complete() + } + }) + } - /** - * Stop the process of transfering the file. (no more events will be fired after this is called) - */ - cancelTransfer() { - this.wasCanceled = true - } -} \ No newline at end of file + /** + * Stop the process of transfering the file. (no more events will be fired after this is called) + */ + cancelTransfer() { + this.wasCanceled = true + } +} diff --git a/src/electron/ipc/download/FilesystemChecker.ts b/src/electron/ipc/download/FilesystemChecker.ts index 59d1693..6eba562 100644 --- a/src/electron/ipc/download/FilesystemChecker.ts +++ b/src/electron/ipc/download/FilesystemChecker.ts @@ -1,121 +1,122 @@ -import { DownloadError } from './ChartDownload' -import { tempPath } from '../../shared/Paths' -import { AnyFunction } from '../../shared/UtilFunctions' -import { devLog } from '../../shared/ElectronUtilFunctions' import { randomBytes as _randomBytes } from 'crypto' -import { mkdir, access, constants } from 'fs' +import { access, constants, mkdir } from 'fs' import { join } from 'path' import { promisify } from 'util' + +import { devLog } from '../../shared/ElectronUtilFunctions' +import { tempPath } from '../../shared/Paths' +import { AnyFunction } from '../../shared/UtilFunctions' import { getSettings } from '../SettingsHandler.ipc' +import { DownloadError } from './ChartDownload' const randomBytes = promisify(_randomBytes) -type EventCallback = { - 'start': () => void - 'error': (err: DownloadError, retry: () => void | Promise) => void - 'complete': (tempPath: string) => void +interface EventCallback { + 'start': () => void + 'error': (err: DownloadError, retry: () => void | Promise) => void + 'complete': (tempPath: string) => void } type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } const filesystemErrors = { - libraryFolder: () => { return { header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' } }, - libraryAccess: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to access library folder.'), - destinationFolderExists: (destinationPath: string) => { - return { header: 'This chart already exists in your library folder.', body: destinationPath, isLink: true } - }, - mkdirError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to create temporary folder.') + libraryFolder: () => { return { header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' } }, + libraryAccess: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to access library folder.'), + destinationFolderExists: (destinationPath: string) => { + return { header: 'This chart already exists in your library folder.', body: destinationPath, isLink: true } + }, + mkdirError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to create temporary folder.'), } function fsError(err: NodeJS.ErrnoException, description: string) { - return { header: description, body: `${err.name}: ${err.message}` } + return { header: description, body: `${err.name}: ${err.message}` } } export class FilesystemChecker { - private callbacks = {} as Callbacks - private wasCanceled = false - constructor(private destinationFolderName: string) { } + private callbacks = {} as Callbacks + private wasCanceled = false + constructor(private destinationFolderName: string) { } - /** - * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) - */ - on(event: E, callback: EventCallback[E]) { - this.callbacks[event] = callback - } + /** + * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) + */ + on(event: E, callback: EventCallback[E]) { + this.callbacks[event] = callback + } - /** - * Check that the filesystem is set up for the download. - */ - beginCheck() { - this.callbacks.start() - this.checkLibraryFolder() - } + /** + * Check that the filesystem is set up for the download. + */ + beginCheck() { + this.callbacks.start() + this.checkLibraryFolder() + } - /** - * Verifies that the user has specified a library folder. - */ - private checkLibraryFolder() { - if (getSettings().libraryPath == undefined) { - this.callbacks.error(filesystemErrors.libraryFolder(), () => this.beginCheck()) - } else { - access(getSettings().libraryPath, constants.W_OK, this.cancelable((err) => { - if (err) { - this.callbacks.error(filesystemErrors.libraryAccess(err), () => this.beginCheck()) - } else { - this.checkDestinationFolder() - } - })) - } - } + /** + * Verifies that the user has specified a library folder. + */ + private checkLibraryFolder() { + if (getSettings().libraryPath == undefined) { + this.callbacks.error(filesystemErrors.libraryFolder(), () => this.beginCheck()) + } else { + access(getSettings().libraryPath, constants.W_OK, this.cancelable(err => { + if (err) { + this.callbacks.error(filesystemErrors.libraryAccess(err), () => this.beginCheck()) + } else { + this.checkDestinationFolder() + } + })) + } + } - /** - * Checks that the destination folder doesn't already exist. - */ - private checkDestinationFolder() { - const destinationPath = join(getSettings().libraryPath, this.destinationFolderName) - access(destinationPath, constants.F_OK, this.cancelable((err) => { - if (err) { // File does not exist - this.createDownloadFolder() - } else { - this.callbacks.error(filesystemErrors.destinationFolderExists(destinationPath), () => this.beginCheck()) - } - })) - } + /** + * Checks that the destination folder doesn't already exist. + */ + private checkDestinationFolder() { + const destinationPath = join(getSettings().libraryPath, this.destinationFolderName) + access(destinationPath, constants.F_OK, this.cancelable(err => { + if (err) { // File does not exist + this.createDownloadFolder() + } else { + this.callbacks.error(filesystemErrors.destinationFolderExists(destinationPath), () => this.beginCheck()) + } + })) + } - /** - * Attempts to create a unique folder in Bridge's data paths. - */ - private async createDownloadFolder(retryCount = 0) { - const tempChartPath = join(tempPath, `chart_${(await randomBytes(5)).toString('hex')}`) + /** + * Attempts to create a unique folder in Bridge's data paths. + */ + private async createDownloadFolder(retryCount = 0) { + const tempChartPath = join(tempPath, `chart_${(await randomBytes(5)).toString('hex')}`) - mkdir(tempChartPath, this.cancelable((err) => { - if (err) { - if (retryCount < 5) { - devLog(`Error creating folder [${tempChartPath}], retrying with a different folder...`) - this.createDownloadFolder(retryCount + 1) - } else { - this.callbacks.error(filesystemErrors.mkdirError(err), () => this.createDownloadFolder()) - } - } else { - this.callbacks.complete(tempChartPath) - } - })) - } + mkdir(tempChartPath, this.cancelable(err => { + if (err) { + if (retryCount < 5) { + devLog(`Error creating folder [${tempChartPath}], retrying with a different folder...`) + this.createDownloadFolder(retryCount + 1) + } else { + this.callbacks.error(filesystemErrors.mkdirError(err), () => this.createDownloadFolder()) + } + } else { + this.callbacks.complete(tempChartPath) + } + })) + } - /** - * Stop the process of checking the filesystem permissions. (no more events will be fired after this is called) - */ - cancelCheck() { - this.wasCanceled = true - } + /** + * Stop the process of checking the filesystem permissions. (no more events will be fired after this is called) + */ + cancelCheck() { + this.wasCanceled = true + } - /** - * Wraps a function that is able to be prevented if `this.cancelCheck()` was called. - */ - private cancelable(fn: F) { - return (...args: Parameters): ReturnType => { - if (this.wasCanceled) { return } - return fn(...Array.from(args)) - } - } -} \ No newline at end of file + /** + * Wraps a function that is able to be prevented if `this.cancelCheck()` was called. + */ + private cancelable(fn: F) { + return (...args: Parameters): ReturnType => { + if (this.wasCanceled) { return } + return fn(...Array.from(args)) + } + } +} diff --git a/src/electron/ipc/download/GoogleTimer.ts b/src/electron/ipc/download/GoogleTimer.ts index 1ef6421..94add51 100644 --- a/src/electron/ipc/download/GoogleTimer.ts +++ b/src/electron/ipc/download/GoogleTimer.ts @@ -1,72 +1,72 @@ import { getSettings } from '../SettingsHandler.ipc' -type EventCallback = { - 'waitProgress': (remainingSeconds: number, totalSeconds: number) => void - 'complete': () => void +interface EventCallback { + 'waitProgress': (remainingSeconds: number, totalSeconds: number) => void + 'complete': () => void } type Callbacks = { [E in keyof EventCallback]?: EventCallback[E] } class GoogleTimer { - private rateLimitCounter = Infinity - private callbacks: Callbacks = {} + private rateLimitCounter = Infinity + private callbacks: Callbacks = {} - /** - * Initializes the timer to call the callbacks if they are defined. - */ - constructor() { - setInterval(() => { - this.rateLimitCounter++ - this.updateCallbacks() - }, 1000) - } + /** + * Initializes the timer to call the callbacks if they are defined. + */ + constructor() { + setInterval(() => { + this.rateLimitCounter++ + this.updateCallbacks() + }, 1000) + } - /** - * Calls `callback` when `event` fires. (no events will be fired after `this.cancelTimer()` is called) - */ - on(event: E, callback: EventCallback[E]) { - this.callbacks[event] = callback - this.updateCallbacks() // Fire events immediately after the listeners have been added - } + /** + * Calls `callback` when `event` fires. (no events will be fired after `this.cancelTimer()` is called) + */ + on(event: E, callback: EventCallback[E]) { + this.callbacks[event] = callback + this.updateCallbacks() // Fire events immediately after the listeners have been added + } - /** - * Check the state of the callbacks and call them if necessary. - */ - private updateCallbacks() { - if (this.hasTimerEnded() && this.callbacks.complete != undefined) { - this.endTimer() - } else if (this.callbacks.waitProgress != undefined) { - const delay = getSettings().rateLimitDelay - this.callbacks.waitProgress(delay - this.rateLimitCounter, delay) - } - } + /** + * Check the state of the callbacks and call them if necessary. + */ + private updateCallbacks() { + if (this.hasTimerEnded() && this.callbacks.complete != undefined) { + this.endTimer() + } else if (this.callbacks.waitProgress != undefined) { + const delay = getSettings().rateLimitDelay + this.callbacks.waitProgress(delay - this.rateLimitCounter, delay) + } + } - /** - * Prevents the callbacks from activating when the timer ends. - */ - cancelTimer() { - this.callbacks = {} - } + /** + * Prevents the callbacks from activating when the timer ends. + */ + cancelTimer() { + this.callbacks = {} + } - /** - * Checks if enough time has elapsed since the last timer activation. - */ - private hasTimerEnded() { - return this.rateLimitCounter > getSettings().rateLimitDelay - } + /** + * Checks if enough time has elapsed since the last timer activation. + */ + private hasTimerEnded() { + return this.rateLimitCounter > getSettings().rateLimitDelay + } - /** - * Activates the completion callback and resets the timer. - */ - private endTimer() { - this.rateLimitCounter = 0 - const completeCallback = this.callbacks.complete - this.callbacks = {} - completeCallback() - } + /** + * Activates the completion callback and resets the timer. + */ + private endTimer() { + this.rateLimitCounter = 0 + const completeCallback = this.callbacks.complete + this.callbacks = {} + completeCallback() + } } /** * Important: this instance cannot be used by more than one file download at a time. */ -export const googleTimer = new GoogleTimer() \ No newline at end of file +export const googleTimer = new GoogleTimer() diff --git a/src/electron/main.ts b/src/electron/main.ts index 4dd63b0..e7b00d8 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -1,16 +1,16 @@ import { app, BrowserWindow, ipcMain } from 'electron' -import { updateChecker } from './ipc/UpdateHandler.ipc' import * as windowStateKeeper from 'electron-window-state' import * as path from 'path' import * as url from 'url' -require('electron-unhandled')({ showDialog: true }) - -// IPC Handlers -import { getIPCInvokeHandlers, getIPCEmitHandlers, IPCEmitEvents } from './shared/IPCHandler' import { getSettingsHandler } from './ipc/SettingsHandler.ipc' +import { updateChecker } from './ipc/UpdateHandler.ipc' +// IPC Handlers +import { getIPCEmitHandlers, getIPCInvokeHandlers, IPCEmitEvents } from './shared/IPCHandler' import { dataPath } from './shared/Paths' +require('electron-unhandled')({ showDialog: true }) + export let mainWindow: BrowserWindow const args = process.argv.slice(1) const isDevBuild = args.some(val => val == '--dev') @@ -21,13 +21,13 @@ remote.initialize() restrictToSingleInstance() handleOSXWindowClosed() app.on('ready', () => { - // Load settings from file before the window is created - getSettingsHandler.initSettings().then(() => { - createBridgeWindow() - if (!isDevBuild) { - updateChecker.checkForUpdates() - } - }) + // Load settings from file before the window is created + getSettingsHandler.initSettings().then(() => { + createBridgeWindow() + if (!isDevBuild) { + updateChecker.checkForUpdates() + } + }) }) /** @@ -35,14 +35,14 @@ app.on('ready', () => { * If this is attempted, restore the open window instead. */ function restrictToSingleInstance() { - const isFirstBridgeInstance = app.requestSingleInstanceLock() - if (!isFirstBridgeInstance) app.quit() - app.on('second-instance', () => { - if (mainWindow != undefined) { - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.focus() - } - }) + const isFirstBridgeInstance = app.requestSingleInstanceLock() + if (!isFirstBridgeInstance) app.quit() + app.on('second-instance', () => { + if (mainWindow != undefined) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + }) } /** @@ -50,17 +50,17 @@ function restrictToSingleInstance() { * minimize when closed and maximize when opened. */ function handleOSXWindowClosed() { - app.on('window-all-closed', () => { - if (process.platform != 'darwin') { - app.quit() - } - }) + app.on('window-all-closed', () => { + if (process.platform != 'darwin') { + app.quit() + } + }) - app.on('activate', () => { - if (mainWindow == undefined) { - createBridgeWindow() - } - }) + app.on('activate', () => { + if (mainWindow == undefined) { + createBridgeWindow() + } + }) } /** @@ -68,81 +68,81 @@ function handleOSXWindowClosed() { */ function createBridgeWindow() { - // Load window size and maximized/restored state from previous session - const windowState = windowStateKeeper({ - defaultWidth: 1000, - defaultHeight: 800, - path: dataPath - }) + // Load window size and maximized/restored state from previous session + const windowState = windowStateKeeper({ + defaultWidth: 1000, + defaultHeight: 800, + path: dataPath, + }) - // Create the browser window - mainWindow = createBrowserWindow(windowState) + // Create the browser window + mainWindow = createBrowserWindow(windowState) - // Store window size and maximized/restored state for next session - windowState.manage(mainWindow) + // Store window size and maximized/restored state for next session + windowState.manage(mainWindow) - // Don't use a system menu - mainWindow.setMenu(null) + // Don't use a system menu + mainWindow.setMenu(null) - // IPC handlers - getIPCInvokeHandlers().map(handler => ipcMain.handle(handler.event, (_event, ...args) => handler.handler(args[0]))) - getIPCEmitHandlers().map(handler => ipcMain.on(handler.event, (_event, ...args) => handler.handler(args[0]))) + // IPC handlers + getIPCInvokeHandlers().map(handler => ipcMain.handle(handler.event, (_event, ...args) => handler.handler(args[0]))) + getIPCEmitHandlers().map(handler => ipcMain.on(handler.event, (_event, ...args) => handler.handler(args[0]))) - // Load angular app - mainWindow.loadURL(getLoadUrl()) + // Load angular app + mainWindow.loadURL(getLoadUrl()) - if (isDevBuild) { - mainWindow.webContents.openDevTools() - } + if (isDevBuild) { + mainWindow.webContents.openDevTools() + } - mainWindow.on('closed', () => { - mainWindow = null // Dereference mainWindow when the window is closed - }) + mainWindow.on('closed', () => { + mainWindow = null // Dereference mainWindow when the window is closed + }) - // enable the remote webcontents - remote.enable(mainWindow.webContents) + // enable the remote webcontents + remote.enable(mainWindow.webContents) } /** * Initialize a BrowserWindow object with initial parameters */ function createBrowserWindow(windowState: windowStateKeeper.State) { - let options: Electron.BrowserWindowConstructorOptions = { - x: windowState.x, - y: windowState.y, - width: windowState.width, - height: windowState.height, - frame: false, - title: 'Bridge', - webPreferences: { - nodeIntegration: true, - allowRunningInsecureContent: (isDevBuild) ? true : false, - textAreasAreResizable: false, - contextIsolation: false - }, - simpleFullscreen: true, - fullscreenable: false, - backgroundColor: '#121212' - } + let options: Electron.BrowserWindowConstructorOptions = { + x: windowState.x, + y: windowState.y, + width: windowState.width, + height: windowState.height, + frame: false, + title: 'Bridge', + webPreferences: { + nodeIntegration: true, + allowRunningInsecureContent: (isDevBuild) ? true : false, + textAreasAreResizable: false, + contextIsolation: false, + }, + simpleFullscreen: true, + fullscreenable: false, + backgroundColor: '#121212', + } - if (process.platform == 'linux' && !isDevBuild) { - options = Object.assign(options, { icon: path.join(__dirname, '..', 'assets', 'images', 'system', 'icons', 'png', '48x48.png' ) }) - } + if (process.platform == 'linux' && !isDevBuild) { + options = Object.assign(options, { icon: path.join(__dirname, '..', 'assets', 'images', 'system', 'icons', 'png', '48x48.png') }) + } - return new BrowserWindow(options) + return new BrowserWindow(options) } /** * Load from localhost during development; load from index.html in production */ function getLoadUrl() { - return url.format({ - protocol: isDevBuild ? 'http:' : 'file:', - pathname: isDevBuild ? '//localhost:4200/' : path.join(__dirname, '..', 'index.html'), - slashes: true - }) + return url.format({ + protocol: isDevBuild ? 'http:' : 'file:', + pathname: isDevBuild ? '//localhost:4200/' : path.join(__dirname, '..', 'index.html'), + slashes: true, + }) } export function emitIPCEvent(event: E, data: IPCEmitEvents[E]) { - mainWindow.webContents.send(event, data) -} \ No newline at end of file + mainWindow.webContents.send(event, data) +} diff --git a/src/electron/shared/ElectronUtilFunctions.ts b/src/electron/shared/ElectronUtilFunctions.ts index 535c247..ff49f1a 100644 --- a/src/electron/shared/ElectronUtilFunctions.ts +++ b/src/electron/shared/ElectronUtilFunctions.ts @@ -1,4 +1,5 @@ import { basename, parse } from 'path' + import { getSettingsHandler } from '../ipc/SettingsHandler.ipc' import { emitIPCEvent } from '../main' import { lower } from './UtilFunctions' @@ -7,15 +8,15 @@ import { lower } from './UtilFunctions' * @returns The relative filepath from the library folder to `absoluteFilepath`. */ export function getRelativeFilepath(absoluteFilepath: string) { - const settings = getSettingsHandler.getSettings() - return basename(settings.libraryPath) + absoluteFilepath.substring(settings.libraryPath.length) + const settings = getSettingsHandler.getSettings() + return basename(settings.libraryPath) + absoluteFilepath.substring(settings.libraryPath.length) } /** * @returns `true` if `name` has a valid video file extension. */ export function hasVideoExtension(name: string) { - return (['.mp4', '.avi', '.webm', '.ogv', '.mpeg'].includes(parse(lower(name)).ext)) + return (['.mp4', '.avi', '.webm', '.ogv', '.mpeg'].includes(parse(lower(name)).ext)) } /** @@ -23,5 +24,5 @@ export function hasVideoExtension(name: string) { * Note: Error objects can't be serialized by this; use inspect(err) before passing it here. */ export function devLog(...messages: any[]) { - emitIPCEvent('log', messages) -} \ No newline at end of file + emitIPCEvent('log', messages) +} diff --git a/src/electron/shared/IPCHandler.ts b/src/electron/shared/IPCHandler.ts index 718b51a..169c958 100644 --- a/src/electron/shared/IPCHandler.ts +++ b/src/electron/shared/IPCHandler.ts @@ -1,17 +1,18 @@ -import { SongSearch, SongResult } from './interfaces/search.interface' -import { VersionResult, AlbumArtResult } from './interfaces/songDetails.interface' +import { UpdateInfo } from 'electron-updater' + +import { albumArtHandler } from '../ipc/browse/AlbumArtHandler.ipc' +import { batchSongDetailsHandler } from '../ipc/browse/BatchSongDetailsHandler.ipc' import { searchHandler } from '../ipc/browse/SearchHandler.ipc' import { songDetailsHandler } from '../ipc/browse/SongDetailsHandler.ipc' -import { albumArtHandler } from '../ipc/browse/AlbumArtHandler.ipc' -import { Download, DownloadProgress } from './interfaces/download.interface' -import { downloadHandler } from '../ipc/download/DownloadHandler' -import { Settings } from './Settings' -import { batchSongDetailsHandler } from '../ipc/browse/BatchSongDetailsHandler.ipc' -import { getSettingsHandler, setSettingsHandler } from '../ipc/SettingsHandler.ipc' import { clearCacheHandler } from '../ipc/CacheHandler.ipc' -import { updateChecker, UpdateProgress, getCurrentVersionHandler, downloadUpdateHandler, quitAndInstallHandler, getUpdateAvailableHandler } from '../ipc/UpdateHandler.ipc' -import { UpdateInfo } from 'electron-updater' +import { downloadHandler } from '../ipc/download/DownloadHandler' import { openURLHandler } from '../ipc/OpenURLHandler.ipc' +import { getSettingsHandler, setSettingsHandler } from '../ipc/SettingsHandler.ipc' +import { downloadUpdateHandler, getCurrentVersionHandler, getUpdateAvailableHandler, quitAndInstallHandler, updateChecker, UpdateProgress } from '../ipc/UpdateHandler.ipc' +import { Download, DownloadProgress } from './interfaces/download.interface' +import { SongResult, SongSearch } from './interfaces/search.interface' +import { AlbumArtResult, VersionResult } from './interfaces/songDetails.interface' +import { Settings } from './Settings' /** * To add a new IPC listener: @@ -22,101 +23,101 @@ import { openURLHandler } from '../ipc/OpenURLHandler.ipc' */ export function getIPCInvokeHandlers(): IPCInvokeHandler[] { - return [ - getSettingsHandler, - clearCacheHandler, - searchHandler, - songDetailsHandler, - batchSongDetailsHandler, - albumArtHandler, - getCurrentVersionHandler, - getUpdateAvailableHandler, - ] + return [ + getSettingsHandler, + clearCacheHandler, + searchHandler, + songDetailsHandler, + batchSongDetailsHandler, + albumArtHandler, + getCurrentVersionHandler, + getUpdateAvailableHandler, + ] } /** * The list of possible async IPC events that return values, mapped to their input and output types. */ -export type IPCInvokeEvents = { - 'get-settings': { - input: undefined - output: Settings - } - 'clear-cache': { - input: undefined - output: void - } - 'song-search': { - input: SongSearch - output: SongResult[] - } - 'album-art': { - input: SongResult['id'] - output: AlbumArtResult - } - 'song-details': { - input: SongResult['id'] - output: VersionResult[] - } - 'batch-song-details': { - input: number[] - output: VersionResult[] - } - 'get-current-version': { - input: undefined - output: string - } - 'get-update-available': { - input: undefined - output: boolean - } +export interface IPCInvokeEvents { + 'get-settings': { + input: undefined + output: Settings + } + 'clear-cache': { + input: undefined + output: void + } + 'song-search': { + input: SongSearch + output: SongResult[] + } + 'album-art': { + input: SongResult['id'] + output: AlbumArtResult + } + 'song-details': { + input: SongResult['id'] + output: VersionResult[] + } + 'batch-song-details': { + input: number[] + output: VersionResult[] + } + 'get-current-version': { + input: undefined + output: string + } + 'get-update-available': { + input: undefined + output: boolean + } } /** * Describes an object that handles the `E` async IPC event that will return a value. */ export interface IPCInvokeHandler { - event: E - handler(data: IPCInvokeEvents[E]['input']): Promise | IPCInvokeEvents[E]['output'] + event: E + handler(data: IPCInvokeEvents[E]['input']): Promise | IPCInvokeEvents[E]['output'] } export function getIPCEmitHandlers(): IPCEmitHandler[] { - return [ - downloadHandler, - setSettingsHandler, - downloadUpdateHandler, - updateChecker, - quitAndInstallHandler, - openURLHandler - ] + return [ + downloadHandler, + setSettingsHandler, + downloadUpdateHandler, + updateChecker, + quitAndInstallHandler, + openURLHandler, + ] } /** * The list of possible async IPC events that don't return values, mapped to their input types. */ -export type IPCEmitEvents = { - 'log': any[] +export interface IPCEmitEvents { + 'log': any[] - 'download': Download - 'download-updated': DownloadProgress - 'set-settings': Settings - 'queue-updated': number[] + 'download': Download + 'download-updated': DownloadProgress + 'set-settings': Settings + 'queue-updated': number[] - 'update-error': Error - 'update-available': UpdateInfo - 'update-progress': UpdateProgress - 'update-downloaded': undefined - 'download-update': undefined - 'retry-update': undefined - 'quit-and-install': undefined - 'open-url': string + 'update-error': Error + 'update-available': UpdateInfo + 'update-progress': UpdateProgress + 'update-downloaded': undefined + 'download-update': undefined + 'retry-update': undefined + 'quit-and-install': undefined + 'open-url': string } /** * Describes an object that handles the `E` async IPC event that will not return a value. */ export interface IPCEmitHandler { - event: E - handler(data: IPCEmitEvents[E]): void -} \ No newline at end of file + event: E + handler(data: IPCEmitEvents[E]): void +} diff --git a/src/electron/shared/Paths.ts b/src/electron/shared/Paths.ts index 7722557..7e801cb 100644 --- a/src/electron/shared/Paths.ts +++ b/src/electron/shared/Paths.ts @@ -1,5 +1,5 @@ -import { join } from 'path' import { app } from 'electron' +import { join } from 'path' // Data paths export const dataPath = join(app.getPath('userData'), 'bridge_data') @@ -15,4 +15,4 @@ export const serverURL = 'bridge-db.net' export const SERVER_PORT = 42813 export const REDIRECT_BASE = `http://127.0.0.1:${SERVER_PORT}` export const REDIRECT_PATH = `/oauth2callback` -export const REDIRECT_URI = `${REDIRECT_BASE}${REDIRECT_PATH}` \ No newline at end of file +export const REDIRECT_URI = `${REDIRECT_BASE}${REDIRECT_PATH}` diff --git a/src/electron/shared/Settings.ts b/src/electron/shared/Settings.ts index 7c2239c..83b16fd 100644 --- a/src/electron/shared/Settings.ts +++ b/src/electron/shared/Settings.ts @@ -2,18 +2,18 @@ * Represents Bridge's user settings. */ export interface Settings { - rateLimitDelay: number // Number of seconds to wait between each file download from Google servers - downloadVideos: boolean // If background videos should be downloaded - theme: string // The name of the currently enabled UI theme - libraryPath: string // The path to the user's library + rateLimitDelay: number // Number of seconds to wait between each file download from Google servers + downloadVideos: boolean // If background videos should be downloaded + theme: string // The name of the currently enabled UI theme + libraryPath: string // The path to the user's library } /** * Bridge's default user settings. */ export const defaultSettings: Settings = { - rateLimitDelay: 31, - downloadVideos: true, - theme: 'Default', - libraryPath: undefined -} \ No newline at end of file + rateLimitDelay: 31, + downloadVideos: true, + theme: 'Default', + libraryPath: undefined, +} diff --git a/src/electron/shared/UtilFunctions.ts b/src/electron/shared/UtilFunctions.ts index 0b5a968..555e7db 100644 --- a/src/electron/shared/UtilFunctions.ts +++ b/src/electron/shared/UtilFunctions.ts @@ -1,4 +1,5 @@ import * as randomBytes from 'randombytes' + const sanitize = require('sanitize-filename') // WARNING: do not import anything related to Electron; the code will not compile correctly. @@ -10,52 +11,52 @@ export type AnyFunction = (...args: any) => any * @returns `filename` with all invalid filename characters replaced. */ export function sanitizeFilename(filename: string): string { - const newFilename = sanitize(filename, { - replacement: ((invalidChar: string) => { - switch (invalidChar) { - case '<': return '❮' - case '>': return '❯' - case ':': return '꞉' - case '"': return "'" - case '/': return '/' - case '\\': return '⧵' - case '|': return '⏐' - case '?': return '?' - case '*': return '⁎' - default: return '_' - } - }) - }) - return (newFilename == '' ? randomBytes(5).toString('hex') : newFilename) + const newFilename = sanitize(filename, { + replacement: ((invalidChar: string) => { + switch (invalidChar) { + case '<': return '❮' + case '>': return '❯' + case ':': return '꞉' + case '"': return "'" + case '/': return '/' + case '\\': return '⧵' + case '|': return '⏐' + case '?': return '?' + case '*': return '⁎' + default: return '_' + } + }), + }) + return (newFilename == '' ? randomBytes(5).toString('hex') : newFilename) } /** * @returns `text` converted to lower case. */ export function lower(text: string) { - return text.toLowerCase() + return text.toLowerCase() } /** * Converts `val` from the range (`fromStart`, `fromEnd`) to the range (`toStart`, `toEnd`). */ export function interpolate(val: number, fromStart: number, fromEnd: number, toStart: number, toEnd: number) { - return ((val - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart) + toStart + return ((val - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart) + toStart } /** * @returns `objectList` split into multiple groups, where each group contains objects where every one of its values in `keys` matches. */ export function groupBy(objectList: T[], ...keys: (keyof T)[]) { - const results: T[][] = [] - for (const object of objectList) { - const matchingGroup = results.find(result => keys.every(key => result[0][key] == object[key])) - if (matchingGroup != undefined) { - matchingGroup.push(object) - } else { - results.push([object]) - } - } + const results: T[][] = [] + for (const object of objectList) { + const matchingGroup = results.find(result => keys.every(key => result[0][key] == object[key])) + if (matchingGroup != undefined) { + matchingGroup.push(object) + } else { + results.push([object]) + } + } - return results -} \ No newline at end of file + return results +} diff --git a/src/electron/shared/interfaces/download.interface.ts b/src/electron/shared/interfaces/download.interface.ts index b899488..8f7a18b 100644 --- a/src/electron/shared/interfaces/download.interface.ts +++ b/src/electron/shared/interfaces/download.interface.ts @@ -4,35 +4,35 @@ import { DriveChart } from './songDetails.interface' * Represents a user's request to interact with the download system. */ export interface Download { - action: 'add' | 'retry' | 'cancel' - versionID: number - data?: NewDownload // Should be defined if action == 'add' + action: 'add' | 'retry' | 'cancel' + versionID: number + data?: NewDownload // Should be defined if action == 'add' } /** * Contains the data required to start downloading a single chart. */ export interface NewDownload { - chartName: string - artist: string - charter: string - driveData: DriveChart & { inChartPack: boolean } + chartName: string + artist: string + charter: string + driveData: DriveChart & { inChartPack: boolean } } /** * Represents the download progress of a single chart. */ export interface DownloadProgress { - versionID: number - title: string - header: string - description: string - percent: number - type: ProgressType - /** If `description` contains a filepath that can be clicked */ - isLink: boolean - /** If the download should not appear in the total download progress */ - stale?: boolean + versionID: number + title: string + header: string + description: string + percent: number + type: ProgressType + /** If `description` contains a filepath that can be clicked */ + isLink: boolean + /** If the download should not appear in the total download progress */ + stale?: boolean } -export type ProgressType = 'good' | 'error' | 'cancel' | 'done' \ No newline at end of file +export type ProgressType = 'good' | 'error' | 'cancel' | 'done' diff --git a/src/electron/shared/interfaces/search.interface.ts b/src/electron/shared/interfaces/search.interface.ts index 0194471..06855b5 100644 --- a/src/electron/shared/interfaces/search.interface.ts +++ b/src/electron/shared/interfaces/search.interface.ts @@ -2,86 +2,90 @@ * Represents a user's song search query. */ export interface SongSearch { // TODO: make limit a setting that's not always 51 - query: string - quantity: 'all' | 'any' - similarity: 'similar' | 'exact' - fields: SearchFields - tags: SearchTags - instruments: SearchInstruments - difficulties: SearchDifficulties - minDiff: number - maxDiff: number - limit: number - offset: number + query: string + quantity: 'all' | 'any' + similarity: 'similar' | 'exact' + fields: SearchFields + tags: SearchTags + instruments: SearchInstruments + difficulties: SearchDifficulties + minDiff: number + maxDiff: number + limit: number + offset: number } export function getDefaultSearch(): SongSearch { - return { - query: '', - quantity: 'all', - similarity: 'similar', - fields: { name: true, artist: true, album: true, genre: true, year: true, charter: true, tag: true }, - tags: { 'sections': false, 'star power': false, 'forcing': false, 'taps': false, 'lyrics': false, - 'video': false, 'stems': false, 'solo sections': false, 'open notes': false }, - instruments: { guitar: false, bass: false, rhythm: false, keys: false, - drums: false, guitarghl: false, bassghl: false, vocals: false }, - difficulties: { expert: false, hard: false, medium: false, easy: false }, - minDiff: 0, - maxDiff: 6, - limit: 50 + 1, - offset: 0 - } + return { + query: '', + quantity: 'all', + similarity: 'similar', + fields: { name: true, artist: true, album: true, genre: true, year: true, charter: true, tag: true }, + tags: { + 'sections': false, 'star power': false, 'forcing': false, 'taps': false, 'lyrics': false, + 'video': false, 'stems': false, 'solo sections': false, 'open notes': false + }, + instruments: { + guitar: false, bass: false, rhythm: false, keys: false, + drums: false, guitarghl: false, bassghl: false, vocals: false + }, + difficulties: { expert: false, hard: false, medium: false, easy: false }, + minDiff: 0, + maxDiff: 6, + limit: 50 + 1, + offset: 0, + } } export interface SearchFields { - name: boolean - artist: boolean - album: boolean - genre: boolean - year: boolean - charter: boolean - tag: boolean + name: boolean + artist: boolean + album: boolean + genre: boolean + year: boolean + charter: boolean + tag: boolean } export interface SearchTags { - 'sections': boolean // Tag inverted - 'star power': boolean // Tag inverted - 'forcing': boolean // Tag inverted - 'taps': boolean - 'lyrics': boolean - 'video': boolean - 'stems': boolean - 'solo sections': boolean - 'open notes': boolean + 'sections': boolean // Tag inverted + 'star power': boolean // Tag inverted + 'forcing': boolean // Tag inverted + 'taps': boolean + 'lyrics': boolean + 'video': boolean + 'stems': boolean + 'solo sections': boolean + 'open notes': boolean } export interface SearchInstruments { - guitar: boolean - bass: boolean - rhythm: boolean - keys: boolean - drums: boolean - guitarghl: boolean - bassghl: boolean - vocals: boolean + guitar: boolean + bass: boolean + rhythm: boolean + keys: boolean + drums: boolean + guitarghl: boolean + bassghl: boolean + vocals: boolean } export interface SearchDifficulties { - expert: boolean - hard: boolean - medium: boolean - easy: boolean + expert: boolean + hard: boolean + medium: boolean + easy: boolean } /** * Represents a single song search result. */ export interface SongResult { - id: number - chartCount: number - name: string - artist: string - album: string - genre: string - year: string -} \ No newline at end of file + id: number + chartCount: number + name: string + artist: string + album: string + genre: string + year: string +} diff --git a/src/electron/shared/interfaces/songDetails.interface.ts b/src/electron/shared/interfaces/songDetails.interface.ts index 5eff949..c6d11ef 100644 --- a/src/electron/shared/interfaces/songDetails.interface.ts +++ b/src/electron/shared/interfaces/songDetails.interface.ts @@ -2,100 +2,100 @@ * The image data for a song's album art. */ export interface AlbumArtResult { - base64Art: string + base64Art: string } /** * Represents a single chart version. */ export interface VersionResult { - versionID: number - chartID: number - songID: number - latestVersionID: number - latestSetlistVersionID: number - name: string - chartName: string - artist: string - album: string - genre: string - year: string - songDataIncorrect: boolean - driveData: DriveChart & { inChartPack: boolean } - md5: string - lastModified: string - icon: string - charters: string - charterIDs: string - tags: string | null - songLength: number - diff_band: number - diff_guitar: number - diff_bass: number - diff_rhythm: number - diff_drums: number - diff_keys: number - diff_guitarghl: number - diff_bassghl: number - chartData: ChartData + versionID: number + chartID: number + songID: number + latestVersionID: number + latestSetlistVersionID: number + name: string + chartName: string + artist: string + album: string + genre: string + year: string + songDataIncorrect: boolean + driveData: DriveChart & { inChartPack: boolean } + md5: string + lastModified: string + icon: string + charters: string + charterIDs: string + tags: string | null + songLength: number + diff_band: number + diff_guitar: number + diff_bass: number + diff_rhythm: number + diff_drums: number + diff_keys: number + diff_guitarghl: number + diff_bassghl: number + chartData: ChartData } export interface DriveChart { - source: DriveSource - isArchive: boolean - downloadPath: string - filesHash: string - folderName: string - folderID: string - files: DriveFile[] + source: DriveSource + isArchive: boolean + downloadPath: string + filesHash: string + folderName: string + folderID: string + files: DriveFile[] } export interface DriveSource { - isSetlistSource: boolean - isDriveFileSource?: boolean - setlistIcon?: string - sourceUserIDs: number[] - sourceName: string - sourceDriveID: string - proxyLink?: string + isSetlistSource: boolean + isDriveFileSource?: boolean + setlistIcon?: string + sourceUserIDs: number[] + sourceName: string + sourceDriveID: string + proxyLink?: string } export interface DriveFile { - id: string - name: string - mimeType: string - webContentLink: string - modifiedTime: string - md5Checksum: string - size: string + id: string + name: string + mimeType: string + webContentLink: string + modifiedTime: string + md5Checksum: string + size: string } export interface ChartData { - hasSections: boolean - hasStarPower: boolean - hasForced: boolean - hasTap: boolean - hasOpen: { - [instrument: string]: boolean - } - hasSoloSections: boolean - hasLyrics: boolean - is120: boolean - hasBrokenNotes: boolean - noteCounts: { - [instrument in Instrument]: { - [difficulty in ChartedDifficulty]: number - } - } - /** number of seconds */ - length: number - /** number of seconds */ - effectiveLength: number + hasSections: boolean + hasStarPower: boolean + hasForced: boolean + hasTap: boolean + hasOpen: { + [instrument: string]: boolean + } + hasSoloSections: boolean + hasLyrics: boolean + is120: boolean + hasBrokenNotes: boolean + noteCounts: { + [instrument in Instrument]: { + [difficulty in ChartedDifficulty]: number + } + } + /** number of seconds */ + length: number + /** number of seconds */ + effectiveLength: number } export type Instrument = 'guitar' | 'bass' | 'rhythm' | 'keys' | 'drums' | 'guitarghl' | 'bassghl' | 'vocals' | 'undefined' export type ChartedDifficulty = 'x' | 'h' | 'm' | 'e' export function getInstrumentIcon(instrument: Instrument) { - return `${instrument}.png` -} \ No newline at end of file + return `${instrument}.png` +} diff --git a/src/electron/shared/typeDefinitions/node-7z.d.ts b/src/electron/shared/typeDefinitions/node-7z.d.ts deleted file mode 100644 index e4e2478..0000000 --- a/src/electron/shared/typeDefinitions/node-7z.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'node-7z' \ No newline at end of file diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index bb1665f..3cce312 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,3 +1,3 @@ export const environment = { - production: true -} \ No newline at end of file + production: true, +} diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 778ff31..fa3251a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -3,7 +3,7 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, } /* @@ -13,4 +13,4 @@ export const environment = { * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ -// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. \ No newline at end of file +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/src/index.html b/src/index.html index dab3b13..6308243 100644 --- a/src/index.html +++ b/src/index.html @@ -1,12 +1,12 @@ - - - Bridge - - - - - - - \ No newline at end of file + + + Bridge + + + + + + + diff --git a/src/main.ts b/src/main.ts index e7d3766..cbcddf2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,8 +5,8 @@ import { AppModule } from './app/app.module' import { environment } from './environments/environment' if (environment.production) { - enableProdMode() + enableProdMode() } platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)) \ No newline at end of file + .catch(err => console.error(err)) diff --git a/src/polyfills.ts b/src/polyfills.ts index 55315bd..efb1a13 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -45,9 +45,8 @@ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js' // Included with Angular CLI. - +import 'zone.js' // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS - */ \ No newline at end of file + */ diff --git a/src/styles.scss b/src/styles.scss index c5a249a..0093dc4 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -46,4 +46,4 @@ .ui.inverted.teal.buttons .button:active { background-color: #089C95; color: white; -} \ No newline at end of file +} diff --git a/src/typings.d.ts b/src/typings.d.ts index c8dd31b..24384eb 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -2,14 +2,13 @@ /* SystemJS module definition */ declare let nodeModule: NodeModule interface NodeModule { - id: string + id: string } -// @ts-ignore // declare let window: Window declare let $: any interface Window { - process: any - require: any - jQuery: any -} \ No newline at end of file + process: any + require: any + jQuery: any +} diff --git a/tsconfig.json b/tsconfig.json index e194f7f..94039a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "baseUrl": "./", "noImplicitAny": true, - "suppressImplicitAnyIndexErrors": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, @@ -19,5 +18,8 @@ ], "module": "commonjs", "useDefineForClassFields": false + }, + "angularCompilerOptions": { + "strictTemplates": true } -} \ No newline at end of file +}