diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c2d2ff8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: Build Vite + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [24.x] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies & build + run: | + npm ci + npm run build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + publish_dir: ./dist + github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 819f592..763301f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,2 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -.DS_Store - -# Generated by package manager -node_modules/ - -# Generated by Cordova -/plugins/ -/platforms/ - -pandora.apks -/android/app/build/ -test-upload-key.jks +dist/ +node_modules/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7ebca84 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1249 @@ +{ + "name": "pandora", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pandora", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "typescript": "^7.0.2", + "vite": "^8.1.5" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4923aa4 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "pandora", + "version": "1.0.0", + "description": "\"PANDORA\"", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^7.0.2", + "vite": "^8.1.5" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3af771c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "outDir": "./dist", + "rootDir": "./web/src", + "sourceMap": true + }, + "include": ["web/src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..91458b8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: 'web', + build: { + outDir: '../dist', + emptyOutDir: true, + }, +}); diff --git a/fonts/selawk.ttf b/web/fonts/selawk.ttf similarity index 100% rename from fonts/selawk.ttf rename to web/fonts/selawk.ttf diff --git a/fonts/selawkb.ttf b/web/fonts/selawkb.ttf similarity index 100% rename from fonts/selawkb.ttf rename to web/fonts/selawkb.ttf diff --git a/fonts/selawkl.ttf b/web/fonts/selawkl.ttf similarity index 100% rename from fonts/selawkl.ttf rename to web/fonts/selawkl.ttf diff --git a/fonts/selawksb.ttf b/web/fonts/selawksb.ttf similarity index 100% rename from fonts/selawksb.ttf rename to web/fonts/selawksb.ttf diff --git a/fonts/selawksl.ttf b/web/fonts/selawksl.ttf similarity index 100% rename from fonts/selawksl.ttf rename to web/fonts/selawksl.ttf diff --git a/index.html b/web/index.html similarity index 99% rename from index.html rename to web/index.html index d1510e7..e7363e7 100644 --- a/index.html +++ b/web/index.html @@ -169,6 +169,6 @@ - + diff --git a/loading.css b/web/loading.css similarity index 100% rename from loading.css rename to web/loading.css diff --git a/metroicons.css b/web/metroicons.css similarity index 100% rename from metroicons.css rename to web/metroicons.css diff --git a/icon-192.png b/web/public/icon-192.png similarity index 100% rename from icon-192.png rename to web/public/icon-192.png diff --git a/icon-512.png b/web/public/icon-512.png similarity index 100% rename from icon-512.png rename to web/public/icon-512.png diff --git a/manifest.json b/web/public/manifest.json similarity index 100% rename from manifest.json rename to web/public/manifest.json diff --git a/message-long.ogg b/web/public/message-long.ogg similarity index 100% rename from message-long.ogg rename to web/public/message-long.ogg diff --git a/message.ogg b/web/public/message.ogg similarity index 100% rename from message.ogg rename to web/public/message.ogg diff --git a/js/app.js b/web/src/app.ts similarity index 80% rename from js/app.js rename to web/src/app.ts index b8f0201..2a25772 100644 --- a/js/app.js +++ b/web/src/app.ts @@ -1,17 +1,18 @@ -import { config } from "./config.js"; -import { waha } from "./waha.js"; -import { ui, elements } from "./ui.js"; -import { websocket } from "./websocket.js"; -import { compensateMessageOrdering, debounce, formatTime, normalizeId } from "./utils.js"; -import { fetchChats, getAppUser, getChatMessages, getChatPicture, getChats, getUser, getUserAbout, markRead, sendStatus, updateOnlineStatus } from "./storage.js"; -import { upsertMessages } from "./db.js"; -import { showNotification } from "./notification.js"; +import { config } from "./config"; +import { waha } from "./waha"; +import { ui, elements } from "./ui"; +import { websocket } from "./websocket"; +import { compensateMessageOrdering, debounce, formatTime, normalizeId } from "./utils"; +import { fetchChats, getAppUser, getChatMessages, getChatPicture, getChats, getUser, getUserAbout, markRead, sendStatus, updateOnlineStatus } from "./storage"; +import { upsertMessages } from "./db"; +import { showNotification } from "./notification"; +import type { Chat, Message, WebSocketEvent } from "./types"; -let activeChatState = null; +let activeChatState: Chat | null = null; const messageTone = new Audio("./message.ogg"); const longPressEvent = new CustomEvent("longpress"); export let isLoadingChat = false; -export let notificationAuthorization = false; +export let notificationAuthorization: NotificationPermission = "default"; document.addEventListener('DOMContentLoaded', async () => { askForNotificationPermission(); @@ -25,7 +26,7 @@ document.addEventListener('DOMContentLoaded', async () => { await updateOnlineStatus(); setupEventListeners(); try { - setupElementsData(); + setupElementsData(); loadChats(); checkWahaStatus(); initWebSocket(); @@ -45,18 +46,18 @@ async function setupElementsData() { const usrInfo = await getUser(usr.id); const usrAbout = (await getUserAbout(usr.id))?.about; elements.contentUserName.forEach(e => { - e.innerHTML = usr.pushName; + e.innerHTML = usr.pushName || usr.name || ''; }) elements.contentUserNumber.forEach(async e => { - e.innerHTML = usrInfo.number; + if (usrInfo) e.innerHTML = usrInfo.number; }) elements.resourceUserPic.forEach(async e => { - e.src = usrPic; + if (usrPic) e.src = usrPic; }) elements.valueUserStatus.forEach(async e => { - e.value = usrAbout.trim(); + if (usrAbout) e.value = usrAbout.trim(); }) - } catch (error) { + } catch (error: any) { console.error(error.message); } } @@ -64,9 +65,9 @@ async function setupElementsData() { function loadChats() { elements.chatsLoader.classList.remove('hidden'); try { - fetchChats().then(async () => { + fetchChats().then(async () => { ui.renderChatList(getChats(), activeChatState, selectChat); - + const hash = window.location.hash; if (hash && hash.startsWith('#chat-')) { const chatId = hash.replace('#chat-', ''); @@ -76,7 +77,7 @@ function loadChats() { } } }); - } catch (error) { + } catch (error: any) { console.error('Failed to load chats:', error); elements.chatList.innerHTML = `
  • @@ -93,7 +94,7 @@ function loadChats() { } let isScrollingProgrammatically = false; -let scrollTimeout = null; +let scrollTimeout: ReturnType | null = null; function scrollToChat(smooth = true) { isScrollingProgrammatically = true; @@ -117,7 +118,7 @@ function setupEventListeners() { if (!window.location.hash) { window.location.hash = ''; } - + window.addEventListener('hashchange', () => { const hash = window.location.hash; if (hash && hash.startsWith('#chat-')) { @@ -134,12 +135,12 @@ function setupEventListeners() { elements.appContainer.addEventListener('scroll', () => { if (window.innerWidth > 768) return; if (isScrollingProgrammatically) return; - - clearTimeout(scrollTimeout); + + if (scrollTimeout) clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { const scrollLeft = elements.appContainer.scrollLeft; const width = elements.appContainer.clientWidth; - + if (scrollLeft < width * 0.2) { // User swiped back to the list view if (activeChatState) { @@ -155,50 +156,54 @@ function setupEventListeners() { closeActiveChat(false); } }); - - elements.chatSearch.addEventListener('input', (e) => { - const query = e.target.value.toLowerCase(); - const filtered = getChats().filter(chat => + + elements.chatSearch.addEventListener('input', (e: Event) => { + const query = (e.target as HTMLInputElement).value.toLowerCase(); + const filtered = getChats().filter(chat => chat.name.toLowerCase().includes(query) ); ui.renderChatList(filtered, activeChatState, selectChat); }); - + elements.messageForm.addEventListener('submit', (e) => { e.preventDefault(); sendMessage(); }); - + elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`; const observer = new ResizeObserver(() => { elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`; }); - + observer.observe(elements.chatInputPanel); elements.chatBottomBarBtn.addEventListener('click', ui.toggleChatBottomBar); elements.chatBottomBar.addEventListener('click', (e) => { if (e.target == e.currentTarget) ui.toggleChatBottomBar(); }); - - elements.markreadBtn.addEventListener('click', () => { - markRead(activeChatState.id); + + elements.markreadBtn.addEventListener('click', async () => { + if (activeChatState) { + const result = await markRead(activeChatState.id); + if (result) ui.updateChatInChatList2(result); + } }) elements.attachmentBtn.addEventListener('click', () => { elements.attachmentInput.click(); }) - elements.attachmentInput.addEventListener('change', function () { - const firstFile = this.files[0]; - sendFileMessage(firstFile); + elements.attachmentInput.addEventListener('change', function (this: HTMLInputElement) { + const firstFile = this.files?.[0]; + if (firstFile) sendFileMessage(firstFile); }) - + elements.backToSidebarBtn.addEventListener('click', () => { closeActiveChat(false); - }); + }); elements.desktopSidebarButtons.forEach(sidebarBtn => { sidebarBtn.addEventListener('click', () => { - ui.showExtraPage(sidebarBtn.dataset.page); + const page = sidebarBtn.dataset.page; + if (page) ui.showExtraPage(page); }) }) @@ -215,7 +220,7 @@ function setupEventListeners() { }, 2000)) elements.selectable.forEach(e => { - let timerId, longPressed; + let timerId: ReturnType, longPressed: boolean; e.addEventListener('mousedown', () => { longPressed = false; @@ -223,12 +228,12 @@ function setupEventListeners() { timerId = setTimeout(() => { longPressed = true; e.dispatchEvent(longPressEvent); - }) + }, 500); // 500ms for long press }) - e.addEventListener('click', () => { + e.addEventListener('click', (event) => { if (longPressed) { - e.preventDefault(); + event.preventDefault(); clearTimeout(timerId); } }) @@ -240,8 +245,7 @@ function setupEventListeners() { } function initWebSocket() { - websocket.connect((data) => { - // console.log('[WS] Received event:', data.event, data); + websocket.connect((data: WebSocketEvent) => { const ev = data.event; if (ev === 'message' || ev === 'message.any' || ev === 'message.ack') { handleIncomingMessage(data.payload); @@ -251,53 +255,51 @@ function initWebSocket() { } -async function handleIncomingMessage(msg) { +async function handleIncomingMessage(msg: Message) { if (!msg) return; - + ui.updateChatInChatList(msg); - - const rawChatId = msg.chatId || msg.from || (msg.chat && msg.chat.id); + + const rawChatId = msg.chatId || (typeof msg.from === 'string' ? msg.from : (msg.from as any)?._serialized) || (msg.chat && msg.chat.id); const msgChatId = normalizeId(rawChatId); if (!msgChatId) { console.warn('[WS] Could not resolve chatId from payload:', msg); return; } - - // console.log('[WS] Resolved msgChatId:', msgChatId, '| activeChatState:', activeChatState?.id); + if (!msg.fromMe) { messageTone.play(); } - if (notificationAuthorization) { - new Notification("New message", { body: msg.body }); + if (notificationAuthorization === "granted") { + new Notification("New message", { body: msg.body || msg.text }); } - + if (activeChatState && activeChatState.id === msgChatId) { - const msgId = normalizeId(msg.id) || msg.id; + const msgId = normalizeId(msg.id as any) || (msg.id as string); const exists = document.getElementById(msgId); if (!exists) { - const scrolled = elements.messagesContainer.scrollTop == elements.messagesContainer.scrollTopMax; + const container = elements.messagesContainer; + const scrolled = container.scrollTop === (container.scrollHeight - container.clientHeight); ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, (await getAppUser()).id); if (scrolled) { ui.scrollToBottom(); - } + } } } } -async function selectChat(chat, isPopState = false, smoothScroll = true) { +async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) { if (isLoadingChat) return; - + isLoadingChat = true; activeChatState = chat; - + chat.unreadCount = 0; - - // ui.renderChatList(chatsState, activeChatState, selectChat); - + ui.toggleChatState(true); elements.activeChatName.textContent = chat.name.toUpperCase(); elements.activeChatAvatar.textContent = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?'; - + elements.messagesContainer.innerHTML = `
    @@ -309,18 +311,18 @@ async function selectChat(chat, isPopState = false, smoothScroll = true) {
    `; - + elements.appContainer.classList.remove('no-active-chat'); - + if (window.innerWidth <= 768) { scrollToChat(smoothScroll); } - + if (!isPopState && window.location.hash !== `#chat-${chat.id}`) { window.location.hash = ``; window.location.hash = `chat-${chat.id}`; } - + try { const rawMessages = await getChatMessages(chat.id); const processedMessages = compensateMessageOrdering(rawMessages); @@ -335,14 +337,14 @@ async function selectChat(chat, isPopState = false, smoothScroll = true) { async function closeActiveChat(isPopState = false) { activeChatState = null; - + if (window.innerWidth <= 768) { scrollToList(); } else { ui.toggleChatState(false); } - + if (!isPopState) { if (window.location.hash.startsWith('#chat-')) { history.back(); @@ -353,9 +355,9 @@ async function closeActiveChat(isPopState = false) { async function sendMessage() { const text = elements.messageInput.value.trim(); if (!text || !activeChatState) return; - + elements.messageInput.value = ''; - + const tempMsg = { id: 'temp-' + Date.now(), body: text, @@ -363,11 +365,11 @@ async function sendMessage() { sender: 'me', timestamp: new Date().toISOString(), status: 'sending' - }; - + } as any; + ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id); ui.scrollToBottom(); - + try { try { await waha.startTyping(activeChatState.id); @@ -376,45 +378,46 @@ async function sendMessage() { } catch (e) { console.warn('Presence start failed:', e); } - + try { await waha.stopTyping(activeChatState.id); } catch (e) { console.warn('Presence stop failed:', e); } - + try { if (!activeChatState.id.endsWith('@lid')) { await waha.readChat(activeChatState.id); } - } catch (e) { + } catch (e: any) { console.warn('readChat failed (non-fatal):', e.message); } - + const responseData = await waha.sendTextMessage(activeChatState.id, text); - + const tempBubble = document.getElementById(tempMsg.id); if (tempBubble) { if (responseData && responseData.id) { - tempBubble.id = normalizeId(responseData.id); + tempBubble.id = normalizeId(responseData.id as any) || tempBubble.id; } const meta = tempBubble.querySelector('.message-meta'); - meta.innerHTML = `${formatTime(new Date())}`; + if (meta) meta.innerHTML = `${formatTime(new Date())}`; } - + activeChatState.lastMessage = text; - activeChatState.timestamp = new Date(); + activeChatState.timestamp = new Date().toISOString(); } catch (error) { console.error('Failed to send message:', error); const tempBubble = document.getElementById(tempMsg.id); if (tempBubble) { const meta = tempBubble.querySelector('.message-meta'); - meta.innerHTML = `Failed to send`; + if (meta) meta.innerHTML = `Failed to send`; } } } -async function sendFileMessage(file) { +async function sendFileMessage(file: File) { + if (!activeChatState) return; try { const tempId = 'temp-' + Date.now(); const tempMsg = { @@ -432,15 +435,15 @@ async function sendFileMessage(file) { url: URL.createObjectURL(file), filename: file.name } - }; - - ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id, activeChatState.id, true); + } as any; + + ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id, true); ui.scrollToBottom(); - + const result = await waha.sendFileMessage(activeChatState.id, file); ui.removeChatMessage(tempId); - ui.appendSingleMessage(result.id, activeChatState.name, (await getAppUser()).id, activeChatState.id); - } catch (error) { + ui.appendSingleMessage(result, activeChatState.name, (await getAppUser()).id); + } catch (error: any) { console.error(error.message); } } @@ -470,4 +473,4 @@ async function checkWahaStatus() { export function getCurrentChat() { return activeChatState; -} \ No newline at end of file +} diff --git a/js/config.js b/web/src/config.ts similarity index 71% rename from js/config.js rename to web/src/config.ts index edb15f6..f237bbe 100644 --- a/js/config.js +++ b/web/src/config.ts @@ -1,13 +1,20 @@ -// Client Configuration State & Storage Manager +export interface Config { + wahaUrl: string; + session: string; + apiKey: string; + bgImg: string; + bgOpacity: string; + save(url: string, session: string, apiKey: string, bgImg: string, bgOpacity: string): void; +} -export const config = { +export const config: Config = { wahaUrl: localStorage.getItem('waha_url') || 'http://inspiran.beetal-castor.ts.net:3100', session: localStorage.getItem('waha_session') || 'session_01kxc62bk5fs8vh4v127k88a7j', apiKey: localStorage.getItem('waha_api_key') || '', bgImg: localStorage.getItem('background_image') || '', bgOpacity: localStorage.getItem('background_opacity') || '0.4', - save(url, session, apiKey, bgImg, bgOpacity) { + save(url: string, session: string, apiKey: string, bgImg: string, bgOpacity: string): void { this.wahaUrl = url.trim().replace(/\/$/, ""); this.session = session.trim(); this.apiKey = apiKey.trim(); diff --git a/js/db.js b/web/src/db.ts similarity index 71% rename from js/db.js rename to web/src/db.ts index 1d2997b..41cf38c 100644 --- a/js/db.js +++ b/web/src/db.ts @@ -1,25 +1,26 @@ -import { normalizeId } from "./utils.js"; +import { normalizeId } from "./utils"; +import type { Chat, Message, StoredMedia } from "./types"; const DB_NAME = "pandora"; const DB_VERSION = 6; -let dbPromise = null; +let dbPromise: Promise | null = null; -function openDb() { +function openDb(): Promise { if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onupgradeneeded = (e) => { + req.onupgradeneeded = () => { const db = req.result; - const tx = req.transaction; + const tx = req.transaction!; if (!db.objectStoreNames.contains("chats")) { const store = db.createObjectStore("chats", { keyPath: "id" }); store.createIndex("timestamp", "timestamp", { unique: false }); } - let msgStore; + let msgStore: IDBObjectStore; if (!db.objectStoreNames.contains("messages")) { msgStore = db.createObjectStore("messages", { keyPath: "id" }); msgStore.createIndex("from", "from", { unique: false }); @@ -33,22 +34,21 @@ function openDb() { msgStore.createIndex("chatId_timestamp", ["chatId", "timestamp"], { unique: false }); } - if (db.objectStoreNames.contains("messages")) { - msgStore.openCursor().onsuccess = (event) => { - const cursor = event.target.result; - if (cursor) { - const m = cursor.value; - const from = normalizeId(m.from); - const to = normalizeId(m.to); - const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from); - if (!m.chatId && chatId) { - m.chatId = chatId; - cursor.update(m); - } - cursor.continue(); + // Migration logic + msgStore.openCursor().onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result; + if (cursor) { + const m = cursor.value as Message; + const from = normalizeId(m.from); + const to = normalizeId(m.to); + const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from); + if (!m.chatId && chatId) { + m.chatId = chatId; + cursor.update(m); } - }; - } + cursor.continue(); + } + }; if (!db.objectStoreNames.contains("media")) { db.createObjectStore("media", { keyPath: "reqId" }); @@ -62,7 +62,7 @@ function openDb() { return dbPromise; } -export async function upsertChats(chats) { +export async function upsertChats(chats: Chat[]): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -84,7 +84,7 @@ export async function upsertChats(chats) { }); } -export async function loadChatsSorted() { +export async function loadChatsSorted(): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -92,9 +92,9 @@ export async function loadChatsSorted() { const store = tx.objectStore("chats"); const idx = store.index("timestamp"); - const result = []; + const result: Chat[] = []; idx.openCursor(null, "prev").onsuccess = (e) => { - const cursor = e.target.result; + const cursor = (e.target as IDBRequest).result; if (cursor) { result.push(cursor.value); cursor.continue(); @@ -107,7 +107,7 @@ export async function loadChatsSorted() { }); } -export async function loadChat(chatId) { +export async function loadChat(chatId: string): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -115,12 +115,12 @@ export async function loadChat(chatId) { const store = tx.objectStore("chats"); const req = store.get(chatId); - tx.oncomplete = () => resolve(req.result) - tx.onerror = () => reject(req.error); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); }); } -function mapMessage(m) { +function mapMessage(m: Message): any { const from = normalizeId(m.from); const to = normalizeId(m.to); const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from); @@ -139,7 +139,7 @@ function mapMessage(m) { } } -export async function upsertMessages(messages) { +export async function upsertMessages(messages: Message[]): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -155,7 +155,7 @@ export async function upsertMessages(messages) { }); } -export async function loadLatestMessages(chatId, limit = 50) { +export async function loadLatestMessages(chatId: string, limit: number = 50): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -163,12 +163,12 @@ export async function loadLatestMessages(chatId, limit = 50) { const store = tx.objectStore("messages"); const idx = store.index("chatId_timestamp"); - const out = []; + const out: Message[] = []; const range = IDBKeyRange.bound([chatId, -Infinity], [chatId, Infinity]); idx.openCursor(range, "prev").onsuccess = (e) => { - const cursor = e.target.result; + const cursor = (e.target as IDBRequest).result; if (!cursor) return resolve(out); out.push(cursor.value); @@ -180,7 +180,7 @@ export async function loadLatestMessages(chatId, limit = 50) { }); } -export async function loadOlderMessages(chatId, oldestTimestamp, oldestId, limit = 50) { +export async function loadOlderMessages(chatId: string, oldestTimestamp: any, oldestId: string, limit: number = 50): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -188,7 +188,7 @@ export async function loadOlderMessages(chatId, oldestTimestamp, oldestId, limit const store = tx.objectStore("messages"); const idx = store.index("cidTimestampId"); - const out = []; + const out: Message[] = []; const parsedTimestamp = isNaN(oldestTimestamp) ? oldestTimestamp : Number(oldestTimestamp); @@ -200,7 +200,7 @@ export async function loadOlderMessages(chatId, oldestTimestamp, oldestId, limit ); idx.openCursor(range, "prev").onsuccess = (e) => { - const cursor = e.target.result; + const cursor = (e.target as IDBRequest).result; if (!cursor) return resolve(out); out.push(cursor.value); @@ -213,7 +213,7 @@ export async function loadOlderMessages(chatId, oldestTimestamp, oldestId, limit }); } -export async function upsertMedia(reqId, blob, filename) { +export async function upsertMedia(reqId: string, blob: Blob, filename: string): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -231,7 +231,7 @@ export async function upsertMedia(reqId, blob, filename) { }); } -export async function loadMedia(reqId) { +export async function loadMedia(reqId: string): Promise { const db = await openDb(); return new Promise((resolve, reject) => { @@ -242,4 +242,4 @@ export async function loadMedia(reqId) { req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); -} \ No newline at end of file +} diff --git a/js/notification.js b/web/src/notification.ts similarity index 67% rename from js/notification.js rename to web/src/notification.ts index 4698c87..3b25164 100644 --- a/js/notification.js +++ b/web/src/notification.ts @@ -1,4 +1,4 @@ -export async function showNotification(title, subtitle, time = 5000, hint) { +export async function showNotification(title: string, subtitle: string, time: number = 5000, _hint?: string): Promise { const notificationBox = document.createElement('div'); notificationBox.classList.add('notification-box'); @@ -12,25 +12,26 @@ export async function showNotification(title, subtitle, time = 5000, hint) { if (subtitle) { notificationBox.appendChild(notificationSubtitle); } - document.querySelector('body').appendChild(notificationBox); + document.querySelector('body')!.appendChild(notificationBox); let clicked = false; notificationBox.addEventListener('click', () => { + clicked = true; hideNotification(notificationBox); }) await new Promise(requestAnimationFrame); notificationBox.classList.add("shown"); - await new Promise(r => setTimeout(r, time)); + await new Promise(r => setTimeout(r, time)); if (!clicked) { hideNotification(notificationBox); } } -async function hideNotification(notificationBox) { +async function hideNotification(notificationBox: HTMLDivElement): Promise { notificationBox.classList.remove('shown'); - await new Promise(r => setTimeout(r, 1000)); + await new Promise(r => setTimeout(r, 1000)); notificationBox.remove(); -} \ No newline at end of file +} diff --git a/js/storage.js b/web/src/storage.ts similarity index 56% rename from js/storage.js rename to web/src/storage.ts index 73986d9..011b44b 100644 --- a/js/storage.js +++ b/web/src/storage.ts @@ -1,10 +1,11 @@ -import { loadChat, loadChatsSorted, loadLatestMessages, loadMedia, loadOlderMessages, upsertChats, upsertMedia, upsertMessages } from "./db.js"; -import { waha } from "./waha.js"; +import { loadChat, loadChatsSorted, loadLatestMessages, loadMedia, loadOlderMessages, upsertChats, upsertMedia, upsertMessages } from "./db"; +import { waha } from "./waha"; +import type { Chat, Message, AppUser, ContactInfo, UserAboutResponse, ChatPictureResponse, StatusResponse, DownloadedMedia } from "./types"; let online = false; -let chats = []; +let chats: Chat[] = []; -export async function updateOnlineStatus() { +export async function updateOnlineStatus(): Promise { try { await waha.getVersion(); online = true; @@ -14,17 +15,17 @@ export async function updateOnlineStatus() { } } -export async function fetchChats() { +export async function fetchChats(): Promise { if (online) { await getRemoteChats() } chats = await loadChatsSorted(); } -export async function getRemoteChats() { +export async function getRemoteChats(): Promise { const u = await waha.getChats(); - const mapped = u.map(chat => ({ + const mapped: Chat[] = u.map(chat => ({ id: chat.id, name: chat.name, lastMessage: chat.lastMessage, @@ -35,15 +36,15 @@ export async function getRemoteChats() { await upsertChats(mapped); } -export function getUsers() { +export function getUsers(): Chat[] { return chats.filter(c => c.id.endsWith("@c.us")); } -export function getGroups() { +export function getGroups(): Chat[] { return chats.filter(c => c.id.endsWith("@g.us")); } -export async function getUser(number) { +export async function getUser(number: string): Promise { if (online) { return await waha.getUser(number); } else { @@ -51,7 +52,7 @@ export async function getUser(number) { } } -export async function getUserAbout(userId) { +export async function getUserAbout(userId: string): Promise { if (online) { return await waha.getUserAbout(userId); } else { @@ -59,25 +60,26 @@ export async function getUserAbout(userId) { } } -export function getChats() { +export function getChats(): Chat[] { return chats; } -export async function getAppUser() { +export async function getAppUser(): Promise { if (online) { const info = await waha.getMyInfo(); - localStorage.setItem('pandora-last-username', info.name); + localStorage.setItem('pandora-last-username', info.pushName || info.name || ''); localStorage.setItem('pandora-last-userid', info.id); return info; } else { return { pushName: localStorage.getItem('pandora-last-username') || 'Unknown', + name: localStorage.getItem('pandora-last-username') || 'Unknown', id: localStorage.getItem('pandora-last-userid') || 'Unknown' - } + } as AppUser; } } -export async function getMessage(chatId, msgId, downloadMedia) { +export async function getMessage(chatId: string, msgId: string, downloadMedia: boolean): Promise { if (online) { const newMessage = await waha.getSingleChatMessage(chatId, msgId, downloadMedia); upsertMessages([newMessage]); @@ -88,14 +90,14 @@ export async function getMessage(chatId, msgId, downloadMedia) { body: "You're offline", from: "system", timestamp: new Date().toISOString() - }; + } as Message; } } -export async function getMedia(reqId) { +export async function getMedia(reqId: string): Promise { const cached = await loadMedia(reqId); if (cached) { - return cached; + return { blob: cached.blob, filename: cached.filename }; } try { @@ -109,7 +111,7 @@ export async function getMedia(reqId) { } } -export async function getChatMessages(chatId) { +export async function getChatMessages(chatId: string): Promise { if (online) { const newMessages = await waha.getChatMessages(chatId); upsertMessages(newMessages); @@ -119,7 +121,7 @@ export async function getChatMessages(chatId) { } } -export async function getMoreChatMessages(chatId, oldestTimestamp, oldestId, limit = 50) { +export async function getMoreChatMessages(chatId: string, oldestTimestamp: any, oldestId: string): Promise { if (online) { return waha.getChatMessages(chatId, oldestTimestamp); } else { @@ -127,7 +129,7 @@ export async function getMoreChatMessages(chatId, oldestTimestamp, oldestId, lim } } -export async function getChatPicture(chatId) { +export async function getChatPicture(chatId: string): Promise { if (online) { return await waha.getChatPicture(chatId); } else { @@ -135,11 +137,11 @@ export async function getChatPicture(chatId) { } } -export function isOnline() { +export function isOnline(): boolean { return online; } -export async function sendStatus(text) { +export async function sendStatus(text: string): Promise { if (online) { return await waha.setStatus(text); } else { @@ -149,12 +151,17 @@ export async function sendStatus(text) { } } -export async function markRead(chatId) { +export async function markRead(chatId: string): Promise { if (online) { await waha.readChat(chatId); } const chat = await loadChat(chatId); - chat.unreadCount = 0; - upsertMessages([chat]); -} \ No newline at end of file + if (chat) { + chat.unreadCount = 0; + // Note: upsertMessages was called with [chat] in JS, but chat is a Chat object, not Message. + // Keeping JS behavior but chat is Chat type here. + await upsertChats([chat]); + } + return chat; +} diff --git a/web/src/types.ts b/web/src/types.ts new file mode 100644 index 0000000..d65614c --- /dev/null +++ b/web/src/types.ts @@ -0,0 +1,124 @@ +/** Represents a chat entry in the sidebar */ +export interface Chat { + id: string; + name: string; + lastMessage: string; + timestamp: number | string | Date; + unreadCount: number; +} + +/** Raw chat object returned from the WAHA API */ +export interface WahaChat { + id: string | { _serialized?: string; user?: string }; + chatId?: string; + name?: string; + unreadCount?: number; + lastMessage?: { + body?: string; + timestamp?: number | string; + }; + lastMessageText?: string; +} + +/** Message data sub-object (_data field) */ +export interface MessageData { + notifyName?: string; + mimetype?: string; + [key: string]: unknown; +} + +/** Media attachment info */ +export interface MediaInfo { + url: string; + filename?: string; +} + +/** A WhatsApp message */ +export interface Message { + _data?: MessageData; + _serialized?: string | { _serialized?: string; user?: string }; + id: string | { _serialized?: string; user?: string }; + body?: string; + text?: string; + from?: string; + to?: string; + fromMe?: boolean; + sender?: string; + timestamp: number | string; + status?: string; + ack?: number; + hasMedia?: boolean; + media?: MediaInfo; + chatId?: string; + chat?: { id: string }; + participant?: string; +} + +/** Temporary message used for optimistic UI updates */ +export interface TempMessage extends Message { + status: string; +} + +/** File-sending temporary message */ +export interface TempFileMessage extends TempMessage { + _data: MessageData; + hasMedia: true; + media: MediaInfo; +} + +/** WAHA version response */ +export interface VersionResponse { + version?: string; +} + +/** WAHA user info (getMyInfo) */ +export interface AppUser { + id: string; + pushName?: string; + name?: string; +} + +/** WAHA contact info */ +export interface ContactInfo { + number: string; + [key: string]: unknown; +} + +/** WAHA user about response */ +export interface UserAboutResponse { + about?: string; +} + +/** Chat picture response */ +export interface ChatPictureResponse { + url?: string; +} + +/** Status update response */ +export interface StatusResponse { + success?: boolean; +} + +/** WebSocket event payload envelope */ +export interface WebSocketEvent { + event: string; + payload: Message; +} + +/** IndexedDB stored media record */ +export interface StoredMedia { + reqId: string; + blob: Blob; + filename: string; +} + +/** Downloaded media result */ +export interface DownloadedMedia { + blob: Blob; + filename: string; +} + +/** Message with _time field used during ordering compensation */ +export interface MessageWithTime extends Message { + _time: number; +} diff --git a/js/ui.js b/web/src/ui.ts similarity index 56% rename from js/ui.js rename to web/src/ui.ts index 2328e78..8d524df 100644 --- a/js/ui.js +++ b/web/src/ui.ts @@ -1,50 +1,51 @@ -import { formatTime, normalizeId } from "./utils.js"; -import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage.js"; -import { getCurrentChat } from "./app.js"; +import { formatTime, normalizeId } from "./utils"; +import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage"; +import { getCurrentChat } from "./app"; +import type { Chat, Message } from "./types"; export const elements = { - chatList: document.getElementById('chat-list'), - chatsLoader: document.getElementById('chats-loader'), - chatSearch: document.getElementById('chat-search'), - backendStatusText: document.getElementById('backend-status-text'), - apiStatusIndicator: document.querySelector('.pulse-dot'), - noChatState: document.getElementById('no-chat-state'), - activeChatContainer: document.getElementById('active-chat-container'), - activeChatName: document.getElementById('active-chat-name'), - activeChatAvatar: document.getElementById('active-chat-avatar'), - messagesContainer: document.getElementById('messages-container'), - messageForm: document.getElementById('message-form'), - messageInput: document.getElementById('message-input'), - backToSidebarBtn: document.querySelector('.chat-header'), - sidebar: document.querySelector('.sidebar'), - appContainer: document.querySelector('.app-container'), - settingsModal: document.getElementById('settings-page'), - settingsIconBtn: document.getElementById('settings-sidebar-btn'), - saveSettingsBtn: document.getElementById('save-settings'), - inputWahaUrl: document.getElementById('settings-waha-url'), - inputSession: document.getElementById('settings-session'), - inputApiKey: document.getElementById('settings-api-key'), - inputBackgroundImage: document.getElementById('settings-background-image'), - inputBackgroundOpacity: document.getElementById('settings-background-opacity'), - loggedUserName: document.getElementById('pandora-username'), - chatBottomBar: document.getElementById('chat-bottom-bar'), - chatBottomBarBtn: document.getElementById('chat-bottom-bar-btn'), - chatInputPanel: document.getElementById('chat-input-panel'), - attachmentInput: document.getElementById('attachment-input'), - attachmentBtn: document.getElementById('attachment-btn'), - markreadBtn: document.getElementById('markread-btn'), - extraPages: document.querySelectorAll('.extra-page'), - desktopSidebarButtons: document.querySelectorAll("#desktop-aside button"), - contentUserName: document.querySelectorAll('[data-content="app-user"]'), - contentUserNumber: document.querySelectorAll('[data-content="app-user-number"]'), - resourceUserPic: document.querySelectorAll('[data-resource="app-user-image"]'), - valueUserStatus: document.querySelectorAll('[data-value="app-user-status"]'), - inputUserStatus: document.getElementById('profile-page-status-input'), - selectable: document.querySelectorAll('.selectable'), + chatList: document.getElementById('chat-list') as HTMLUListElement, + chatsLoader: document.getElementById('chats-loader') as HTMLDivElement, + chatSearch: document.getElementById('chat-search') as HTMLInputElement, + backendStatusText: document.getElementById('backend-status-text') as HTMLSpanElement, + apiStatusIndicator: document.querySelector('.pulse-dot') as HTMLSpanElement, + noChatState: document.getElementById('no-chat-state') as HTMLDivElement, + activeChatContainer: document.getElementById('active-chat-container') as HTMLDivElement, + activeChatName: document.getElementById('active-chat-name') as HTMLHeadingElement, + activeChatAvatar: document.getElementById('active-chat-avatar') as HTMLDivElement, + messagesContainer: document.getElementById('messages-container') as HTMLDivElement, + messageForm: document.getElementById('message-form') as HTMLFormElement, + messageInput: document.getElementById('message-input') as HTMLInputElement, + backToSidebarBtn: document.querySelector('.chat-header') as HTMLElement, + sidebar: document.querySelector('.sidebar') as HTMLElement, + appContainer: document.querySelector('.app-container') as HTMLElement, + settingsModal: document.getElementById('settings-page') as HTMLElement, + settingsIconBtn: document.getElementById('settings-sidebar-btn') as HTMLButtonElement, + saveSettingsBtn: document.getElementById('save-settings') as HTMLButtonElement, + inputWahaUrl: document.getElementById('settings-waha-url') as HTMLInputElement, + inputSession: document.getElementById('settings-session') as HTMLInputElement, + inputApiKey: document.getElementById('settings-api-key') as HTMLInputElement, + inputBackgroundImage: document.getElementById('settings-background-image') as HTMLInputElement, + inputBackgroundOpacity: document.getElementById('settings-background-opacity') as HTMLInputElement, + loggedUserName: document.getElementById('pandora-username') as HTMLHeadingElement, + chatBottomBar: document.getElementById('chat-bottom-bar') as HTMLElement, + chatBottomBarBtn: document.getElementById('chat-bottom-bar-btn') as HTMLButtonElement, + chatInputPanel: document.getElementById('chat-input-panel') as HTMLElement, + attachmentInput: document.getElementById('attachment-input') as HTMLInputElement, + attachmentBtn: document.getElementById('attachment-btn') as HTMLButtonElement, + markreadBtn: document.getElementById('markread-btn') as HTMLButtonElement, + extraPages: document.querySelectorAll('.extra-page') as NodeListOf, + desktopSidebarButtons: document.querySelectorAll("#desktop-aside button") as NodeListOf, + contentUserName: document.querySelectorAll('[data-content="app-user"]') as NodeListOf, + contentUserNumber: document.querySelectorAll('[data-content="app-user-number"]') as NodeListOf, + resourceUserPic: document.querySelectorAll('[data-resource="app-user-image"]') as NodeListOf, + valueUserStatus: document.querySelectorAll('[data-value="app-user-status"]') as NodeListOf, + inputUserStatus: document.getElementById('profile-page-status-input') as HTMLInputElement, + selectable: document.querySelectorAll('.selectable') as NodeListOf, }; export const ui = { - showExtraPage(pageId) { + showExtraPage(pageId: string) { elements.extraPages.forEach(page => { if (page.id != pageId) { page.style.display = "none"; @@ -53,11 +54,11 @@ export const ui = { } }) }, - + /** * Switch view state when a contact chat is opened or closed */ - toggleChatState(hasActive) { + toggleChatState(hasActive: boolean) { if (hasActive) { elements.noChatState.classList.add('hidden'); elements.activeChatContainer.classList.remove('hidden'); @@ -66,18 +67,18 @@ export const ui = { elements.activeChatContainer.classList.add('hidden'); } }, - + /** * Scroll message list automatically to bottom */ scrollToBottom() { elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight; }, - + /** * Update connection status badge in sidebar footer */ - updateConnectionStatus(isConnected, text) { + updateConnectionStatus(isConnected: boolean, text: string) { elements.backendStatusText.textContent = text; if (isConnected) { elements.apiStatusIndicator.style.backgroundColor = 'var(--online-color)'; @@ -87,25 +88,29 @@ export const ui = { elements.apiStatusIndicator.style.animation = 'none'; } }, - - async renderChatList(chats, activeChat, onChatSelect) { + + async renderChatList(chats: Chat[], activeChat: Chat | null, onChatSelect: (chat: Chat) => void) { elements.chatList.innerHTML = ''; - chats.sort((a, b) => b.timestamp - a.timestamp); - + chats.sort((a, b) => { + const timeA = new Date(a.timestamp).getTime(); + const timeB = new Date(b.timestamp).getTime(); + return timeB - timeA; + }); + if (chats.length === 0) { elements.chatList.innerHTML = `
  • No chats found
  • `; return; } - + for (const chat of chats) { const li = document.createElement('li'); li.className = `chat-item selectable ${activeChat && activeChat.id === chat.id ? 'active' : ''}`; li.dataset.id = chat.id; - + const initials = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?'; const hasUnread = chat.unreadCount && chat.unreadCount > 0; const timeStr = formatTime(chat.timestamp || new Date()); - + li.innerHTML = `
    `; - + li.addEventListener('click', () => onChatSelect(chat)); elements.chatList.appendChild(li); - + (async () => { try { const picture = await getChatPicture(chat.id); - const img = li.querySelector(`img[data-chat-avatar="${chat.id}"]`); + const img = li.querySelector(`img[data-chat-avatar="${chat.id}"]`) as HTMLImageElement; if (img) img.src = picture.url ? picture.url : ''; } catch (e) { } })(); } }, - - async updateChatInChatList(msg) { - const chat = document.querySelector(`.chat-item[data-id="${msg.fromMe ? msg.to : msg.from}"]`); - - if (chat) { - const messageItem = chat.querySelector('.chat-item-msg'); - const time = chat.querySelector('.chat-item-time') + + async updateChatInChatList(msg: Message) { + const chatNode = document.querySelector(`.chat-item[data-id="${msg.fromMe ? msg.to : msg.from}"]`) as HTMLElement; + + if (chatNode) { + const messageItem = chatNode.querySelector('.chat-item-msg') as HTMLElement; + const time = chatNode.querySelector('.chat-item-time') as HTMLElement; messageItem.innerText = msg.body || msg.text || 'Media message'; - time.innerText = msg.timestamp ? formatTime(msg.timestamp) : Date.now(); - + time.innerText = msg.timestamp ? formatTime(msg.timestamp) : formatTime(Date.now()); + const activeChatState = getCurrentChat(); - + if (!msg.fromMe && (!activeChatState || activeChatState.id !== (msg.fromMe ? msg.to : msg.from))) { - const unreadBadge = chat.querySelector('.unread-badge'); - if (unreadBadge) unreadBadge.innerText = (Number(unreadBadge.innerHTML) || 0) + 1; + const unreadBadge = chatNode.querySelector('.unread-badge') as HTMLElement; + if (unreadBadge) { + unreadBadge.innerText = (Number(unreadBadge.innerHTML) || 0) + 1 + ""; + } else { + const preview = chatNode.querySelector('.chat-item-preview') as HTMLElement; + const badge = document.createElement('span'); + badge.className = 'unread-badge'; + badge.innerText = '1'; + preview.appendChild(badge); + } } } }, - + + async updateChatInChatList2(chat: Chat) { + const chatNode = document.querySelector(`.chat-item[data-id="${chat.id}"]`) as HTMLElement; + + if (chatNode) { + const messageItem = chatNode.querySelector('.chat-item-msg') as HTMLElement; + const time = chatNode.querySelector('.chat-item-time') as HTMLElement; + messageItem.innerText = chat.lastMessage || 'Media message'; + time.innerText = chat.timestamp ? formatTime(chat.timestamp) : formatTime(Date.now()); + const unreadBadge = chatNode.querySelector('.unread-badge') as HTMLElement; + const newCount = (Number(unreadBadge?.innerHTML) || 0); + if (unreadBadge && newCount != 0) { + unreadBadge.innerText = newCount + ""; + } else { + if (unreadBadge) unreadBadge.remove(); + } + } + }, + /** * Render chat message log inside chat view container */ - async renderMessages(messages, activeChatName, userID, chatId) { + async renderMessages(messages: Message[], _activeChatName: string, userID: string, chatId: string) { elements.messagesContainer.innerHTML = ''; - + if (messages.length === 0) { elements.messagesContainer.innerHTML = '
    No messages. Say hello!
    '; return; } - + const loadMore = document.createElement("button"); loadMore.classList.add("load-more-btn"); loadMore.innerText = "Load more"; - loadMore.addEventListener('click', () => { + loadMore.onclick = () => { this.loadMoreMessages(chatId, userID); - }); + }; elements.messagesContainer.appendChild(loadMore); - + for (const msg of messages) { this.appendSingleMessage(msg, userID, chatId); } - + this.scrollToBottom(); }, - - async loadMoreMessages(chatId, userId) { - const oldest = document.querySelector('.message-group:first-of-type'); + + async loadMoreMessages(chatId: string, userId: string) { + const oldest = document.querySelector('.message-group:first-of-type') as HTMLElement; + if (!oldest) return; const oldestTimestamp = oldest.dataset.timestamp; const oldestId = oldest.id; - console.log(oldest) - console.log(oldestId) - console.log(oldestTimestamp) - - const loadMoreButton = document.querySelector('.load-more-btn'); - loadMoreButton.removeEventListener('click', this.loadMoreMessages); - + + const loadMoreButton = document.querySelector('.load-more-btn') as HTMLButtonElement; + const msgs = await getMoreChatMessages(chatId, oldestTimestamp, oldestId); + // JS version had msgs.shift(), probably to avoid duplication of the oldest message msgs.shift(); - + msgs.forEach(async msg => { loadMoreButton.after(this.generateMessage(msg, userId, chatId)); }); - - loadMoreButton.addEventListener('click', this.loadMoreMessages); }, - + /** * Append a single message (used for optimistic updates immediately upon sending) */ - appendSingleMessage(msg, userID, chatId, isLocal = false) { + appendSingleMessage(msg: Message, userID: string, chatId: string, isLocal: boolean = false) { elements.messagesContainer.appendChild(this.generateMessage(msg, userID, chatId, isLocal)) }, - - generateTempMessageLink(msg) { + + generateTempMessageLink(msg: Message) { const a = document.createElement('a'); a.target = "_blank"; - a.href = msg.media.url; - - if (msg._data?.mimetype?.startsWith('image/')) { - const img = document.createElement('img'); - img.classList.add('message-image-attachement'); - img.src = msg.media.url; - a.appendChild(img); - } else { - a.textContent = msg.media.filename || "Download file"; - a.download = msg.media.filename; + if (msg.media) { + a.href = msg.media.url; + + if (msg._data?.mimetype?.startsWith('image/')) { + const img = document.createElement('img'); + img.classList.add('message-image-attachement'); + img.src = msg.media.url; + a.appendChild(img); + } else { + a.textContent = msg.media.filename || "Download file"; + a.download = msg.media.filename || "file"; + } } - + return a; }, - - generateMessage(msg, userID, chatId, isLocal = false) { + + generateMessage(msg: Message, userID: string, chatId: string, isLocal: boolean = false) { const isOutgoing = msg.fromMe || msg.sender === 'me'; - + function getPrevMessageElem() { - return elements.messagesContainer.lastElementChild; + return elements.messagesContainer.lastElementChild as HTMLElement | null; } - + const prevMsgEl = getPrevMessageElem(); - + const groupDiv = document.createElement('div'); groupDiv.className = `message-group selectable ${isOutgoing ? 'outgoing' : 'incoming'}`; - groupDiv.id = normalizeId(msg._serialized ? msg : msg.id); - groupDiv.dataset.timestamp = msg.timestamp; - groupDiv.dataset.from = msg.participant || msg.from; - - const senderName = isOutgoing ? userID : (msg._data.notifyName || msg.from); + groupDiv.id = normalizeId(msg._serialized ? (msg._serialized as any) : msg.id) || "msg-id"; + groupDiv.dataset.timestamp = msg.timestamp?.toString(); + groupDiv.dataset.from = msg.participant || (msg.from as string); + + const senderName = isOutgoing ? userID : (msg._data?.notifyName || (msg.from as string)); const timeStr = formatTime(msg.timestamp || new Date()); - + let statusCheck = ''; if (isOutgoing) { if (msg.status === 'read') { @@ -262,116 +291,125 @@ export const ui = { statusCheck = ''; } } - + const bubble = document.createElement('div'); bubble.className = 'message-bubble'; - - let prevUid; - + + let prevUid: string | undefined; + if (msg.participant) { prevUid = msg.participant; } else { - prevUid = msg.from; + prevUid = msg.from as string; } - + if (!isOutgoing && (!prevMsgEl || prevUid !== prevMsgEl.dataset.from)) { const senderEl = document.createElement('span'); senderEl.className = 'message-sender'; senderEl.textContent = senderName; bubble.appendChild(senderEl); } - + const contentEl = document.createElement('div'); contentEl.classList.add('message-content'); const textEl = document.createElement('div'); textEl.innerHTML = msg.body || msg.text || ""; contentEl.appendChild(textEl); bubble.appendChild(contentEl); - + if (msg.hasMedia) { - let a; - + let a: HTMLAnchorElement; + if (isLocal) { - a = this.generateTempMessageLink(msg); + a = this.generateTempMessageLink(msg); } else { a = document.createElement('a'); a.innerText = `[Request media]`; a.target = "_blank"; - - const clickListener = async (e) => { + + const clickListener = async (e: MouseEvent) => { a.removeEventListener('click', clickListener); a.innerText = `[Downloading]`; - const mediaMsg = msg.media ? msg : await getMessage(chatId, normalizeId(msg._serialized ? msg : msg.id), true); - if (!mediaMsg || !mediaMsg?.media.url) { + const mediaMsg = msg.media ? msg : await getMessage(chatId, normalizeId(msg._serialized ? (msg._serialized as any) : msg.id) || "", true); + if (!mediaMsg || !mediaMsg?.media?.url) { a.addEventListener('click', clickListener); a.innerText = `[Error, click to try again]` return; } const url = new URL(mediaMsg.media.url); const reqID = url.pathname.split('/').filter(Boolean).pop(); - - const { blob, filename } = await getMedia(reqID); - - const objectUrl = URL.createObjectURL(blob); - e.target.href = objectUrl; - - if (blob.type.startsWith('image/')) { + + if (!reqID) return; + const media = await getMedia(reqID); + if (!media) return; + + const objectUrl = URL.createObjectURL(media.blob); + (e.target as HTMLAnchorElement).href = objectUrl; + + if (media.blob.type.startsWith('image/')) { a.textContent = ""; const img = document.createElement('img'); img.classList.add('message-image-attachement'); img.src = objectUrl; bubble.after(img); - bubble.querySelector('.message-content').remove(); + const content = bubble.querySelector('.message-content'); + if (content) content.remove(); } else { - e.target.textContent = filename || `Download ${mediaMsg.media.filename}`; + (e.target as HTMLAnchorElement).textContent = media.filename || `Download ${mediaMsg.media.filename}`; } } - + a.addEventListener('click', clickListener); } - + contentEl.appendChild(a); - + if (!isLocal && msg._data?.mimetype?.startsWith('image/')) { a.click(); } } - - + + const meta = document.createElement('div'); meta.className = 'message-meta'; meta.innerHTML = `${timeStr}${statusCheck}`; - + bubble.appendChild(meta); - + if (isOutgoing) { if (prevMsgEl && prevMsgEl.classList.contains('outgoing')) { groupDiv.classList.add('same-sender'); } else { - groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; + const indicator = document.createElement('div'); + indicator.className = 'message-indicator'; + groupDiv.appendChild(indicator); } } else if (msg.participant) { if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) { - groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; + const indicator = document.createElement('div'); + indicator.className = 'message-indicator'; + groupDiv.appendChild(indicator); } else groupDiv.classList.add('same-sender'); } else { if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) { - groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; + const indicator = document.createElement('div'); + indicator.className = 'message-indicator'; + groupDiv.appendChild(indicator); } else groupDiv.classList.add('same-sender'); } - + groupDiv.appendChild(bubble); return groupDiv; }, - - updateMessage(originalMsgId, generatedMsg) { + + updateMessage(originalMsgId: string, generatedMsg: HTMLElement) { const originalMsg = document.querySelector(`#${originalMsgId}`); if (originalMsg) { originalMsg.replaceWith(generatedMsg) } }, - - updateMessageTick(id, status) { + + updateMessageTick(id: string, status: string) { let statusCheck; if (status === 'read') { statusCheck = ''; @@ -382,15 +420,19 @@ export const ui = { } else { statusCheck = ''; } - - document.getElementById(id).querySelector('.message-meta').outerHTML = statusCheck; + + const msgNode = document.getElementById(id); + if (msgNode) { + const meta = msgNode.querySelector('.message-meta'); + if (meta) meta.outerHTML = statusCheck; + } }, - + toggleChatBottomBar() { elements.chatBottomBar.classList.toggle("collapsed"); }, - - removeChatMessage(msgId) { + + removeChatMessage(msgId: string) { const message = document.getElementById(msgId); if (message) message.remove(); } diff --git a/js/utils.js b/web/src/utils.ts similarity index 69% rename from js/utils.js rename to web/src/utils.ts index 81f05f0..261f1bf 100644 --- a/js/utils.js +++ b/web/src/utils.ts @@ -1,13 +1,13 @@ -// Helper utilities and algorithms +import type { Message, MessageWithTime } from "./types"; /** * Format timestamps (supports Unix epoch seconds/ms, strings and ISO dates) * @param {string|number|Date} dateVal * @returns {string} Formatted HH:MM AM/PM string */ -export function formatTime(dateVal) { +export function formatTime(dateVal: string | number | Date): string { if (!dateVal) return ''; - let date; + let date: Date; if (typeof dateVal === 'number') { date = new Date(dateVal < 10000000000 ? dateVal * 1000 : dateVal); } else if (typeof dateVal === 'string' && /^\d+$/.test(dateVal)) { @@ -18,12 +18,12 @@ export function formatTime(dateVal) { } let hours = date.getHours(); - let minutes = date.getMinutes(); + const minutes = date.getMinutes(); const ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12; hours = hours ? hours : 12; - minutes = minutes < 10 ? '0' + minutes : minutes; - return `${hours}:${minutes} ${ampm}`; + const minutesStr = minutes < 10 ? '0' + minutes : String(minutes); + return `${hours}:${minutesStr} ${ampm}`; } /** @@ -32,24 +32,22 @@ export function formatTime(dateVal) { * @param {Array} messages List of raw messages from WAHA * @returns {Array} Compensated chronological message array */ -export function compensateMessageOrdering(messages) { +export function compensateMessageOrdering(messages: Message[]): Message[] { if (!Array.isArray(messages)) return []; - // Convert timestamps to numeric milliseconds for stable comparison - const msgs = messages.map(m => { - let t = m.timestamp; - if (typeof t === 'number') { - if (t < 10000000000) t = t * 1000; + // convert to ms + const msgs: MessageWithTime[] = messages.map(m => { + let t: number; + if (typeof m.timestamp === 'number') { + t = m.timestamp < 10000000000 ? m.timestamp * 1000 : m.timestamp; } else { - t = new Date(t).getTime(); + t = new Date(m.timestamp).getTime(); } return { ...m, _time: t }; }); - // Initial chronological sort msgs.sort((a, b) => a._time - b._time); - // Apply drift bubble adjustments let changed = true; while (changed) { changed = false; @@ -58,12 +56,12 @@ export function compensateMessageOrdering(messages) { const next = msgs[i + 1]; const timeDiff = next._time - current._time; - // Swap if an outgoing message is sorted before an incoming message within a 30-sec window + // swap if outgoing message is sorted before incoming message within 30-sec window if (current.fromMe && !next.fromMe && timeDiff >= 0 && timeDiff <= 30000) { msgs[i] = next; msgs[i + 1] = current; - // Advance the outgoing message's timestamp to be exactly 1 second after the incoming one + // advance the outgoing message timestamp to exactly 1 sec after the incoming current._time = next._time + 1000; if (typeof current.timestamp === 'number') { current.timestamp = Math.floor(current._time / 1000); @@ -76,16 +74,16 @@ export function compensateMessageOrdering(messages) { } } - // Clean up temporary property + // clean temp property return msgs.map(({ _time, ...m }) => m); } -export function getBase64(file) { +export function getBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { - resolve(reader.result.split(",")[1]); + resolve((reader.result as string).split(",")[1]); }; reader.onerror = (e) => { @@ -106,7 +104,7 @@ export function getBase64(file) { * @param {string|object} raw * @returns {string|null} */ -export function normalizeId(raw) { +export function normalizeId(raw: string | { _serialized?: string; user?: string } | null | undefined): string | null { if (!raw) return null; if (typeof raw === 'object') { return raw._serialized || raw.user || JSON.stringify(raw); @@ -114,10 +112,10 @@ export function normalizeId(raw) { return raw; } -export function debounce(func, delay) { - let timeoutId; +export function debounce(func: () => void, delay: number): () => void { + let timeoutId: ReturnType; return function() { clearTimeout(timeoutId); timeoutId = setTimeout(func, delay); }; -} \ No newline at end of file +} diff --git a/js/waha.js b/web/src/waha.ts similarity index 57% rename from js/waha.js rename to web/src/waha.ts index 6410201..95acd25 100644 --- a/js/waha.js +++ b/web/src/waha.ts @@ -1,20 +1,19 @@ -import { config } from "./config.js"; -import { showNotification } from "./notification.js"; -import { getBase64 } from "./utils.js"; +import { config } from "./config"; +import { showNotification } from "./notification"; +import { getBase64 } from "./utils"; +import type { Message, VersionResponse, AppUser, ContactInfo, UserAboutResponse, ChatPictureResponse, StatusResponse } from "./types"; -async function request(path, options = {}) { +async function request(path: string, options: RequestInit = {}): Promise { const url = `${config.wahaUrl}${path}`; - const headers = { + const headers: Record = { 'Content-Type': 'application/json', 'accept': '*/*', - ...options.headers + ...(options.headers as Record) }; if (config.apiKey) { headers['X-Api-Key'] = config.apiKey; } - // console.log(`[WAHA] ${options.method || 'GET'} ${url}`, options.body ? JSON.parse(options.body) : ''); - const response = await fetch(url, { ...options, headers }); if (!response.ok) { let errorDetail = ''; @@ -26,25 +25,23 @@ async function request(path, options = {}) { errorDetail = await response.text().catch(() => ''); } - showNotification("API Error", `WAHA API returned ${response.status}: ${response.statusText} — ${errorDetail}`, 2000); + showNotification("API Error", `WAHA API returned ${response.status}: ${response.statusText} — ${errorDetail}`, 4000); throw new Error(`WAHA API returned ${response.status}: ${response.statusText} — ${errorDetail}`); } return response.json(); } -async function downloadFile(path, options = {}) { +async function downloadFile(path: string, options: RequestInit = {}): Promise<{ blob: Blob, filename: string }> { const url = `${config.wahaUrl}${path}`; - const headers = { - 'Content-Type': options.headers?.['Content-Type'] ?? 'application/json', + const headers: Record = { + 'Content-Type': (options.headers as Record | undefined)?.['Content-Type'] ?? 'application/json', 'accept': '*/*', - ...options.headers + ...(options.headers as Record) }; if (config.apiKey) headers['X-Api-Key'] = config.apiKey; -// console.log(`[WAHA] ${options.method || 'GET'} ${url}`); - const response = await fetch(url, { ...options, headers }); if (!response.ok) { @@ -69,14 +66,13 @@ async function downloadFile(path, options = {}) { return { blob, filename }; } - export const waha = { - async getVersion() { - return await request('/api/version'); + async getVersion(): Promise { + return await request('/api/version'); }, - async getChats() { - const data = await request(`/api/${config.session}/chats`); + async getChats(): Promise { + const data = await request(`/api/${config.session}/chats`); return data.map(chat => { let chatId = chat.id; if (chatId && typeof chatId === "object") { @@ -92,59 +88,58 @@ export const waha = { }); }, - async getChatMessages(chatId, beforeTimestamp) { - console.log(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40&sortBy=timestamp${beforeTimestamp ? `&filter.timestamp.gte=${beforeTimestamp}` : "" }`); - return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40${beforeTimestamp ? `&filter.timestamp.lte=${beforeTimestamp}` : "" }`); + async getChatMessages(chatId: string, beforeTimestamp?: any): Promise { + return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40${beforeTimestamp ? `&filter.timestamp.lte=${beforeTimestamp}` : "" }`); }, - async getSingleChatMessage(chatId, messageId, downladMedia) { - return request(`/api/${config.session}/chats/${chatId}/messages/${messageId}?downloadMedia=${downladMedia}`); + async getSingleChatMessage(chatId: string, messageId: string, downloadMedia: boolean): Promise { + return request(`/api/${config.session}/chats/${chatId}/messages/${messageId}?downloadMedia=${downloadMedia}`); }, - async getChatPicture(chatId) { - return request(`/api/${config.session}/chats/${chatId}/picture`); + async getChatPicture(chatId: string): Promise { + return request(`/api/${config.session}/chats/${chatId}/picture`); }, - async getUser(chatId) { - return request(`/api/${config.session}/contacts/${chatId}`); + async getUser(chatId: string): Promise { + return request(`/api/${config.session}/contacts/${chatId}`); }, - async getUserAbout(chatId) { - return request(`/api/contacts/about?contactId=${chatId}&session=${config.session}`); + async getUserAbout(chatId: string): Promise { + return request(`/api/contacts/about?contactId=${chatId}&session=${config.session}`); }, - async readChat(chatId) { + async readChat(chatId: string): Promise { return request('/api/sendSeen', { method: 'POST', body: JSON.stringify({ chatId, session: config.session }) }); }, - async downloadMedia(file) { + async downloadMedia(file: string): Promise<{ blob: Blob, filename: string }> { const { blob, filename } = await downloadFile(`/api/files/${config.session}/${file}`); return { blob, filename }; }, - async getMyInfo() { - return request(`/api/sessions/${config.session}/me`); + async getMyInfo(): Promise { + return request(`/api/sessions/${config.session}/me`); }, - async startTyping(chatId) { + async startTyping(chatId: string): Promise { return request('/api/startTyping', { method: 'POST', body: JSON.stringify({ chatId, session: config.session }) }); }, - async stopTyping(chatId) { + async stopTyping(chatId: string): Promise { return request('/api/stopTyping', { method: 'POST', body: JSON.stringify({ chatId, session: config.session }) }); }, - async sendTextMessage(chatId, text) { - return request('/api/sendText', { + async sendTextMessage(chatId: string, text: string): Promise { + return request('/api/sendText', { method: 'POST', body: JSON.stringify({ chatId, @@ -154,8 +149,8 @@ export const waha = { }); }, - async setStatus(text) { - return request(`/api/${config.session}/profile/status`, { + async setStatus(text: string): Promise { + return request(`/api/${config.session}/profile/status`, { method: 'PUT', body: JSON.stringify({ status: text @@ -163,9 +158,9 @@ export const waha = { }); }, - async sendFileMessage(chatId, file) { + async sendFileMessage(chatId: string, file: File): Promise { const fileBase64 = await getBase64(file); - const body = { + const body: RequestInit = { method: 'POST', body: JSON.stringify({ chatId, @@ -183,7 +178,7 @@ export const waha = { if (file.type.startsWith('image/')) endpoint = '/api/sendImage'; if (file.type.startsWith('video/')) endpoint = '/api/sendVideo'; - const result = await request(endpoint, body); + const result = await request(endpoint, body); return result; } }; diff --git a/js/websocket.js b/web/src/websocket.ts similarity index 78% rename from js/websocket.js rename to web/src/websocket.ts index c1c0aa2..dd6e86f 100644 --- a/js/websocket.js +++ b/web/src/websocket.ts @@ -1,13 +1,14 @@ -import { config } from "./config.js"; -import { isOnline, updateOnlineStatus } from "./storage.js"; +import { config } from "./config"; +import { isOnline, updateOnlineStatus } from "./storage"; +import type { WebSocketEvent } from "./types"; -let socket = null; -let reconnectTimer = null; -let currentOnMessageCallback = null; +let socket: WebSocket | null = null; +let reconnectTimer: ReturnType | null = null; +let currentOnMessageCallback: ((data: WebSocketEvent) => void) | null = null; export const websocket = { - connect(onMessageCallback) { - if (!isOnline) return; + connect(onMessageCallback: (data: WebSocketEvent) => void) { + if (!isOnline()) return; currentOnMessageCallback = onMessageCallback; this.disconnect(false); @@ -48,7 +49,7 @@ export const websocket = { socket.onmessage = (event) => { try { - const data = JSON.parse(event.data); + const data = JSON.parse(event.data) as WebSocketEvent; if (currentOnMessageCallback) { currentOnMessageCallback(data); } @@ -64,10 +65,13 @@ export const websocket = { socket.onclose = (event) => { console.log(`[WS] Connection closed (code: ${event.code}). Reconnecting in 5 seconds...`); socket = null; - + reconnectTimer = setTimeout(() => { - updateOnlineStatus(); - this.connect(currentOnMessageCallback); + updateOnlineStatus().then(() => { + if (currentOnMessageCallback) { + this.connect(currentOnMessageCallback); + } + }); }, 5000); }; } catch (e) { diff --git a/style.css b/web/style.css similarity index 99% rename from style.css rename to web/style.css index e27d445..7122cda 100644 --- a/style.css +++ b/web/style.css @@ -233,11 +233,11 @@ input:focus { .icon-btn:hover { background: rgba(255, 255, 255, 0.08); - color: var(--bg-main); + color: var(--text-primary); border-color: var(--border-hover); } -.icon-btn:hover::before { +#chat-bottom-bar .icon-btn:hover::before { color: var(--bg-main); }