diff --git a/package-lock.json b/package-lock.json index 236e9e9..bdcdb22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,13 +54,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -69,9 +69,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -79,21 +79,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -110,14 +110,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -127,14 +127,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -144,9 +144,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -154,29 +154,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -196,9 +196,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -206,9 +206,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -216,9 +216,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -226,27 +226,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -297,33 +297,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -331,14 +331,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2570,13 +2570,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.25", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", - "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { @@ -2604,9 +2607,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, "funding": [ { @@ -2624,11 +2627,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -2661,9 +2664,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001754", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", - "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -2875,9 +2878,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.250", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz", - "integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==", + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", "dev": true, "license": "ISC" }, @@ -3338,16 +3341,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3537,9 +3540,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3742,10 +3745,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4292,11 +4305,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/object-assign": { "version": "4.1.1", @@ -5137,9 +5153,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -5206,9 +5222,9 @@ } }, "node_modules/vite": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "license": "MIT", "dependencies": { "esbuild": "^0.27.0", diff --git a/src/pages/viewer/components/BookViewer.tsx b/src/pages/viewer/components/BookViewer.tsx index 671c2fd..097ba4f 100644 --- a/src/pages/viewer/components/BookViewer.tsx +++ b/src/pages/viewer/components/BookViewer.tsx @@ -1,16 +1,16 @@ -import { type FC, useRef, useCallback, useState, useEffect, useMemo, useSyncExternalStore } from 'react'; -import HTMLFlipBook from 'react-pageflip'; -import { type PageData } from '../types'; -import BookPage from './BookPage'; -import { ArrowLeft2, ArrowRight2, Maximize4, Pause, Play, RowVertical } from 'iconsax-react'; -import { getPaperDimensions } from '@/config/paperSizes'; -import type { DocumentSettings } from '@/pages/editor/store/editorStore'; -import { useBookEntranceController } from '@/pages/viewer/hooks/useBookEntranceController'; -import { useViewerViewport } from '@/pages/viewer/hooks/useViewerViewport'; -import { useIsBrowserFullscreen } from '@/pages/viewer/hooks/useIsBrowserFullscreen'; -import { toggleViewerFullscreen } from '@/pages/viewer/utils/viewerFullscreen'; -import { isLegacyIOSSafari } from '@/pages/viewer/utils/isLegacyIOSSafari'; -import { clx } from '@/helpers/utils'; +import { getPaperDimensions } from "@/config/paperSizes"; +import { clx } from "@/helpers/utils"; +import type { DocumentSettings } from "@/pages/editor/store/editorStore"; +import { useBookEntranceController } from "@/pages/viewer/hooks/useBookEntranceController"; +import { useIsBrowserFullscreen } from "@/pages/viewer/hooks/useIsBrowserFullscreen"; +import { useViewerViewport } from "@/pages/viewer/hooks/useViewerViewport"; +import { isLegacyIOSSafari } from "@/pages/viewer/utils/isLegacyIOSSafari"; +import { toggleViewerFullscreen } from "@/pages/viewer/utils/viewerFullscreen"; +import { ArrowLeft2, ArrowRight2, Maximize4, Pause, Play, RowVertical } from "iconsax-react"; +import { type FC, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; +import HTMLFlipBook from "react-pageflip"; +import { type PageData } from "../types"; +import BookPage from "./BookPage"; /** فاصله پیش‌فرض بین هر ورق در پخش خودکار (میلی‌ثانیه) */ const AUTO_PLAY_INTERVAL_MS = 3000; @@ -22,49 +22,49 @@ const BOOK_TOP_GAP_MOBILE = 32; /** نسخهٔ فعلی `page-flip.mp3` دو ضربهٔ ورق پشت‌هم دارد؛ فقط تا این سهم از طول پخش می‌شود */ const PAGE_FLIP_SOUND_PLAY_FRACTION = 0.5; /** هم‌راستا با `md` تیلویند (768px) — موبایل: زیر این عرض */ -const MOBILE_QUERY = '(min-width: 768px)'; +const MOBILE_QUERY = "(min-width: 768px)"; function subscribeDesktopMql(callback: () => void) { - const mq = window.matchMedia(MOBILE_QUERY); - mq.addEventListener('change', callback); - return () => mq.removeEventListener('change', callback); + const mq = window.matchMedia(MOBILE_QUERY); + mq.addEventListener("change", callback); + return () => mq.removeEventListener("change", callback); } function useIsDesktop() { - return useSyncExternalStore( - subscribeDesktopMql, - () => window.matchMedia(MOBILE_QUERY).matches, - () => true - ); + return useSyncExternalStore( + subscribeDesktopMql, + () => window.matchMedia(MOBILE_QUERY).matches, + () => true, + ); } function useLegacyIOSSafari() { - return useSyncExternalStore( - () => () => {}, - isLegacyIOSSafari, - () => false - ); + return useSyncExternalStore( + () => () => {}, + isLegacyIOSSafari, + () => false, + ); } export type BookViewerProps = { - pages: PageData[]; - catalogSize?: string; - documentSettings?: Partial; + pages: PageData[]; + catalogSize?: string; + documentSettings?: Partial; }; interface PageFlipAPI { - flipNext: () => void; - flipPrev: () => void; - turnToPage: (page: number) => void; - getCurrentPageIndex?: () => number; + flipNext: () => void; + flipPrev: () => void; + turnToPage: (page: number) => void; + getCurrentPageIndex?: () => number; } interface FlipBookInstance { - pageFlip: () => PageFlipAPI | undefined; + pageFlip: () => PageFlipAPI | undefined; } interface FlipEvent { - data: number; + data: number; } type PageFlipInitEvent = { data: { page: number; mode: string } }; @@ -74,29 +74,25 @@ type PageFlipStateEvent = { data: string }; type FlipBookRect = { left: number; top: number; width: number; height: number; pageWidth: number }; type PageFlipWithFlipController = PageFlipAPI & { - getBoundsRect: () => FlipBookRect; - getFlipController: () => { flip: (p: { x: number; y: number }) => void }; + getBoundsRect: () => FlipBookRect; + getFlipController: () => { flip: (p: { x: number; y: number }) => void }; }; type AutoPlayToggleButtonProps = { - isActive: boolean; - onToggle: () => void; + isActive: boolean; + onToggle: () => void; }; const AutoPlayToggleButton: FC = ({ isActive, onToggle }) => ( - + ); /** @@ -104,15 +100,15 @@ const AutoPlayToggleButton: FC = ({ isActive, onToggl * (چون bookPos.x = 10 - rect.left معمولاً ≤ 0). flipNext همان rect.left را جمع می‌کند. */ function triggerFlipPrev(api: PageFlipWithFlipController | undefined) { - if (!api?.getBoundsRect || !api.getFlipController) { - api?.flipPrev(); - return; - } - const rect = api.getBoundsRect(); - api.getFlipController().flip({ - x: rect.left + 12, - y: rect.top + 2, - }); + if (!api?.getBoundsRect || !api.getFlipController) { + api?.flipPrev(); + return; + } + const rect = api.getBoundsRect(); + api.getFlipController().flip({ + x: rect.left + 12, + y: rect.top + 2, + }); } /** @@ -121,455 +117,471 @@ function triggerFlipPrev(api: PageFlipWithFlipController | undefined) { * از HTML برای رندر صفحات استفاده می‌شود (نه Canvas) برای سازگاری با Konva در آینده */ const BookViewer: FC = ({ pages, catalogSize, documentSettings }) => { - const bookRef = useRef(null); - const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => { }); - const [currentPage, setCurrentPage] = useState(0); - const [isAutoPlayActive, setIsAutoPlayActive] = useState(false); - const [startPage, setStartPage] = useState(0); - const isDesktop = useIsDesktop(); - const legacyIOS = useLegacyIOSSafari(); - const isBrowserFullscreen = useIsBrowserFullscreen(); + const bookRef = useRef(null); + const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {}); + const [currentPage, setCurrentPage] = useState(0); + const [isAutoPlayActive, setIsAutoPlayActive] = useState(false); + const [startPage, setStartPage] = useState(0); + const isDesktop = useIsDesktop(); + const legacyIOS = useLegacyIOSSafari(); + const isBrowserFullscreen = useIsBrowserFullscreen(); - const autoPlay = documentSettings?.autoPlay ?? false; - const pageFlipSound = documentSettings?.pageFlipSound ?? true; - const leftToRightFlip = documentSettings?.leftToRightFlip ?? false; - const displayStyle = documentSettings?.displayStyle ?? 'double'; - const backgroundType = documentSettings?.backgroundType ?? 'color'; - const backgroundColor = documentSettings?.backgroundColor ?? '#ffffff'; - const backgroundGradient = documentSettings?.backgroundGradient; - const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? ''; + const autoPlay = documentSettings?.autoPlay ?? false; + const pageFlipSound = documentSettings?.pageFlipSound ?? true; + const leftToRightFlip = documentSettings?.leftToRightFlip ?? false; + /** react-pageflip از RTL پشتیبانی نمی‌کند؛ ترتیب صفحات برعکس + شروع از آخر (فقط دسکتاپ) */ + const isRtlBook = !leftToRightFlip && isDesktop; + const displayPages = useMemo(() => (isRtlBook ? [...pages].reverse() : pages), [pages, isRtlBook]); + const toLogicalIndex = useCallback((flipIndex: number) => (isRtlBook ? pages.length - 1 - flipIndex : flipIndex), [isRtlBook, pages.length]); + const toFlipIndex = useCallback((logicalIndex: number) => (isRtlBook ? pages.length - 1 - logicalIndex : logicalIndex), [isRtlBook, pages.length]); + const displayStyle = documentSettings?.displayStyle ?? "double"; + const backgroundType = documentSettings?.backgroundType ?? "color"; + const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff"; + const backgroundGradient = documentSettings?.backgroundGradient; + const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? ""; - useEffect(() => { - setIsAutoPlayActive(autoPlay); - }, [autoPlay]); + useEffect(() => { + setIsAutoPlayActive(autoPlay); + }, [autoPlay]); - // سایز کتاب بر اساس catalogSize (A3/A4/A5) - const { width: bookWidth, height: bookHeight } = useMemo( - () => getPaperDimensions(catalogSize ?? 'a4'), - [catalogSize] - ); - const hasMultiplePages = pages.length > 1; - const viewport = useViewerViewport(); - const usePortrait = !isDesktop || displayStyle === 'single'; + // سایز کتاب بر اساس catalogSize (A3/A4/A5) + const { width: bookWidth, height: bookHeight } = useMemo(() => getPaperDimensions(catalogSize ?? "a4"), [catalogSize]); + const hasMultiplePages = pages.length > 1; + const viewport = useViewerViewport(); + const usePortrait = !isDesktop || displayStyle === "single"; - const viewportFit = useMemo(() => { - const padX = isDesktop ? 48 : 20; - const edgeBleed = isDesktop ? 0 : 6; - const reservedNav = isDesktop ? 96 : 120; - const reservedTop = isDesktop ? BOOK_TOP_GAP_DESKTOP : BOOK_TOP_GAP_MOBILE; - const maxW = Math.max(1, viewport.w - padX - edgeBleed * 2); - const maxH = Math.max(1, viewport.h - reservedNav - reservedTop - edgeBleed); - const spreadCount = usePortrait ? 1 : 2; - const basePageW = bookWidth * VIEWER_SCALE; - const basePageH = bookHeight * VIEWER_SCALE; - const baseSpreadW = basePageW * spreadCount; - const scale = Math.min(maxW / baseSpreadW, maxH / basePageH); - return { - w: Math.max(1, Math.round(basePageW * scale)), - h: Math.max(1, Math.round(basePageH * scale)), - contentScale: VIEWER_SCALE * scale, - }; - }, [isDesktop, bookWidth, bookHeight, viewport.w, viewport.h, usePortrait]); + const viewportFit = useMemo(() => { + const padX = isDesktop ? 48 : 20; + const edgeBleed = isDesktop ? 0 : 6; + const reservedNav = isDesktop ? 96 : 120; + const reservedTop = isDesktop ? BOOK_TOP_GAP_DESKTOP : BOOK_TOP_GAP_MOBILE; + const maxW = Math.max(1, viewport.w - padX - edgeBleed * 2); + const maxH = Math.max(1, viewport.h - reservedNav - reservedTop - edgeBleed); + const spreadCount = usePortrait ? 1 : 2; + const basePageW = bookWidth * VIEWER_SCALE; + const basePageH = bookHeight * VIEWER_SCALE; + const baseSpreadW = basePageW * spreadCount; + const scale = Math.min(maxW / baseSpreadW, maxH / basePageH); + return { + w: Math.max(1, Math.round(basePageW * scale)), + h: Math.max(1, Math.round(basePageH * scale)), + contentScale: VIEWER_SCALE * scale, + }; + }, [isDesktop, bookWidth, bookHeight, viewport.w, viewport.h, usePortrait]); - const pagePixelWidth = viewportFit.w; - const pagePixelHeight = viewportFit.h; - const contentScale = viewportFit.contentScale; + const pagePixelWidth = viewportFit.w; + const pagePixelHeight = viewportFit.h; + const contentScale = viewportFit.contentScale; - const flipKey = `${usePortrait ? 'portrait' : 'spread'}-${pagePixelWidth}-${pagePixelHeight}`; + const flipKey = `${usePortrait ? "portrait" : "spread"}-${pagePixelWidth}-${pagePixelHeight}`; - const { - scheduleEntranceForSpread, - getEntrancePhase, - reset: resetEntrance, - } = useBookEntranceController({ - pages, - portrait: usePortrait, - showCover: hasMultiplePages, + const { + scheduleEntranceForSpread, + getEntrancePhase, + reset: resetEntrance, + } = useBookEntranceController({ + pages, + portrait: usePortrait, + showCover: hasMultiplePages, + }); + + const commitEntranceAtCurrentSpread = useCallback(() => { + const api = bookRef.current?.pageFlip(); + const idx = api?.getCurrentPageIndex?.(); + if (typeof idx !== "number" || pages.length <= 0) return; + const flipIdx = Math.max(0, Math.min(idx, pages.length - 1)); + scheduleEntranceForSpread(toLogicalIndex(flipIdx)); + }, [pages.length, scheduleEntranceForSpread, toLogicalIndex]); + + useEffect(() => { + resetEntrance(); + const frame = requestAnimationFrame(() => { + commitEntranceAtCurrentSpread(); }); + return () => cancelAnimationFrame(frame); + }, [flipKey, pages.length, pages[0]?.id, resetEntrance, commitEntranceAtCurrentSpread]); - const commitEntranceAtCurrentSpread = useCallback(() => { - const api = bookRef.current?.pageFlip(); - const idx = api?.getCurrentPageIndex?.(); - if (typeof idx !== 'number' || pages.length <= 0) return; - const clamped = Math.max(0, Math.min(idx, pages.length - 1)); - scheduleEntranceForSpread(clamped); - }, [pages.length, scheduleEntranceForSpread]); + // اطمینان از اینکه currentPage همیشه در محدوده معتبر است + useEffect(() => { + if (currentPage < 0) { + setCurrentPage(0); + } else if (currentPage >= pages.length) { + setCurrentPage(pages.length - 1); + } + }, [currentPage, pages.length]); - useEffect(() => { - resetEntrance(); - const frame = requestAnimationFrame(() => { - commitEntranceAtCurrentSpread(); - }); - return () => cancelAnimationFrame(frame); - }, [flipKey, pages.length, pages[0]?.id, resetEntrance, commitEntranceAtCurrentSpread]); + const pageFlipSoundRef = useRef(null); + const pageFlipSoundTrimListenerRef = useRef<(() => void) | null>(null); + const lastFlipPageRef = useRef(null); - // اطمینان از اینکه currentPage همیشه در محدوده معتبر است - useEffect(() => { - if (currentPage < 0) { - setCurrentPage(0); - } else if (currentPage >= pages.length) { - setCurrentPage(pages.length - 1); + useEffect(() => { + const base = import.meta.env.BASE_URL; + const src = `${base}${base.endsWith("/") ? "" : "/"}sounds/page-flip.mp3`; + const audio = new Audio(src); + audio.preload = "auto"; + pageFlipSoundRef.current = audio; + return () => { + const trim = pageFlipSoundTrimListenerRef.current; + if (trim) { + audio.removeEventListener("timeupdate", trim); + pageFlipSoundTrimListenerRef.current = null; + } + audio.pause(); + pageFlipSoundRef.current = null; + }; + }, []); + + const playPageFlipSound = useCallback(() => { + const audio = pageFlipSoundRef.current; + if (!audio) return; + const prevTrim = pageFlipSoundTrimListenerRef.current; + if (prevTrim) { + audio.removeEventListener("timeupdate", prevTrim); + pageFlipSoundTrimListenerRef.current = null; + } + const onTimeUpdate = () => { + const d = audio.duration; + if (!Number.isFinite(d) || d <= 0.04) return; + const end = d * PAGE_FLIP_SOUND_PLAY_FRACTION; + if (audio.currentTime >= end) { + audio.pause(); + audio.currentTime = 0; + audio.removeEventListener("timeupdate", onTimeUpdate); + pageFlipSoundTrimListenerRef.current = null; + } + }; + pageFlipSoundTrimListenerRef.current = onTimeUpdate; + try { + audio.currentTime = 0; + audio.addEventListener("timeupdate", onTimeUpdate); + void audio.play().catch(() => { + audio.removeEventListener("timeupdate", onTimeUpdate); + pageFlipSoundTrimListenerRef.current = null; + /* autoplay policy — معمولاً بعد از تعامل کاربر ورق می‌خورد */ + }); + } catch { + audio.removeEventListener("timeupdate", onTimeUpdate); + pageFlipSoundTrimListenerRef.current = null; + } + }, []); + + const handlePageFlip = useCallback( + (e: FlipEvent) => { + // react-pageflip شماره صفحه را به صورت 0-based برمی‌گرداند + // اما باید مطمئن شویم که در محدوده معتبر است + let flipIndex = e.data; + + // اگر flipIndex منفی است یا بزرگتر از تعداد صفحات، آن را محدود می‌کنیم + if (flipIndex < 0) { + flipIndex = 0; + } else if (flipIndex >= pages.length) { + flipIndex = pages.length - 1; + } + + const pageNum = toLogicalIndex(flipIndex); + + if (pageFlipSound && lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) { + playPageFlipSound(); + } + lastFlipPageRef.current = pageNum; + + setCurrentPage(pageNum); + }, + [pages.length, pageFlipSound, playPageFlipSound, toLogicalIndex], + ); + + pageFlipHandlerRef.current = handlePageFlip; + + const onFlipForwarded = useCallback((e: FlipEvent) => { + pageFlipHandlerRef.current(e); + }, []); + + const syncPageIndexFromBook = useCallback(() => { + const api = bookRef.current?.pageFlip(); + const idx = api?.getCurrentPageIndex?.(); + if (typeof idx !== "number" || pages.length <= 0) return; + const flipIdx = Math.max(0, Math.min(idx, pages.length - 1)); + setCurrentPage(toLogicalIndex(flipIdx)); + }, [pages.length, toLogicalIndex]); + + const handleBookInit = useCallback( + (e: PageFlipInitEvent) => { + const p = e.data?.page; + if (typeof p !== "number" || pages.length <= 0) return; + const flipIdx = Math.max(0, Math.min(p, pages.length - 1)); + const logicalIdx = toLogicalIndex(flipIdx); + setCurrentPage(logicalIdx); + lastFlipPageRef.current = logicalIdx; + // اولین اسپرد قبل از onChangeState('read') هم باید انیمیشن ورود بگیرد + scheduleEntranceForSpread(logicalIdx); + }, + [pages.length, scheduleEntranceForSpread, toLogicalIndex], + ); + + /** بعد از اتمام انیمیشن ورق، ایندکس واقعی کتاب را می‌گیریم و انیمیشن ورود را یک‌بار شروع می‌کنیم */ + const handleChangeState = useCallback( + (e: PageFlipStateEvent) => { + if (e.data !== "read") return; + syncPageIndexFromBook(); + commitEntranceAtCurrentSpread(); + }, + [syncPageIndexFromBook, commitEntranceAtCurrentSpread], + ); + + const goToNextPage = useCallback(() => { + if (currentPage >= pages.length - 1) return; + const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined; + if (isRtlBook) { + triggerFlipPrev(api); + } else { + api?.flipNext(); + } + }, [currentPage, pages.length, isRtlBook]); + + const goToPrevPage = useCallback(() => { + if (currentPage <= 0) return; + const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined; + if (isRtlBook) { + api?.flipNext(); + } else { + triggerFlipPrev(api); + } + }, [currentPage, isRtlBook]); + + const stopAutoPlayByUser = useCallback(() => { + if (isAutoPlayActive) { + setIsAutoPlayActive(false); + } + }, [isAutoPlayActive]); + + const toggleAutoPlay = useCallback(() => { + if (pages.length <= 1) return; + setIsAutoPlayActive((prev) => !prev); + }, [pages.length]); + + const navigateToPageIndex = useCallback( + (targetIndex: number) => { + const pageFlipAPI = bookRef.current?.pageFlip(); + if (!pageFlipAPI) return; + + const logicalIdx = Math.max(0, Math.min(targetIndex, pages.length - 1)); + const flipIdx = toFlipIndex(logicalIdx); + pageFlipAPI.turnToPage(flipIdx); + lastFlipPageRef.current = logicalIdx; + scheduleEntranceForSpread(logicalIdx); + }, + [pages.length, scheduleEntranceForSpread, toFlipIndex], + ); + + const handleLinkClick = useCallback( + (linkUrl: string) => { + if (pages.length <= 1) return; + stopAutoPlayByUser(); + + if (!linkUrl.startsWith("page://")) return; + + const action = linkUrl.replace("page://", ""); + + if (action === "next") { + goToNextPage(); + } else if (action === "prev") { + goToPrevPage(); + } else if (action === "first") { + navigateToPageIndex(0); + } else if (action === "last") { + navigateToPageIndex(pages.length - 1); + } else { + const pageNum = parseInt(action, 10); + if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) { + navigateToPageIndex(pageNum - 1); } - }, [currentPage, pages.length]); + } + }, + [pages.length, goToNextPage, goToPrevPage, navigateToPageIndex, stopAutoPlayByUser], + ); - const pageFlipSoundRef = useRef(null); - const pageFlipSoundTrimListenerRef = useRef<(() => void) | null>(null); - const lastFlipPageRef = useRef(null); + const prevFlipKeyRef = useRef(flipKey); + useEffect(() => { + if (prevFlipKeyRef.current !== flipKey) { + prevFlipKeyRef.current = flipKey; + lastFlipPageRef.current = currentPage; + setStartPage(toFlipIndex(currentPage)); + } + }, [flipKey, currentPage, toFlipIndex]); - useEffect(() => { - const base = import.meta.env.BASE_URL; - const src = `${base}${base.endsWith('/') ? '' : '/'}sounds/page-flip.mp3`; - const audio = new Audio(src); - audio.preload = 'auto'; - pageFlipSoundRef.current = audio; - return () => { - const trim = pageFlipSoundTrimListenerRef.current; - if (trim) { - audio.removeEventListener('timeupdate', trim); - pageFlipSoundTrimListenerRef.current = null; - } - audio.pause(); - pageFlipSoundRef.current = null; - }; - }, []); + useEffect(() => { + if (pages.length <= 0) return; + const initialFlipPage = isRtlBook ? pages.length - 1 : 0; + setStartPage(initialFlipPage); + setCurrentPage(0); + lastFlipPageRef.current = 0; + }, [pages.length, pages[0]?.id, isRtlBook]); - const playPageFlipSound = useCallback(() => { - const audio = pageFlipSoundRef.current; - if (!audio) return; - const prevTrim = pageFlipSoundTrimListenerRef.current; - if (prevTrim) { - audio.removeEventListener('timeupdate', prevTrim); - pageFlipSoundTrimListenerRef.current = null; - } - const onTimeUpdate = () => { - const d = audio.duration; - if (!Number.isFinite(d) || d <= 0.04) return; - const end = d * PAGE_FLIP_SOUND_PLAY_FRACTION; - if (audio.currentTime >= end) { - audio.pause(); - audio.currentTime = 0; - audio.removeEventListener('timeupdate', onTimeUpdate); - pageFlipSoundTrimListenerRef.current = null; - } - }; - pageFlipSoundTrimListenerRef.current = onTimeUpdate; - try { - audio.currentTime = 0; - audio.addEventListener('timeupdate', onTimeUpdate); - void audio.play().catch(() => { - audio.removeEventListener('timeupdate', onTimeUpdate); - pageFlipSoundTrimListenerRef.current = null; - /* autoplay policy — معمولاً بعد از تعامل کاربر ورق می‌خورد */ - }); - } catch { - audio.removeEventListener('timeupdate', onTimeUpdate); - pageFlipSoundTrimListenerRef.current = null; - } - }, []); - - const handlePageFlip = useCallback((e: FlipEvent) => { - // react-pageflip شماره صفحه را به صورت 0-based برمی‌گرداند - // اما باید مطمئن شویم که در محدوده معتبر است - let pageNum = e.data; - - // اگر pageNum منفی است یا بزرگتر از تعداد صفحات، آن را محدود می‌کنیم - if (pageNum < 0) { - pageNum = 0; - } else if (pageNum >= pages.length) { - pageNum = pages.length - 1; - } - - if (pageFlipSound && lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) { - playPageFlipSound(); - } - lastFlipPageRef.current = pageNum; - - setCurrentPage(pageNum); - }, [pages.length, pageFlipSound, playPageFlipSound]); - - pageFlipHandlerRef.current = handlePageFlip; - - const onFlipForwarded = useCallback((e: FlipEvent) => { - pageFlipHandlerRef.current(e); - }, []); - - const syncPageIndexFromBook = useCallback(() => { - const api = bookRef.current?.pageFlip(); - const idx = api?.getCurrentPageIndex?.(); - if (typeof idx !== 'number' || pages.length <= 0) return; - const clamped = Math.max(0, Math.min(idx, pages.length - 1)); - setCurrentPage(clamped); - }, [pages.length]); - - const handleBookInit = useCallback( - (e: PageFlipInitEvent) => { - const p = e.data?.page; - if (typeof p !== 'number' || pages.length <= 0) return; - const clamped = Math.max(0, Math.min(p, pages.length - 1)); - setCurrentPage(clamped); - lastFlipPageRef.current = clamped; - // اولین اسپرد قبل از onChangeState('read') هم باید انیمیشن ورود بگیرد - scheduleEntranceForSpread(clamped); - }, - [pages.length, scheduleEntranceForSpread], - ); - - /** بعد از اتمام انیمیشن ورق، ایندکس واقعی کتاب را می‌گیریم و انیمیشن ورود را یک‌بار شروع می‌کنیم */ - const handleChangeState = useCallback( - (e: PageFlipStateEvent) => { - if (e.data !== 'read') return; - syncPageIndexFromBook(); - commitEntranceAtCurrentSpread(); - }, - [syncPageIndexFromBook, commitEntranceAtCurrentSpread], - ); - - const goToNextPage = useCallback(() => { - if (currentPage < pages.length - 1) { - bookRef.current?.pageFlip()?.flipNext(); - } - }, [currentPage, pages.length]); - - const goToPrevPage = useCallback(() => { - if (currentPage <= 0) return; - const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined; + // پخش خودکار + useEffect(() => { + if (!isAutoPlayActive || pages.length <= 1) return; + const timer = setInterval(() => { + if (currentPage >= pages.length - 1) { + const firstFlipPage = isRtlBook ? pages.length - 1 : 0; + bookRef.current?.pageFlip()?.turnToPage?.(firstFlipPage); + setCurrentPage(0); + lastFlipPageRef.current = 0; + return; + } + const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined; + if (isRtlBook) { triggerFlipPrev(api); - }, [currentPage]); + } else { + api?.flipNext(); + } + }, AUTO_PLAY_INTERVAL_MS); + return () => clearInterval(timer); + }, [isAutoPlayActive, currentPage, pages.length, isRtlBook]); - const stopAutoPlayByUser = useCallback(() => { - if (isAutoPlayActive) { - setIsAutoPlayActive(false); - } - }, [isAutoPlayActive]); - - const toggleAutoPlay = useCallback(() => { - if (pages.length <= 1) return; - setIsAutoPlayActive(prev => !prev); - }, [pages.length]); - - const navigateToPageIndex = useCallback( - (targetIndex: number) => { - const pageFlipAPI = bookRef.current?.pageFlip(); - if (!pageFlipAPI) return; - - const clamped = Math.max(0, Math.min(targetIndex, pages.length - 1)); - pageFlipAPI.turnToPage(clamped); - lastFlipPageRef.current = clamped; - scheduleEntranceForSpread(clamped); - }, - [pages.length, scheduleEntranceForSpread], - ); - - const handleLinkClick = useCallback( - (linkUrl: string) => { - if (pages.length <= 1) return; - stopAutoPlayByUser(); - - if (!linkUrl.startsWith('page://')) return; - - const action = linkUrl.replace('page://', ''); - - if (action === 'next') { - goToNextPage(); - } else if (action === 'prev') { - goToPrevPage(); - } else if (action === 'first') { - navigateToPageIndex(0); - } else if (action === 'last') { - navigateToPageIndex(pages.length - 1); - } else { - const pageNum = parseInt(action, 10); - if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) { - navigateToPageIndex(pageNum - 1); - } - } - }, - [pages.length, goToNextPage, goToPrevPage, navigateToPageIndex, stopAutoPlayByUser], - ); - - const prevFlipKeyRef = useRef(flipKey); - useEffect(() => { - if (prevFlipKeyRef.current !== flipKey) { - prevFlipKeyRef.current = flipKey; - lastFlipPageRef.current = currentPage; - setStartPage(currentPage); - } - }, [flipKey, currentPage]); - - // پخش خودکار - useEffect(() => { - if (!isAutoPlayActive || pages.length <= 1) return; - const timer = setInterval(() => { - if (currentPage >= pages.length - 1) { - bookRef.current?.pageFlip()?.turnToPage?.(0); - setCurrentPage(0); - lastFlipPageRef.current = 0; - return; - } - bookRef.current?.pageFlip()?.flipNext(); - }, AUTO_PLAY_INTERVAL_MS); - return () => clearInterval(timer); - }, [isAutoPlayActive, currentPage, pages.length]); - - return ( -
+ return ( +
+
+
+ {/* Border و Shadow عمودی در وسط (فقط حالت دوصفحه‌ای دسکتاپ) */} + {!usePortrait && (
-
- {/* Border و Shadow عمودی در وسط (فقط حالت دوصفحه‌ای دسکتاپ) */} - {!usePortrait && ( -
- )} - {/* + className="hidden md:block absolute inset-y-0 left-1/2 -translate-x-1/2 w-px border-r border-gray-300 pointer-events-none z-50" + style={{ + boxShadow: "-5px 0 15px rgba(0, 0, 0, 0.15), 5px 0 15px rgba(0, 0, 0, 0.15)", + }} + /> + )} + {/* page-flip با مختصات LTR کار می‌کند؛ direction:rtl روی روت کتاب باعث ناهماهنگی ایندکس/onFlip با دکمه‌ها می‌شود. محتوای فارسی داخل BookPage با dir=rtl است. */} -
- - {pages.map((page) => ( - - ))} - -
-
-
- -
- {hasMultiplePages && ( - <> - {/* دکمه راست: فارسی = صفحه بعد | لاتین = صفحه قبل */} - - - )} - - {/* شماره صفحه */} -
- - صفحه {Math.min(currentPage + 1, pages.length)} از {pages.length} - -
- - - - {hasMultiplePages && ( - <> - {/* دکمه چپ: فارسی = صفحه قبل | لاتین = صفحه بعد */} - - - )} - - {hasMultiplePages && autoPlay && ( - - )} - - -
+
+ + {displayPages.map((page) => ( + + ))} + +
- ); +
+ +
+ {hasMultiplePages && ( + <> + {/* دکمه راست: فارسی = صفحه بعد | لاتین = صفحه قبل */} + + + )} + + {/* شماره صفحه */} +
+ + صفحه {Math.min(currentPage + 1, pages.length)} از {pages.length} + +
+ + {hasMultiplePages && ( + <> + {/* دکمه چپ: فارسی = صفحه قبل | لاتین = صفحه بعد */} + + + )} + + {hasMultiplePages && autoPlay && } + + +
+
+ ); }; export default BookViewer;