mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-10 02:43:23 +00:00
Add file sharing and QR code features
- Add file sharing with chunked transfer over data channel - Display file messages with download button - Add QR code generation for connection sharing - Add QR scanner for easy connection joining - Update UI with file button and scan option - Add responsive CSS styling for new features
This commit is contained in:
338
package-lock.json
generated
338
package-lock.json
generated
@@ -9,6 +9,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtr-dev/rondevu-client": "^0.0.4",
|
"@xtr-dev/rondevu-client": "^0.0.4",
|
||||||
|
"@zxing/library": "^0.21.3",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
@@ -1173,6 +1175,52 @@
|
|||||||
"integrity": "sha512-hjvCvjUatIxKsEzykDjCjdax9e2/CXFW39EqVmSOdJI4BOySAta6dIjiACL2aPkM/WZI+lmJpY0+qnGcJz3c9g==",
|
"integrity": "sha512-hjvCvjUatIxKsEzykDjCjdax9e2/CXFW39EqVmSOdJI4BOySAta6dIjiACL2aPkM/WZI+lmJpY0+qnGcJz3c9g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@zxing/library": {
|
||||||
|
"version": "0.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
|
||||||
|
"integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ts-custom-error": "^3.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.4.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@zxing/text-encoding": "~0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zxing/text-encoding": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
|
||||||
|
"license": "(Unlicense OR Apache-2.0)",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.23",
|
"version": "2.8.23",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz",
|
||||||
@@ -1217,6 +1265,15 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001753",
|
"version": "1.0.30001753",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
|
||||||
@@ -1238,6 +1295,35 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -1270,6 +1356,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.244",
|
"version": "1.5.244",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
|
||||||
@@ -1277,6 +1378,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
@@ -1326,6 +1433,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1351,6 +1471,24 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -1383,6 +1521,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -1438,6 +1588,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1445,6 +1640,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -1474,6 +1678,23 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
@@ -1509,6 +1730,21 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.52.5",
|
"version": "4.52.5",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
||||||
@@ -1570,6 +1806,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1580,6 +1822,41 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-custom-error": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||||
@@ -1671,12 +1948,73 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtr-dev/rondevu-client": "^0.0.4",
|
"@xtr-dev/rondevu-client": "^0.0.4",
|
||||||
|
"@zxing/library": "^0.21.3",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
330
src/App.jsx
330
src/App.jsx
@@ -1,5 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Rondevu, RondevuClient } from '@xtr-dev/rondevu-client';
|
import { Rondevu, RondevuClient } from '@xtr-dev/rondevu-client';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
import { BrowserQRCodeReader } from '@zxing/library';
|
||||||
|
|
||||||
const rdv = new Rondevu({
|
const rdv = new Rondevu({
|
||||||
baseUrl: 'https://rondevu.xtrdev.workers.dev',
|
baseUrl: 'https://rondevu.xtrdev.workers.dev',
|
||||||
@@ -23,8 +25,9 @@ const client = new RondevuClient({
|
|||||||
function App() {
|
function App() {
|
||||||
// Step-based state
|
// Step-based state
|
||||||
const [step, setStep] = useState(1); // 1: action, 2: method, 3: details, 4: connected
|
const [step, setStep] = useState(1); // 1: action, 2: method, 3: details, 4: connected
|
||||||
const [action, setAction] = useState(null); // 'create' or 'join'
|
const [action, setAction] = useState(null); // 'create', 'join', or 'scan'
|
||||||
const [method, setMethod] = useState(null); // 'topic', 'peer-id', 'connection-id'
|
const [method, setMethod] = useState(null); // 'topic', 'peer-id', 'connection-id'
|
||||||
|
const [qrCodeUrl, setQrCodeUrl] = useState('');
|
||||||
|
|
||||||
// Connection state
|
// Connection state
|
||||||
const [topic, setTopic] = useState('');
|
const [topic, setTopic] = useState('');
|
||||||
@@ -40,9 +43,14 @@ function App() {
|
|||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [messageInput, setMessageInput] = useState('');
|
const [messageInput, setMessageInput] = useState('');
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [channelReady, setChannelReady] = useState(false);
|
||||||
|
|
||||||
const connectionRef = useRef(null);
|
const connectionRef = useRef(null);
|
||||||
const dataChannelRef = useRef(null);
|
const dataChannelRef = useRef(null);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
const fileTransfersRef = useRef(new Map()); // Track ongoing file transfers
|
||||||
|
const videoRef = useRef(null);
|
||||||
|
const scannerRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
log('Demo initialized', 'info');
|
log('Demo initialized', 'info');
|
||||||
@@ -107,13 +115,25 @@ function App() {
|
|||||||
const setupDataChannel = (channel) => {
|
const setupDataChannel = (channel) => {
|
||||||
dataChannelRef.current = channel;
|
dataChannelRef.current = channel;
|
||||||
|
|
||||||
channel.onmessage = (event) => {
|
channel.onopen = () => {
|
||||||
setMessages(prev => [...prev, {
|
log('Data channel ready', 'success');
|
||||||
text: event.data,
|
setChannelReady(true);
|
||||||
type: 'received',
|
|
||||||
timestamp: new Date()
|
|
||||||
}]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
channel.onclose = () => {
|
||||||
|
log('Data channel closed', 'info');
|
||||||
|
setChannelReady(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.onmessage = (event) => {
|
||||||
|
handleReceivedMessage(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If channel is already open (for channels we create)
|
||||||
|
if (channel.readyState === 'open') {
|
||||||
|
log('Data channel ready', 'success');
|
||||||
|
setChannelReady(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConnect = async () => {
|
const handleConnect = async () => {
|
||||||
@@ -152,30 +172,259 @@ function App() {
|
|||||||
|
|
||||||
setConnectedPeer(connection.remotePeerId || 'Waiting...');
|
setConnectedPeer(connection.remotePeerId || 'Waiting...');
|
||||||
setupConnection(connection);
|
setupConnection(connection);
|
||||||
|
|
||||||
|
// Generate QR code if creating a connection
|
||||||
|
if (action === 'create' && currentConnectionId) {
|
||||||
|
try {
|
||||||
|
const qrUrl = await QRCode.toDataURL(currentConnectionId, {
|
||||||
|
width: 256,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#667eea',
|
||||||
|
light: '#ffffff'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setQrCodeUrl(qrUrl);
|
||||||
|
} catch (err) {
|
||||||
|
log(`QR code generation error: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Error: ${error.message}`, 'error');
|
log(`Error: ${error.message}`, 'error');
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('disconnected');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessage = () => {
|
const startScanning = async () => {
|
||||||
if (!messageInput || !dataChannelRef.current || dataChannelRef.current.readyState !== 'open') {
|
try {
|
||||||
|
scannerRef.current = new BrowserQRCodeReader();
|
||||||
|
log('Starting QR scanner...', 'info');
|
||||||
|
|
||||||
|
const videoInputDevices = await scannerRef.current.listVideoInputDevices();
|
||||||
|
|
||||||
|
if (videoInputDevices.length === 0) {
|
||||||
|
log('No camera found', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dataChannelRef.current.send(messageInput);
|
const selectedDeviceId = videoInputDevices[0].deviceId;
|
||||||
|
|
||||||
|
scannerRef.current.decodeFromVideoDevice(
|
||||||
|
selectedDeviceId,
|
||||||
|
videoRef.current,
|
||||||
|
(result, err) => {
|
||||||
|
if (result) {
|
||||||
|
const scannedId = result.getText();
|
||||||
|
log(`Scanned: ${scannedId}`, 'success');
|
||||||
|
setConnectionId(scannedId);
|
||||||
|
stopScanning();
|
||||||
|
setMethod('connection-id');
|
||||||
|
setStep(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Scanner error: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopScanning = () => {
|
||||||
|
if (scannerRef.current) {
|
||||||
|
scannerRef.current.reset();
|
||||||
|
log('Scanner stopped', 'info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (action === 'scan') {
|
||||||
|
startScanning();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
stopScanning();
|
||||||
|
};
|
||||||
|
}, [action]);
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (!messageInput || !channelReady || !dataChannelRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = { type: 'text', content: messageInput };
|
||||||
|
dataChannelRef.current.send(JSON.stringify(message));
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
text: messageInput,
|
text: messageInput,
|
||||||
|
messageType: 'text',
|
||||||
type: 'sent',
|
type: 'sent',
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
}]);
|
}]);
|
||||||
setMessageInput('');
|
setMessageInput('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file || !channelReady || !dataChannelRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHUNK_SIZE = 16384; // 16KB chunks
|
||||||
|
const fileId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
log(`Sending file: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`, 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send file metadata
|
||||||
|
const metadata = {
|
||||||
|
type: 'file-start',
|
||||||
|
fileId,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mimeType: file.type,
|
||||||
|
chunks: Math.ceil(file.size / CHUNK_SIZE)
|
||||||
|
};
|
||||||
|
dataChannelRef.current.send(JSON.stringify(metadata));
|
||||||
|
|
||||||
|
// Read and send file in chunks
|
||||||
|
const reader = new FileReader();
|
||||||
|
let offset = 0;
|
||||||
|
let chunkIndex = 0;
|
||||||
|
|
||||||
|
const readChunk = () => {
|
||||||
|
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
||||||
|
reader.readAsArrayBuffer(slice);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const chunk = {
|
||||||
|
type: 'file-chunk',
|
||||||
|
fileId,
|
||||||
|
index: chunkIndex,
|
||||||
|
data: Array.from(new Uint8Array(e.target.result))
|
||||||
|
};
|
||||||
|
dataChannelRef.current.send(JSON.stringify(chunk));
|
||||||
|
|
||||||
|
offset += CHUNK_SIZE;
|
||||||
|
chunkIndex++;
|
||||||
|
|
||||||
|
if (offset < file.size) {
|
||||||
|
readChunk();
|
||||||
|
} else {
|
||||||
|
// Send completion message
|
||||||
|
const complete = { type: 'file-complete', fileId };
|
||||||
|
dataChannelRef.current.send(JSON.stringify(complete));
|
||||||
|
|
||||||
|
// Add to local messages
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
messageType: 'file',
|
||||||
|
file: {
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mimeType: file.type,
|
||||||
|
data: file
|
||||||
|
},
|
||||||
|
type: 'sent',
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
|
||||||
|
log(`File sent: ${file.name}`, 'success');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
log(`Error reading file: ${file.name}`, 'error');
|
||||||
|
};
|
||||||
|
|
||||||
|
readChunk();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error sending file: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReceivedMessage = (data) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data);
|
||||||
|
|
||||||
|
if (message.type === 'text') {
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
text: message.content,
|
||||||
|
messageType: 'text',
|
||||||
|
type: 'received',
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
} else if (message.type === 'file-start') {
|
||||||
|
fileTransfersRef.current.set(message.fileId, {
|
||||||
|
name: message.name,
|
||||||
|
size: message.size,
|
||||||
|
mimeType: message.mimeType,
|
||||||
|
chunks: new Array(message.chunks),
|
||||||
|
receivedChunks: 0
|
||||||
|
});
|
||||||
|
log(`Receiving file: ${message.name}`, 'info');
|
||||||
|
} else if (message.type === 'file-chunk') {
|
||||||
|
const transfer = fileTransfersRef.current.get(message.fileId);
|
||||||
|
if (transfer) {
|
||||||
|
transfer.chunks[message.index] = new Uint8Array(message.data);
|
||||||
|
transfer.receivedChunks++;
|
||||||
|
}
|
||||||
|
} else if (message.type === 'file-complete') {
|
||||||
|
const transfer = fileTransfersRef.current.get(message.fileId);
|
||||||
|
if (transfer) {
|
||||||
|
// Combine all chunks
|
||||||
|
const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||||
|
const combined = new Uint8Array(totalSize);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of transfer.chunks) {
|
||||||
|
combined.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([combined], { type: transfer.mimeType });
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
messageType: 'file',
|
||||||
|
file: {
|
||||||
|
name: transfer.name,
|
||||||
|
size: transfer.size,
|
||||||
|
mimeType: transfer.mimeType,
|
||||||
|
data: blob
|
||||||
|
},
|
||||||
|
type: 'received',
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
|
||||||
|
log(`File received: ${transfer.name}`, 'success');
|
||||||
|
fileTransfersRef.current.delete(message.fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Assume it's a plain text message (backward compatibility)
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
text: data,
|
||||||
|
messageType: 'text',
|
||||||
|
type: 'received',
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = (file) => {
|
||||||
|
const url = URL.createObjectURL(file.data);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
if (connectionRef.current) {
|
if (connectionRef.current) {
|
||||||
connectionRef.current.close();
|
connectionRef.current.close();
|
||||||
}
|
}
|
||||||
|
stopScanning();
|
||||||
setStep(1);
|
setStep(1);
|
||||||
setAction(null);
|
setAction(null);
|
||||||
setMethod(null);
|
setMethod(null);
|
||||||
@@ -187,6 +436,8 @@ function App() {
|
|||||||
setConnectedPeer(null);
|
setConnectedPeer(null);
|
||||||
setCurrentConnectionId(null);
|
setCurrentConnectionId(null);
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
|
setChannelReady(false);
|
||||||
|
setQrCodeUrl('');
|
||||||
connectionRef.current = null;
|
connectionRef.current = null;
|
||||||
dataChannelRef.current = null;
|
dataChannelRef.current = null;
|
||||||
};
|
};
|
||||||
@@ -224,7 +475,7 @@ function App() {
|
|||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="step-container">
|
<div className="step-container">
|
||||||
<h2>Choose Action</h2>
|
<h2>Choose Action</h2>
|
||||||
<div className="button-grid">
|
<div className="button-grid button-grid-three">
|
||||||
<button
|
<button
|
||||||
className="action-button"
|
className="action-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -245,7 +496,22 @@ function App() {
|
|||||||
<div className="button-title">Join</div>
|
<div className="button-title">Join</div>
|
||||||
<div className="button-description">Connect to existing peers</div>
|
<div className="button-description">Connect to existing peers</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
onClick={() => {
|
||||||
|
setAction('scan');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="button-title">Scan QR</div>
|
||||||
|
<div className="button-description">Scan a connection code</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{action === 'scan' && (
|
||||||
|
<div className="scanner-container">
|
||||||
|
<video ref={videoRef} className="scanner-video" />
|
||||||
|
<button className="back-button" onClick={() => setAction(null)}>← Cancel</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -398,13 +664,37 @@ function App() {
|
|||||||
<button className="disconnect-button" onClick={reset}>Disconnect</button>
|
<button className="disconnect-button" onClick={reset}>Disconnect</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{qrCodeUrl && connectionStatus === 'connecting' && (
|
||||||
|
<div className="qr-code-container">
|
||||||
|
<p className="qr-label">Scan to connect:</p>
|
||||||
|
<img src={qrCodeUrl} alt="Connection QR Code" className="qr-code" />
|
||||||
|
<p className="connection-id-display">{currentConnectionId}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="messages">
|
<div className="messages">
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<p className="empty">No messages yet. Start chatting!</p>
|
<p className="empty">No messages yet. Start chatting!</p>
|
||||||
) : (
|
) : (
|
||||||
messages.map((msg, idx) => (
|
messages.map((msg, idx) => (
|
||||||
<div key={idx} className={`message ${msg.type}`}>
|
<div key={idx} className={`message ${msg.type}`}>
|
||||||
|
{msg.messageType === 'text' ? (
|
||||||
<div className="message-text">{msg.text}</div>
|
<div className="message-text">{msg.text}</div>
|
||||||
|
) : (
|
||||||
|
<div className="message-file">
|
||||||
|
<div className="file-icon">📎</div>
|
||||||
|
<div className="file-info">
|
||||||
|
<div className="file-name">{msg.file.name}</div>
|
||||||
|
<div className="file-size">{(msg.file.size / 1024).toFixed(2)} KB</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="file-download"
|
||||||
|
onClick={() => downloadFile(msg.file)}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="message-time">{msg.timestamp.toLocaleTimeString()}</div>
|
<div className="message-time">{msg.timestamp.toLocaleTimeString()}</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -412,17 +702,31 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="message-input">
|
<div className="message-input">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="file-button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={!channelReady}
|
||||||
|
title="Send file"
|
||||||
|
>
|
||||||
|
📎
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={messageInput}
|
value={messageInput}
|
||||||
onChange={(e) => setMessageInput(e.target.value)}
|
onChange={(e) => setMessageInput(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
disabled={!dataChannelRef.current || dataChannelRef.current.readyState !== 'open'}
|
disabled={!channelReady}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={sendMessage}
|
onClick={sendMessage}
|
||||||
disabled={!dataChannelRef.current || dataChannelRef.current.readyState !== 'open'}
|
disabled={!channelReady}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
149
src/index.css
149
src/index.css
@@ -103,6 +103,10 @@ body {
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-grid-three {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
background: white;
|
background: white;
|
||||||
border: 3px solid #e0e0e0;
|
border: 3px solid #e0e0e0;
|
||||||
@@ -392,7 +396,7 @@ input[type="text"]:disabled {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-input input {
|
.message-input input[type="text"] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -409,6 +413,11 @@ input[type="text"]:disabled {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-input .file-button {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.message-input button:hover:not(:disabled) {
|
.message-input button:hover:not(:disabled) {
|
||||||
background: #5568d3;
|
background: #5568d3;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@@ -421,6 +430,90 @@ input[type="text"]:disabled {
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: inherit;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .message-file {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.received .message-file {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .file-name {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.received .file-name {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .file-size {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.received .file-size {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-download {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: #667eea;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .file-download {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-download:hover {
|
||||||
|
background: white;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
.logs {
|
.logs {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
border-top: 2px solid #f0f0f0;
|
border-top: 2px solid #f0f0f0;
|
||||||
@@ -478,6 +571,57 @@ input[type="text"]:disabled {
|
|||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scanner-container {
|
||||||
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-id-display {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #333;
|
||||||
|
background: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px 20px 30px;
|
padding: 40px 20px 30px;
|
||||||
@@ -517,7 +661,8 @@ input[type="text"]:disabled {
|
|||||||
padding: 32px 24px;
|
padding: 32px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-grid {
|
.button-grid,
|
||||||
|
.button-grid-three {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user