mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-22 23:20:54 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11a988e550 | |||
| ff8f28660a | |||
| 5fc8e85f75 | |||
| 5eeaae01fe | |||
| 660e523263 | |||
| cdfbc7a2df | |||
| c9dca7e083 | |||
| 79af04d95a | |||
| 954e9c7c3a | |||
| c0d504f6a8 | |||
| 36e152dc7c | |||
| fdf024f378 | |||
| 9f2e4c5f8f | |||
| 8e219914ec | |||
| d1273ef9cc | |||
| 27ac7786d0 | |||
| edf2ab5eb3 | |||
| c3863a9dd3 | |||
| 5934e94761 | |||
| 1bc23dc4b3 | |||
| cc78b34d2e | |||
| f34f5bd4b2 | |||
| b2f6a75c99 | |||
| 82138c06f3 | |||
| ee820ed6e0 | |||
| b7e7fd1b68 | |||
| 96ed0e53b1 | |||
| 77b76a3b8d | |||
| e37f9bd9fb | |||
| 451173caac | |||
| 460e8ec79c | |||
| 002b31a113 | |||
| 1eb53498b1 | |||
| d56ee87437 |
@@ -42,9 +42,9 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
|||||||
* Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/)
|
* Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/)
|
||||||
|
|
||||||
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
|
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
|
||||||
* Files are transferred only after a request is accepted first. On transfer completion they are downloaded automatically if possible.
|
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
||||||
* Multiple files are downloaded as ZIP file
|
* Multiple files are downloaded as a ZIP file
|
||||||
* On iOS and Android the devices share menu is opened instead of downloading the files
|
* On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu.
|
||||||
* Multiple files are transferred at once with an overall progress indicator
|
* Multiple files are transferred at once with an overall progress indicator
|
||||||
|
|
||||||
### Send Files or Text Directly From Share Menu, Context Menu or CLI
|
### Send Files or Text Directly From Share Menu, Context Menu or CLI
|
||||||
@@ -54,7 +54,8 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
|||||||
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||||
|
|
||||||
### Other changes
|
### Other changes
|
||||||
* [Paste Mode](https://github.com/RobinLinus/snapdrop/pull/534)
|
* Change your display name permanently to easily differentiate your devices
|
||||||
|
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||||
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
|
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
|
||||||
* Warn user before PairDrop is closed on file transfer
|
* Warn user before PairDrop is closed on file transfer
|
||||||
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
|
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
|
||||||
@@ -79,6 +80,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
|||||||
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
|
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
|
||||||
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
||||||
|
* [cyrb53](https://github.com/bryc) super fast hash function
|
||||||
|
|
||||||
Have any questions? Read our [FAQ](/docs/faq.md).
|
Have any questions? Read our [FAQ](/docs/faq.md).
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ const crypto = require('crypto')
|
|||||||
const {spawn} = require('child_process')
|
const {spawn} = require('child_process')
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const parser = require('ua-parser-js');
|
||||||
|
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
||||||
|
const express = require('express');
|
||||||
|
const RateLimit = require('express-rate-limit');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
// Handle SIGINT
|
// Handle SIGINT
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
@@ -52,7 +57,7 @@ if (process.argv.includes('--auto-restart')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rtcConfig = process.env.RTC_CONFIG
|
const rtcConfig = process.env.RTC_CONFIG
|
||||||
? fs.readFileSync(process.env.RTC_CONFIG, 'utf8')
|
? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
|
||||||
: {
|
: {
|
||||||
"sdpSemantics": "unified-plan",
|
"sdpSemantics": "unified-plan",
|
||||||
"iceServers": [
|
"iceServers": [
|
||||||
@@ -70,10 +75,6 @@ const rtcConfig = process.env.RTC_CONFIG
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const RateLimit = require('express-rate-limit');
|
|
||||||
const http = require('http');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
if (process.argv.includes('--rate-limit')) {
|
if (process.argv.includes('--rate-limit')) {
|
||||||
@@ -114,9 +115,6 @@ if (process.argv.includes('--localhost-only')) {
|
|||||||
server.listen(port);
|
server.listen(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = require('ua-parser-js');
|
|
||||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
|
||||||
|
|
||||||
class PairDropServer {
|
class PairDropServer {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -145,7 +143,8 @@ class PairDropServer {
|
|||||||
message: {
|
message: {
|
||||||
displayName: peer.name.displayName,
|
displayName: peer.name.displayName,
|
||||||
deviceName: peer.name.deviceName,
|
deviceName: peer.name.deviceName,
|
||||||
peerId: peer.id
|
peerId: peer.id,
|
||||||
|
peerIdHash: peer.id.hashCode128BitSalted()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -369,6 +368,10 @@ class PairDropServer {
|
|||||||
// delete the peer
|
// delete the peer
|
||||||
delete this._rooms[room][peer.id];
|
delete this._rooms[room][peer.id];
|
||||||
|
|
||||||
|
if (roomType === 'ip') {
|
||||||
|
peer.socket.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
//if room is empty, delete the room
|
//if room is empty, delete the room
|
||||||
if (!Object.keys(this._rooms[room]).length) {
|
if (!Object.keys(this._rooms[room]).length) {
|
||||||
delete this._rooms[room];
|
delete this._rooms[room];
|
||||||
@@ -550,9 +553,11 @@ class Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setPeerId(request) {
|
_setPeerId(request) {
|
||||||
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id");
|
const searchParams = new URL(request.url, "http://server").searchParams;
|
||||||
if (peer_id && Peer.isValidUuid(peer_id)) {
|
let peerId = searchParams.get("peer_id");
|
||||||
this.id = peer_id;
|
let peerIdHash = searchParams.get("peer_id_hash");
|
||||||
|
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
|
||||||
|
this.id = peerId;
|
||||||
} else {
|
} else {
|
||||||
this.id = crypto.randomUUID();
|
this.id = crypto.randomUUID();
|
||||||
}
|
}
|
||||||
@@ -611,6 +616,10 @@ class Peer {
|
|||||||
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
|
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPeerIdHashValid(peerId, peerIdHash) {
|
||||||
|
return peerIdHash === peerId.hashCode128BitSalted();
|
||||||
|
}
|
||||||
|
|
||||||
addRoomSecret(roomSecret) {
|
addRoomSecret(roomSecret) {
|
||||||
if (!(roomSecret in this.roomSecrets)) {
|
if (!(roomSecret in this.roomSecrets)) {
|
||||||
this.roomSecrets.push(roomSecret);
|
this.roomSecrets.push(roomSecret);
|
||||||
@@ -626,14 +635,55 @@ class Peer {
|
|||||||
|
|
||||||
Object.defineProperty(String.prototype, 'hashCode', {
|
Object.defineProperty(String.prototype, 'hashCode', {
|
||||||
value: function() {
|
value: function() {
|
||||||
var hash = 0, i, chr;
|
return cyrb53(this);
|
||||||
for (i = 0; i < this.length; i++) {
|
|
||||||
chr = this.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + chr;
|
|
||||||
hash |= 0; // Convert to 32bit integer
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
|
||||||
|
value: function() {
|
||||||
|
return hasher.hashCode128BitSalted(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasher = (() => {
|
||||||
|
let seeds;
|
||||||
|
return {
|
||||||
|
hashCode128BitSalted(str) {
|
||||||
|
if (!seeds) {
|
||||||
|
// seeds are created on first call to salt hash.
|
||||||
|
seeds = [4];
|
||||||
|
for (let i=0; i<4; i++) {
|
||||||
|
const randomBuffer = new Uint32Array(1);
|
||||||
|
crypto.webcrypto.getRandomValues(randomBuffer);
|
||||||
|
seeds[i] = randomBuffer[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hashCode = "";
|
||||||
|
for (let i=0; i<4; i++) {
|
||||||
|
hashCode += cyrb53(str, seeds[i]);
|
||||||
|
}
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})()
|
||||||
|
|
||||||
|
/*
|
||||||
|
cyrb53 (c) 2018 bryc (github.com/bryc)
|
||||||
|
A fast and simple hash function with decent collision resistance.
|
||||||
|
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||||
|
Public domain. Attribution appreciated.
|
||||||
|
*/
|
||||||
|
const cyrb53 = function(str, seed = 0) {
|
||||||
|
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||||
|
for (let i = 0, ch; i < str.length; i++) {
|
||||||
|
ch = str.charCodeAt(i);
|
||||||
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||||
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||||
|
}
|
||||||
|
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
|
||||||
|
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
|
||||||
|
return 4294967296 * (2097151 & h2) + (h1>>>0);
|
||||||
|
};
|
||||||
|
|
||||||
new PairDropServer();
|
new PairDropServer();
|
||||||
|
|||||||
Generated
+9
-9
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.3.0",
|
"version": "1.4.4",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.3.0",
|
"version": "1.4.4",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"ua-parser-js": "^1.0.33",
|
"ua-parser-js": "^1.0.34",
|
||||||
"unique-names-generator": "^4.3.0",
|
"unique-names-generator": "^4.3.0",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
@@ -583,9 +583,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ua-parser-js": {
|
"node_modules/ua-parser-js": {
|
||||||
"version": "1.0.33",
|
"version": "1.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
||||||
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
|
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1070,9 +1070,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ua-parser-js": {
|
"ua-parser-js": {
|
||||||
"version": "1.0.33",
|
"version": "1.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
||||||
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ=="
|
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
|
||||||
},
|
},
|
||||||
"unique-names-generator": {
|
"unique-names-generator": {
|
||||||
"version": "4.7.1",
|
"version": "4.7.1",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.3.0",
|
"version": "1.4.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"ua-parser-js": "^1.0.33",
|
"ua-parser-js": "^1.0.34",
|
||||||
"unique-names-generator": "^4.3.0",
|
"unique-names-generator": "^4.3.0",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,7 +106,11 @@ sendFiles()
|
|||||||
zip -q -b /tmp/ -r "$zipPath" "$path"
|
zip -q -b /tmp/ -r "$zipPath" "$path"
|
||||||
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
|
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
|
||||||
|
|
||||||
|
if [[ $OS == "Mac" ]];then
|
||||||
|
hash=$(base64 -i "$zipPathTemp")
|
||||||
|
else
|
||||||
hash=$(base64 -w 0 "$zipPathTemp")
|
hash=$(base64 -w 0 "$zipPathTemp")
|
||||||
|
fi
|
||||||
|
|
||||||
# remove temporary temp file
|
# remove temporary temp file
|
||||||
rm "$zipPathTemp"
|
rm "$zipPathTemp"
|
||||||
@@ -116,8 +120,12 @@ sendFiles()
|
|||||||
# Create zip file temporarily to send file
|
# Create zip file temporarily to send file
|
||||||
zip -q -b /tmp/ "$zipPath" "$path"
|
zip -q -b /tmp/ "$zipPath" "$path"
|
||||||
|
|
||||||
|
if [[ $OS == "Mac" ]];then
|
||||||
|
hash=$(base64 -i "$zipPath")
|
||||||
|
else
|
||||||
hash=$(base64 -w 0 "$zipPath")
|
hash=$(base64 -w 0 "$zipPath")
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# remove temporary temp file
|
# remove temporary temp file
|
||||||
rm "$zipPath"
|
rm "$zipPath"
|
||||||
|
|||||||
+16
-5
@@ -59,7 +59,7 @@
|
|||||||
<use xlink:href="#homescreen" />
|
<use xlink:href="#homescreen" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a id="pair-device" class="icon-button" title="Pair Device" >
|
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#pair-device-icon" />
|
<use xlink:href="#pair-device-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -89,7 +89,13 @@
|
|||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
<div id="display-name" placeholder=" "></div>
|
<div>
|
||||||
|
<span>You are known as:</span>
|
||||||
|
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||||
|
<svg id="edit-pen" class="icon">
|
||||||
|
<use xlink:href="#edit-pen-icon" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div class="font-body2">
|
<div class="font-body2">
|
||||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||||
@@ -116,7 +122,7 @@
|
|||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<button class="button" type="submit" disabled>Pair</button>
|
||||||
<button class="button" close>Cancel</button>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -131,7 +137,7 @@
|
|||||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit">Unpair Devices</button>
|
<button class="button" type="submit">Unpair Devices</button>
|
||||||
<button class="button" close>Cancel</button>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -203,7 +209,7 @@
|
|||||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<button class="button" title="ESCAPE" close>Cancel</button>
|
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -232,6 +238,7 @@
|
|||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||||
|
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||||
<button class="button center" close>Close</button>
|
<button class="button center" close>Close</button>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -332,6 +339,10 @@
|
|||||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||||
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="scripts/util.js"></script>
|
<script src="scripts/util.js"></script>
|
||||||
|
|||||||
+74
-37
@@ -21,10 +21,10 @@ class ServerConnection {
|
|||||||
Events.on('online', _ => this._connect());
|
Events.on('online', _ => this._connect());
|
||||||
}
|
}
|
||||||
|
|
||||||
async _connect() {
|
_connect() {
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
if (this._isConnected() || this._isConnecting()) return;
|
if (this._isConnected() || this._isConnecting()) return;
|
||||||
const ws = new WebSocket(await this._endpoint());
|
const ws = new WebSocket(this._endpoint());
|
||||||
ws.binaryType = 'arraybuffer';
|
ws.binaryType = 'arraybuffer';
|
||||||
ws.onopen = _ => this._onOpen();
|
ws.onopen = _ => this._onOpen();
|
||||||
ws.onmessage = e => this._onMessage(e.data);
|
ws.onmessage = e => this._onMessage(e.data);
|
||||||
@@ -53,7 +53,7 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onPairDeviceJoin(roomKey) {
|
_onPairDeviceJoin(roomKey) {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||||
@@ -118,32 +118,22 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onDisplayName(msg) {
|
_onDisplayName(msg) {
|
||||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||||
PersistentStorage.get('peerId').then(peerId => {
|
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||||
if (!peerId) {
|
|
||||||
// save peerId to indexedDB to retrieve after PWA is installed
|
|
||||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
|
||||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
|
||||||
Events.fire('display-name', msg);
|
Events.fire('display-name', msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _endpoint() {
|
_endpoint() {
|
||||||
// hack to detect if deployment or development environment
|
// hack to detect if deployment or development environment
|
||||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||||
const peerId = await this._peerId();
|
const peerId = sessionStorage.getItem("peerId");
|
||||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||||
return ws_url.toString();
|
if (peerId && peerIdHash) {
|
||||||
|
ws_url.searchParams.append('peer_id', peerId);
|
||||||
|
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||||
}
|
}
|
||||||
|
return ws_url.toString();
|
||||||
async _peerId() {
|
|
||||||
// make peerId persistent when pwa is installed
|
|
||||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
|
||||||
? await PersistentStorage.get('peerId')
|
|
||||||
: sessionStorage.getItem("peerId");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_disconnect() {
|
_disconnect() {
|
||||||
@@ -161,7 +151,7 @@ class ServerConnection {
|
|||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'Connecting..');
|
Events.fire('notify-user', 'Connecting..');
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
this._isReconnect = true;
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
@@ -204,6 +194,10 @@ class Peer {
|
|||||||
this._send(JSON.stringify(message));
|
this._send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||||
|
}
|
||||||
|
|
||||||
async createHeader(file) {
|
async createHeader(file) {
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -358,6 +352,9 @@ class Peer {
|
|||||||
case 'text':
|
case 'text':
|
||||||
this._onTextReceived(messageJSON);
|
this._onTextReceived(messageJSON);
|
||||||
break;
|
break;
|
||||||
|
case 'display-name-changed':
|
||||||
|
this._onDisplayNameChanged(messageJSON);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,6 +492,12 @@ class Peer {
|
|||||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||||
this.sendJSON({ type: 'message-transfer-complete' });
|
this.sendJSON({ type: 'message-transfer-complete' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDisplayNameChanged(message) {
|
||||||
|
if (!message.displayName || this._displayName === message.displayName) return;
|
||||||
|
this._displayName = message.displayName;
|
||||||
|
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RTCPeer extends Peer {
|
class RTCPeer extends Peer {
|
||||||
@@ -568,14 +571,14 @@ class RTCPeer extends Peer {
|
|||||||
|
|
||||||
_onChannelOpened(event) {
|
_onChannelOpened(event) {
|
||||||
console.log('RTC: channel opened with', this._peerId);
|
console.log('RTC: channel opened with', this._peerId);
|
||||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
|
||||||
const channel = event.channel || event.target;
|
const channel = event.channel || event.target;
|
||||||
channel.binaryType = 'arraybuffer';
|
channel.binaryType = 'arraybuffer';
|
||||||
channel.onmessage = e => this._onMessage(e.data);
|
channel.onmessage = e => this._onMessage(e.data);
|
||||||
channel.onclose = _ => this._onChannelClosed();
|
channel.onclose = _ => this._onChannelClosed();
|
||||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
|
||||||
Events.on('pagehide', _ => this._closeChannel());
|
|
||||||
this._channel = channel;
|
this._channel = channel;
|
||||||
|
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||||
|
Events.on('pagehide', _ => this._onPageHide());
|
||||||
|
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessage(message) {
|
_onMessage(message) {
|
||||||
@@ -618,10 +621,16 @@ class RTCPeer extends Peer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_closeChannel() {
|
_onPageHide() {
|
||||||
if (this._channel) this._channel.onclose = null;
|
this._disconnect();
|
||||||
if (this._conn) this._conn.close();
|
}
|
||||||
this._conn = null;
|
|
||||||
|
_disconnect() {
|
||||||
|
if (this._conn && this._channel) {
|
||||||
|
this._channel.onclose = null;
|
||||||
|
this._channel.close();
|
||||||
|
}
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChannelClosed() {
|
_onChannelClosed() {
|
||||||
@@ -635,9 +644,11 @@ class RTCPeer extends Peer {
|
|||||||
console.log('RTC: state changed:', this._conn.connectionState);
|
console.log('RTC: state changed:', this._conn.connectionState);
|
||||||
switch (this._conn.connectionState) {
|
switch (this._conn.connectionState) {
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection disconnected');
|
this._onError('rtc connection disconnected');
|
||||||
break;
|
break;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection failed');
|
this._onError('rtc connection failed');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -683,6 +694,11 @@ class RTCPeer extends Peer {
|
|||||||
_isConnecting() {
|
_isConnecting() {
|
||||||
return this._channel && this._channel.readyState === 'connecting';
|
return this._channel && this._channel.readyState === 'connecting';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
if (!this._isConnected()) return;
|
||||||
|
super.sendDisplayName(displayName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PeersManager {
|
class PeersManager {
|
||||||
@@ -696,8 +712,12 @@ class PeersManager {
|
|||||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||||
Events.on('send-text', e => this._onSendText(e.detail));
|
Events.on('send-text', e => this._onSendText(e.detail));
|
||||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||||
|
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||||
|
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||||
|
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||||
|
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessage(message) {
|
_onMessage(message) {
|
||||||
@@ -721,10 +741,6 @@ class PeersManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTo(peerId, message) {
|
|
||||||
this.peers[peerId].send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRespondToFileTransferRequest(detail) {
|
_onRespondToFileTransferRequest(detail) {
|
||||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||||
}
|
}
|
||||||
@@ -756,6 +772,10 @@ class PeersManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPeerConnected(peerId) {
|
||||||
|
this._notifyPeerDisplayNameChanged(peerId);
|
||||||
|
}
|
||||||
|
|
||||||
_onPeerDisconnected(peerId) {
|
_onPeerDisconnected(peerId) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
delete this.peers[peerId];
|
delete this.peers[peerId];
|
||||||
@@ -773,6 +793,23 @@ class PeersManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||||
|
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||||
|
for (const peerId in this.peers) {
|
||||||
|
this._notifyPeerDisplayNameChanged(peerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_notifyPeerDisplayNameChanged(peerId) {
|
||||||
|
const peer = this.peers[peerId];
|
||||||
|
if (!peer) return;
|
||||||
|
this.peers[peerId].sendDisplayName(this._displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisplayName(displayName) {
|
||||||
|
this._originalDisplayName = displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileChunker {
|
class FileChunker {
|
||||||
@@ -861,11 +898,11 @@ class Events {
|
|||||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
static on(type, callback) {
|
static on(type, callback, options = false) {
|
||||||
return window.addEventListener(type, callback, false);
|
return window.addEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
static off(type, callback) {
|
static off(type, callback, options = false) {
|
||||||
return window.removeEventListener(type, callback, false);
|
return window.removeEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+194
-54
@@ -9,9 +9,8 @@ window.pasteMode.activated = false;
|
|||||||
// set display name
|
// set display name
|
||||||
Events.on('display-name', e => {
|
Events.on('display-name', e => {
|
||||||
const me = e.detail.message;
|
const me = e.detail.message;
|
||||||
const $displayName = $('display-name')
|
const $displayName = $('display-name');
|
||||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
$displayName.setAttribute('placeholder', me.displayName);
|
||||||
$displayName.title = me.deviceName;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class PeersUI {
|
class PeersUI {
|
||||||
@@ -43,6 +42,80 @@ class PeersUI {
|
|||||||
|
|
||||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||||
|
|
||||||
|
this.$displayName = $('display-name');
|
||||||
|
|
||||||
|
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||||
|
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||||
|
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||||
|
|
||||||
|
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||||
|
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||||
|
|
||||||
|
// Load saved display name on page load
|
||||||
|
this._getSavedDisplayName().then(displayName => {
|
||||||
|
console.log("Retrieved edited display name:", displayName)
|
||||||
|
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_insertDisplayName(displayName) {
|
||||||
|
this.$displayName.textContent = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyDownDisplayName(e) {
|
||||||
|
if (e.key === "Enter" || e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyUpDisplayName(e) {
|
||||||
|
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||||
|
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async _saveDisplayName(newDisplayName) {
|
||||||
|
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
|
||||||
|
const savedDisplayName = await this._getSavedDisplayName();
|
||||||
|
if (newDisplayName === savedDisplayName) return;
|
||||||
|
|
||||||
|
if (newDisplayName) {
|
||||||
|
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||||
|
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||||
|
}).catch(_ => {
|
||||||
|
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||||
|
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||||
|
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||||
|
}).finally(_ => {
|
||||||
|
Events.fire('self-display-name-changed', newDisplayName);
|
||||||
|
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||||
|
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||||
|
localStorage.removeItem('editedDisplayName');
|
||||||
|
Events.fire('notify-user', 'Random Display name is used again.');
|
||||||
|
}).finally(_ => {
|
||||||
|
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||||
|
Events.fire('self-display-name-changed', '');
|
||||||
|
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSavedDisplayName() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
PersistentStorage.get('editedDisplayName')
|
||||||
|
.then(displayName => resolve(displayName ?? ""))
|
||||||
|
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_changePeerDisplayName(peerId, displayName) {
|
||||||
|
this.peers[peerId].name.displayName = displayName;
|
||||||
|
const peerIdNode = $(peerId);
|
||||||
|
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown(e) {
|
_onKeyDown(e) {
|
||||||
@@ -168,7 +241,7 @@ class PeersUI {
|
|||||||
|
|
||||||
const _callback = (e) => this._sendClipboardData(e, files, text);
|
const _callback = (e) => this._sendClipboardData(e, files, text);
|
||||||
Events.on('paste-pointerdown', _callback);
|
Events.on('paste-pointerdown', _callback);
|
||||||
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback));
|
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
|
||||||
|
|
||||||
this.$cancelPasteModeBtn.removeAttribute('hidden');
|
this.$cancelPasteModeBtn.removeAttribute('hidden');
|
||||||
|
|
||||||
@@ -227,7 +300,8 @@ class PeerUI {
|
|||||||
|
|
||||||
constructor(peer, connectionHash) {
|
constructor(peer, connectionHash) {
|
||||||
this._peer = peer;
|
this._peer = peer;
|
||||||
this._connectionHash = connectionHash;
|
this._connectionHash =
|
||||||
|
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
|
||||||
this._initDom();
|
this._initDom();
|
||||||
this._bindListeners();
|
this._bindListeners();
|
||||||
|
|
||||||
@@ -272,8 +346,7 @@ class PeerUI {
|
|||||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||||
this.$el.querySelector('.name').textContent = this._displayName();
|
this.$el.querySelector('.name').textContent = this._displayName();
|
||||||
this.$el.querySelector('.device-name').textContent = this._deviceName();
|
this.$el.querySelector('.device-name').textContent = this._deviceName();
|
||||||
this.$el.querySelector('.connection-hash').textContent =
|
this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
|
||||||
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_initDom() {
|
_initDom() {
|
||||||
@@ -496,7 +569,7 @@ class ReceiveDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseFileData(displayName, files, imagesOnly, totalSize) {
|
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
|
||||||
if (files.length > 1) {
|
if (files.length > 1) {
|
||||||
let fileOtherText = ` and ${files.length - 1} other `;
|
let fileOtherText = ` and ${files.length - 1} other `;
|
||||||
if (files.length === 2) {
|
if (files.length === 2) {
|
||||||
@@ -513,6 +586,7 @@ class ReceiveDialog extends Dialog {
|
|||||||
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||||
this.$fileExtension.innerText = fileExtension;
|
this.$fileExtension.innerText = fileExtension;
|
||||||
this.$displayName.innerText = displayName;
|
this.$displayName.innerText = displayName;
|
||||||
|
this.$displayName.title = connectionHash;
|
||||||
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,8 +604,9 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||||
const displayName = $(sender).ui._displayName()
|
const displayName = $(sender).ui._displayName();
|
||||||
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
const connectionHash = $(sender).ui._connectionHash;
|
||||||
|
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||||
this._nextFiles();
|
this._nextFiles();
|
||||||
window.blop.play();
|
window.blop.play();
|
||||||
}
|
}
|
||||||
@@ -539,8 +614,8 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
_nextFiles() {
|
_nextFiles() {
|
||||||
if (this._busy) return;
|
if (this._busy) return;
|
||||||
this._busy = true;
|
this._busy = true;
|
||||||
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||||
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
_dequeueFile() {
|
_dequeueFile() {
|
||||||
@@ -581,8 +656,8 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
|
||||||
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
|
||||||
|
|
||||||
let descriptor, url, filenameDownload;
|
let descriptor, url, filenameDownload;
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
@@ -729,7 +804,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
this.correspondingPeerId = peerId;
|
this.correspondingPeerId = peerId;
|
||||||
|
|
||||||
const displayName = $(peerId).ui._displayName();
|
const displayName = $(peerId).ui._displayName();
|
||||||
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
const connectionHash = $(peerId).ui._connectionHash;
|
||||||
|
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize);
|
||||||
|
|
||||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||||
let element = document.createElement('img');
|
let element = document.createElement('img');
|
||||||
@@ -766,15 +842,17 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
class PairDeviceDialog extends Dialog {
|
class PairDeviceDialog extends Dialog {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('pair-device-dialog');
|
super('pair-device-dialog');
|
||||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
|
||||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||||
this.$roomKey = this.$el.querySelector('#room-key');
|
this.$roomKey = this.$el.querySelector('#room-key');
|
||||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||||
|
this.$pairDeviceBtn = $('pair-device');
|
||||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||||
let createJoinForm = this.$el.querySelector('form');
|
this.$createJoinForm = this.$el.querySelector('form');
|
||||||
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
|
||||||
|
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||||
|
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
|
||||||
|
|
||||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||||
@@ -867,6 +945,7 @@ class PairDeviceDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onWsConnected() {
|
_onWsConnected() {
|
||||||
|
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||||
Events.fire('room-secrets', roomSecrets);
|
Events.fire('room-secrets', roomSecrets);
|
||||||
this._evaluateNumberRoomSecrets();
|
this._evaluateNumberRoomSecrets();
|
||||||
@@ -1168,21 +1247,21 @@ class Base64ZipDialog extends Dialog {
|
|||||||
const base64Hash = window.location.hash.substring(1);
|
const base64Hash = window.location.hash.substring(1);
|
||||||
|
|
||||||
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
||||||
|
this.$fallbackTextarea = this.$el.querySelector('.textarea');
|
||||||
|
|
||||||
if (base64Text) {
|
if (base64Text) {
|
||||||
this.show();
|
this.show();
|
||||||
if (base64Text === "paste") {
|
if (base64Text === "paste") {
|
||||||
// ?base64text=paste
|
// ?base64text=paste
|
||||||
// base64 encoded string is ready to be pasted from clipboard
|
// base64 encoded string is ready to be pasted from clipboard
|
||||||
this.$pasteBtn.innerText = 'Tap here to paste text';
|
this.preparePasting("text");
|
||||||
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
|
|
||||||
} else if (base64Text === "hash") {
|
} else if (base64Text === "hash") {
|
||||||
// ?base64text=hash#BASE64ENCODED
|
// ?base64text=hash#BASE64ENCODED
|
||||||
// base64 encoded string is url hash which is never sent to server and faster (recommended)
|
// base64 encoded string is url hash which is never sent to server and faster (recommended)
|
||||||
this.processBase64Text(base64Hash)
|
this.processBase64Text(base64Hash)
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
Events.fire('notify-user', 'Text content is incorrect.');
|
Events.fire('notify-user', 'Text content is incorrect.');
|
||||||
console.log("Text content incorrect.")
|
console.log("Text content incorrect.");
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
@@ -1192,7 +1271,7 @@ class Base64ZipDialog extends Dialog {
|
|||||||
this.processBase64Text(base64Text)
|
this.processBase64Text(base64Text)
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
Events.fire('notify-user', 'Text content is incorrect.');
|
Events.fire('notify-user', 'Text content is incorrect.');
|
||||||
console.log("Text content incorrect.")
|
console.log("Text content incorrect.");
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
@@ -1205,14 +1284,13 @@ class Base64ZipDialog extends Dialog {
|
|||||||
this.processBase64Zip(base64Hash)
|
this.processBase64Zip(base64Hash)
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
Events.fire('notify-user', 'File content is incorrect.');
|
Events.fire('notify-user', 'File content is incorrect.');
|
||||||
console.log("File content incorrect.")
|
console.log("File content incorrect.");
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// ?base64zip=paste || ?base64zip=true
|
// ?base64zip=paste || ?base64zip=true
|
||||||
this.$pasteBtn.innerText = 'Tap here to paste files';
|
this.preparePasting('files');
|
||||||
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1222,37 +1300,58 @@ class Base64ZipDialog extends Dialog {
|
|||||||
this.$pasteBtn.innerText = "Processing...";
|
this.$pasteBtn.innerText = "Processing...";
|
||||||
}
|
}
|
||||||
|
|
||||||
async processClipboard(type) {
|
preparePasting(type) {
|
||||||
if (!navigator.clipboard.readText) {
|
if (navigator.clipboard.readText) {
|
||||||
Events.fire('notify-user', 'This feature is not available on your browser.');
|
this.$pasteBtn.innerText = `Tap here to paste ${type}`;
|
||||||
console.log("navigator.clipboard.readText() is not available on your browser.")
|
this._clickCallback = _ => this.processClipboard(type);
|
||||||
this.hide();
|
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._setPasteBtnToProcessing();
|
|
||||||
|
|
||||||
const base64 = await navigator.clipboard.readText();
|
|
||||||
|
|
||||||
if (!base64) return;
|
|
||||||
|
|
||||||
if (type === "text") {
|
|
||||||
this.processBase64Text(base64)
|
|
||||||
.catch(_ => {
|
|
||||||
Events.fire('notify-user', 'Clipboard content is incorrect.');
|
|
||||||
console.log("Clipboard content is incorrect.")
|
|
||||||
}).finally(_ => {
|
|
||||||
this.hide();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.processBase64Zip(base64)
|
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
|
||||||
.catch(_ => {
|
this.$pasteBtn.setAttribute('hidden', '');
|
||||||
|
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
|
||||||
|
this.$fallbackTextarea.removeAttribute('hidden');
|
||||||
|
this._inputCallback = _ => this.processInput(type);
|
||||||
|
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
|
||||||
|
this.$fallbackTextarea.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processInput(type) {
|
||||||
|
const base64 = this.$fallbackTextarea.textContent;
|
||||||
|
this.$fallbackTextarea.textContent = '';
|
||||||
|
await this.processBase64(type, base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
async processClipboard(type) {
|
||||||
|
const base64 = await navigator.clipboard.readText();
|
||||||
|
await this.processBase64(type, base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidBase64(base64) {
|
||||||
|
try {
|
||||||
|
// check if input is base64 encoded
|
||||||
|
window.atob(base64);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
// input is not base64 string.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processBase64(type, base64) {
|
||||||
|
if (!base64 || !this.isValidBase64(base64)) return;
|
||||||
|
this._setPasteBtnToProcessing();
|
||||||
|
try {
|
||||||
|
if (type === "text") {
|
||||||
|
await this.processBase64Text(base64);
|
||||||
|
} else {
|
||||||
|
await this.processBase64Zip(base64);
|
||||||
|
}
|
||||||
|
} catch(_) {
|
||||||
Events.fire('notify-user', 'Clipboard content is incorrect.');
|
Events.fire('notify-user', 'Clipboard content is incorrect.');
|
||||||
console.log("Clipboard content is incorrect.")
|
console.log("Clipboard content is incorrect.")
|
||||||
}).finally(_ => {
|
|
||||||
this.hide();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
processBase64Text(base64Text){
|
processBase64Text(base64Text){
|
||||||
@@ -1288,6 +1387,8 @@ class Base64ZipDialog extends Dialog {
|
|||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.clearBrowserHistory();
|
this.clearBrowserHistory();
|
||||||
|
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
|
||||||
|
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
|
||||||
super.hide();
|
super.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1318,9 +1419,9 @@ class Notifications {
|
|||||||
this.$button.removeAttribute('hidden');
|
this.$button.removeAttribute('hidden');
|
||||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||||
}
|
}
|
||||||
// Todo: fix Notifications
|
|
||||||
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
||||||
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
||||||
|
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestPermission() {
|
_requestPermission() {
|
||||||
@@ -1393,8 +1494,29 @@ class Notifications {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_requestNotification(request, peerId) {
|
||||||
|
if (document.visibilityState !== 'visible') {
|
||||||
|
let imagesOnly = true;
|
||||||
|
for(let i=0; i<request.header.length; i++) {
|
||||||
|
if (request.header[i].mime.split('/')[0] !== 'image') {
|
||||||
|
imagesOnly = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let descriptor;
|
||||||
|
if (request.header.length > 1) {
|
||||||
|
descriptor = imagesOnly ? ' images' : ' files';
|
||||||
|
} else {
|
||||||
|
descriptor = imagesOnly ? ' image' : ' file';
|
||||||
|
}
|
||||||
|
let displayName = $(peerId).querySelector('.name').textContent
|
||||||
|
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
|
||||||
|
const notification = this._notify(title, 'Click to show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_download(notification) {
|
_download(notification) {
|
||||||
$('share-or-download').click();
|
$('download-btn').click();
|
||||||
notification.close();
|
notification.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1723,6 +1845,23 @@ class PersistentStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Broadcast {
|
||||||
|
constructor() {
|
||||||
|
this.bc = new BroadcastChannel('pairdrop');
|
||||||
|
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||||
|
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
_broadcastMessage(message) {
|
||||||
|
this.bc.postMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMessage(e) {
|
||||||
|
console.log('Broadcast message received:', e.data)
|
||||||
|
Events.fire(e.data.type, e.data.detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PairDrop {
|
class PairDrop {
|
||||||
constructor() {
|
constructor() {
|
||||||
Events.on('load', _ => {
|
Events.on('load', _ => {
|
||||||
@@ -1742,6 +1881,7 @@ class PairDrop {
|
|||||||
const webShareTargetUI = new WebShareTargetUI();
|
const webShareTargetUI = new WebShareTargetUI();
|
||||||
const webFileHandlersUI = new WebFileHandlersUI();
|
const webFileHandlersUI = new WebFileHandlersUI();
|
||||||
const noSleepUI = new NoSleepUI();
|
const noSleepUI = new NoSleepUI();
|
||||||
|
const broadCast = new Broadcast();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.3.0';
|
const cacheVersion = 'v1.4.4';
|
||||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+59
-5
@@ -450,6 +450,7 @@ x-peer[status] x-icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-descriptor {
|
.device-descriptor {
|
||||||
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,6 +534,7 @@ footer {
|
|||||||
padding: 0 0 16px 0;
|
padding: 0 0 16px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: color 300ms;
|
transition: color 300ms;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .logo {
|
footer .logo {
|
||||||
@@ -557,6 +559,39 @@ footer .font-body2 {
|
|||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#display-name {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
max-width: 15em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: text;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-bottom: -6px;
|
||||||
|
padding-right: 0.3rem;
|
||||||
|
padding-left: 0.3em;
|
||||||
|
padding-bottom: 0.1rem;
|
||||||
|
border-radius: 1.3rem/30%;
|
||||||
|
border-right: solid 1rem transparent;
|
||||||
|
border-left: solid 1rem transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
background-color: rgba(var(--text-color), 43%);
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-pen {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
position: relative;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dialog */
|
/* Dialog */
|
||||||
|
|
||||||
x-dialog x-background {
|
x-dialog x-background {
|
||||||
@@ -756,10 +791,29 @@ x-dialog .dialog-subheader {
|
|||||||
margin: auto -24px;
|
margin: auto -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-btn {
|
#base64-paste-btn,
|
||||||
|
#base64-paste-dialog .textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
border: solid 12px #438cff;
|
border: solid 12px #438cff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog .textarea {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog .textarea::before {
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
content: attr(placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-dialog button {
|
#base64-paste-dialog button {
|
||||||
@@ -995,11 +1049,11 @@ button::-moz-focus-inner {
|
|||||||
x-toast {
|
x-toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
bottom: 24px;
|
top: 50px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 344px;
|
max-width: 344px;
|
||||||
background-color: #323232;
|
background-color: rgb(var(--text-color));
|
||||||
color: rgba(255, 255, 255, 0.95);
|
color: rgb(var(--bg-color));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
@@ -1013,7 +1067,7 @@ x-toast {
|
|||||||
|
|
||||||
x-toast:not([show]):not(:hover) {
|
x-toast:not([show]):not(:hover) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(100px);
|
transform: translateY(-100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<use xlink:href="#homescreen" />
|
<use xlink:href="#homescreen" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a id="pair-device" class="icon-button" title="Pair Device" >
|
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#pair-device-icon" />
|
<use xlink:href="#pair-device-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -89,7 +89,13 @@
|
|||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
<div id="display-name" placeholder=" "></div>
|
<div>
|
||||||
|
<span>You are known as:</span>
|
||||||
|
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||||
|
<svg id="edit-pen" class="icon">
|
||||||
|
<use xlink:href="#edit-pen-icon" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div class="font-body2">
|
<div class="font-body2">
|
||||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||||
@@ -119,7 +125,7 @@
|
|||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<button class="button" type="submit" disabled>Pair</button>
|
||||||
<button class="button" close>Cancel</button>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -134,7 +140,7 @@
|
|||||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit">Unpair Devices</button>
|
<button class="button" type="submit">Unpair Devices</button>
|
||||||
<button class="button" close>Cancel</button>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -206,7 +212,7 @@
|
|||||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<button class="button" title="ESCAPE" close>Cancel</button>
|
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -235,6 +241,7 @@
|
|||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||||
|
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||||
<button class="button center" close>Close</button>
|
<button class="button center" close>Close</button>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -245,14 +252,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- About Page -->
|
<!-- About Page -->
|
||||||
<x-about id="about" class="full center column">
|
<x-about id="about" class="full center column">
|
||||||
<section class="center column fade-in">
|
<header class="row-reverse fade-in">
|
||||||
<header class="row-reverse">
|
|
||||||
<a href="#" class="close icon-button">
|
<a href="#" class="close icon-button">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#close-icon" />
|
<use xlink:href="#close-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
<section class="center column fade-in">
|
||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -335,6 +342,10 @@
|
|||||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||||
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="scripts/util.js"></script>
|
<script src="scripts/util.js"></script>
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ class ServerConnection {
|
|||||||
Events.on('online', _ => this._connect());
|
Events.on('online', _ => this._connect());
|
||||||
}
|
}
|
||||||
|
|
||||||
async _connect() {
|
_connect() {
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
if (this._isConnected() || this._isConnecting()) return;
|
if (this._isConnected() || this._isConnecting()) return;
|
||||||
const ws = new WebSocket(await this._endpoint());
|
const ws = new WebSocket(this._endpoint());
|
||||||
ws.binaryType = 'arraybuffer';
|
ws.binaryType = 'arraybuffer';
|
||||||
ws.onopen = _ => this._onOpen();
|
ws.onopen = _ => this._onOpen();
|
||||||
ws.onmessage = e => this._onMessage(e.data);
|
ws.onmessage = e => this._onMessage(e.data);
|
||||||
@@ -51,7 +51,7 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onPairDeviceJoin(roomKey) {
|
_onPairDeviceJoin(roomKey) {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||||
@@ -113,6 +113,7 @@ class ServerConnection {
|
|||||||
case 'file-transfer-complete':
|
case 'file-transfer-complete':
|
||||||
case 'message-transfer-complete':
|
case 'message-transfer-complete':
|
||||||
case 'text':
|
case 'text':
|
||||||
|
case 'display-name-changed':
|
||||||
case 'ws-chunk':
|
case 'ws-chunk':
|
||||||
Events.fire('ws-relay', JSON.stringify(msg));
|
Events.fire('ws-relay', JSON.stringify(msg));
|
||||||
break;
|
break;
|
||||||
@@ -128,32 +129,22 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onDisplayName(msg) {
|
_onDisplayName(msg) {
|
||||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||||
PersistentStorage.get('peerId').then(peerId => {
|
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||||
if (!peerId) {
|
|
||||||
// save peerId to indexedDB to retrieve after PWA is installed
|
|
||||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
|
||||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
|
||||||
Events.fire('display-name', msg);
|
Events.fire('display-name', msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _endpoint() {
|
_endpoint() {
|
||||||
// hack to detect if deployment or development environment
|
// hack to detect if deployment or development environment
|
||||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||||
const peerId = await this._peerId();
|
const peerId = sessionStorage.getItem("peerId");
|
||||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||||
return ws_url.toString();
|
if (peerId && peerIdHash) {
|
||||||
|
ws_url.searchParams.append('peer_id', peerId);
|
||||||
|
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||||
}
|
}
|
||||||
|
return ws_url.toString();
|
||||||
async _peerId() {
|
|
||||||
// make peerId persistent when pwa is installed
|
|
||||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
|
||||||
? await PersistentStorage.get('peerId')
|
|
||||||
: sessionStorage.getItem("peerId");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_disconnect() {
|
_disconnect() {
|
||||||
@@ -171,7 +162,7 @@ class ServerConnection {
|
|||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'Connecting..');
|
Events.fire('notify-user', 'Connecting..');
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
this._isReconnect = true;
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
@@ -214,6 +205,10 @@ class Peer {
|
|||||||
this._send(JSON.stringify(message));
|
this._send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||||
|
}
|
||||||
|
|
||||||
async createHeader(file) {
|
async createHeader(file) {
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -368,6 +363,9 @@ class Peer {
|
|||||||
case 'text':
|
case 'text':
|
||||||
this._onTextReceived(messageJSON);
|
this._onTextReceived(messageJSON);
|
||||||
break;
|
break;
|
||||||
|
case 'display-name-changed':
|
||||||
|
this._onDisplayNameChanged(messageJSON);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +503,12 @@ class Peer {
|
|||||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||||
this.sendJSON({ type: 'message-transfer-complete' });
|
this.sendJSON({ type: 'message-transfer-complete' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDisplayNameChanged(message) {
|
||||||
|
if (!message.displayName || this._displayName === message.displayName) return;
|
||||||
|
this._displayName = message.displayName;
|
||||||
|
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RTCPeer extends Peer {
|
class RTCPeer extends Peer {
|
||||||
@@ -578,14 +582,14 @@ class RTCPeer extends Peer {
|
|||||||
|
|
||||||
_onChannelOpened(event) {
|
_onChannelOpened(event) {
|
||||||
console.log('RTC: channel opened with', this._peerId);
|
console.log('RTC: channel opened with', this._peerId);
|
||||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
|
||||||
const channel = event.channel || event.target;
|
const channel = event.channel || event.target;
|
||||||
channel.binaryType = 'arraybuffer';
|
channel.binaryType = 'arraybuffer';
|
||||||
channel.onmessage = e => this._onMessage(e.data);
|
channel.onmessage = e => this._onMessage(e.data);
|
||||||
channel.onclose = _ => this._onChannelClosed();
|
channel.onclose = _ => this._onChannelClosed();
|
||||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
|
||||||
Events.on('pagehide', _ => this._closeChannel());
|
|
||||||
this._channel = channel;
|
this._channel = channel;
|
||||||
|
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||||
|
Events.on('pagehide', _ => this._onPageHide());
|
||||||
|
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessage(message) {
|
_onMessage(message) {
|
||||||
@@ -628,10 +632,16 @@ class RTCPeer extends Peer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_closeChannel() {
|
_onPageHide() {
|
||||||
if (this._channel) this._channel.onclose = null;
|
this._disconnect();
|
||||||
if (this._conn) this._conn.close();
|
}
|
||||||
this._conn = null;
|
|
||||||
|
_disconnect() {
|
||||||
|
if (this._conn && this._channel) {
|
||||||
|
this._channel.onclose = null;
|
||||||
|
this._channel.close();
|
||||||
|
}
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChannelClosed() {
|
_onChannelClosed() {
|
||||||
@@ -645,9 +655,11 @@ class RTCPeer extends Peer {
|
|||||||
console.log('RTC: state changed:', this._conn.connectionState);
|
console.log('RTC: state changed:', this._conn.connectionState);
|
||||||
switch (this._conn.connectionState) {
|
switch (this._conn.connectionState) {
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection disconnected');
|
this._onError('rtc connection disconnected');
|
||||||
break;
|
break;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection failed');
|
this._onError('rtc connection failed');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -693,6 +705,11 @@ class RTCPeer extends Peer {
|
|||||||
_isConnecting() {
|
_isConnecting() {
|
||||||
return this._channel && this._channel.readyState === 'connecting';
|
return this._channel && this._channel.readyState === 'connecting';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
if (!this._isConnected()) return;
|
||||||
|
super.sendDisplayName(displayName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WSPeer extends Peer {
|
class WSPeer extends Peer {
|
||||||
@@ -701,6 +718,7 @@ class WSPeer extends Peer {
|
|||||||
super(serverConnection, peerId, roomType, roomSecret);
|
super(serverConnection, peerId, roomType, roomSecret);
|
||||||
this.rtcSupported = false;
|
this.rtcSupported = false;
|
||||||
if (!peerId) return; // we will listen for a caller
|
if (!peerId) return; // we will listen for a caller
|
||||||
|
this._isCaller = true;
|
||||||
this._sendSignal();
|
this._sendSignal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,6 +730,7 @@ class WSPeer extends Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendJSON(message) {
|
sendJSON(message) {
|
||||||
|
console.debug(message)
|
||||||
message.to = this._peerId;
|
message.to = this._peerId;
|
||||||
message.roomType = this._roomType;
|
message.roomType = this._roomType;
|
||||||
message.roomSecret = this._roomSecret;
|
message.roomSecret = this._roomSecret;
|
||||||
@@ -723,9 +742,9 @@ class WSPeer extends Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onServerMessage(message) {
|
onServerMessage(message) {
|
||||||
|
this._peerId = message.sender.id;
|
||||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||||
if (message.connected) return;
|
if (message.connected) return;
|
||||||
this._peerId = message.sender.id;
|
|
||||||
this._sendSignal(true);
|
this._sendSignal(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,8 +765,12 @@ class PeersManager {
|
|||||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||||
Events.on('send-text', e => this._onSendText(e.detail));
|
Events.on('send-text', e => this._onSendText(e.detail));
|
||||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||||
|
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||||
|
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||||
|
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||||
|
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
|
||||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||||
}
|
}
|
||||||
@@ -787,10 +810,6 @@ class PeersManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTo(peerId, message) {
|
|
||||||
this.peers[peerId].send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRespondToFileTransferRequest(detail) {
|
_onRespondToFileTransferRequest(detail) {
|
||||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||||
}
|
}
|
||||||
@@ -825,6 +844,10 @@ class PeersManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPeerConnected(peerId) {
|
||||||
|
this._notifyPeerDisplayNameChanged(peerId);
|
||||||
|
}
|
||||||
|
|
||||||
_onWsDisconnected() {
|
_onWsDisconnected() {
|
||||||
for (const peerId in this.peers) {
|
for (const peerId in this.peers) {
|
||||||
console.debug(this.peers[peerId].rtcSupported);
|
console.debug(this.peers[peerId].rtcSupported);
|
||||||
@@ -851,6 +874,23 @@ class PeersManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||||
|
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||||
|
for (const peerId in this.peers) {
|
||||||
|
this._notifyPeerDisplayNameChanged(peerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_notifyPeerDisplayNameChanged(peerId) {
|
||||||
|
const peer = this.peers[peerId];
|
||||||
|
if (!peer) return;
|
||||||
|
this.peers[peerId].sendDisplayName(this._displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisplayName(displayName) {
|
||||||
|
this._originalDisplayName = displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileChunker {
|
class FileChunker {
|
||||||
@@ -939,11 +979,11 @@ class Events {
|
|||||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
static on(type, callback) {
|
static on(type, callback, options = false) {
|
||||||
return window.addEventListener(type, callback, false);
|
return window.addEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
static off(type, callback) {
|
static off(type, callback, options = false) {
|
||||||
return window.removeEventListener(type, callback, false);
|
return window.removeEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ window.pasteMode.activated = false;
|
|||||||
// set display name
|
// set display name
|
||||||
Events.on('display-name', e => {
|
Events.on('display-name', e => {
|
||||||
const me = e.detail.message;
|
const me = e.detail.message;
|
||||||
const $displayName = $('display-name')
|
const $displayName = $('display-name');
|
||||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
$displayName.setAttribute('placeholder', me.displayName);
|
||||||
$displayName.title = me.deviceName;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class PeersUI {
|
class PeersUI {
|
||||||
@@ -43,6 +42,80 @@ class PeersUI {
|
|||||||
|
|
||||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||||
|
|
||||||
|
this.$displayName = $('display-name');
|
||||||
|
|
||||||
|
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||||
|
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||||
|
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||||
|
|
||||||
|
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||||
|
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||||
|
|
||||||
|
// Load saved display name on page load
|
||||||
|
this._getSavedDisplayName().then(displayName => {
|
||||||
|
console.log("Retrieved edited display name:", displayName)
|
||||||
|
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_insertDisplayName(displayName) {
|
||||||
|
this.$displayName.textContent = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyDownDisplayName(e) {
|
||||||
|
if (e.key === "Enter" || e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyUpDisplayName(e) {
|
||||||
|
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||||
|
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async _saveDisplayName(newDisplayName) {
|
||||||
|
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
|
||||||
|
const savedDisplayName = await this._getSavedDisplayName();
|
||||||
|
if (newDisplayName === savedDisplayName) return;
|
||||||
|
|
||||||
|
if (newDisplayName) {
|
||||||
|
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||||
|
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||||
|
}).catch(_ => {
|
||||||
|
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||||
|
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||||
|
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||||
|
}).finally(_ => {
|
||||||
|
Events.fire('self-display-name-changed', newDisplayName);
|
||||||
|
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||||
|
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||||
|
localStorage.removeItem('editedDisplayName');
|
||||||
|
Events.fire('notify-user', 'Random Display name is used again.');
|
||||||
|
}).finally(_ => {
|
||||||
|
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||||
|
Events.fire('self-display-name-changed', '');
|
||||||
|
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSavedDisplayName() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
PersistentStorage.get('editedDisplayName')
|
||||||
|
.then(displayName => resolve(displayName ?? ""))
|
||||||
|
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_changePeerDisplayName(peerId, displayName) {
|
||||||
|
this.peers[peerId].name.displayName = displayName;
|
||||||
|
const peerIdNode = $(peerId);
|
||||||
|
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown(e) {
|
_onKeyDown(e) {
|
||||||
@@ -168,7 +241,7 @@ class PeersUI {
|
|||||||
|
|
||||||
const _callback = (e) => this._sendClipboardData(e, files, text);
|
const _callback = (e) => this._sendClipboardData(e, files, text);
|
||||||
Events.on('paste-pointerdown', _callback);
|
Events.on('paste-pointerdown', _callback);
|
||||||
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback));
|
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
|
||||||
|
|
||||||
this.$cancelPasteModeBtn.removeAttribute('hidden');
|
this.$cancelPasteModeBtn.removeAttribute('hidden');
|
||||||
|
|
||||||
@@ -227,7 +300,8 @@ class PeerUI {
|
|||||||
|
|
||||||
constructor(peer, connectionHash) {
|
constructor(peer, connectionHash) {
|
||||||
this._peer = peer;
|
this._peer = peer;
|
||||||
this._connectionHash = connectionHash;
|
this._connectionHash =
|
||||||
|
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
|
||||||
this._initDom();
|
this._initDom();
|
||||||
this._bindListeners();
|
this._bindListeners();
|
||||||
|
|
||||||
@@ -272,8 +346,7 @@ class PeerUI {
|
|||||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||||
this.$el.querySelector('.name').textContent = this._displayName();
|
this.$el.querySelector('.name').textContent = this._displayName();
|
||||||
this.$el.querySelector('.device-name').textContent = this._deviceName();
|
this.$el.querySelector('.device-name').textContent = this._deviceName();
|
||||||
this.$el.querySelector('.connection-hash').textContent =
|
this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
|
||||||
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_initDom() {
|
_initDom() {
|
||||||
@@ -497,7 +570,7 @@ class ReceiveDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseFileData(displayName, files, imagesOnly, totalSize) {
|
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
|
||||||
if (files.length > 1) {
|
if (files.length > 1) {
|
||||||
let fileOtherText = ` and ${files.length - 1} other `;
|
let fileOtherText = ` and ${files.length - 1} other `;
|
||||||
if (files.length === 2) {
|
if (files.length === 2) {
|
||||||
@@ -514,6 +587,7 @@ class ReceiveDialog extends Dialog {
|
|||||||
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||||
this.$fileExtension.innerText = fileExtension;
|
this.$fileExtension.innerText = fileExtension;
|
||||||
this.$displayName.innerText = displayName;
|
this.$displayName.innerText = displayName;
|
||||||
|
this.$displayName.title = connectionHash;
|
||||||
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,8 +605,9 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||||
const displayName = $(sender).ui._displayName()
|
const displayName = $(sender).ui._displayName();
|
||||||
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
const connectionHash = $(sender).ui._connectionHash;
|
||||||
|
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||||
this._nextFiles();
|
this._nextFiles();
|
||||||
window.blop.play();
|
window.blop.play();
|
||||||
}
|
}
|
||||||
@@ -540,8 +615,8 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
_nextFiles() {
|
_nextFiles() {
|
||||||
if (this._busy) return;
|
if (this._busy) return;
|
||||||
this._busy = true;
|
this._busy = true;
|
||||||
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||||
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
_dequeueFile() {
|
_dequeueFile() {
|
||||||
@@ -582,8 +657,8 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
|
||||||
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
|
||||||
|
|
||||||
let descriptor, url, filenameDownload;
|
let descriptor, url, filenameDownload;
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
@@ -730,7 +805,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
this.correspondingPeerId = peerId;
|
this.correspondingPeerId = peerId;
|
||||||
|
|
||||||
const displayName = $(peerId).ui._displayName();
|
const displayName = $(peerId).ui._displayName();
|
||||||
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
const connectionHash = $(peerId).ui._connectionHash;
|
||||||
|
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize);
|
||||||
|
|
||||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||||
let element = document.createElement('img');
|
let element = document.createElement('img');
|
||||||
@@ -767,15 +843,17 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
class PairDeviceDialog extends Dialog {
|
class PairDeviceDialog extends Dialog {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('pair-device-dialog');
|
super('pair-device-dialog');
|
||||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
|
||||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||||
this.$roomKey = this.$el.querySelector('#room-key');
|
this.$roomKey = this.$el.querySelector('#room-key');
|
||||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||||
|
this.$pairDeviceBtn = $('pair-device');
|
||||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||||
let createJoinForm = this.$el.querySelector('form');
|
this.$createJoinForm = this.$el.querySelector('form');
|
||||||
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
|
||||||
|
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||||
|
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
|
||||||
|
|
||||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||||
@@ -868,6 +946,7 @@ class PairDeviceDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onWsConnected() {
|
_onWsConnected() {
|
||||||
|
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||||
Events.fire('room-secrets', roomSecrets);
|
Events.fire('room-secrets', roomSecrets);
|
||||||
this._evaluateNumberRoomSecrets();
|
this._evaluateNumberRoomSecrets();
|
||||||
@@ -1169,21 +1248,21 @@ class Base64ZipDialog extends Dialog {
|
|||||||
const base64Hash = window.location.hash.substring(1);
|
const base64Hash = window.location.hash.substring(1);
|
||||||
|
|
||||||
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
||||||
|
this.$fallbackTextarea = this.$el.querySelector('.textarea');
|
||||||
|
|
||||||
if (base64Text) {
|
if (base64Text) {
|
||||||
this.show();
|
this.show();
|
||||||
if (base64Text === "paste") {
|
if (base64Text === "paste") {
|
||||||
// ?base64text=paste
|
// ?base64text=paste
|
||||||
// base64 encoded string is ready to be pasted from clipboard
|
// base64 encoded string is ready to be pasted from clipboard
|
||||||
this.$pasteBtn.innerText = 'Tap here to paste text';
|
this.preparePasting("text");
|
||||||
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
|
|
||||||
} else if (base64Text === "hash") {
|
} else if (base64Text === "hash") {
|
||||||
// ?base64text=hash#BASE64ENCODED
|
// ?base64text=hash#BASE64ENCODED
|
||||||
// base64 encoded string is url hash which is never sent to server and faster (recommended)
|
// base64 encoded string is url hash which is never sent to server and faster (recommended)
|
||||||
this.processBase64Text(base64Hash)
|
this.processBase64Text(base64Hash)
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
Events.fire('notify-user', 'Text content is incorrect.');
|
Events.fire('notify-user', 'Text content is incorrect.');
|
||||||
console.log("Text content incorrect.")
|
console.log("Text content incorrect.");
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
@@ -1193,7 +1272,7 @@ class Base64ZipDialog extends Dialog {
|
|||||||
this.processBase64Text(base64Text)
|
this.processBase64Text(base64Text)
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
Events.fire('notify-user', 'Text content is incorrect.');
|
Events.fire('notify-user', 'Text content is incorrect.');
|
||||||
console.log("Text content incorrect.")
|
console.log("Text content incorrect.");
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
@@ -1206,14 +1285,13 @@ class Base64ZipDialog extends Dialog {
|
|||||||
this.processBase64Zip(base64Hash)
|
this.processBase64Zip(base64Hash)
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
Events.fire('notify-user', 'File content is incorrect.');
|
Events.fire('notify-user', 'File content is incorrect.');
|
||||||
console.log("File content incorrect.")
|
console.log("File content incorrect.");
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// ?base64zip=paste || ?base64zip=true
|
// ?base64zip=paste || ?base64zip=true
|
||||||
this.$pasteBtn.innerText = 'Tap here to paste files';
|
this.preparePasting('files');
|
||||||
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1223,37 +1301,58 @@ class Base64ZipDialog extends Dialog {
|
|||||||
this.$pasteBtn.innerText = "Processing...";
|
this.$pasteBtn.innerText = "Processing...";
|
||||||
}
|
}
|
||||||
|
|
||||||
async processClipboard(type) {
|
preparePasting(type) {
|
||||||
if (!navigator.clipboard.readText) {
|
if (navigator.clipboard.readText) {
|
||||||
Events.fire('notify-user', 'This feature is not available on your browser.');
|
this.$pasteBtn.innerText = `Tap here to paste ${type}`;
|
||||||
console.log("navigator.clipboard.readText() is not available on your browser.")
|
this._clickCallback = _ => this.processClipboard(type);
|
||||||
this.hide();
|
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._setPasteBtnToProcessing();
|
|
||||||
|
|
||||||
const base64 = await navigator.clipboard.readText();
|
|
||||||
|
|
||||||
if (!base64) return;
|
|
||||||
|
|
||||||
if (type === "text") {
|
|
||||||
this.processBase64Text(base64)
|
|
||||||
.catch(_ => {
|
|
||||||
Events.fire('notify-user', 'Clipboard content is incorrect.');
|
|
||||||
console.log("Clipboard content is incorrect.")
|
|
||||||
}).finally(_ => {
|
|
||||||
this.hide();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.processBase64Zip(base64)
|
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
|
||||||
.catch(_ => {
|
this.$pasteBtn.setAttribute('hidden', '');
|
||||||
|
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
|
||||||
|
this.$fallbackTextarea.removeAttribute('hidden');
|
||||||
|
this._inputCallback = _ => this.processInput(type);
|
||||||
|
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
|
||||||
|
this.$fallbackTextarea.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processInput(type) {
|
||||||
|
const base64 = this.$fallbackTextarea.textContent;
|
||||||
|
this.$fallbackTextarea.textContent = '';
|
||||||
|
await this.processBase64(type, base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
async processClipboard(type) {
|
||||||
|
const base64 = await navigator.clipboard.readText();
|
||||||
|
await this.processBase64(type, base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidBase64(base64) {
|
||||||
|
try {
|
||||||
|
// check if input is base64 encoded
|
||||||
|
window.atob(base64);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
// input is not base64 string.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processBase64(type, base64) {
|
||||||
|
if (!base64 || !this.isValidBase64(base64)) return;
|
||||||
|
this._setPasteBtnToProcessing();
|
||||||
|
try {
|
||||||
|
if (type === "text") {
|
||||||
|
await this.processBase64Text(base64);
|
||||||
|
} else {
|
||||||
|
await this.processBase64Zip(base64);
|
||||||
|
}
|
||||||
|
} catch(_) {
|
||||||
Events.fire('notify-user', 'Clipboard content is incorrect.');
|
Events.fire('notify-user', 'Clipboard content is incorrect.');
|
||||||
console.log("Clipboard content is incorrect.")
|
console.log("Clipboard content is incorrect.")
|
||||||
}).finally(_ => {
|
|
||||||
this.hide();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
processBase64Text(base64Text){
|
processBase64Text(base64Text){
|
||||||
@@ -1289,6 +1388,8 @@ class Base64ZipDialog extends Dialog {
|
|||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.clearBrowserHistory();
|
this.clearBrowserHistory();
|
||||||
|
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
|
||||||
|
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
|
||||||
super.hide();
|
super.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1319,9 +1420,9 @@ class Notifications {
|
|||||||
this.$button.removeAttribute('hidden');
|
this.$button.removeAttribute('hidden');
|
||||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||||
}
|
}
|
||||||
// Todo: fix Notifications
|
|
||||||
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
||||||
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
||||||
|
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestPermission() {
|
_requestPermission() {
|
||||||
@@ -1394,8 +1495,29 @@ class Notifications {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_requestNotification(request, peerId) {
|
||||||
|
if (document.visibilityState !== 'visible') {
|
||||||
|
let imagesOnly = true;
|
||||||
|
for(let i=0; i<request.header.length; i++) {
|
||||||
|
if (request.header[i].mime.split('/')[0] !== 'image') {
|
||||||
|
imagesOnly = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let descriptor;
|
||||||
|
if (request.header.length > 1) {
|
||||||
|
descriptor = imagesOnly ? ' images' : ' files';
|
||||||
|
} else {
|
||||||
|
descriptor = imagesOnly ? ' image' : ' file';
|
||||||
|
}
|
||||||
|
let displayName = $(peerId).querySelector('.name').textContent
|
||||||
|
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
|
||||||
|
const notification = this._notify(title, 'Click to show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_download(notification) {
|
_download(notification) {
|
||||||
$('share-or-download').click();
|
$('download-btn').click();
|
||||||
notification.close();
|
notification.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1724,6 +1846,23 @@ class PersistentStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Broadcast {
|
||||||
|
constructor() {
|
||||||
|
this.bc = new BroadcastChannel('pairdrop');
|
||||||
|
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||||
|
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
_broadcastMessage(message) {
|
||||||
|
this.bc.postMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMessage(e) {
|
||||||
|
console.log('Broadcast message received:', e.data)
|
||||||
|
Events.fire(e.data.type, e.data.detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PairDrop {
|
class PairDrop {
|
||||||
constructor() {
|
constructor() {
|
||||||
Events.on('load', _ => {
|
Events.on('load', _ => {
|
||||||
@@ -1743,6 +1882,7 @@ class PairDrop {
|
|||||||
const webShareTargetUI = new WebShareTargetUI();
|
const webShareTargetUI = new WebShareTargetUI();
|
||||||
const webFileHandlersUI = new WebFileHandlersUI();
|
const webFileHandlersUI = new WebFileHandlersUI();
|
||||||
const noSleepUI = new NoSleepUI();
|
const noSleepUI = new NoSleepUI();
|
||||||
|
const broadCast = new Broadcast();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.3.0';
|
const cacheVersion = 'v1.4.4';
|
||||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
@@ -477,6 +477,7 @@ x-peer.ws-peer .highlight-wrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-descriptor {
|
.device-descriptor {
|
||||||
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +560,7 @@ footer {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: color 300ms;
|
transition: color 300ms;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .logo {
|
footer .logo {
|
||||||
@@ -583,6 +585,39 @@ footer .font-body2 {
|
|||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#display-name {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
max-width: 15em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: text;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-bottom: -6px;
|
||||||
|
padding-right: 0.3rem;
|
||||||
|
padding-left: 0.3em;
|
||||||
|
padding-bottom: 0.1rem;
|
||||||
|
border-radius: 1.3rem/30%;
|
||||||
|
border-right: solid 1rem transparent;
|
||||||
|
border-left: solid 1rem transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
background-color: rgba(var(--text-color), 43%);
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-pen {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
position: relative;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dialog */
|
/* Dialog */
|
||||||
|
|
||||||
x-dialog x-background {
|
x-dialog x-background {
|
||||||
@@ -782,10 +817,29 @@ x-dialog .dialog-subheader {
|
|||||||
margin: auto -24px;
|
margin: auto -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-btn {
|
#base64-paste-btn,
|
||||||
|
#base64-paste-dialog .textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
border: solid 12px #438cff;
|
border: solid 12px #438cff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog .textarea {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog .textarea::before {
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
content: attr(placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-dialog button {
|
#base64-paste-dialog button {
|
||||||
@@ -1021,11 +1075,11 @@ button::-moz-focus-inner {
|
|||||||
x-toast {
|
x-toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
bottom: 24px;
|
top: 50px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 344px;
|
max-width: 344px;
|
||||||
background-color: #323232;
|
background-color: rgb(var(--text-color));
|
||||||
color: rgba(255, 255, 255, 0.95);
|
color: rgb(var(--bg-color));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
@@ -1039,7 +1093,7 @@ x-toast {
|
|||||||
|
|
||||||
x-toast:not([show]):not(:hover) {
|
x-toast:not([show]):not(:hover) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(100px);
|
transform: translateY(-100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user