implement device pairing via 6-digit code and qr-code
This commit is contained in:
parent
e559aecde7
commit
3c07a4199b
11 changed files with 1098 additions and 195 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ node_modules
|
|||
.DS_Store
|
||||
fqdn.env
|
||||
/docker/certs
|
||||
qrcode-svg/
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#Todo: fix turn server
|
||||
version: "3"
|
||||
services:
|
||||
node:
|
||||
|
|
321
index.js
321
index.js
|
@ -1,6 +1,8 @@
|
|||
var process = require('process')
|
||||
var crypto = require('crypto')
|
||||
var {spawn} = require('child_process')
|
||||
var net = require('net')
|
||||
|
||||
// Handle SIGINT
|
||||
process.on('SIGINT', () => {
|
||||
console.info("SIGINT Received, exiting...")
|
||||
|
@ -87,6 +89,7 @@ class SnapdropServer {
|
|||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||
|
||||
this._rooms = {};
|
||||
this._roomSecrets = {};
|
||||
|
||||
console.log('Snapdrop is running on port', port);
|
||||
}
|
||||
|
@ -94,7 +97,8 @@ class SnapdropServer {
|
|||
_onConnection(peer) {
|
||||
this._joinRoom(peer);
|
||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||
peer.socket.on('error', console.error);
|
||||
peer.socket.onerror = e => console.error(e);
|
||||
peer.socket.onclose = _ => console.log('disconnect');
|
||||
this._keepAlive(peer);
|
||||
|
||||
// send displayName
|
||||
|
@ -118,74 +122,270 @@ class SnapdropServer {
|
|||
|
||||
switch (message.type) {
|
||||
case 'disconnect':
|
||||
this._leaveRoom(sender);
|
||||
this._onDisconnect(sender);
|
||||
break;
|
||||
case 'pong':
|
||||
sender.lastBeat = Date.now();
|
||||
break;
|
||||
case 'room-secrets':
|
||||
this._onRoomSecrets(sender, message);
|
||||
break;
|
||||
case 'room-secret-deleted':
|
||||
this._onRoomSecretDeleted(sender, message);
|
||||
break;
|
||||
case 'room-secrets-cleared':
|
||||
this._onRoomSecretsCleared(sender, message);
|
||||
break;
|
||||
case 'pair-device-initiate':
|
||||
this._onPairDeviceInitiate(sender);
|
||||
break;
|
||||
case 'pair-device-join':
|
||||
this._onPairDeviceJoin(sender, message);
|
||||
break;
|
||||
case 'pair-device-cancel':
|
||||
this._onPairDeviceCancel(sender);
|
||||
break;
|
||||
case 'resend-peers':
|
||||
this._notifyPeers(sender);
|
||||
break;
|
||||
case 'signal':
|
||||
this._onSignal(sender, message);
|
||||
}
|
||||
}
|
||||
|
||||
_onSignal(sender, message) {
|
||||
const room = message.roomType === 'ip' ? sender.ip : message.roomSecret;
|
||||
|
||||
// relay message to recipient
|
||||
if (message.to && this._rooms[sender.ip]) {
|
||||
const recipientId = message.to; // TODO: sanitize
|
||||
const recipient = this._rooms[sender.ip][recipientId];
|
||||
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
|
||||
const recipientId = message.to;
|
||||
const recipient = this._rooms[room][recipientId];
|
||||
delete message.to;
|
||||
// add sender id
|
||||
message.sender = sender.id;
|
||||
this._send(recipient, message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_joinRoom(peer) {
|
||||
// if room doesn't exist, create it
|
||||
if (!this._rooms[peer.ip]) {
|
||||
this._rooms[peer.ip] = {};
|
||||
_onDisconnect(sender) {
|
||||
this._leaveRoom(sender);
|
||||
this._leaveAllSecretRooms(sender);
|
||||
this._removeRoomKey(sender.roomKey);
|
||||
sender.roomKey = null;
|
||||
}
|
||||
|
||||
_onRoomSecrets(sender, message) {
|
||||
const roomSecrets = message.roomSecrets.filter(roomSecret => {
|
||||
return /^[\x00-\x7F]{64}$/.test(roomSecret);
|
||||
})
|
||||
this._joinSecretRooms(sender, roomSecrets);
|
||||
}
|
||||
|
||||
_onRoomSecretDeleted(sender, message) {
|
||||
this._deleteSecretRoom(sender, message.roomSecret)
|
||||
}
|
||||
|
||||
_onRoomSecretsCleared(sender, message) {
|
||||
for (let i = 0; i<message.roomSecrets.length; i++) {
|
||||
this._deleteSecretRoom(sender, message.roomSecrets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_deleteSecretRoom(sender, roomSecret) {
|
||||
const room = this._rooms[roomSecret];
|
||||
if (room) {
|
||||
for (const peerId in room) {
|
||||
const peer = room[peerId];
|
||||
this._leaveRoom(peer, 'secret', roomSecret);
|
||||
this._send(peer, {
|
||||
type: 'secret-room-deleted',
|
||||
roomSecret: roomSecret,
|
||||
});
|
||||
}
|
||||
}
|
||||
this._notifyPeers(sender);
|
||||
}
|
||||
|
||||
getRandomString(length) {
|
||||
let string = "";
|
||||
while (string.length < length) {
|
||||
let arr = new Uint16Array(length);
|
||||
crypto.webcrypto.getRandomValues(arr);
|
||||
arr = Array.apply([], arr); /* turn into non-typed array */
|
||||
arr = arr.map(function (r) {
|
||||
return r % 128
|
||||
})
|
||||
arr = arr.filter(function (r) {
|
||||
/* strip non-printables: if we transform into desirable range we have a propability bias, so I suppose we better skip this character */
|
||||
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
|
||||
});
|
||||
string += String.fromCharCode.apply(String, arr);
|
||||
}
|
||||
return string.substring(0, length)
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate(sender) {
|
||||
let roomSecret = this.getRandomString(64);
|
||||
let roomKey = this._createRoomKey(sender, roomSecret);
|
||||
sender.roomKey = roomKey;
|
||||
this._send(sender, {
|
||||
type: 'pair-device-initiated',
|
||||
roomSecret: roomSecret,
|
||||
roomKey: roomKey
|
||||
});
|
||||
this._joinRoom(sender, 'secret', roomSecret);
|
||||
}
|
||||
|
||||
_onPairDeviceJoin(sender, message) {
|
||||
if (sender.roomKeyRate >= 10) {
|
||||
this._send(sender, { type: 'pair-device-join-key-rate-limit' });
|
||||
return;
|
||||
}
|
||||
sender.roomKeyRate += 1;
|
||||
setTimeout(_ => sender.roomKeyRate -= 1, 10000);
|
||||
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
|
||||
this._send(sender, { type: 'pair-device-join-key-invalid' });
|
||||
return;
|
||||
}
|
||||
const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
|
||||
const creator = this._roomSecrets[message.roomKey].creator;
|
||||
this._removeRoomKey(message.roomKey);
|
||||
this._send(sender, {
|
||||
type: 'pair-device-joined',
|
||||
roomSecret: roomSecret,
|
||||
});
|
||||
this._send(creator, {
|
||||
type: 'pair-device-joined',
|
||||
roomSecret: roomSecret,
|
||||
});
|
||||
this._joinRoom(sender, 'secret', roomSecret);
|
||||
this._removeRoomKey(sender.roomKey);
|
||||
}
|
||||
|
||||
_onPairDeviceCancel(sender) {
|
||||
if (sender.roomKey) {
|
||||
this._send(sender, {
|
||||
type: 'pair-device-canceled',
|
||||
roomKey: sender.roomKey,
|
||||
});
|
||||
this._removeRoomKey(sender.roomKey);
|
||||
}
|
||||
}
|
||||
|
||||
_createRoomKey(creator, roomSecret) {
|
||||
let roomKey;
|
||||
do {
|
||||
// get randomInt until keyRoom not occupied
|
||||
roomKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
|
||||
} while (roomKey in this._roomSecrets)
|
||||
|
||||
this._roomSecrets[roomKey] = {
|
||||
roomSecret: roomSecret,
|
||||
creator: creator
|
||||
}
|
||||
|
||||
return roomKey;
|
||||
}
|
||||
|
||||
_removeRoomKey(roomKey) {
|
||||
if (roomKey in this._roomSecrets) {
|
||||
this._roomSecrets[roomKey].creator.roomKey = null
|
||||
delete this._roomSecrets[roomKey];
|
||||
}
|
||||
}
|
||||
|
||||
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
|
||||
// if room doesn't exist, create it
|
||||
if (!this._rooms[room]) {
|
||||
this._rooms[room] = {};
|
||||
}
|
||||
|
||||
this._notifyPeers(peer, roomType, roomSecret);
|
||||
|
||||
// add peer to room
|
||||
this._rooms[room][peer.id] = peer;
|
||||
// add secret to peer
|
||||
if (roomType === 'secret') {
|
||||
peer.addRoomSecret(roomSecret);
|
||||
}
|
||||
}
|
||||
|
||||
_leaveRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
|
||||
if (!this._rooms[room] || !this._rooms[room][peer.id]) return;
|
||||
this._cancelKeepAlive(this._rooms[room][peer.id]);
|
||||
|
||||
// delete the peer
|
||||
delete this._rooms[room][peer.id];
|
||||
|
||||
if (roomType === 'ip') {
|
||||
peer.socket.terminate();
|
||||
}
|
||||
|
||||
//if room is empty, delete the room
|
||||
if (!Object.keys(this._rooms[room]).length) {
|
||||
delete this._rooms[room];
|
||||
} else {
|
||||
// notify all other peers
|
||||
for (const otherPeerId in this._rooms[room]) {
|
||||
const otherPeer = this._rooms[room][otherPeerId];
|
||||
this._send(otherPeer, {
|
||||
type: 'peer-left',
|
||||
peerId: peer.id,
|
||||
roomType: roomType,
|
||||
roomSecret: roomSecret
|
||||
});
|
||||
}
|
||||
}
|
||||
//remove secret from peer
|
||||
if (roomType === 'secret') {
|
||||
peer.removeRoomSecret(roomSecret);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeers(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
if (!this._rooms[room]) return;
|
||||
|
||||
// notify all other peers
|
||||
for (const otherPeerId in this._rooms[peer.ip]) {
|
||||
for (const otherPeerId in this._rooms[room]) {
|
||||
if (otherPeerId === peer.id) continue;
|
||||
const otherPeer = this._rooms[peer.ip][otherPeerId];
|
||||
const otherPeer = this._rooms[room][otherPeerId];
|
||||
this._send(otherPeer, {
|
||||
type: 'peer-joined',
|
||||
peer: peer.getInfo()
|
||||
peer: peer.getInfo(),
|
||||
roomType: roomType,
|
||||
roomSecret: roomSecret
|
||||
});
|
||||
}
|
||||
|
||||
// notify peer about the other peers
|
||||
const otherPeers = [];
|
||||
for (const otherPeerId in this._rooms[peer.ip]) {
|
||||
for (const otherPeerId in this._rooms[room]) {
|
||||
if (otherPeerId === peer.id) continue;
|
||||
otherPeers.push(this._rooms[peer.ip][otherPeerId].getInfo());
|
||||
otherPeers.push(this._rooms[room][otherPeerId].getInfo());
|
||||
}
|
||||
|
||||
this._send(peer, {
|
||||
type: 'peers',
|
||||
peers: otherPeers
|
||||
peers: otherPeers,
|
||||
roomType: roomType,
|
||||
roomSecret: roomSecret
|
||||
});
|
||||
|
||||
// add peer to room
|
||||
this._rooms[peer.ip][peer.id] = peer;
|
||||
}
|
||||
|
||||
_leaveRoom(peer) {
|
||||
if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return;
|
||||
this._cancelKeepAlive(this._rooms[peer.ip][peer.id]);
|
||||
_joinSecretRooms(peer, roomSecrets) {
|
||||
for (let i=0; i<roomSecrets.length; i++) {
|
||||
this._joinRoom(peer, 'secret', roomSecrets[i])
|
||||
}
|
||||
}
|
||||
|
||||
// delete the peer
|
||||
delete this._rooms[peer.ip][peer.id];
|
||||
|
||||
peer.socket.terminate();
|
||||
//if room is empty, delete the room
|
||||
if (!Object.keys(this._rooms[peer.ip]).length) {
|
||||
delete this._rooms[peer.ip];
|
||||
} else {
|
||||
// notify all other peers
|
||||
for (const otherPeerId in this._rooms[peer.ip]) {
|
||||
const otherPeer = this._rooms[peer.ip][otherPeerId];
|
||||
this._send(otherPeer, { type: 'peer-left', peerId: peer.id });
|
||||
}
|
||||
_leaveAllSecretRooms(peer) {
|
||||
for (const roomSecret in peer.roomSecrets) {
|
||||
this._leaveRoom(peer, 'secret', roomSecret);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,7 +393,7 @@ class SnapdropServer {
|
|||
if (!peer) return;
|
||||
if (this._wss.readyState !== this._wss.OPEN) return;
|
||||
message = JSON.stringify(message);
|
||||
peer.socket.send(message, error => '');
|
||||
peer.socket.send(message);
|
||||
}
|
||||
|
||||
_keepAlive(peer) {
|
||||
|
@ -204,6 +404,7 @@ class SnapdropServer {
|
|||
}
|
||||
if (Date.now() - peer.lastBeat > 2 * timeout) {
|
||||
this._leaveRoom(peer);
|
||||
this._leaveAllSecretRooms(peer);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -242,7 +443,10 @@ class Peer {
|
|||
// for keepalive
|
||||
this.timerId = 0;
|
||||
this.lastBeat = Date.now();
|
||||
console.debug(this.name.displayName)
|
||||
|
||||
this.roomSecrets = [];
|
||||
this.roomKey = null;
|
||||
this.roomKeyRate = 0;
|
||||
}
|
||||
|
||||
_setIP(request) {
|
||||
|
@ -258,7 +462,6 @@ class Peer {
|
|||
if (this.ip === '::1' || this.ip === '::ffff:127.0.0.1' || this.ip === '::1' || this.ipIsPrivate(this.ip)) {
|
||||
this.ip = '127.0.0.1';
|
||||
}
|
||||
console.debug(this.ip)
|
||||
}
|
||||
|
||||
ipIsPrivate(ip) {
|
||||
|
@ -300,10 +503,10 @@ class Peer {
|
|||
|
||||
_setPeerId(request) {
|
||||
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id");
|
||||
if (peer_id && /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(peer_id)) {
|
||||
if (peer_id && Peer.isValidUuid(peer_id)) {
|
||||
this.id = peer_id;
|
||||
} else {
|
||||
this.id = Peer.uuid();
|
||||
this.id = crypto.randomUUID();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -356,31 +559,21 @@ class Peer {
|
|||
}
|
||||
}
|
||||
|
||||
// return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
static uuid() {
|
||||
let uuid = '',
|
||||
ii;
|
||||
for (ii = 0; ii < 32; ii += 1) {
|
||||
switch (ii) {
|
||||
case 8:
|
||||
case 20:
|
||||
uuid += '-';
|
||||
uuid += (Math.random() * 16 | 0).toString(16);
|
||||
break;
|
||||
case 12:
|
||||
uuid += '-';
|
||||
uuid += '4';
|
||||
break;
|
||||
case 16:
|
||||
uuid += '-';
|
||||
uuid += (Math.random() * 4 | 8).toString(16);
|
||||
break;
|
||||
default:
|
||||
uuid += (Math.random() * 16 | 0).toString(16);
|
||||
}
|
||||
static isValidUuid(uuid) {
|
||||
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
|
||||
}
|
||||
|
||||
addRoomSecret(roomSecret) {
|
||||
if (!roomSecret in this.roomSecrets) {
|
||||
this.roomSecrets.push(roomSecret);
|
||||
}
|
||||
return uuid;
|
||||
};
|
||||
}
|
||||
|
||||
removeRoomSecret(roomSecret) {
|
||||
if (roomSecret in this.roomSecrets) {
|
||||
delete this.roomSecrets[roomSecret];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode', {
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="apple-mobile-web-app-capable" content="no">
|
||||
<meta name="apple-mobile-web-app-title" content="Snapdrop">
|
||||
<meta name="application-name" content="Snapdrop">
|
||||
<!-- Descriptions -->
|
||||
<meta name="description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
|
||||
<meta name="keywords" content="File, Transfer, Share, Peer2Peer">
|
||||
|
@ -43,26 +44,37 @@
|
|||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" id="theme" class="icon-button" title="Switch Darkmode/Lightmode" >
|
||||
<a id="theme" class="icon-button" title="Switch Darkmode/Lightmode" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" id="notification" class="icon-button" title="Enable Notifications" hidden>
|
||||
<a id="notification" class="icon-button" title="Enable Notifications" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" id="install" class="icon-button" title="Install Snapdrop" hidden>
|
||||
<a id="install" class="icon-button" title="Install Snapdrop" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="pair-device" class="icon-button" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="clear-pair-devices" class="icon-button" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open Snapdrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message"></x-instructions>
|
||||
<a id="cancelPasteModeBtn" class="button" close hidden style="z-index: 2">Cancel</a>
|
||||
|
@ -71,9 +83,50 @@
|
|||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="displayName" placeholder="The easiest way to transfer data across devices"></div>
|
||||
<div id="displayName" placeholder=" "></div>
|
||||
<div class="font-body2">You can be discovered by everyone on this network</div>
|
||||
</footer>
|
||||
<!-- JoinRoom Dialog -->
|
||||
<x-dialog id="pairDeviceDialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Pair Devices</h2>
|
||||
<div class="center" id="roomKeyQrCode"></div>
|
||||
<h1 class="center" id="roomKey">000 000</h1>
|
||||
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="keyInputContainer">
|
||||
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="">
|
||||
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="">
|
||||
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="">
|
||||
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="">
|
||||
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="">
|
||||
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="">
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="row-reverse space-between">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<x-dialog id="clearDevicesDialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<div class="row-reverse space-between">
|
||||
<button class="button" type="submit">Unpair all devices</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Dialog -->
|
||||
<x-dialog id="receiveDialog">
|
||||
<x-background class="full center">
|
||||
|
@ -211,9 +264,18 @@
|
|||
<symbol id="icon-theme" viewBox="0 0 24 24">
|
||||
<rect fill="none" height="24" width="24"/><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/>
|
||||
</symbol>
|
||||
<symbol id="pair-device-icon" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M579.8 267.7c56.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 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.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.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
|
||||
</symbol>
|
||||
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
|
||||
<!--! 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"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/network.js"></script>
|
||||
<script src="scripts/qrcode.js" async></script>
|
||||
<script src="scripts/ui.js"></script>
|
||||
<script src="scripts/theme.js" async></script>
|
||||
<script src="scripts/clipboard.js" async></script>
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
}],
|
||||
"background_color": "#efefef",
|
||||
"start_url": "/",
|
||||
"display": "minimal-ui",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3367d6",
|
||||
"share_target": {
|
||||
"method":"GET",
|
||||
|
|
|
@ -9,6 +9,13 @@ class ServerConnection {
|
|||
Events.on('pagehide', _ => this._disconnect());
|
||||
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
|
||||
Events.on('reconnect', _ => this._reconnect());
|
||||
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
|
||||
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
|
||||
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
|
||||
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
||||
}
|
||||
|
||||
_connect() {
|
||||
|
@ -25,11 +32,27 @@ class ServerConnection {
|
|||
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
if (!this.firstConnect) {
|
||||
this.firstConnect = true;
|
||||
Events.fire('ws-connected');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
||||
return;
|
||||
}
|
||||
Events.fire('ws-connected');
|
||||
this.send({ type: 'pair-device-initiate' })
|
||||
}
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 200);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
|
@ -37,10 +60,10 @@ class ServerConnection {
|
|||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'peers':
|
||||
Events.fire('peers', msg.peers);
|
||||
Events.fire('peers', msg);
|
||||
break;
|
||||
case 'peer-joined':
|
||||
Events.fire('peer-joined', msg.peer);
|
||||
Events.fire('peer-joined', msg);
|
||||
break;
|
||||
case 'peer-left':
|
||||
Events.fire('peer-left', msg.peerId);
|
||||
|
@ -52,28 +75,61 @@ class ServerConnection {
|
|||
this.send({ type: 'pong' });
|
||||
break;
|
||||
case 'display-name':
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
Events.fire('display-name', msg);
|
||||
this._onDisplayName(msg);
|
||||
break;
|
||||
case 'pair-device-initiated':
|
||||
Events.fire('pair-device-initiated', msg);
|
||||
break;
|
||||
case 'pair-device-joined':
|
||||
Events.fire('pair-device-joined', msg.roomSecret);
|
||||
break;
|
||||
case 'pair-device-join-key-invalid':
|
||||
Events.fire('pair-device-join-key-invalid');
|
||||
break;
|
||||
case 'pair-device-canceled':
|
||||
Events.fire('pair-device-canceled', msg.roomKey);
|
||||
break;
|
||||
case 'pair-device-join-key-rate-limit':
|
||||
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
|
||||
break;
|
||||
case 'secret-room-deleted':
|
||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||
break;
|
||||
default:
|
||||
console.error('WS: unknown message type', msg);
|
||||
}
|
||||
}
|
||||
|
||||
send(message) {
|
||||
send(msg) {
|
||||
if (!this._isConnected()) return;
|
||||
this._socket.send(JSON.stringify(message));
|
||||
this._socket.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
// make peerId persistent when pwa installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
}).catch(e => console.error(e));
|
||||
}
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
if (sessionStorage.getItem('peerId')) {
|
||||
url.searchParams.append('peer_id', sessionStorage.getItem('peerId'))
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = this._peerId();
|
||||
if (peerId) {
|
||||
ws_url.searchParams.append('peer_id', peerId)
|
||||
}
|
||||
return url.toString();
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
_peerId() {
|
||||
return sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
|
@ -81,7 +137,7 @@ class ServerConnection {
|
|||
this._socket.onclose = null;
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnect');
|
||||
Events.fire('ws-disconnected');
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
|
@ -89,7 +145,7 @@ class ServerConnection {
|
|||
Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
Events.fire('ws-disconnect');
|
||||
Events.fire('ws-disconnected');
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
|
@ -117,9 +173,11 @@ class ServerConnection {
|
|||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, peerId) {
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
this._server = serverConnection;
|
||||
this._peerId = peerId;
|
||||
this._roomType = roomType;
|
||||
this._roomSecret = roomSecret;
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
}
|
||||
|
@ -262,8 +320,8 @@ class Peer {
|
|||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId) {
|
||||
super(serverConnection, peerId);
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
|
@ -283,7 +341,7 @@ class RTCPeer extends Peer {
|
|||
this._peerId = peerId;
|
||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onconnectionstatechange = e => this._onConnectionStateChange(e);
|
||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||
}
|
||||
|
||||
|
@ -342,7 +400,7 @@ class RTCPeer extends Peer {
|
|||
this._connect(this._peerId, true); // reopen the channel
|
||||
}
|
||||
|
||||
_onConnectionStateChange(e) {
|
||||
_onConnectionStateChange() {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
|
@ -379,11 +437,15 @@ class RTCPeer extends Peer {
|
|||
_sendSignal(signal) {
|
||||
signal.type = 'signal';
|
||||
signal.to = this._peerId;
|
||||
signal.roomType = this._roomType;
|
||||
signal.roomSecret = this._roomSecret;
|
||||
this._server.send(signal);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// check if channel is open. otherwise create one
|
||||
console.debug("refresh:");
|
||||
console.debug(this._conn);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
this._connect(this._peerId, this._isCaller);
|
||||
}
|
||||
|
@ -397,6 +459,15 @@ class RTCPeer extends Peer {
|
|||
}
|
||||
}
|
||||
|
||||
class WSPeer extends Peer {
|
||||
_send(message) {
|
||||
message.to = this._peerId;
|
||||
message.roomType = this._roomType;
|
||||
message.roomSecret = this._roomSecret;
|
||||
this._server.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
class PeersManager {
|
||||
|
||||
constructor(serverConnection) {
|
||||
|
@ -408,26 +479,40 @@ class PeersManager {
|
|||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('ws-disconnect', _ => this._clearPeers());
|
||||
Events.on('ws-disconnected', _ => this._clearPeers());
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (!this.peers[message.sender]) {
|
||||
this.peers[message.sender] = new RTCPeer(this._server);
|
||||
}
|
||||
this._refreshOrCreatePeer(message.sender, message.roomType, message.roomSecret);
|
||||
this.peers[message.sender].onServerMessage(message);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
peers.forEach(peer => {
|
||||
_refreshOrCreatePeer(id, roomType, roomSecret) {
|
||||
if (!this.peers[id]) {
|
||||
this.peers[id] = new RTCPeer(this._server, undefined, roomType, roomSecret);
|
||||
}else if (this.peers[id]._roomType !== roomType) {
|
||||
this.peers[id]._roomType = roomType;
|
||||
this.peers[id]._roomSecret = roomSecret;
|
||||
}
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
console.debug(msg)
|
||||
msg.peers.forEach(peer => {
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].refresh();
|
||||
if (this.peers[peer.id].roomType === msg.roomType) {
|
||||
this.peers[peer.id].refresh();
|
||||
} else {
|
||||
this.peers[peer.id].roomType = msg.roomType;
|
||||
this.peers[peer.id].roomSecret = msg.roomSecret;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (window.isRtcSupported && peer.rtcSupported) {
|
||||
this.peers[peer.id] = new RTCPeer(this._server, peer.id);
|
||||
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
|
||||
} else {
|
||||
this.peers[peer.id] = new WSPeer(this._server, peer.id);
|
||||
this.peers[peer.id] = new WSPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -444,8 +529,8 @@ class PeersManager {
|
|||
this.peers[message.to].sendText(message.text);
|
||||
}
|
||||
|
||||
_onPeerJoined(peer) {
|
||||
this._onMessage(peer.id);
|
||||
_onPeerJoined(message) {
|
||||
this._onMessage({sender: message.peer.id, roomType: message.roomType, roomSecret: message.roomSecret});
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
|
@ -461,12 +546,14 @@ class PeersManager {
|
|||
Object.keys(this.peers).forEach(peerId => this._onPeerLeft(peerId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WSPeer extends Peer {
|
||||
_send(message) {
|
||||
message.to = this._peerId;
|
||||
this._server.send(message);
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer._roomSecret === roomSecret) {
|
||||
this._onPeerLeft(peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -538,7 +625,6 @@ class FileDigester {
|
|||
unchunk(chunk) {
|
||||
this._buffer.push(chunk);
|
||||
this._bytesReceived += chunk.byteLength || chunk.size;
|
||||
const totalChunks = this._buffer.length;
|
||||
this.progress = this._bytesReceived / this._size;
|
||||
if (isNaN(this.progress)) this.progress = 1
|
||||
|
||||
|
@ -571,7 +657,7 @@ class Events {
|
|||
|
||||
RTCPeer.config = {
|
||||
'sdpSemantics': 'unified-plan',
|
||||
iceServers: [
|
||||
'iceServers': [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
},
|
||||
|
|
2
public/scripts/qrcode.js
Normal file
2
public/scripts/qrcode.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -8,27 +8,29 @@
|
|||
// Get the user's theme preference from local storage, if it's available
|
||||
const currentTheme = localStorage.getItem('theme');
|
||||
// If the user's preference in localStorage is dark...
|
||||
if (currentTheme == 'dark') {
|
||||
if (currentTheme === 'dark') {
|
||||
// ...let's toggle the .dark-theme class on the body
|
||||
document.body.classList.toggle('dark-theme');
|
||||
// Otherwise, if the user's preference in localStorage is light...
|
||||
} else if (currentTheme == 'light') {
|
||||
} else if (currentTheme === 'light') {
|
||||
// ...let's toggle the .light-theme class on the body
|
||||
document.body.classList.toggle('light-theme');
|
||||
}
|
||||
|
||||
// Listen for a click on the button
|
||||
btnTheme.addEventListener('click', function() {
|
||||
btnTheme.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// If the user's OS setting is dark and matches our .dark-theme class...
|
||||
let theme;
|
||||
if (prefersDarkScheme.matches) {
|
||||
// ...then toggle the light mode class
|
||||
document.body.classList.toggle('light-theme');
|
||||
// ...but use .dark-theme if the .light-theme class is already on the body,
|
||||
var theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
|
||||
theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
|
||||
} else {
|
||||
// Otherwise, let's do the same thing, but for .dark-theme
|
||||
document.body.classList.toggle('dark-theme');
|
||||
var theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||
}
|
||||
// Finally, let's save the current preference to localStorage to keep using it
|
||||
localStorage.setItem('theme', theme);
|
||||
|
|
|
@ -25,30 +25,56 @@ class PeersUI {
|
|||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('file-progress', e => this._onFileProgress(e.detail));
|
||||
Events.on('paste', e => this._onPaste(e));
|
||||
Events.on('ws-disconnect', _ => this._clearPeers());
|
||||
Events.on('ws-disconnected', _ => this._clearPeers());
|
||||
Events.on('secret-room-deleted', _ => this._clearPeers('secret'));
|
||||
this.peers = {};
|
||||
}
|
||||
|
||||
_onPeerJoined(peer) {
|
||||
if (this.peers[peer.id]) return; // peer already exists
|
||||
_onPeerJoined(msg) {
|
||||
this._joinPeer(msg.peer, msg.roomType, msg.roomType);
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomType = roomType;
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].roomType = peer.roomType;
|
||||
this._redrawPeer(peer);
|
||||
return; // peer already exists
|
||||
}
|
||||
this.peers[peer.id] = peer;
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
if(this.peers[peerId])
|
||||
if(this.peers[peerId] && !$(peerId))
|
||||
new PeerUI(this.peers[peerId]);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
_redrawPeer(peer) {
|
||||
const peerNode = $(peer.id);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peerNode.classList.add(`type-${peer.roomType}`)
|
||||
}
|
||||
|
||||
_redrawPeers() {
|
||||
const peers = this._getPeers();
|
||||
this._clearPeers();
|
||||
peers.forEach(peer => this._onPeerJoined(peer));
|
||||
peers.forEach(peer => {
|
||||
this._joinPeer(peer, peer.roomType, peer.roomSecret);
|
||||
this._onPeerConnected(peer.id);
|
||||
});
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
msg.peers.forEach(peer => this._joinPeer(peer, msg.roomType, msg.roomSecret));
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
setTimeout(e => window.animateBackground(true), 1750); // Start animation again
|
||||
setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
|
@ -56,6 +82,16 @@ class PeersUI {
|
|||
delete this.peers[peerId];
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
console.debug(peer);
|
||||
if (peer.roomSecret === roomSecret) {
|
||||
this._onPeerLeft(peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onFileProgress(progress) {
|
||||
const peerId = progress.sender || progress.recipient;
|
||||
const $peer = $(peerId);
|
||||
|
@ -63,10 +99,17 @@ class PeersUI {
|
|||
$peer.ui.setProgress(progress.progress);
|
||||
}
|
||||
|
||||
_clearPeers() {
|
||||
const $peers = $$('x-peers').innerHTML = '';
|
||||
Object.keys(this.peers).forEach(peerId => delete this.peers[peerId]);
|
||||
setTimeout(e => window.animateBackground(true), 1750); // Start animation again
|
||||
_clearPeers(roomType = 'all') {
|
||||
for (const peerId in this.peers) {
|
||||
if (roomType === 'all' || this.peers[peerId].roomType === roomType) {
|
||||
const peerNode = $(peerId);
|
||||
if(peerNode) peerNode.remove();
|
||||
delete this.peers[peerId];
|
||||
}
|
||||
}
|
||||
if ($$('x-peers').innerHTML === '') {
|
||||
setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
}
|
||||
|
||||
_getPeers() {
|
||||
|
@ -76,7 +119,9 @@ class PeersUI {
|
|||
peers.push({
|
||||
id: peersNode.id,
|
||||
name: peersNode.name,
|
||||
rtcSupported: peersNode.rtcSupported
|
||||
rtcSupported: peersNode.rtcSupported,
|
||||
roomType: peersNode.roomType,
|
||||
roomSecret: peersNode.roomSecret
|
||||
})
|
||||
});
|
||||
return peers;
|
||||
|
@ -103,7 +148,6 @@ class PeersUI {
|
|||
descriptor = files[0].name;
|
||||
noPeersMessage = `Open Snapdrop on other devices to send <i>${descriptor}</i> directly`;
|
||||
} else if (files.length > 1) {
|
||||
console.debug(files);
|
||||
descriptor = `${files.length} files`;
|
||||
noPeersMessage = `Open Snapdrop on other devices to send ${descriptor} directly`;
|
||||
} else if (text.length > 0) {
|
||||
|
@ -132,7 +176,7 @@ class PeersUI {
|
|||
window.pasteMode.activated = true;
|
||||
console.log('Paste mode activated.')
|
||||
|
||||
this._onPeers(this._getPeers());
|
||||
this._redrawPeers();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,7 +203,7 @@ class PeersUI {
|
|||
cancelPasteModeBtn.removeEventListener('click', this._cancelPasteMode);
|
||||
cancelPasteModeBtn.setAttribute('hidden', "");
|
||||
|
||||
this._onPeers(this._getPeers());
|
||||
this._redrawPeers();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,22 +257,23 @@ class PeerUI {
|
|||
|
||||
constructor(peer) {
|
||||
this._peer = peer;
|
||||
this._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._initDom();
|
||||
this._bindListeners(this.$el);
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
setTimeout(e => window.animateBackground(false), 1750); // Stop animation
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
const el = document.createElement('x-peer');
|
||||
el.id = this._peer.id;
|
||||
el.name = this._peer.name;
|
||||
el.rtcSupported = this._peer.rtcSupported;
|
||||
el.innerHTML = this.html();
|
||||
el.ui = this;
|
||||
el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
el.querySelector('.name').textContent = this._displayName();
|
||||
el.querySelector('.device-name').textContent = this._deviceName();
|
||||
el.classList.add(`type-${this._roomType}`);
|
||||
this.$el = el;
|
||||
this.$progress = el.querySelector('.progress');
|
||||
}
|
||||
|
@ -241,7 +286,7 @@ class PeerUI {
|
|||
el.addEventListener('dragleave', e => this._onDragEnd(e));
|
||||
el.addEventListener('dragover', e => this._onDragOver(e));
|
||||
el.addEventListener('contextmenu', e => this._onRightClick(e));
|
||||
el.addEventListener('touchstart', e => this._onTouchStart(e));
|
||||
el.addEventListener('touchstart', _ => this._onTouchStart());
|
||||
el.addEventListener('touchend', e => this._onTouchEnd(e));
|
||||
// prevent browser's default file drop behavior
|
||||
Events.on('dragover', e => e.preventDefault());
|
||||
|
@ -329,7 +374,7 @@ class PeerUI {
|
|||
Events.fire('text-recipient', this._peer.id);
|
||||
}
|
||||
|
||||
_onTouchStart(e) {
|
||||
_onTouchStart() {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
}
|
||||
|
@ -348,8 +393,9 @@ class PeerUI {
|
|||
class Dialog {
|
||||
constructor(id) {
|
||||
this.$el = $(id);
|
||||
this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', e => this.hide()))
|
||||
this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', _ => this.hide()))
|
||||
this.$autoFocus = this.$el.querySelector('[autofocus]');
|
||||
Events.on('ws-disconnected', _ => this.hide());
|
||||
}
|
||||
|
||||
show() {
|
||||
|
@ -359,8 +405,10 @@ class Dialog {
|
|||
|
||||
hide() {
|
||||
this.$el.removeAttribute('show');
|
||||
document.activeElement.blur();
|
||||
window.blur();
|
||||
if (this.$autoFocus) {
|
||||
document.activeElement.blur();
|
||||
window.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -419,7 +467,7 @@ class ReceiveDialog extends Dialog {
|
|||
// fallback for iOS
|
||||
$a.target = '_blank';
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => $a.href = reader.result;
|
||||
reader.onload = _ => $a.href = reader.result;
|
||||
reader.readAsDataURL(file.blob);
|
||||
}
|
||||
|
||||
|
@ -448,10 +496,254 @@ class ReceiveDialog extends Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pairDeviceDialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
this.$roomKey = this.$el.querySelector('#roomKey');
|
||||
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$footerInstructions = $$('footer>.font-body2');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
||||
|
||||
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('keyup', _ => this.evaluateRoomKeyChars()));
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('keydown', e => this._onCharsKeyDown(e)));
|
||||
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
Events.on('ws-connected', _ => this._onWsConnected());
|
||||
Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail));
|
||||
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail));
|
||||
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
|
||||
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
|
||||
Events.on('room-secret-delete', e => this._onRoomSecretDelete(e.detail))
|
||||
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
this.$el.addEventListener('paste', e => this._onPaste(e));
|
||||
|
||||
this.evaluateRoomKeyChars();
|
||||
this.evaluateUrlAttributes();
|
||||
}
|
||||
|
||||
_onCharsInput(e) {
|
||||
e.target.value = e.target.value.replace(/\D/g,'');
|
||||
if (!e.target.value) return;
|
||||
let nextSibling = e.target.nextElementSibling;
|
||||
if (nextSibling) {
|
||||
e.preventDefault();
|
||||
nextSibling.focus();
|
||||
nextSibling.select();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
this.hide();
|
||||
this._pairDeviceCancel();
|
||||
}
|
||||
if (this.$el.attributes["show"] && e.code === "keyO") {
|
||||
this._onRoomSecretDelete()
|
||||
}
|
||||
}
|
||||
|
||||
_onCharsKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
this.hide();
|
||||
this._pairDeviceCancel();
|
||||
}
|
||||
let previousSibling = e.target.previousElementSibling;
|
||||
let nextSibling = e.target.nextElementSibling;
|
||||
if (e.key === "Backspace" && previousSibling && !e.target.value) {
|
||||
previousSibling.value = '';
|
||||
previousSibling.focus();
|
||||
} else if (e.key === "ArrowRight" && nextSibling) {
|
||||
e.preventDefault();
|
||||
nextSibling.focus();
|
||||
nextSibling.select();
|
||||
} else if (e.key === "ArrowLeft" && previousSibling) {
|
||||
e.preventDefault();
|
||||
previousSibling.focus();
|
||||
previousSibling.select();
|
||||
}
|
||||
}
|
||||
|
||||
_onPaste(e) {
|
||||
e.preventDefault();
|
||||
let num = e.clipboardData.getData("Text").replace(/\D/g,'').substring(0, 6);
|
||||
for (let i = 0; i < num.length; i++) {
|
||||
document.activeElement.value = num.charAt(i);
|
||||
let nextSibling = document.activeElement.nextElementSibling;
|
||||
if (!nextSibling) break;
|
||||
nextSibling.focus();
|
||||
nextSibling.select();
|
||||
}
|
||||
}
|
||||
|
||||
evaluateRoomKeyChars() {
|
||||
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
|
||||
this.$submitBtn.setAttribute("disabled", "");
|
||||
} else {
|
||||
this.inputRoomKey = "";
|
||||
this.$inputRoomKeyChars.forEach(el => {
|
||||
this.inputRoomKey += el.value;
|
||||
})
|
||||
this.$submitBtn.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
evaluateUrlAttributes() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('room_key')) {
|
||||
this._pairDeviceJoin(urlParams.get('room_key'));
|
||||
window.history.replaceState({}, "title**", '/'); //remove room_key from url
|
||||
}
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_pairDeviceInitiate() {
|
||||
Events.fire('pair-device-initiate');
|
||||
}
|
||||
|
||||
_pairDeviceInitiated(msg) {
|
||||
this.roomKey = msg.roomKey;
|
||||
this.roomSecret = msg.roomSecret;
|
||||
this.$roomKey.innerText = `${this.roomKey.substring(0,3)} ${this.roomKey.substring(3,6)}`
|
||||
// Display the QR code for the url
|
||||
const qr = new QRCode({
|
||||
content: this._getShareRoomURL(),
|
||||
width: 80,
|
||||
height: 80,
|
||||
padding: 0,
|
||||
background: "transparent",
|
||||
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
|
||||
ecl: "L",
|
||||
join: true
|
||||
});
|
||||
this.$qrCode.innerHTML = qr.svg();
|
||||
this.show();
|
||||
}
|
||||
|
||||
_getShareRoomURL() {
|
||||
let url = new URL(location.href);
|
||||
url.searchParams.append('room_key', this.roomKey)
|
||||
return url.href;
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
|
||||
_pairDeviceJoin(roomKey) {
|
||||
if (/^\d{6}$/g.test(roomKey)) {
|
||||
roomKey = roomKey.substring(0,6);
|
||||
Events.fire('pair-device-join', roomKey);
|
||||
let lastChar = this.$inputRoomKeyChars[5];
|
||||
lastChar.focus();
|
||||
lastChar.select();
|
||||
}
|
||||
}
|
||||
|
||||
_pairDeviceJoined(roomSecret) {
|
||||
this.hide();
|
||||
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
|
||||
Events.fire('notify-user', 'Devices paired successfully.')
|
||||
this._evaluateNumberRoomSecrets()
|
||||
}).finally(_ => {
|
||||
this._cleanUp()
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_pairDeviceJoinKeyInvalid() {
|
||||
Events.fire('notify-user', 'Key not valid')
|
||||
}
|
||||
|
||||
_pairDeviceCancel() {
|
||||
this.hide();
|
||||
this._cleanUp();
|
||||
Events.fire('pair-device-cancel');
|
||||
}
|
||||
|
||||
_pairDeviceCanceled(roomKey) {
|
||||
Events.fire('notify-user', `Key ${roomKey} invalidated.`)
|
||||
}
|
||||
|
||||
_cleanUp() {
|
||||
this.roomSecret = null;
|
||||
this.roomKey = null;
|
||||
this.inputRoomKey = '';
|
||||
this.$inputRoomKeyChars.forEach(el => el.value = '');
|
||||
}
|
||||
|
||||
_onRoomSecretDelete(roomSecret) {
|
||||
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
|
||||
console.debug("then secret: " + roomSecret)
|
||||
Events.fire('room-secret-deleted', roomSecret)
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_onClearRoomSecrets() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets-cleared', roomSecrets);
|
||||
PersistentStorage.clearRoomSecrets().finally(_ => {
|
||||
Events.fire('notify-user', 'All Devices unpaired.')
|
||||
this._evaluateNumberRoomSecrets();
|
||||
})
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch(e => console.error(e));
|
||||
}
|
||||
|
||||
_evaluateNumberRoomSecrets() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
if (roomSecrets.length > 0) {
|
||||
this.$clearSecretsBtn.removeAttribute('hidden');
|
||||
this.$footerInstructions.innerText = "You can be discovered on this network and by paired devices";
|
||||
} else {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructions.innerText = "You can be discovered by everyone on this network";
|
||||
}
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clearDevicesDialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail))
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail));
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
const button = this.$el.querySelector('form');
|
||||
button.addEventListener('submit', e => this._send(e));
|
||||
|
@ -490,6 +782,7 @@ class SendTextDialog extends Dialog {
|
|||
to: this._recipient,
|
||||
text: this.$text.innerText
|
||||
});
|
||||
this.$text.innerText = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -545,7 +838,6 @@ class Toast extends Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class Notifications {
|
||||
|
||||
constructor() {
|
||||
|
@ -556,7 +848,7 @@ class Notifications {
|
|||
if (Notification.permission !== 'granted') {
|
||||
this.$button = $('notification');
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', e => this._requestPermission());
|
||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||
}
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text));
|
||||
Events.on('file-received', e => this._downloadNotification(e.detail.name));
|
||||
|
@ -568,7 +860,7 @@ class Notifications {
|
|||
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
|
||||
return;
|
||||
}
|
||||
this._notify('Even more snappy sharing!');
|
||||
this._notify('Notifications enabled.');
|
||||
this.$button.setAttribute('hidden', 1);
|
||||
});
|
||||
}
|
||||
|
@ -603,10 +895,10 @@ class Notifications {
|
|||
if (document.visibilityState !== 'visible') {
|
||||
if (isURL(message)) {
|
||||
const notification = this._notify(message, 'Click to open link');
|
||||
this._bind(notification, e => window.open(message, '_blank', null, true));
|
||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||
} else {
|
||||
const notification = this._notify(message, 'Click to copy text');
|
||||
this._bind(notification, e => this._copyText(message, notification));
|
||||
this._bind(notification, _ => this._copyText(message, notification));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -615,7 +907,7 @@ class Notifications {
|
|||
if (document.visibilityState !== 'visible') {
|
||||
const notification = this._notify(message, 'Click to download');
|
||||
if (!window.isDownloadSupported) return;
|
||||
this._bind(notification, e => this._download(notification));
|
||||
this._bind(notification, _ => this._download(notification));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -625,14 +917,18 @@ class Notifications {
|
|||
}
|
||||
|
||||
_copyText(message, notification) {
|
||||
notification.close();
|
||||
if (!navigator.clipboard.writeText(message)) return;
|
||||
this._notify('Copied text to clipboard');
|
||||
if (navigator.clipboard.writeText(message)) {
|
||||
notification.close();
|
||||
this._notify('Copied text to clipboard');
|
||||
} else {
|
||||
this._notify('Writing to clipboard failed. Copy manually!');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_bind(notification, handler) {
|
||||
if (notification.then) {
|
||||
notification.then(e => serviceWorker.getNotifications().then(notifications => {
|
||||
notification.then(_ => serviceWorker.getNotifications().then(notifications => {
|
||||
serviceWorker.addEventListener('notificationclick', handler);
|
||||
}));
|
||||
} else {
|
||||
|
@ -641,7 +937,6 @@ class Notifications {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class NetworkStatusUI {
|
||||
|
||||
constructor() {
|
||||
|
@ -658,6 +953,10 @@ class NetworkStatusUI {
|
|||
}
|
||||
|
||||
_showOnlineMessage() {
|
||||
if (!this.firstConnect) {
|
||||
this.firstConnect = true;
|
||||
return;
|
||||
}
|
||||
Events.fire('notify-user', 'You are back online');
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
@ -682,16 +981,193 @@ class WebShareTargetUI {
|
|||
}
|
||||
}
|
||||
|
||||
class PersistentStorage {
|
||||
constructor() {
|
||||
if (!('indexedDB' in window)) {
|
||||
this.logBrowserNotCapable();
|
||||
return;
|
||||
}
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
this.logBrowserNotCapable();
|
||||
console.log('Error initializing database: ');
|
||||
console.error(e)
|
||||
};
|
||||
DBOpenRequest.onsuccess = () => {
|
||||
console.log('Database initialised.');
|
||||
};
|
||||
DBOpenRequest.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
db.onerror = e => console.log('Error loading database: ' + e);
|
||||
db.createObjectStore('keyval');
|
||||
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
|
||||
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
|
||||
}
|
||||
}
|
||||
|
||||
logBrowserNotCapable() {
|
||||
console.log("This browser does not support IndexedDB. Paired devices will be gone after closing the browser.");
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.put(value, key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Added key-pair: ${key} - ${value}`);
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static get(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.get(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
|
||||
resolve(objectStoreRequest.result);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
static delete(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.delete(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted key: ${key}`);
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static addRoomSecret(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.add({'secret': roomSecret});
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. RoomSecret added: ${roomSecret}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static getAllRoomSecrets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.getAll();
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
let secrets = [];
|
||||
for (let i=0; i<e.target.result.length; i++) {
|
||||
secrets.push(e.target.result[i].secret);
|
||||
}
|
||||
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
resolve(secrets);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static deleteRoomSecret(room_secret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(room_secret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
if (!e.target.result) {
|
||||
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
|
||||
objectStoreRequestDeletion.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
|
||||
resolve();
|
||||
}
|
||||
objectStoreRequestDeletion.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static clearRoomSecrets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.clear();
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log('Request successful. All room_secrets cleared');
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Snapdrop {
|
||||
constructor() {
|
||||
const server = new ServerConnection();
|
||||
const peers = new PeersManager(server);
|
||||
const peersUI = new PeersUI();
|
||||
Events.on('load', e => {
|
||||
Events.on('load', _ => {
|
||||
const server = new ServerConnection();
|
||||
const peers = new PeersManager(server);
|
||||
const peersUI = new PeersUI();
|
||||
const receiveDialog = new ReceiveDialog();
|
||||
const sendTextDialog = new SendTextDialog();
|
||||
const receiveTextDialog = new ReceiveTextDialog();
|
||||
const pairDeviceDialog = new PairDeviceDialog();
|
||||
const clearDevicesDialog = new ClearDevicesDialog();
|
||||
const toast = new Toast();
|
||||
const notifications = new Notifications();
|
||||
const networkStatusUI = new NetworkStatusUI();
|
||||
|
@ -700,10 +1176,10 @@ class Snapdrop {
|
|||
}
|
||||
}
|
||||
|
||||
const persistentStorage = new PersistentStorage();
|
||||
const snapdrop = new Snapdrop();
|
||||
|
||||
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.then(serviceWorker => {
|
||||
|
@ -714,6 +1190,11 @@ if ('serviceWorker' in navigator) {
|
|||
|
||||
window.addEventListener('beforeinstallprompt', e => {
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
// make peerId persistent when pwa installed
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
sessionStorage.setItem("peerId", peerId);
|
||||
}).catch(e => console.error(e));
|
||||
|
||||
// don't display install banner when installed
|
||||
return e.preventDefault();
|
||||
} else {
|
||||
|
@ -805,7 +1286,7 @@ as the user has dismissed the permission prompt several times.
|
|||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = e => { // safari hack to fix audio
|
||||
document.body.onclick = _ => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
|
|
|
@ -35,8 +35,13 @@ body {
|
|||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
@ -106,13 +111,18 @@ h3 {
|
|||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin: 16px 0;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.font-subheading {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
word-break: break-all;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.font-body1,
|
||||
|
@ -183,6 +193,7 @@ x-peers {
|
|||
overflow: hidden;
|
||||
flex-flow: row wrap;
|
||||
z-index: 2;
|
||||
transition: color 300ms;
|
||||
}
|
||||
|
||||
/* Empty Peers List */
|
||||
|
@ -199,6 +210,7 @@ x-no-peers {
|
|||
x-no-peers h2,
|
||||
x-no-peers a {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
x-peers:not(:empty)+x-no-peers {
|
||||
|
@ -249,6 +261,10 @@ x-peer x-icon {
|
|||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip) x-icon {
|
||||
background: #00a69c;
|
||||
}
|
||||
|
||||
x-peer:not([transfer]):hover x-icon,
|
||||
x-peer:not([transfer]):focus x-icon {
|
||||
transform: scale(1.05);
|
||||
|
@ -266,6 +282,11 @@ x-peer[transfer] x-icon {
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
x-peer[transfer] .status:before {
|
||||
content: 'Transferring...';
|
||||
}
|
||||
|
@ -305,6 +326,7 @@ footer {
|
|||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
|
@ -317,13 +339,6 @@ footer .font-body2 {
|
|||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
|
@ -359,7 +374,7 @@ x-dialog:not([show]) x-background {
|
|||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 16px;
|
||||
margin-top: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
|
@ -367,12 +382,77 @@ x-dialog a {
|
|||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
x-dialog .font-subheading {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* PairDevicesDialog */
|
||||
|
||||
#keyInputContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
display: -webkit-box !important;
|
||||
display: -webkit-flex !important;
|
||||
display: -moz-flex !important;
|
||||
display: -ms-flexbox !important;
|
||||
display: flex !important;
|
||||
-webkit-justify-content: center;
|
||||
-ms-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#keyInputContainer>input:nth-of-type(4) {
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
#roomKey {
|
||||
font-size: 50px;
|
||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||
display: inline-block;
|
||||
text-indent: calc(0.5 * (11px + min(calc((100vw - 80px - 99px) / 100 * 6), 23px)));
|
||||
margin: 15px -15px;
|
||||
}
|
||||
|
||||
#roomKeyQrCode {
|
||||
padding: inherit;
|
||||
margin: auto;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog>*>*>*>hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Receive Dialog */
|
||||
|
||||
#receiveDialog .row {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#fileName{
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#fileSize{
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
|
||||
#receiveTextDialog #text {
|
||||
|
@ -420,6 +500,11 @@ x-dialog a {
|
|||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
color: #5B5B66;
|
||||
}
|
||||
|
||||
|
||||
.button,
|
||||
.icon-button {
|
||||
position: relative;
|
||||
|
@ -445,7 +530,7 @@ x-dialog a {
|
|||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
.button:hover:before,
|
||||
.button:not([disabled]):hover:before,
|
||||
.icon-button:hover:before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
@ -487,7 +572,7 @@ button::-moz-focus-inner {
|
|||
outline: none;
|
||||
padding: 16px 24px;
|
||||
border-radius: 16px;
|
||||
margin: 8px 0;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
background: #f1f3f4;
|
||||
|
@ -521,7 +606,7 @@ button::-moz-focus-inner {
|
|||
#about:not(:target) .fade-in {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition-delay: 0;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#about .logo {
|
||||
|
@ -561,7 +646,7 @@ button::-moz-focus-inner {
|
|||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
top: 0;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
|
@ -756,3 +841,15 @@ x-dialog x-paper {
|
|||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* webkit scrollbar style*/
|
||||
|
||||
::-webkit-scrollbar{
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb{
|
||||
background: #bfbfbf;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
23
server/package-lock.json
generated
23
server/package-lock.json
generated
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"name": "snapdrop",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.24",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
|
||||
"integrity": "sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw=="
|
||||
},
|
||||
"unique-names-generator": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.3.0.tgz",
|
||||
"integrity": "sha512-uNX6jVFjBXfZtsc7B8jVPJ3QdfCF/Sjde4gxsy3rNQmHuWGFarnU7IFGdxZKJ4h4uRjANQc6rG7GiGolRW9fgA=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue