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:
2025-11-07 22:12:12 +01:00
parent 6446f21924
commit 5219b79bb5
4 changed files with 804 additions and 15 deletions

338
package-lock.json generated
View File

@@ -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"
}
} }
} }
} }

View File

@@ -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"
}, },

View File

@@ -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>

View File

@@ -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;
} }