From ae9909f59699c2a3ce004fc31d7afa95ad7f4e4f Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 11 May 2023 19:49:57 +0200 Subject: [PATCH 001/519] fix notification "Key null invalidated" on cancel device pairing --- index.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index a56b1b1..5242217 100644 --- a/index.js +++ b/index.js @@ -286,13 +286,15 @@ class PairDropServer { } _onPairDeviceCancel(sender) { - if (sender.roomKey) { - this._removeRoomKey(sender.roomKey); - this._send(sender, { - type: 'pair-device-canceled', - roomKey: sender.roomKey, - }); - } + const roomKey = sender.roomKey + + if (!roomKey) return; + + this._removeRoomKey(roomKey); + this._send(sender, { + type: 'pair-device-canceled', + roomKey: roomKey, + }); } _onRegenerateRoomSecret(sender, message) { From 347f9b87c0deeadf7bc1386c37c5604a7454ce86 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 11 May 2023 21:04:10 +0200 Subject: [PATCH 002/519] fix check whether peer is same browser --- index.js | 4 +- public/scripts/network.js | 133 ++++++++------- public/scripts/ui.js | 44 +++-- .../scripts/network.js | 155 +++++++++--------- public_included_ws_fallback/scripts/ui.js | 44 +++-- 5 files changed, 213 insertions(+), 167 deletions(-) diff --git a/index.js b/index.js index 5242217..fe0baaf 100644 --- a/index.js +++ b/index.js @@ -133,7 +133,6 @@ class PairDropServer { type: 'rtc-config', config: rtcConfig }); - this._joinRoom(peer); // send displayName this._send(peer, { @@ -162,6 +161,9 @@ class PairDropServer { case 'pong': sender.lastBeat = Date.now(); break; + case 'join-ip-room': + this._joinRoom(sender); + break; case 'room-secrets': this._onRoomSecrets(sender, message); break; diff --git a/public/scripts/network.js b/public/scripts/network.js index 47241f0..ec448ac 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -19,7 +19,8 @@ class ServerConnection { Events.on('pagehide', _ => this._disconnect()); document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); - Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); + Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail })); + Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'})); Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail})); Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail})); Events.on('resend-peers', _ => this.send({ type: 'resend-peers'})); @@ -48,10 +49,6 @@ class ServerConnection { if (this._isReconnect) Events.fire('notify-user', '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.'); @@ -131,13 +128,6 @@ class ServerConnection { _onPeers(msg) { Events.fire('peers', msg); - if (msg.roomType === "ip" && msg.peers.length === 0) { - BrowserTabsConnector.removePeerIdsFromLocalStorage(); - BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => { - if (peerId) return; - console.log("successfully added peerId from localStorage"); - }); - } } _onDisplayName(msg) { @@ -145,10 +135,16 @@ class ServerConnection { sessionStorage.setItem("peerId", msg.message.peerId); sessionStorage.setItem("peerIdHash", msg.message.peerIdHash); - // Add peerId to localStorage to mark it on other PairDrop tabs on the same browser + // Add peerId to localStorage to mark it for other PairDrop tabs on the same browser BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => { - if (peerId) return; - console.log("successfully added peerId from localStorage"); + if (!peerId) return; + console.log("successfully added peerId to localStorage"); + + // Only now join rooms + Events.fire('join-ip-room'); + PersistentStorage.getAllRoomSecrets().then(roomSecrets => { + Events.fire('room-secrets', roomSecrets); + }); }); Events.fire('display-name', msg); @@ -219,9 +215,9 @@ class ServerConnection { class Peer { - constructor(serverConnection, peerId, roomType, roomSecret) { + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { this._server = serverConnection; - this._isCaller = !!peerId; + this._isCaller = isCaller; this._peerId = peerId; this._roomType = roomType; this._updateRoomSecret(roomSecret); @@ -241,15 +237,14 @@ class Peer { this.sendJSON({type: 'display-name-changed', displayName: displayName}); } + _isSameBrowser() { + return BrowserTabsConnector.peerIsSameBrowser(this._peerId); + } + _updateRoomSecret(roomSecret) { // if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets - // -> abort - if (BrowserTabsConnector.peerIsSameBrowser(this._peerId)) { - this._roomSecret = ""; - return; - } - - if (this._roomSecret && this._roomSecret !== roomSecret) { + // -> do not delete duplicates and do not regenerate room secrets + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) { // remove old roomSecrets to prevent multiple pairings with same peer PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => { if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret); @@ -258,7 +253,7 @@ class Peer { this._roomSecret = roomSecret; - if (this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) { + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) { // increase security by increasing roomSecret length console.log('RoomSecret is regenerated to increase security') Events.fire('regenerate-room-secret', this._roomSecret); @@ -603,15 +598,15 @@ class Peer { class RTCPeer extends Peer { - constructor(serverConnection, peerId, roomType, roomSecret) { - super(serverConnection, peerId, roomType, roomSecret); + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { + super(serverConnection, isCaller, peerId, roomType, roomSecret); this.rtcSupported = true; if (!this._isCaller) return; // we will listen for a caller - this._connect(peerId, true); + this._connect(); } - _connect(peerId) { - if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId); + _connect() { + if (!this._conn || this._conn.signalingState === "closed") this._openConnection(); if (this._isCaller) { this._openChannel(); @@ -620,8 +615,7 @@ class RTCPeer extends Peer { } } - _openConnection(peerId) { - this._peerId = peerId; + _openConnection() { this._conn = new RTCPeerConnection(window.rtcConfig); this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onicecandidateerror = e => this._onError(e); @@ -653,7 +647,7 @@ class RTCPeer extends Peer { } onServerMessage(message) { - if (!this._conn) this._connect(message.sender.id, false); + if (!this._conn) this._connect(); if (message.sdp) { this._conn.setRemoteDescription(message.sdp) @@ -738,7 +732,7 @@ class RTCPeer extends Peer { console.log('RTC: channel closed', this._peerId); Events.fire('peer-disconnected', this._peerId); if (!this._isCaller) return; - this._connect(this._peerId, true); // reopen the channel + this._connect(); // reopen the channel } _onConnectionStateChange() { @@ -789,7 +783,7 @@ class RTCPeer extends Peer { // only reconnect if peer is caller if (!this._isCaller) return; - this._connect(this._peerId); + this._connect(); } _isConnected() { @@ -833,44 +827,47 @@ class PeersManager { this.peers[peerId].onServerMessage(message); } - _refreshExistingPeer(peerId, roomType, roomSecret) { - const peer = this.peers[peerId]; - if (peer) { - const roomTypeIsSecret = roomType === "secret"; - const roomSecretsDiffer = peer._roomSecret !== roomSecret; + _refreshPeer(peer, roomType, roomSecret) { + if (!peer) return false; - // if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept - if (roomTypeIsSecret && roomSecretsDiffer) { - peer._updateRoomSecret(roomSecret); - peer._evaluateAutoAccept(); + const roomTypeIsSecret = roomType === "secret"; + const roomSecretsDiffer = peer._roomSecret !== roomSecret; - return true; - } - - const roomTypesDiffer = peer._roomType !== roomType; - - // if roomTypes differ peer is already connected -> abort - if (roomTypesDiffer) return true; - - peer.refresh(); + // if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept + if (roomTypeIsSecret && roomSecretsDiffer) { + peer._updateRoomSecret(roomSecret); + peer._evaluateAutoAccept(); return true; } - // peer does not yet exist: return false - return false; + + const roomTypesDiffer = peer._roomType !== roomType; + + // if roomTypes differ peer is already connected -> abort + if (roomTypesDiffer) return true; + + peer.refresh(); + + return true; + } + + _createOrRefreshPeer(isCaller, peerId, roomType, roomSecret) { + const peer = this.peers[peerId]; + if (peer) { + this._refreshPeer(peer, roomType, roomSecret); + return; + } + + this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret); } _onPeerJoined(message) { - if (this._refreshExistingPeer(message.peer.id, message.roomType, message.roomSecret)) return; - - this.peers[message.peer.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret); + this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret); } _onPeers(message) { - message.peers.forEach(messagePeer => { - if (this._refreshExistingPeer(messagePeer.id, message.roomType, message.roomSecret)) return; - - this.peers[messagePeer.id] = new RTCPeer(this._server, messagePeer.id, message.roomType, message.roomSecret); + message.peers.forEach(peer => { + this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret); }) } @@ -902,6 +899,15 @@ class PeersManager { if (message.disconnect === true) { // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately Events.fire('peer-disconnected', message.peerId); + + // If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected: + // Tidy up peerIds in localStorage + if (Object.keys(this.peers).length === 0) { + BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => { + if (!peerIds) return; + console.log("successfully removed other peerIds from localStorage"); + }); + } } } @@ -962,7 +968,8 @@ class PeersManager { _getPeerIdFromRoomSecret(roomSecret) { for (const peerId in this.peers) { const peer = this.peers[peerId]; - if (peer._roomSecret === roomSecret) { + // peer must have same roomSecret and not be on the same browser. + if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) { return peer._peerId; } } diff --git a/public/scripts/ui.js b/public/scripts/ui.js index eaa1f2e..4321d31 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -169,8 +169,12 @@ class PeersUI { return; } peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id); - peer.roomTypes = [roomType]; - peer.roomSecret = roomSecret; + + if (!(roomType === "secret" && peer.sameBrowser())) { + peer.roomTypes = [roomType]; + peer.roomSecret = roomSecret; + } + this.peers[peer.id] = peer; } @@ -1026,10 +1030,7 @@ class PairDeviceDialog extends Dialog { _onWsConnected() { this.$pairDeviceBtn.removeAttribute('hidden'); - PersistentStorage.getAllRoomSecrets().then(roomSecrets => { - Events.fire('room-secrets', roomSecrets); - this._evaluateNumberRoomSecrets(); - }); + this._evaluateNumberRoomSecrets(); } _pairDeviceInitiate() { @@ -1972,13 +1973,18 @@ class PersistentStorage { } static async getAllRoomSecrets() { - const roomSecrets = await this.getAllRoomSecretEntries(); - let secrets = []; - for (let i=0; i this._disconnect()); document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); - Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); + Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail })); + Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'})); Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail})); Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail})); Events.on('resend-peers', _ => this.send({ type: 'resend-peers'})); @@ -46,10 +47,6 @@ class ServerConnection { if (this._isReconnect) Events.fire('notify-user', '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.'); @@ -143,10 +140,9 @@ class ServerConnection { _onPeers(msg) { Events.fire('peers', msg); if (msg.roomType === "ip" && msg.peers.length === 0) { - BrowserTabsConnector.removePeerIdsFromLocalStorage(); - BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => { - if (peerId) return; - console.log("successfully added peerId from localStorage"); + BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => { + if (!peerId) return; + console.log("successfully removed other peerIds from localStorage"); }); } } @@ -156,10 +152,16 @@ class ServerConnection { sessionStorage.setItem("peerId", msg.message.peerId); sessionStorage.setItem("peerIdHash", msg.message.peerIdHash); - // Add peerId to localStorage to mark it on other PairDrop tabs on the same browser + // Add peerId to localStorage to mark it for other PairDrop tabs on the same browser BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => { - if (peerId) return; - console.log("successfully added peerId from localStorage"); + if (!peerId) return; + console.log("successfully added peerId to localStorage"); + + // Only now join rooms + Events.fire('join-ip-room'); + PersistentStorage.getAllRoomSecrets().then(roomSecrets => { + Events.fire('room-secrets', roomSecrets); + }); }); Events.fire('display-name', msg); @@ -230,9 +232,9 @@ class ServerConnection { class Peer { - constructor(serverConnection, peerId, roomType, roomSecret) { + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { this._server = serverConnection; - this._isCaller = !!peerId; + this._isCaller = isCaller; this._peerId = peerId; this._roomType = roomType; this._updateRoomSecret(roomSecret); @@ -252,15 +254,14 @@ class Peer { this.sendJSON({type: 'display-name-changed', displayName: displayName}); } + _isSameBrowser() { + return BrowserTabsConnector.peerIsSameBrowser(this._peerId); + } + _updateRoomSecret(roomSecret) { // if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets - // -> abort - if (BrowserTabsConnector.peerIsSameBrowser(this._peerId)) { - this._roomSecret = ""; - return; - } - - if (this._roomSecret && this._roomSecret !== roomSecret) { + // -> do not delete duplicates and do not regenerate room secrets + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) { // remove old roomSecrets to prevent multiple pairings with same peer PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => { if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret); @@ -269,7 +270,7 @@ class Peer { this._roomSecret = roomSecret; - if (this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) { + if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) { // increase security by increasing roomSecret length console.log('RoomSecret is regenerated to increase security') Events.fire('regenerate-room-secret', this._roomSecret); @@ -614,15 +615,15 @@ class Peer { class RTCPeer extends Peer { - constructor(serverConnection, peerId, roomType, roomSecret) { - super(serverConnection, peerId, roomType, roomSecret); + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { + super(serverConnection, isCaller, peerId, roomType, roomSecret); this.rtcSupported = true; if (!this._isCaller) return; // we will listen for a caller - this._connect(peerId, true); + this._connect(); } - _connect(peerId) { - if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId); + _connect() { + if (!this._conn || this._conn.signalingState === "closed") this._openConnection(); if (this._isCaller) { this._openChannel(); @@ -631,8 +632,7 @@ class RTCPeer extends Peer { } } - _openConnection(peerId) { - this._peerId = peerId; + _openConnection() { this._conn = new RTCPeerConnection(window.rtcConfig); this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onicecandidateerror = e => this._onError(e); @@ -664,7 +664,7 @@ class RTCPeer extends Peer { } onServerMessage(message) { - if (!this._conn) this._connect(message.sender.id, false); + if (!this._conn) this._connect(); if (message.sdp) { this._conn.setRemoteDescription(message.sdp) @@ -749,7 +749,7 @@ class RTCPeer extends Peer { console.log('RTC: channel closed', this._peerId); Events.fire('peer-disconnected', this._peerId); if (!this._isCaller) return; - this._connect(this._peerId, true); // reopen the channel + this._connect(); // reopen the channel } _onConnectionStateChange() { @@ -800,7 +800,7 @@ class RTCPeer extends Peer { // only reconnect if peer is caller if (!this._isCaller) return; - this._connect(this._peerId); + this._connect(); } _isConnected() { @@ -819,8 +819,8 @@ class RTCPeer extends Peer { class WSPeer extends Peer { - constructor(serverConnection, peerId, roomType, roomSecret) { - super(serverConnection, peerId, roomType, roomSecret); + constructor(serverConnection, isCaller, peerId, roomType, roomSecret) { + super(serverConnection, isCaller, peerId, roomType, roomSecret); this.rtcSupported = false; if (!this._isCaller) return; // we will listen for a caller this._sendSignal(); @@ -886,41 +886,52 @@ class PeersManager { this.peers[peerId].onServerMessage(message); } - _refreshExistingPeer(peerId, roomType, roomSecret) { - const peer = this.peers[peerId]; - if (peer) { - const roomTypeIsSecret = roomType === "secret"; - const roomSecretsDiffer = peer._roomSecret !== roomSecret; + _refreshPeer(peer, roomType, roomSecret) { + if (!peer) return false; - // if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept - if (roomTypeIsSecret && roomSecretsDiffer) { - peer._updateRoomSecret(roomSecret); - peer._evaluateAutoAccept(); + const roomTypeIsSecret = roomType === "secret"; + const roomSecretsDiffer = peer._roomSecret !== roomSecret; - return true; - } - - const roomTypesDiffer = peer._roomType !== roomType; - - // if roomTypes differ peer is already connected -> abort - if (roomTypesDiffer) return true; - - peer.refresh(); + // if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept + if (roomTypeIsSecret && roomSecretsDiffer) { + peer._updateRoomSecret(roomSecret); + peer._evaluateAutoAccept(); return true; } - // peer does not yet exist: return false - return false; + + const roomTypesDiffer = peer._roomType !== roomType; + + // if roomTypes differ peer is already connected -> abort + if (roomTypesDiffer) return true; + + peer.refresh(); + + return true; + } + + _createOrRefreshPeer(isCaller, peerId, roomType, roomSecret, rtcSupported) { + const peer = this.peers[peerId]; + if (peer) { + this._refreshPeer(peer, roomType, roomSecret); + return; + } + + if (window.isRtcSupported && rtcSupported) { + this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret); + } else { + this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret); + } } _onPeerJoined(message) { - if (this._refreshExistingPeer(message.peer.id, message.roomType, message.roomSecret)) return; + this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported); + } - if (window.isRtcSupported && message.sender.rtcSupported) { - this.peers[message.peer.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret); - } else { - this.peers[message.peer.id] = new WSPeer(this._server, undefined, message.roomType, message.roomSecret); - } + _onPeers(message) { + message.peers.forEach(peer => { + this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported); + }) } _onWsRelay(message) { @@ -929,18 +940,6 @@ class PeersManager { this.peers[messageJSON.sender.id]._onMessage(message); } - _onPeers(message) { - message.peers.forEach(messagePeer => { - if (this._refreshExistingPeer(messagePeer.id, message.roomType, message.roomSecret)) return; - - if (window.isRtcSupported && messagePeer.rtcSupported) { - this.peers[messagePeer.id] = new RTCPeer(this._server, messagePeer.id, message.roomType, message.roomSecret); - } else { - this.peers[messagePeer.id] = new WSPeer(this._server, messagePeer.id, message.roomType, message.roomSecret); - } - }) - } - _onRespondToFileTransferRequest(detail) { this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); } @@ -972,6 +971,15 @@ class PeersManager { if (message.disconnect === true) { // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately Events.fire('peer-disconnected', message.peerId); + + // If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected: + // Tidy up peerIds in localStorage + if (Object.keys(this.peers).length === 0) { + BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => { + if (!peerIds) return; + console.log("successfully removed other peerIds from localStorage"); + }); + } } } @@ -1040,7 +1048,8 @@ class PeersManager { _getPeerIdFromRoomSecret(roomSecret) { for (const peerId in this.peers) { const peer = this.peers[peerId]; - if (peer._roomSecret === roomSecret) { + // peer must have same roomSecret and not be on the same browser. + if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) { return peer._peerId; } } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index da3990b..4a27f44 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -169,8 +169,12 @@ class PeersUI { return; } peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id); - peer.roomTypes = [roomType]; - peer.roomSecret = roomSecret; + + if (!(roomType === "secret" && peer.sameBrowser())) { + peer.roomTypes = [roomType]; + peer.roomSecret = roomSecret; + } + this.peers[peer.id] = peer; } @@ -1027,10 +1031,7 @@ class PairDeviceDialog extends Dialog { _onWsConnected() { this.$pairDeviceBtn.removeAttribute('hidden'); - PersistentStorage.getAllRoomSecrets().then(roomSecrets => { - Events.fire('room-secrets', roomSecrets); - this._evaluateNumberRoomSecrets(); - }); + this._evaluateNumberRoomSecrets(); } _pairDeviceInitiate() { @@ -1973,13 +1974,18 @@ class PersistentStorage { } static async getAllRoomSecrets() { - const roomSecrets = await this.getAllRoomSecretEntries(); - let secrets = []; - for (let i=0; i Date: Thu, 11 May 2023 22:42:45 +0200 Subject: [PATCH 003/519] fix error on empty roomSecrets --- index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index fe0baaf..cca34c8 100644 --- a/index.js +++ b/index.js @@ -215,9 +215,14 @@ class PairDropServer { } _onRoomSecrets(sender, message) { + if (!message.roomSecrets) return; + const roomSecrets = message.roomSecrets.filter(roomSecret => { return /^[\x00-\x7F]{64,256}$/.test(roomSecret); }) + + if (!roomSecrets) return; + this._joinSecretRooms(sender, roomSecrets); } @@ -481,7 +486,7 @@ class Peer { this._setIP(request); // set peer id - this._setPeerId(request) + this._setPeerId(request); // is WebRTC supported ? this.rtcSupported = request.url.indexOf('webrtc') > -1; From c629d7cd88070d829c746666def5bb1ffca98cd0 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 12 May 2023 01:41:10 +0200 Subject: [PATCH 004/519] increase version to v1.7.1 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6198df4..681ac93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.0", + "version": "1.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.0", + "version": "1.7.1", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 3ff7100..282aa66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.0", + "version": "1.7.1", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 54d9bd9..a98803c 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.0'; +const cacheVersion = 'v1.7.1'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 79a7868..c7ada0a 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.0'; +const cacheVersion = 'v1.7.1'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 3c2e73fc0c01b009c7ca68b190db46b234f29bd0 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 12 May 2023 04:59:44 +0200 Subject: [PATCH 005/519] fix position of about background circle --- public/styles.css | 4 +++- public_included_ws_fallback/styles.css | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index ddf5521..0f5afd9 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1144,6 +1144,8 @@ button::-moz-focus-inner { #about x-background { position: absolute; + top: calc(28px - 250px); + right: calc(36px - 250px); width: 500px; height: 500px; border-radius: 50%; @@ -1282,7 +1284,7 @@ x-peers:empty~x-instructions { @media (hover: none) and (pointer: coarse) { x-peer { transform: scale(0.95); - padding: 4px 0; + padding: 4px; } } diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 51334a1..3ba9d45 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1170,6 +1170,8 @@ button::-moz-focus-inner { #about x-background { position: absolute; + top: calc(28px - 250px); + right: calc(36px - 250px); width: 500px; height: 500px; border-radius: 50%; @@ -1308,7 +1310,7 @@ x-peers:empty~x-instructions { @media (hover: none) and (pointer: coarse) { x-peer { transform: scale(0.95); - padding: 4px 0; + padding: 4px; } } From 0baced640ad8d1b8bcdc2a185954139e631a11bc Mon Sep 17 00:00:00 2001 From: Lopolin-LP <82715586+Lopolin-LP@users.noreply.github.com> Date: Tue, 16 May 2023 01:50:12 +0200 Subject: [PATCH 006/519] Fix About Background not filling up full viewport under certain circumstances (#109) * Fix About Background Not filling up full viewport under certain circumstances It is now based on vw/vh instead of px. It can also easily be adjusted, mostly. There is no way it will not fill up the viewport. * add fix for about bg size to websocket fallback too and tidy up --------- Co-authored-by: schlagmichdoch --- public/styles.css | 12 +++++++----- public_included_ws_fallback/styles.css | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/public/styles.css b/public/styles.css index 0f5afd9..1a16425 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1144,10 +1144,12 @@ button::-moz-focus-inner { #about x-background { position: absolute; - top: calc(28px - 250px); - right: calc(36px - 250px); - width: 500px; - height: 500px; + --size: max(max(230vw, 230vh), calc(150vh + 150vw)); + --size-half: calc(var(--size)/2); + top: calc(28px - var(--size-half)); + right: calc(36px - var(--size-half)); + width: var(--size); + height: var(--size); border-radius: 50%; background: var(--primary-color); transform: scale(0); @@ -1161,7 +1163,7 @@ button::-moz-focus-inner { } #about:target x-background { - transform: scale(10); + transform: scale(1); } #about .row a { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 3ba9d45..9c34a99 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1170,10 +1170,12 @@ button::-moz-focus-inner { #about x-background { position: absolute; - top: calc(28px - 250px); - right: calc(36px - 250px); - width: 500px; - height: 500px; + --size: max(max(230vw, 230vh), calc(150vh + 150vw)); + --size-half: calc(var(--size)/2); + top: calc(28px - var(--size-half)); + right: calc(36px - var(--size-half)); + width: var(--size); + height: var(--size); border-radius: 50%; background: var(--primary-color); transform: scale(0); @@ -1187,7 +1189,7 @@ button::-moz-focus-inner { } #about:target x-background { - transform: scale(10); + transform: scale(1); } #about .row a { From 6e4bda0adf5ade71c124cc5e3d1493693963824a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 16 May 2023 02:25:50 +0200 Subject: [PATCH 007/519] Fix message sending via submit button. Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com> --- public/index.html | 2 +- public_included_ws_fallback/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index 36018ec..afa8c5d 100644 --- a/public/index.html +++ b/public/index.html @@ -224,7 +224,7 @@
- +
diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index cdf076d..4ced906 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -227,7 +227,7 @@
- +
From 56eb29c91bf1dd604a5e6ba9fea061fb43068209 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 16 May 2023 02:35:03 +0200 Subject: [PATCH 008/519] increase version to v1.7.2 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 681ac93..2f1442c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.1", + "version": "1.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.1", + "version": "1.7.2", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 282aa66..62a8c48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.1", + "version": "1.7.2", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index a98803c..20d2cc8 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.1'; +const cacheVersion = 'v1.7.2'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index c7ada0a..85b9e1d 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.1'; +const cacheVersion = 'v1.7.2'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 8a17b82fa473cf536e22078a4ab373c32ac2679b Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 16 May 2023 02:53:56 +0200 Subject: [PATCH 009/519] Fix _textInputEmpty() for Chromium based browsers Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com> --- public/scripts/ui.js | 2 +- public_included_ws_fallback/scripts/ui.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 4321d31..8801675 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1306,7 +1306,7 @@ class SendTextDialog extends Dialog { } _textInputEmpty() { - return this.$text.innerText === "\n"; + return !this.$text.innerText || this.$text.innerText === "\n"; } _onChange(e) { diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 4a27f44..8443d11 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1307,7 +1307,7 @@ class SendTextDialog extends Dialog { } _textInputEmpty() { - return this.$text.innerText === "\n"; + return !this.$text.innerText || this.$text.innerText === "\n"; } _onChange(e) { From df778ba42ca44823318844d2a14db54cf3195119 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 16 May 2023 18:55:36 +0200 Subject: [PATCH 010/519] Speed up canvas by removing fade-in animation --- public/index.html | 1 + public/scripts/ui.js | 14 ++------------ public/styles.css | 8 ++++++++ public_included_ws_fallback/index.html | 1 + public_included_ws_fallback/scripts/ui.js | 14 ++------------ public_included_ws_fallback/styles.css | 8 ++++++++ 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/public/index.html b/public/index.html index afa8c5d..8fcf05e 100644 --- a/public/index.html +++ b/public/index.html @@ -303,6 +303,7 @@ + diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 8801675..673726d 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -2249,14 +2249,7 @@ window.addEventListener('beforeinstallprompt', e => { // Background Circles Events.on('load', () => { - let c = document.createElement('canvas'); - let style = c.style; - style.width = '100%'; - style.position = 'absolute'; - style.zIndex = -1; - style.top = 0; - style.left = 0; - style.animation = "fade-in 800ms"; + let c = $$('canvas'); let cCtx = c.getContext('2d'); let x0, y0, w, h, dw, offset; @@ -2277,11 +2270,7 @@ Events.on('load', () => { y0 = h - offset; dw = Math.round(Math.max(w, h, 1000) / 13); - if (document.body.contains(c)) { - document.body.removeChild(c); - } drawCircles(cCtx, dw); - document.body.appendChild(c); } Events.on('bg-resize', _ => init()); @@ -2297,6 +2286,7 @@ Events.on('load', () => { } function drawCircles(ctx, frame) { + ctx.clearRect(0, 0, w, h); for (let i = 0; i < 13; i++) { drawCircle(ctx, dw * i + frame + 33); } diff --git a/public/styles.css b/public/styles.css index 1a16425..efb70c5 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1178,6 +1178,14 @@ button::-moz-focus-inner { align-self: end; } +canvas .circles { + width: 100vw; + position: absolute; + z-index: -10; + top: 0; + left: 0; +} + /* Loading Indicator */ .progress { diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 4ced906..ff896c3 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -306,6 +306,7 @@ + diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 8443d11..4f2229b 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -2250,14 +2250,7 @@ window.addEventListener('beforeinstallprompt', e => { // Background Circles Events.on('load', () => { - let c = document.createElement('canvas'); - let style = c.style; - style.width = '100%'; - style.position = 'absolute'; - style.zIndex = -1; - style.top = 0; - style.left = 0; - style.animation = "fade-in 800ms"; + let c = $$('canvas'); let cCtx = c.getContext('2d'); let x0, y0, w, h, dw, offset; @@ -2277,11 +2270,7 @@ Events.on('load', () => { y0 = h - offset; dw = Math.round(Math.max(w, h, 1000) / 13); - if (document.body.contains(c)) { - document.body.removeChild(c); - } drawCircles(cCtx, dw); - document.body.appendChild(c); } Events.on('bg-resize', _ => init()); @@ -2297,6 +2286,7 @@ Events.on('load', () => { } function drawCircles(ctx, frame) { + ctx.clearRect(0, 0, w, h); for (let i = 0; i < 13; i++) { drawCircle(ctx, dw * i + frame + 33); } diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 9c34a99..a96e5e3 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1204,6 +1204,14 @@ button::-moz-focus-inner { align-self: end; } +canvas .circles { + width: 100vw; + position: absolute; + z-index: -10; + top: 0; + left: 0; +} + /* Loading Indicator */ .progress { From a444be226f883a19dc7c8b7120039a8665adaaa2 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 16 May 2023 19:15:47 +0200 Subject: [PATCH 011/519] Fix canvas selector --- public/styles.css | 2 +- public_included_ws_fallback/styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index efb70c5..9273504 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1178,7 +1178,7 @@ button::-moz-focus-inner { align-self: end; } -canvas .circles { +canvas.circles { width: 100vw; position: absolute; z-index: -10; diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index a96e5e3..bd2062c 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1204,7 +1204,7 @@ button::-moz-focus-inner { align-self: end; } -canvas .circles { +canvas.circles { width: 100vw; position: absolute; z-index: -10; From 32e909b8c2112461556c8896165c382dbb547113 Mon Sep 17 00:00:00 2001 From: luckman212 <1992842+luckman212@users.noreply.github.com> Date: Mon, 15 May 2023 13:29:49 -0400 Subject: [PATCH 012/519] fixes for https://github.com/schlagmichdoch/PairDrop/issues/69 (squashed, docs updated, IPV6_LOCALIZE input validation) --- docs/host-your-own.md | 14 ++++++++++++++ index.js | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 4953b16..41ebe9b 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -35,6 +35,14 @@ Set options by using the following flags in the `docker run` command: ``` > Limits clients to 1000 requests per 5 min +##### IPv6 Localization +```bash +-e IPV6_LOCALIZE=4 +``` +> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments of the client IPv6 address to be evaluated as the peer's IP. This can be especially useful when using Cloudflare as a proxy. +> +> The flag must be set to an **integer** between `1` and `7`. The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) to match the client IP against. The most common value would be `4`, which will group peers within the same `/64` subnet. + ##### Websocket Fallback (for VPN) ```bash -e WS_FALLBACK=true @@ -200,6 +208,12 @@ $env:PORT=3010; npm start ``` > Specify the port PairDrop is running on. (Default: 3000) +#### IPv6 Localization +```bash +IPV6_LOCALIZE=4 +``` +> Truncate a portion of the client IPv6 address to make peers more discoverable. See [Options/Flags](#options--flags) above. + #### Specify STUN/TURN Server On Unix based systems ```bash diff --git a/index.js b/index.js index cca34c8..f9a9a34 100644 --- a/index.js +++ b/index.js @@ -96,6 +96,16 @@ if (debugMode) { console.log("DEBUG_MODE is active. To protect privacy, do not use in production.") } +if (process.env.IPV6_LOCALIZE) { + let ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE); + if (!ipv6_lcl || !(0 < ipv6_lcl && ipv6_lcl < 8)) { + console.error("IPV6_LOCALIZE must be an integer between 1 and 7"); + return; + } else { + console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment"); + } +} + app.use(function(req, res) { res.redirect('/'); }); @@ -516,11 +526,19 @@ class Peer { if (this.ip.substring(0,7) === "::ffff:") this.ip = this.ip.substring(7); + let ipv6_was_localized = false; + if (ipv6_lcl && this.ip.includes(':')) { + this.ip = this.ip.split(':',ipv6_lcl).join(':'); + ipv6_was_localized = true; + } + if (debugMode) { console.debug("----DEBUGGING-PEER-IP-START----"); console.debug("remoteAddress:", request.connection.remoteAddress); console.debug("x-forwarded-for:", request.headers['x-forwarded-for']); console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']); + if (ipv6_was_localized) + console.debug("IPv6 client IP was localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment"); console.debug("PairDrop uses:", this.ip); console.debug("IP is private:", this.ipIsPrivate(this.ip)); console.debug("if IP is private, '127.0.0.1' is used instead"); From b106d90b64a6e6f0cc480112ddd7556b3fac8bf6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 23 May 2023 02:43:17 +0200 Subject: [PATCH 013/519] Fix ReferenceError: ipv6_lcl is not defined --- index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index f9a9a34..e7e47f1 100644 --- a/index.js +++ b/index.js @@ -96,14 +96,15 @@ if (debugMode) { console.log("DEBUG_MODE is active. To protect privacy, do not use in production.") } +let ipv6_lcl; if (process.env.IPV6_LOCALIZE) { - let ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE); + ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE); if (!ipv6_lcl || !(0 < ipv6_lcl && ipv6_lcl < 8)) { console.error("IPV6_LOCALIZE must be an integer between 1 and 7"); return; - } else { - console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment"); } + + console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl === 1 ? "segment" : "segments"); } app.use(function(req, res) { From 4433e1c58f65061474af40568a7792a62b238005 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 23 May 2023 02:44:25 +0200 Subject: [PATCH 014/519] add version number to about page --- public/index.html | 5 ++++- public/styles.css | 9 +++++++++ public_included_ws_fallback/index.html | 5 ++++- public_included_ws_fallback/styles.css | 9 +++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index 8fcf05e..93a5086 100644 --- a/public/index.html +++ b/public/index.html @@ -276,7 +276,10 @@ -

PairDrop

+
+

PairDrop

+
v1.7.2
+
The easiest way to transfer files across devices
diff --git a/public/styles.css b/public/styles.css index 9273504..7f2913b 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1142,6 +1142,15 @@ button::-moz-focus-inner { --icon-size: 96px; } +#about .title-wrapper { + display: flex; + align-items: baseline; +} + +#about .title-wrapper > div { + margin-left: 0.5em; +} + #about x-background { position: absolute; --size: max(max(230vw, 230vh), calc(150vh + 150vw)); diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index ff896c3..f8eb380 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -279,7 +279,10 @@ -

PairDrop

+
+

PairDrop

+
v1.7.2
+
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index bd2062c..fb2a5cb 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1168,6 +1168,15 @@ button::-moz-focus-inner { --icon-size: 96px; } +#about .title-wrapper { + display: flex; + align-items: baseline; +} + +#about .title-wrapper > div { + margin-left: 0.5em; +} + #about x-background { position: absolute; --size: max(max(230vw, 230vh), calc(150vh + 150vw)); From b5987cf01752c61387921d7b03f6478a35beb9ff Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 23 May 2023 02:45:29 +0200 Subject: [PATCH 015/519] increase version to v1.7.3 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f1442c..abef59e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.2", + "version": "1.7.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.2", + "version": "1.7.3", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 62a8c48..5eb4e1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.2", + "version": "1.7.3", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 93a5086..42c209e 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.2
+
v1.7.3
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index 20d2cc8..70d5f25 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.2'; +const cacheVersion = 'v1.7.3'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index f8eb380..78b59eb 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.2
+
v1.7.3
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 85b9e1d..1cd1932 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.2'; +const cacheVersion = 'v1.7.3'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 58b7f6bb7cab637c38bc0d873a322cef02ce65f7 Mon Sep 17 00:00:00 2001 From: fm-sys <64581222+fm-sys@users.noreply.github.com> Date: Fri, 26 May 2023 09:52:17 +0200 Subject: [PATCH 016/519] Add 'files-sent' event --- public/scripts/network.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/network.js b/public/scripts/network.js index ec448ac..517aea3 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -529,7 +529,7 @@ class Peer { this._abortTransfer(); } - // include for compatibility with Snapdrop for Android app + // include for compatibility with 'Snapdrop & PairDrop for Android' app Events.fire('file-received', fileBlob); this._filesReceived.push(fileBlob); @@ -547,6 +547,7 @@ class Peer { if (!this._filesQueue.length) { this._busy = false; Events.fire('notify-user', 'File transfer completed.'); + Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app } else { this._dequeueFile(); } From 27bf0fa74fbce8b7a850ce8254b4cfa2571b9cfa Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 26 May 2023 20:05:50 +0200 Subject: [PATCH 017/519] fix #113 --- public/styles.css | 5 ++++- public_included_ws_fallback/styles.css | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index 7f2913b..db86b60 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1124,11 +1124,14 @@ button::-moz-focus-inner { text-align: center; } +#about header { + z-index: 1; +} + #about .fade-in { transition: opacity 300ms; will-change: opacity; transition-delay: 300ms; - z-index: 11; pointer-events: all; } diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index fb2a5cb..e384b51 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1150,11 +1150,14 @@ button::-moz-focus-inner { text-align: center; } +#about header { + z-index: 1; +} + #about .fade-in { transition: opacity 300ms; will-change: opacity; transition-delay: 300ms; - z-index: 11; pointer-events: all; } From 520b772bc8fe8b4521f4ac8491fd2092f1d9fda0 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 26 May 2023 20:24:29 +0200 Subject: [PATCH 018/519] fix #112 and differentiate between textContent and innerText --- public/scripts/ui.js | 4 ++-- public/scripts/util.js | 2 +- public_included_ws_fallback/scripts/ui.js | 4 ++-- public_included_ws_fallback/scripts/util.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 673726d..bfbca32 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1419,7 +1419,7 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.textContent); + await navigator.clipboard.writeText(this.$text.innerText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } @@ -1594,7 +1594,7 @@ class Toast extends Dialog { _onNotify(message) { if (this.hideTimeout) clearTimeout(this.hideTimeout); - this.$el.textContent = message; + this.$el.innerText = message; this.show(); this.hideTimeout = setTimeout(_ => this.hide(), 5000); } diff --git a/public/scripts/util.js b/public/scripts/util.js index bae1e39..f6156f8 100644 --- a/public/scripts/util.js +++ b/public/scripts/util.js @@ -5,7 +5,7 @@ if (!navigator.clipboard) { // A contains the text to copy const span = document.createElement('span'); - span.textContent = text; + span.innerText = text; span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines // Paint the span outside the viewport diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 4f2229b..971fe21 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1420,7 +1420,7 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.textContent); + await navigator.clipboard.writeText(this.$text.innerText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } @@ -1595,7 +1595,7 @@ class Toast extends Dialog { _onNotify(message) { if (this.hideTimeout) clearTimeout(this.hideTimeout); - this.$el.textContent = message; + this.$el.innerText = message; this.show(); this.hideTimeout = setTimeout(_ => this.hide(), 5000); } diff --git a/public_included_ws_fallback/scripts/util.js b/public_included_ws_fallback/scripts/util.js index e0cbf24..9b7548c 100644 --- a/public_included_ws_fallback/scripts/util.js +++ b/public_included_ws_fallback/scripts/util.js @@ -5,7 +5,7 @@ if (!navigator.clipboard) { // A contains the text to copy const span = document.createElement('span'); - span.textContent = text; + span.innerText = text; span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines // Paint the span outside the viewport From a3a8228327e191d46cc0ade61c0ec625502ce50b Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 26 May 2023 20:37:38 +0200 Subject: [PATCH 019/519] increase version to v1.7.4 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index abef59e..c30c6e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.3", + "version": "1.7.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.3", + "version": "1.7.4", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 5eb4e1c..0958a8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.3", + "version": "1.7.4", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 42c209e..dfb79b4 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.3
+
v1.7.4
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index 70d5f25..1927638 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.3'; +const cacheVersion = 'v1.7.4'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 78b59eb..e298b5a 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.3
+
v1.7.4
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 1cd1932..347ec70 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.3'; +const cacheVersion = 'v1.7.4'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From d36cd3524cace84b2b032ca0fe1bdccb94632625 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 30 May 2023 01:21:17 +0200 Subject: [PATCH 020/519] Fix clearBrowserHistory: url should not always be replaced by "/" as PairDrop might not always be hosted at domain root --- public/scripts/ui.js | 12 ++++--- public/scripts/util.js | 4 +++ public/service-worker.js | 34 +++++++++---------- public_included_ws_fallback/scripts/ui.js | 12 ++++--- public_included_ws_fallback/scripts/util.js | 4 +++ public_included_ws_fallback/service-worker.js | 34 +++++++++---------- 6 files changed, 58 insertions(+), 42 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index bfbca32..cb28677 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1024,7 +1024,8 @@ class PairDeviceDialog extends Dialog { 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 + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); //remove room_key from url } } @@ -1575,7 +1576,8 @@ class Base64ZipDialog extends Dialog { } clearBrowserHistory() { - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } hide() { @@ -1791,7 +1793,8 @@ class WebShareTargetUI { } } } - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } } } @@ -1815,7 +1818,8 @@ class WebFileHandlersUI { Events.fire('activate-paste-mode', {files: files, text: ""}) launchParams = null; }); - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } } } diff --git a/public/scripts/util.js b/public/scripts/util.js index f6156f8..848dca7 100644 --- a/public/scripts/util.js +++ b/public/scripts/util.js @@ -402,3 +402,7 @@ const cyrb53 = function(str, seed = 0) { function onlyUnique (value, index, array) { return array.indexOf(value) === index; } + +function getUrlWithoutArguments() { + return `${window.location.protocol}//${window.location.host}${window.location.pathname}`; +} diff --git a/public/service-worker.js b/public/service-worker.js index 1927638..238e2e2 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -72,8 +72,7 @@ self.addEventListener('fetch', function(event) { if (event.request.method === "POST") { // Requests related to Web Share Target. event.respondWith((async () => { - let share_url = await evaluateRequestData(event.request); - share_url = event.request.url + share_url; + const share_url = await evaluateRequestData(event.request); return Response.redirect(encodeURI(share_url), 302); })()); } else { @@ -101,15 +100,16 @@ self.addEventListener('activate', evt => ) ); -const evaluateRequestData = async function (request) { - const formData = await request.formData(); - const title = formData.get("title"); - const text = formData.get("text"); - const url = formData.get("url"); - const files = formData.getAll("allfiles"); - - +const evaluateRequestData = function (request) { return new Promise(async (resolve) => { + const formData = await request.formData(); + const title = formData.get("title"); + const text = formData.get("text"); + const url = formData.get("url"); + const files = formData.getAll("allfiles"); + + const pairDropUrl = request.url; + if (files && files.length > 0) { let fileObjects = []; for (let i=0; i { - if (i === fileObjects.length - 1) resolve('?share-target=files'); + if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files'); } } } DBOpenRequest.onerror = _ => { - resolve(''); + resolve(pairDropUrl); } } else { - let share_url = '?share-target=text'; + let urlArgument = '?share-target=text'; - if (title) share_url += `&title=${title}`; - if (text) share_url += `&text=${text}`; - if (url) share_url += `&url=${url}`; + if (title) urlArgument += `&title=${title}`; + if (text) urlArgument += `&text=${text}`; + if (url) urlArgument += `&url=${url}`; - resolve(share_url); + resolve(pairDropUrl + urlArgument); } }); } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 971fe21..52055ad 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1025,7 +1025,8 @@ class PairDeviceDialog extends Dialog { 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 + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); //remove room_key from url } } @@ -1576,7 +1577,8 @@ class Base64ZipDialog extends Dialog { } clearBrowserHistory() { - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } hide() { @@ -1792,7 +1794,8 @@ class WebShareTargetUI { } } } - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } } } @@ -1816,7 +1819,8 @@ class WebFileHandlersUI { Events.fire('activate-paste-mode', {files: files, text: ""}) launchParams = null; }); - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } } } diff --git a/public_included_ws_fallback/scripts/util.js b/public_included_ws_fallback/scripts/util.js index 9b7548c..a5266f8 100644 --- a/public_included_ws_fallback/scripts/util.js +++ b/public_included_ws_fallback/scripts/util.js @@ -403,6 +403,10 @@ function onlyUnique (value, index, array) { return array.indexOf(value) === index; } +function getUrlWithoutArguments() { + return `${window.location.protocol}//${window.location.host}${window.location.pathname}`; +} + function arrayBufferToBase64(buffer) { var binary = ''; var bytes = new Uint8Array(buffer); diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 347ec70..103a6f1 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -72,8 +72,7 @@ self.addEventListener('fetch', function(event) { if (event.request.method === "POST") { // Requests related to Web Share Target. event.respondWith((async () => { - let share_url = await evaluateRequestData(event.request); - share_url = event.request.url + share_url; + const share_url = await evaluateRequestData(event.request); return Response.redirect(encodeURI(share_url), 302); })()); } else { @@ -101,15 +100,16 @@ self.addEventListener('activate', evt => ) ); -const evaluateRequestData = async function (request) { - const formData = await request.formData(); - const title = formData.get("title"); - const text = formData.get("text"); - const url = formData.get("url"); - const files = formData.getAll("allfiles"); - - +const evaluateRequestData = function (request) { return new Promise(async (resolve) => { + const formData = await request.formData(); + const title = formData.get("title"); + const text = formData.get("text"); + const url = formData.get("url"); + const files = formData.getAll("allfiles"); + + const pairDropUrl = request.url; + if (files && files.length > 0) { let fileObjects = []; for (let i=0; i { - if (i === fileObjects.length - 1) resolve('?share-target=files'); + if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files'); } } } DBOpenRequest.onerror = _ => { - resolve(''); + resolve(pairDropUrl); } } else { - let share_url = '?share-target=text'; + let urlArgument = '?share-target=text'; - if (title) share_url += `&title=${title}`; - if (text) share_url += `&text=${text}`; - if (url) share_url += `&url=${url}`; + if (title) urlArgument += `&title=${title}`; + if (text) urlArgument += `&text=${text}`; + if (url) urlArgument += `&url=${url}`; - resolve(share_url); + resolve(pairDropUrl + urlArgument); } }); } From 3e2368c0c9f371e7001a38c68fed50d6493da7c4 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:26:53 +0200 Subject: [PATCH 021/519] stabilize connection on reconnect by terminating websocket only on timeout and not always when peer leaves its ip room --- index.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index e7e47f1..2e5bdcb 100644 --- a/index.js +++ b/index.js @@ -219,10 +219,15 @@ class PairDropServer { } _onDisconnect(sender) { + this._disconnect(sender); + } + + _disconnect(sender) { this._leaveRoom(sender, 'ip', '', true); this._leaveAllSecretRooms(sender, true); this._removeRoomKey(sender.roomKey); sender.roomKey = null; + sender.socket.terminate(); } _onRoomSecrets(sender, message) { @@ -357,10 +362,6 @@ class PairDropServer { _joinRoom(peer, roomType = 'ip', roomSecret = '') { const room = roomType === 'ip' ? peer.ip : roomSecret; - if (this._rooms[room] && this._rooms[room][peer.id]) { - this._leaveRoom(peer, roomType, roomSecret); - } - // if room doesn't exist, create it if (!this._rooms[room]) { this._rooms[room] = {}; @@ -385,10 +386,6 @@ class PairDropServer { // 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]; @@ -468,8 +465,7 @@ class PairDropServer { peer.lastBeat = Date.now(); } if (Date.now() - peer.lastBeat > 2 * timeout) { - this._leaveRoom(peer); - this._leaveAllSecretRooms(peer); + this._disconnect(peer); return; } From 3505f161c6797d05bc4e440319827ffe37fb7140 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 27 May 2023 01:13:49 +0200 Subject: [PATCH 022/519] strip 'NO-BREAK SPACE' (U+00A0) of received text as some browsers seem to add them when pasting text --- public/scripts/ui.js | 17 +++++++++-------- public_included_ws_fallback/scripts/ui.js | 17 +++++++++-------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index cb28677..b494f58 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1296,13 +1296,13 @@ class SendTextDialog extends Dialog { } async _onKeyDown(e) { - if (this.isShown()) { - if (e.code === "Escape") { - this.hide(); - } else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) { - if (this._textInputEmpty()) return; - this._send(); - } + if (!this.isShown()) return; + + if (e.code === "Escape") { + this.hide(); + } else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) { + if (this._textInputEmpty()) return; + this._send(); } } @@ -1420,7 +1420,8 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.innerText); + const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); + await navigator.clipboard.writeText(sanitizedText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 52055ad..d355468 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1297,13 +1297,13 @@ class SendTextDialog extends Dialog { } async _onKeyDown(e) { - if (this.isShown()) { - if (e.code === "Escape") { - this.hide(); - } else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) { - if (this._textInputEmpty()) return; - this._send(); - } + if (!this.isShown()) return; + + if (e.code === "Escape") { + this.hide(); + } else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) { + if (this._textInputEmpty()) return; + this._send(); } } @@ -1421,7 +1421,8 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.innerText); + const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); + await navigator.clipboard.writeText(sanitizedText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } From f195c686e7fb9627d1f1aacb74afef9f0c7d2f96 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:32:06 +0200 Subject: [PATCH 023/519] increase version to v1.7.5 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index c30c6e8..b2ccb52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.4", + "version": "1.7.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.4", + "version": "1.7.5", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 0958a8b..c466e69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.4", + "version": "1.7.5", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index dfb79b4..20007bf 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.4
+
v1.7.5
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index 238e2e2..cfa4d1e 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.4'; +const cacheVersion = 'v1.7.5'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index e298b5a..db1022d 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.4
+
v1.7.5
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 103a6f1..ee0dcb0 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.4'; +const cacheVersion = 'v1.7.5'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 26bf4d6dc30d5314603cb45ab09894f43d48ac8e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:47:28 +0200 Subject: [PATCH 024/519] ensure that otherPeers never receive `peer-left` after `peer-joined` on reconnect by leaving room before rejoining it --- index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.js b/index.js index 2e5bdcb..029a955 100644 --- a/index.js +++ b/index.js @@ -362,6 +362,11 @@ class PairDropServer { _joinRoom(peer, roomType = 'ip', roomSecret = '') { const room = roomType === 'ip' ? peer.ip : roomSecret; + if (this._rooms[room] && this._rooms[room][peer.id]) { + // ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect. + this._leaveRoom(peer, roomType, roomSecret); + } + // if room doesn't exist, create it if (!this._rooms[room]) { this._rooms[room] = {}; From 29b91cb17aa7e830f1cb6a6659ac5e2c0b5b265e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:51:51 +0200 Subject: [PATCH 025/519] increase version to v1.7.6 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2ccb52..c49aca5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.5", + "version": "1.7.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.5", + "version": "1.7.6", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index c466e69..aae3823 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.5", + "version": "1.7.6", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 20007bf..755673a 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.5
+
v1.7.6
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index cfa4d1e..b409118 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.5'; +const cacheVersion = 'v1.7.6'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index db1022d..6beae65 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.5
+
v1.7.6
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index ee0dcb0..78249b2 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.5'; +const cacheVersion = 'v1.7.6'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 2d0ea9a2f1005eb1b4b0edd950235633930cf99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Wed, 21 Jun 2023 22:55:42 +0000 Subject: [PATCH 026/519] Update FAQ reworked --- docs/faq.md | 124 +++++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index cc95f17..651a826 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -5,20 +5,18 @@ Help! I can't install the PWA! -if you are using a Chromium-based browser (Chrome, Edge, Brave, etc.), you can easily install PairDrop PWA on your desktop +if you are using a Chromium-based browser (Chrome, Edge, Vivaldi, Brave, etc.), you can easily install PairDrop PWA on your desktop by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net). Example on how to install a pwa with Edge On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/) -
Self-Hosted Instance? To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs). -See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more information. - +See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info.
@@ -28,12 +26,11 @@ See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob Shortcuts? -Shortcuts! +Shortcuts - Send a message with `CTRL + ENTER` -- Close all send and pair dialogs by pressing `Escape`. -- Copy a received message to clipboard with `CTRL/⌘ + C`. -- Accept file transfer request with `Enter` and decline with `Escape`. - +- Close all "Send" and "Pair" dialogs by pressing `Esc`. +- Copy a received message to the clipboard with `CTRL/⌘ + C`. +- Accept file-transfer requests with `Enter` and decline with `Esc`.
@@ -44,28 +41,24 @@ Shortcuts! Apparently, iOS does not allow images shared from a website to be saved to the gallery directly. -It simply does not offer the option for images shared from a website. +It simply does not offer that option for images shared from a website. -iOS Shortcuts to the win: +iOS Shortcuts saves the day: I created a simple iOS shortcut that takes your photos and saves them to your gallery: https://routinehub.co/shortcut/13988/ - -
- Is it possible to send files or text directly from the context or share menu? + Is it possible to send files or text directly from the "Context" or "Share" menu? -Yes, it finally is! -* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) -* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) -* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android) - - +Yes, it finally is. +* [Send files directly from the "Context" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) +* [Send directly from the "Share" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) +* [Send directly from the "Share" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
@@ -75,71 +68,81 @@ Yes, it finally is! Is it possible to send files or text directly via CLI? -Yes, it is! - -* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) - +Yes. +* [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
- Are there any Third-Party Apps? + Are there any third-party Apps? -Here's a list of some third-party apps compatible with PairDrop: +These third-party apps are compatible with PairDrop: 1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android) 2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon) 3. Feel free to make one :) -
- What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? + What about the connection? Is it a P2P connection directly from device to device or is there any third-party-server? -It uses a WebRTC peer to peer connection. WebRTC needs a Signaling Server that is only used to establish a connection. The server is not involved in the file transfer. +It uses a WebRTC peer-to-peer connection. +WebRTC needs a signaling server that is only used to establish a connection. +The server is not involved in the file transfer. -If devices are on the same network, none of your files are ever sent to any server. +If the devices are on the same network, +none of your files are ever sent to any server. -If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC. - -If you host your own instance and want to support devices that do not support WebRTC, you can [start the PairDrop instance with an activated Websocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn). +If your devices are paired and behind a NAT, +the PairDrop TURN Server is used to route your files and messages. +See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) +to learn more about STUN, TURN and WebRTC. +If you host your own instance +and want to support devices that do not support WebRTC, +you can [start the PairDrop instance with an activated WebSocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
- What about privacy? Will files be saved on third-party-servers? + What about privacy? Will files be saved on third-party servers? -Files are sent directly between peers. PairDrop doesn't even use a database. If you are curious, have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js). -WebRTC encrypts the files on transit. +Files are sent directly between peers. +PairDrop doesn't even use a database. +If curious, study [the server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js). +WebRTC encrypts the files in transit. -If devices are on the same network, none of your files are ever sent to any server. - -If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC. +If the devices are on the same network, +none of your files are ever sent to any server. +If your devices are paired and behind a NAT, +the PairDrop TURN Server is used to route your files and messages. +See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) +to learn more about STUN, TURN and WebRTC.
- What about security? Are my files encrypted while being sent between the computers? + What about security? Are my files encrypted while sent between the computers? -Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection. - - +Yes. Your files are sent using WebRTC, encrypting them in transit. +To ensure the connection is secure and there is no [MITM](https://wikiless.org/wiki/Man-in-the-middle_attack), +compare the security number shown under the device name on both devices. +The security number is different for every connection.
@@ -149,18 +152,18 @@ Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure Transferring many files with paired devices takes too long -Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases. +Naturally, if traffic needs to be routed through the TURN server +because your devices are behind different NATs, transfer speed decreases. -You can open a hotspot on one of your devices to bridge the connection which omits the need of the TURN server. +You can open a hotspot on one of your devices to bridge the connection, +which omits the need of the TURN server. - [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11) -- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) +- [How to open a hotspot on macOS](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) - [Library to open a hotspot on Linux](https://github.com/lakinduakash/linux-wifi-hotspot) You can also use mobile hotspots on phones to do that. -Then, all data should be sent directly between devices and your data plan should not be charged. - - +Then, all data should be sent directly between devices and not use your data plan.
@@ -170,10 +173,16 @@ Then, all data should be sent directly between devices and your data plan should Why don't you implement feature xyz? -Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer. -We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity. +Snapdrop and PairDrop are a study in radical simplicity. +The user interface is insanely simple. +Features are chosen very carefully because complexity grows quadratically +since every feature potentially interferes with each other feature. +We focus very narrowly on a single use case: instant file transfer. +Not facilitating optimal edge-cases means better flow for average users. +Don't be sad. We may decline your feature request for the sake of simplicity. -If you want to learn more about simplicity you can read *Insanely Simple: The Obsession that Drives Apple's Success* or *Thinking, Fast and Slow*. +Read *Insanely Simple: The Obsession that Drives Apple's Success*, +and/or *Thinking, Fast and Slow* to learn more.
@@ -182,17 +191,15 @@ If you want to learn more about simplicity you can read *Insanely Simple: The Ob
- Snapdrop and PairDrop are awesome! How can I support them? + Snapdrop and PairDrop are awesome. How can I support them? -* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support open source software +* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software. * [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues) * Share PairDrop on social media. * Fix bugs and make a pull request. -* Do security analysis and suggestions +* Do some security analysis and make suggestions. * To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop) - -
@@ -202,8 +209,7 @@ If you want to learn more about simplicity you can read *Insanely Simple: The Ob How does it work? -[See here for Information about the Technical Implementation](/docs/technical-documentation.md) - +[See here for info about the technical implementation](/docs/technical-documentation.md)
From f50d7438b68e7a13d48016868fef4e54d4745b8e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 6 Jul 2023 21:29:36 +0200 Subject: [PATCH 027/519] implement localization --- public/index.html | 112 ++++++---- public/lang/en.json | 136 ++++++++++++ public/scripts/localization.js | 102 +++++++++ public/scripts/network.js | 18 +- public/scripts/ui.js | 194 +++++++++++------- public/styles.css | 34 +-- public_included_ws_fallback/index.html | 116 ++++++----- public_included_ws_fallback/lang/en.json | 136 ++++++++++++ .../scripts/localization.js | 102 +++++++++ .../scripts/network.js | 21 +- public_included_ws_fallback/scripts/ui.js | 194 +++++++++++------- public_included_ws_fallback/styles.css | 4 +- 12 files changed, 883 insertions(+), 286 deletions(-) create mode 100644 public/lang/en.json create mode 100644 public/scripts/localization.js create mode 100644 public_included_ws_fallback/lang/en.json create mode 100644 public_included_ws_fallback/scripts/localization.js diff --git a/public/index.html b/public/index.html index 755673a..15b82da 100644 --- a/public/index.html +++ b/public/index.html @@ -39,62 +39,66 @@
- +
-
+
-
+
-
+
-
- -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
+ +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
- +

@@ -104,15 +108,21 @@
- You are known as: -
+ You are known as: +
- You can be discovered by everyone on this network - +
+ You can be discovered by everyone + on this network +
+
@@ -120,10 +130,13 @@
-

Pair Devices

+

Pair Devices

000 000

-
Input this key on another device
or scan the QR-Code.
+
+ Input this key on another device + or scan the QR-Code. +

@@ -133,10 +146,10 @@
-
Enter key from another device to continue.
+
Enter key from another device to continue.
- - + +
@@ -147,13 +160,21 @@ -

Edit Paired Devices

-
+

Edit Paired Devices

+
-

Activate auto-accept to automatically accept all files sent from that device.

+

+ + Activate + + auto-accept + + to automatically accept all files sent from that device. + +

- +
@@ -167,7 +188,7 @@
- would like to share + would like to share
@@ -179,8 +200,8 @@
- - + +
@@ -193,7 +214,7 @@
-
The easiest way to transfer files across devices
+
The easiest way to transfer files across devices
@@ -373,6 +394,7 @@ + diff --git a/public/lang/en.json b/public/lang/en.json new file mode 100644 index 0000000..7ae2e56 --- /dev/null +++ b/public/lang/en.json @@ -0,0 +1,136 @@ +{ + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading...", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-Code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about-aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded successfully", + "pairing-tabs-error": "Pairing of two browser tabs is not possible.", + "pairing-success": "Devices paired successfully.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Key not valid", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting...", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing...", + "waiting": "Waiting...", + "processing": "Processing...", + "transferring": "Transferring..." + } +} diff --git a/public/scripts/localization.js b/public/scripts/localization.js new file mode 100644 index 0000000..d09d5c0 --- /dev/null +++ b/public/scripts/localization.js @@ -0,0 +1,102 @@ +class Localization { + constructor() { + Localization.defaultLocale = "en"; + Localization.supportedLocales = ["en"]; + + Localization.translations = {}; + + const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + + Localization.setLocale(initialLocale) + .then(_ => { + Localization.translatePage(); + }) + } + + static isSupported(locale) { + return Localization.supportedLocales.indexOf(locale) > -1; + } + + static supportedOrDefault(locales) { + return locales.find(Localization.isSupported) || Localization.defaultLocale; + } + + static browserLocales() { + return navigator.languages.map(locale => + locale.split("-")[0] + ); + } + + static async setLocale(newLocale) { + if (newLocale === Localization.locale) return false; + + const newTranslations = await Localization.fetchTranslationsFor(newLocale); + + if(!newTranslations) return false; + + const firstTranslation = !Localization.locale + + Localization.locale = newLocale; + Localization.translations = newTranslations; + + if (firstTranslation) { + Events.fire("translation-loaded"); + } + } + + static async fetchTranslationsFor(newLocale) { + const response = await fetch(`lang/${newLocale}.json`) + + if (response.redirected === true || response.status !== 200) return false; + + return await response.json(); + } + + static translatePage() { + document + .querySelectorAll("[data-i18n-key]") + .forEach(element => Localization.translateElement(element)); + } + + static async translateElement(element) { + const key = element.getAttribute("data-i18n-key"); + const attrs = element.getAttribute("data-i18n-attrs").split(" "); + + for (let i in attrs) { + let attr = attrs[i]; + if (attr === "text") { + element.innerText = await Localization.getTranslation(key); + } else { + element.attr = await Localization.getTranslation(key, attr); + } + } + + } + + static getTranslation(key, attr, data) { + const keys = key.split("."); + + let translationCandidates = Localization.translations; + + for (let i=0; i this._connect(), 1000); Events.fire('ws-disconnected'); @@ -488,7 +488,7 @@ class Peer { _abortTransfer() { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); - Events.fire('notify-user', 'Files are incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect")); this._filesReceived = []; this._requestAccepted = null; this._digester = null; @@ -546,7 +546,7 @@ class Peer { this._chunker = null; if (!this._filesQueue.length) { this._busy = false; - Events.fire('notify-user', 'File transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app } else { this._dequeueFile(); @@ -558,7 +558,7 @@ class Peer { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); this._filesRequested = null; if (message.reason === 'ios-memory-limit') { - Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); + Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit")); } return; } @@ -568,7 +568,7 @@ class Peer { } _onMessageTransferCompleted() { - Events.fire('notify-user', 'Message transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed")); } sendText(text) { @@ -713,7 +713,7 @@ class RTCPeer extends Peer { _onBeforeUnload(e) { if (this._busy) { e.preventDefault(); - return "There are unfinished transfers. Are you sure you want to close?"; + return Localization.getTranslation("notifications.unfinished-transfers-warning"); } } diff --git a/public/scripts/ui.js b/public/scripts/ui.js index b494f58..f3d08d8 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -89,12 +89,12 @@ class PeersUI { if (newDisplayName) { PersistentStorage.set('editedDisplayName', newDisplayName) .then(_ => { - Events.fire('notify-user', 'Device name is changed permanently.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-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.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily")); }) .finally(_ => { Events.fire('self-display-name-changed', newDisplayName); @@ -105,10 +105,9 @@ class PeersUI { .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('notify-user', Localization.getTranslation("notifications.display-name-random-again")); Events.fire('self-display-name-changed', ''); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); }); @@ -275,21 +274,22 @@ class PeersUI { let descriptor; let noPeersMessage; + const openPairDrop = Localization.getTranslation("dialogs.activate-paste-mode-base"); + const andOtherFiles = Localization.getTranslation("dialogs.activate-paste-mode-and-other-files", null, {count: files.length-1}); + const sharedText = Localization.getTranslation("dialogs.activate-paste-mode-shared-text"); + if (files.length === 1) { - descriptor = files[0].name; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name}`; } else if (files.length > 1) { - descriptor = `${files[0].name} and ${files.length-1} other files`; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name} ${andOtherFiles}`; } else { - descriptor = "shared text"; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${sharedText}`; } - this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + this.$xInstructions.querySelector('p').innerHTML = noPeersMessage; this.$xInstructions.querySelector('p').style.display = 'block'; - this.$xInstructions.setAttribute('desktop', `Click to send`); - this.$xInstructions.setAttribute('mobile', `Tap to send`); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.click-to-send")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.tap-to-send")); this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; @@ -320,10 +320,10 @@ class PeersUI { this.$xInstructions.querySelector('p').innerText = ''; this.$xInstructions.querySelector('p').style.display = 'none'; - this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); - this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.x-instructions", "desktop")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.x-instructions", "mobile")); - this.$xNoPeers.querySelector('h2').innerHTML = 'Open PairDrop on other devices to send files'; + this.$xNoPeers.querySelector('h2').innerHTML = Localization.getTranslation("instructions.no-peers-title"); this.$cancelPasteModeBtn.setAttribute('hidden', ""); @@ -368,9 +368,9 @@ class PeerUI { let title; let input = ''; if (window.pasteMode.activated) { - title = `Click to send ${window.pasteMode.descriptor}`; + title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: window.pasteMode.descriptor}); } else { - title = 'Click to send files or right click to send a message'; + title = Localization.getTranslation("peer-ui.click-to-send"); input = ''; } this.$el.innerHTML = ` @@ -392,7 +392,7 @@ class PeerUI {
- +
`; @@ -509,10 +509,23 @@ class PeerUI { $progress.classList.remove('over50'); } if (progress < 1) { - this.$el.setAttribute('status', status); + if (status !== this.currentStatus) { + let statusName = { + "prepare": Localization.getTranslation("peer-ui.preparing"), + "transfer": Localization.getTranslation("peer-ui.transferring"), + "process": Localization.getTranslation("peer-ui.processing"), + "wait": Localization.getTranslation("peer-ui.waiting") + }[status]; + + this.$el.setAttribute('status', status); + this.$el.querySelector('.status').innerText = statusName; + this.currentStatus = status; + } } else { this.$el.removeAttribute('status'); + this.$el.querySelector('.status').innerHTML = ''; progress = 0; + this.currentStatus = null; } const degrees = `rotate(${360 * progress}deg)`; $progress.style.setProperty('--progress', degrees); @@ -595,7 +608,7 @@ class Dialog { _onPeerDisconnected(peerId) { if (this.isShown() && this.correspondingPeerId === peerId) { this.hide(); - Events.fire('notify-user', 'Selected peer left.') + Events.fire('notify-user', Localization.getTranslation("notifications.selected-peer-left")); } } } @@ -629,13 +642,17 @@ class ReceiveDialog extends Dialog { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { if (files.length > 1) { - let fileOtherText = ` and ${files.length - 1} other `; + let fileOther; if (files.length === 2) { - fileOtherText += imagesOnly ? 'image' : 'file'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); } else { - fileOtherText += imagesOnly ? 'images' : 'files'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } - this.$fileOther.innerText = fileOtherText; + this.$fileOther.innerText = fileOther; } const fileName = files[0].name; @@ -727,11 +744,15 @@ class ReceiveFileDialog extends ReceiveDialog { let descriptor, url, filenameDownload; if (files.length === 1) { - descriptor = imagesOnly ? 'Image' : 'File'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); } else { - descriptor = imagesOnly ? 'Images' : 'Files'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); } - this.$receiveTitle.innerText = `${descriptor} Received`; + this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor}); const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); if (canShare) { @@ -781,7 +802,7 @@ class ReceiveFileDialog extends ReceiveDialog { } } - this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download"); this.$downloadBtn.onclick = _ => { if (downloadZipped) { let tmpZipBtn = document.createElement("a"); @@ -793,17 +814,18 @@ class ReceiveFileDialog extends ReceiveDialog { } if (!canShare) { - this.$downloadBtn.innerText = "Download again"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again"); } - Events.fire('notify-user', `${descriptor} downloaded successfully`); + Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor})); this.$downloadBtn.style.pointerEvents = "none"; setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); }; document.title = files.length === 1 - ? 'File received - PairDrop' - : `${files.length} Files received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) this.show(); @@ -891,7 +913,7 @@ class ReceiveRequestDialog extends ReceiveDialog { this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` - document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; + document.title = `${ Localization.getTranslation("document-titles.file-transfer-requested") } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } @@ -1083,7 +1105,7 @@ class PairDeviceDialog extends Dialog { if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { this._cleanUp(); this.hide(); - Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-tabs-error")); return; } @@ -1129,7 +1151,7 @@ class PairDeviceDialog extends Dialog { PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) .then(_ => { - Events.fire('notify-user', 'Devices paired successfully.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success")); this._evaluateNumberRoomSecrets(); }) .finally(_ => { @@ -1137,13 +1159,13 @@ class PairDeviceDialog extends Dialog { this.hide(); }) .catch(_ => { - Events.fire('notify-user', 'Paired devices are not persistent.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent")); PersistentStorage.logBrowserNotCapable(); }); } _pairDeviceJoinKeyInvalid() { - Events.fire('notify-user', 'Key not valid'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid")); } _pairDeviceCancel() { @@ -1153,7 +1175,7 @@ class PairDeviceDialog extends Dialog { } _pairDeviceCanceled(roomKey) { - Events.fire('notify-user', `Key ${roomKey} invalidated.`); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: roomKey})); } _cleanUp() { @@ -1260,7 +1282,7 @@ class EditPairedDevicesDialog extends Dialog { PersistentStorage.clearRoomSecrets().finally(_ => { Events.fire('room-secrets-deleted', roomSecrets); Events.fire('evaluate-number-room-secrets'); - Events.fire('notify-user', 'All Devices unpaired.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared")); this.hide(); }) }); @@ -1415,14 +1437,14 @@ class ReceiveTextDialog extends Dialog { _setDocumentTitleMessages() { document.title = !this._receiveTextQueue.length - ? 'Message Received - PairDrop' - : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`; } async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', 'Copied to clipboard'); + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); this.hide(); } @@ -1449,13 +1471,13 @@ class Base64ZipDialog extends Dialog { if (base64Text === "paste") { // ?base64text=paste // base64 encoded string is ready to be pasted from clipboard - this.preparePasting("text"); + this.preparePasting(Localization.getTranslation("dialogs.base64-text")); } else if (base64Text === "hash") { // ?base64text=hash#BASE64ENCODED // base64 encoded string is url hash which is never sent to server and faster (recommended) this.processBase64Text(base64Hash) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1465,7 +1487,7 @@ class Base64ZipDialog extends Dialog { // base64 encoded string was part of url param (not recommended) this.processBase64Text(base64Text) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1478,32 +1500,32 @@ class Base64ZipDialog extends Dialog { // base64 encoded zip file is url hash which is never sent to the server this.processBase64Zip(base64Hash) .catch(_ => { - Events.fire('notify-user', 'File content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect")); console.log("File content incorrect."); }).finally(_ => { this.hide(); }); } else { // ?base64zip=paste || ?base64zip=true - this.preparePasting('files'); + this.preparePasting(Localization.getTranslation("dialogs.base64-files")); } } } _setPasteBtnToProcessing() { this.$pasteBtn.style.pointerEvents = "none"; - this.$pasteBtn.innerText = "Processing..."; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing"); } preparePasting(type) { if (navigator.clipboard.readText) { - this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", {type: type}); this._clickCallback = _ => this.processClipboard(type); this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); } else { 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.") this.$pasteBtn.setAttribute('hidden', ''); - this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation("dialogs.base64-paste-to-send", {type: type})); this.$fallbackTextarea.removeAttribute('hidden'); this._inputCallback = _ => this.processInput(type); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); @@ -1543,7 +1565,7 @@ class Base64ZipDialog extends Dialog { await this.processBase64Zip(base64); } } catch(_) { - Events.fire('notify-user', 'Clipboard content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect")); console.log("Clipboard content is incorrect.") } this.hide(); @@ -1626,7 +1648,7 @@ class Notifications { Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); return; } - Events.fire('notify-user', 'Notifications enabled.'); + Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled")); this.$button.setAttribute('hidden', 1); }); } @@ -1661,10 +1683,10 @@ class Notifications { if (document.visibilityState !== 'visible') { const peerDisplayName = $(peerId).ui._displayName(); if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { - const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); + const notification = this._notify(Localization.getTranslation("notifications.link-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => window.open(message, '_blank', null, true)); } else { - const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); + const notification = this._notify(Localization.getTranslation("notifications.message-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => this._copyText(message, notification)); } } @@ -1679,13 +1701,23 @@ class Notifications { break; } } - let title = files[0].name; - if (files.length >= 2) { - title += ` and ${files.length - 1} other `; - title += imagesOnly ? 'image' : 'file'; - if (files.length > 2) title += "s"; + let title; + if (files.length === 1) { + title = `${files[0].name}`; + } else { + let fileOther; + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); + } + title = `${files[0].name} ${fileOther}` } - const notification = this._notify(title, 'Click to download'); + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download")); this._bind(notification, _ => this._download(notification)); } } @@ -1699,15 +1731,27 @@ class Notifications { 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'); + + let descriptor; + if (request.header.length === 1) { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); + } else { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); + } + + let title = Localization.getTranslation("notifications.request-title", null, { + name: displayName, + count: request.header.length, + descriptor: descriptor.toLowerCase() + }); + + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-show")); } } @@ -1719,10 +1763,9 @@ class Notifications { _copyText(message, notification) { if (navigator.clipboard.writeText(message)) { notification.close(); - this._notify('Copied text to clipboard'); + this._notify(Localization.getTranslation("notifications.copied-text")); } else { - this._notify('Writing to clipboard failed. Copy manually!'); - + this._notify(Localization.getTranslation("notifications.copied-text-error")); } } @@ -1746,11 +1789,11 @@ class NetworkStatusUI { } _showOfflineMessage() { - Events.fire('notify-user', 'You are offline'); + Events.fire('notify-user', Localization.getTranslation("notifications.offline")); } _showOnlineMessage() { - Events.fire('notify-user', 'You are back online'); + Events.fire('notify-user', Localization.getTranslation("notifications.online")); } } @@ -2208,7 +2251,7 @@ class BrowserTabsConnector { class PairDrop { constructor() { - Events.on('load', _ => { + Events.on('translation-loaded', _ => { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); @@ -2232,6 +2275,7 @@ class PairDrop { const persistentStorage = new PersistentStorage(); const pairDrop = new PairDrop(); +const localization = new Localization(); if ('serviceWorker' in navigator) { diff --git a/public/styles.css b/public/styles.css index db86b60..1375b46 100644 --- a/public/styles.css +++ b/public/styles.css @@ -442,7 +442,7 @@ x-no-peers::before { } x-no-peers[drop-bg]::before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-no-peers[drop-bg] * { @@ -553,22 +553,6 @@ x-peer[status] x-icon { white-space: nowrap; } -x-peer[status=transfer] .status:before { - content: 'Transferring...'; -} - -x-peer[status=prepare] .status:before { - content: 'Preparing...'; -} - -x-peer[status=wait] .status:before { - content: 'Waiting...'; -} - -x-peer[status=process] .status:before { - content: 'Processing...'; -} - x-peer:not([status]) .status, x-peer[status] .device-name { display: none; @@ -626,11 +610,13 @@ footer .font-body2 { #on-this-network { border-bottom: solid 4px var(--primary-color); padding-bottom: 1px; + word-break: keep-all; } #paired-devices { border-bottom: solid 4px var(--paired-device-color); padding-bottom: 1px; + word-break: keep-all; } #display-name { @@ -723,10 +709,6 @@ x-dialog a { color: var(--primary-color); } -x-dialog .font-subheading { - margin-bottom: 5px; -} - /* Pair Devices Dialog */ #key-input-container { @@ -774,6 +756,10 @@ x-dialog .font-subheading { margin: 16px; } +#pair-instructions { + flex-direction: column; +} + x-dialog hr { margin: 40px -24px 30px -24px; border: solid 1.25px var(--border-color); @@ -785,7 +771,7 @@ x-dialog hr { /* Edit Paired Devices Dialog */ .paired-devices-wrapper:empty:before { - content: "No paired devices."; + content: attr(data-empty); } .paired-devices-wrapper:empty { @@ -1288,11 +1274,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before { } x-instructions[drop-peer]:before { - content: "Release to send to peer"; + content: attr(data-drop-peer); } x-instructions[drop-bg]:not([drop-peer]):before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-instructions p { diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 6beae65..e42f324 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -39,62 +39,66 @@
- +
-
+
-
+
-
+
-
- -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
+ +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
- +

@@ -104,18 +108,26 @@
- You are known as: -
+ You are known as: +
- You can be discovered by everyone on this network - +
+ You can be discovered by everyone + on this network +
+
- Traffic is routed through the server if WebRTC is not available. + Traffic is + routed through the server + if WebRTC is not available.
@@ -123,10 +135,13 @@ -

Pair Devices

+

Pair Devices

000 000

-
Input this key on another device
or scan the QR-Code.
+
+ Input this key on another device + or scan the QR-Code. +

@@ -136,10 +151,10 @@
-
Enter key from another device to continue.
+
Enter key from another device to continue.
- - + +
@@ -150,13 +165,21 @@ -

Edit Paired Devices

-
+

Edit Paired Devices

+
-

Activate auto-accept to automatically accept all files sent from that device.

+

+ + Activate + + auto-accept + + to automatically accept all files sent from that device. + +

- +
@@ -170,7 +193,7 @@
- would like to share + would like to share
@@ -182,8 +205,8 @@
- - + +
@@ -196,7 +219,7 @@
- has sent + has sent
@@ -207,9 +230,9 @@
- - - + + +
@@ -219,16 +242,16 @@ -

Send Message

+

Send Message

- Send a Message to + Send a Message to
- - + +
@@ -238,16 +261,16 @@ -

Message Received

+

Message Received

- has sent: + has sent:
- - + +
@@ -256,9 +279,9 @@ - + - + @@ -269,7 +292,7 @@
- + @@ -283,7 +306,7 @@

PairDrop

v1.7.6
-
The easiest way to transfer files across devices
+
The easiest way to transfer files across devices
@@ -376,6 +399,7 @@ + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json new file mode 100644 index 0000000..7ae2e56 --- /dev/null +++ b/public_included_ws_fallback/lang/en.json @@ -0,0 +1,136 @@ +{ + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading...", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-Code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about-aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded successfully", + "pairing-tabs-error": "Pairing of two browser tabs is not possible.", + "pairing-success": "Devices paired successfully.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Key not valid", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting...", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing...", + "waiting": "Waiting...", + "processing": "Processing...", + "transferring": "Transferring..." + } +} diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js new file mode 100644 index 0000000..d09d5c0 --- /dev/null +++ b/public_included_ws_fallback/scripts/localization.js @@ -0,0 +1,102 @@ +class Localization { + constructor() { + Localization.defaultLocale = "en"; + Localization.supportedLocales = ["en"]; + + Localization.translations = {}; + + const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + + Localization.setLocale(initialLocale) + .then(_ => { + Localization.translatePage(); + }) + } + + static isSupported(locale) { + return Localization.supportedLocales.indexOf(locale) > -1; + } + + static supportedOrDefault(locales) { + return locales.find(Localization.isSupported) || Localization.defaultLocale; + } + + static browserLocales() { + return navigator.languages.map(locale => + locale.split("-")[0] + ); + } + + static async setLocale(newLocale) { + if (newLocale === Localization.locale) return false; + + const newTranslations = await Localization.fetchTranslationsFor(newLocale); + + if(!newTranslations) return false; + + const firstTranslation = !Localization.locale + + Localization.locale = newLocale; + Localization.translations = newTranslations; + + if (firstTranslation) { + Events.fire("translation-loaded"); + } + } + + static async fetchTranslationsFor(newLocale) { + const response = await fetch(`lang/${newLocale}.json`) + + if (response.redirected === true || response.status !== 200) return false; + + return await response.json(); + } + + static translatePage() { + document + .querySelectorAll("[data-i18n-key]") + .forEach(element => Localization.translateElement(element)); + } + + static async translateElement(element) { + const key = element.getAttribute("data-i18n-key"); + const attrs = element.getAttribute("data-i18n-attrs").split(" "); + + for (let i in attrs) { + let attr = attrs[i]; + if (attr === "text") { + element.innerText = await Localization.getTranslation(key); + } else { + element.attr = await Localization.getTranslation(key, attr); + } + } + + } + + static getTranslation(key, attr, data) { + const keys = key.split("."); + + let translationCandidates = Localization.translations; + + for (let i=0; i this._connect(), 1000); Events.fire('ws-disconnected'); @@ -505,7 +505,7 @@ class Peer { _abortTransfer() { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); - Events.fire('notify-user', 'Files are incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect")); this._filesReceived = []; this._requestAccepted = null; this._digester = null; @@ -546,7 +546,7 @@ class Peer { this._abortTransfer(); } - // include for compatibility with Snapdrop for Android app + // include for compatibility with 'Snapdrop & PairDrop for Android' app Events.fire('file-received', fileBlob); this._filesReceived.push(fileBlob); @@ -563,7 +563,8 @@ class Peer { this._chunker = null; if (!this._filesQueue.length) { this._busy = false; - Events.fire('notify-user', 'File transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); + Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app } else { this._dequeueFile(); } @@ -574,7 +575,7 @@ class Peer { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); this._filesRequested = null; if (message.reason === 'ios-memory-limit') { - Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); + Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit")); } return; } @@ -584,7 +585,7 @@ class Peer { } _onMessageTransferCompleted() { - Events.fire('notify-user', 'Message transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed")); } sendText(text) { @@ -729,7 +730,7 @@ class RTCPeer extends Peer { _onBeforeUnload(e) { if (this._busy) { e.preventDefault(); - return "There are unfinished transfers. Are you sure you want to close?"; + return Localization.getTranslation("notifications.unfinished-transfers-warning"); } } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index d355468..b3afac4 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -89,12 +89,12 @@ class PeersUI { if (newDisplayName) { PersistentStorage.set('editedDisplayName', newDisplayName) .then(_ => { - Events.fire('notify-user', 'Device name is changed permanently.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-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.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily")); }) .finally(_ => { Events.fire('self-display-name-changed', newDisplayName); @@ -105,10 +105,9 @@ class PeersUI { .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('notify-user', Localization.getTranslation("notifications.display-name-random-again")); Events.fire('self-display-name-changed', ''); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); }); @@ -275,21 +274,22 @@ class PeersUI { let descriptor; let noPeersMessage; + const openPairDrop = Localization.getTranslation("dialogs.activate-paste-mode-base"); + const andOtherFiles = Localization.getTranslation("dialogs.activate-paste-mode-and-other-files", null, {count: files.length-1}); + const sharedText = Localization.getTranslation("dialogs.activate-paste-mode-shared-text"); + if (files.length === 1) { - descriptor = files[0].name; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name}`; } else if (files.length > 1) { - descriptor = `${files[0].name} and ${files.length-1} other files`; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name} ${andOtherFiles}`; } else { - descriptor = "shared text"; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${sharedText}`; } - this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + this.$xInstructions.querySelector('p').innerHTML = noPeersMessage; this.$xInstructions.querySelector('p').style.display = 'block'; - this.$xInstructions.setAttribute('desktop', `Click to send`); - this.$xInstructions.setAttribute('mobile', `Tap to send`); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.click-to-send")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.tap-to-send")); this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; @@ -320,10 +320,10 @@ class PeersUI { this.$xInstructions.querySelector('p').innerText = ''; this.$xInstructions.querySelector('p').style.display = 'none'; - this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); - this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.x-instructions", "desktop")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.x-instructions", "mobile")); - this.$xNoPeers.querySelector('h2').innerHTML = 'Open PairDrop on other devices to send files'; + this.$xNoPeers.querySelector('h2').innerHTML = Localization.getTranslation("instructions.no-peers-title"); this.$cancelPasteModeBtn.setAttribute('hidden', ""); @@ -368,9 +368,9 @@ class PeerUI { let title; let input = ''; if (window.pasteMode.activated) { - title = `Click to send ${window.pasteMode.descriptor}`; + title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: window.pasteMode.descriptor}); } else { - title = 'Click to send files or right click to send a message'; + title = Localization.getTranslation("peer-ui.click-to-send"); input = ''; } this.$el.innerHTML = ` @@ -392,7 +392,7 @@ class PeerUI {
- +
`; @@ -510,10 +510,23 @@ class PeerUI { $progress.classList.remove('over50'); } if (progress < 1) { - this.$el.setAttribute('status', status); + if (status !== this.currentStatus) { + let statusName = { + "prepare": Localization.getTranslation("peer-ui.preparing"), + "transfer": Localization.getTranslation("peer-ui.transferring"), + "process": Localization.getTranslation("peer-ui.processing"), + "wait": Localization.getTranslation("peer-ui.waiting") + }[status]; + + this.$el.setAttribute('status', status); + this.$el.querySelector('.status').innerText = statusName; + this.currentStatus = status; + } } else { this.$el.removeAttribute('status'); + this.$el.querySelector('.status').innerHTML = ''; progress = 0; + this.currentStatus = null; } const degrees = `rotate(${360 * progress}deg)`; $progress.style.setProperty('--progress', degrees); @@ -596,7 +609,7 @@ class Dialog { _onPeerDisconnected(peerId) { if (this.isShown() && this.correspondingPeerId === peerId) { this.hide(); - Events.fire('notify-user', 'Selected peer left.') + Events.fire('notify-user', Localization.getTranslation("notifications.selected-peer-left")); } } } @@ -630,13 +643,17 @@ class ReceiveDialog extends Dialog { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { if (files.length > 1) { - let fileOtherText = ` and ${files.length - 1} other `; + let fileOther; if (files.length === 2) { - fileOtherText += imagesOnly ? 'image' : 'file'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); } else { - fileOtherText += imagesOnly ? 'images' : 'files'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } - this.$fileOther.innerText = fileOtherText; + this.$fileOther.innerText = fileOther; } const fileName = files[0].name; @@ -728,11 +745,15 @@ class ReceiveFileDialog extends ReceiveDialog { let descriptor, url, filenameDownload; if (files.length === 1) { - descriptor = imagesOnly ? 'Image' : 'File'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); } else { - descriptor = imagesOnly ? 'Images' : 'Files'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); } - this.$receiveTitle.innerText = `${descriptor} Received`; + this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor}); const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); if (canShare) { @@ -782,7 +803,7 @@ class ReceiveFileDialog extends ReceiveDialog { } } - this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download"); this.$downloadBtn.onclick = _ => { if (downloadZipped) { let tmpZipBtn = document.createElement("a"); @@ -794,17 +815,18 @@ class ReceiveFileDialog extends ReceiveDialog { } if (!canShare) { - this.$downloadBtn.innerText = "Download again"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again"); } - Events.fire('notify-user', `${descriptor} downloaded successfully`); + Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor})); this.$downloadBtn.style.pointerEvents = "none"; setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); }; document.title = files.length === 1 - ? 'File received - PairDrop' - : `${files.length} Files received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) this.show(); @@ -892,7 +914,7 @@ class ReceiveRequestDialog extends ReceiveDialog { this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` - document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; + document.title = `${ Localization.getTranslation("document-titles.file-transfer-requested") } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } @@ -1084,7 +1106,7 @@ class PairDeviceDialog extends Dialog { if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { this._cleanUp(); this.hide(); - Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-tabs-error")); return; } @@ -1130,7 +1152,7 @@ class PairDeviceDialog extends Dialog { PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) .then(_ => { - Events.fire('notify-user', 'Devices paired successfully.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success")); this._evaluateNumberRoomSecrets(); }) .finally(_ => { @@ -1138,13 +1160,13 @@ class PairDeviceDialog extends Dialog { this.hide(); }) .catch(_ => { - Events.fire('notify-user', 'Paired devices are not persistent.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent")); PersistentStorage.logBrowserNotCapable(); }); } _pairDeviceJoinKeyInvalid() { - Events.fire('notify-user', 'Key not valid'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid")); } _pairDeviceCancel() { @@ -1154,7 +1176,7 @@ class PairDeviceDialog extends Dialog { } _pairDeviceCanceled(roomKey) { - Events.fire('notify-user', `Key ${roomKey} invalidated.`); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: roomKey})); } _cleanUp() { @@ -1261,7 +1283,7 @@ class EditPairedDevicesDialog extends Dialog { PersistentStorage.clearRoomSecrets().finally(_ => { Events.fire('room-secrets-deleted', roomSecrets); Events.fire('evaluate-number-room-secrets'); - Events.fire('notify-user', 'All Devices unpaired.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared")); this.hide(); }) }); @@ -1416,14 +1438,14 @@ class ReceiveTextDialog extends Dialog { _setDocumentTitleMessages() { document.title = !this._receiveTextQueue.length - ? 'Message Received - PairDrop' - : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`; } async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', 'Copied to clipboard'); + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); this.hide(); } @@ -1450,13 +1472,13 @@ class Base64ZipDialog extends Dialog { if (base64Text === "paste") { // ?base64text=paste // base64 encoded string is ready to be pasted from clipboard - this.preparePasting("text"); + this.preparePasting(Localization.getTranslation("dialogs.base64-text")); } else if (base64Text === "hash") { // ?base64text=hash#BASE64ENCODED // base64 encoded string is url hash which is never sent to server and faster (recommended) this.processBase64Text(base64Hash) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1466,7 +1488,7 @@ class Base64ZipDialog extends Dialog { // base64 encoded string was part of url param (not recommended) this.processBase64Text(base64Text) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1479,32 +1501,32 @@ class Base64ZipDialog extends Dialog { // base64 encoded zip file is url hash which is never sent to the server this.processBase64Zip(base64Hash) .catch(_ => { - Events.fire('notify-user', 'File content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect")); console.log("File content incorrect."); }).finally(_ => { this.hide(); }); } else { // ?base64zip=paste || ?base64zip=true - this.preparePasting('files'); + this.preparePasting(Localization.getTranslation("dialogs.base64-files")); } } } _setPasteBtnToProcessing() { this.$pasteBtn.style.pointerEvents = "none"; - this.$pasteBtn.innerText = "Processing..."; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing"); } preparePasting(type) { if (navigator.clipboard.readText) { - this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", {type: type}); this._clickCallback = _ => this.processClipboard(type); this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); } else { 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.") this.$pasteBtn.setAttribute('hidden', ''); - this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation("dialogs.base64-paste-to-send", {type: type})); this.$fallbackTextarea.removeAttribute('hidden'); this._inputCallback = _ => this.processInput(type); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); @@ -1544,7 +1566,7 @@ class Base64ZipDialog extends Dialog { await this.processBase64Zip(base64); } } catch(_) { - Events.fire('notify-user', 'Clipboard content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect")); console.log("Clipboard content is incorrect.") } this.hide(); @@ -1627,7 +1649,7 @@ class Notifications { Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); return; } - Events.fire('notify-user', 'Notifications enabled.'); + Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled")); this.$button.setAttribute('hidden', 1); }); } @@ -1662,10 +1684,10 @@ class Notifications { if (document.visibilityState !== 'visible') { const peerDisplayName = $(peerId).ui._displayName(); if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { - const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); + const notification = this._notify(Localization.getTranslation("notifications.link-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => window.open(message, '_blank', null, true)); } else { - const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); + const notification = this._notify(Localization.getTranslation("notifications.message-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => this._copyText(message, notification)); } } @@ -1680,13 +1702,23 @@ class Notifications { break; } } - let title = files[0].name; - if (files.length >= 2) { - title += ` and ${files.length - 1} other `; - title += imagesOnly ? 'image' : 'file'; - if (files.length > 2) title += "s"; + let title; + if (files.length === 1) { + title = `${files[0].name}`; + } else { + let fileOther; + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); + } + title = `${files[0].name} ${fileOther}` } - const notification = this._notify(title, 'Click to download'); + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download")); this._bind(notification, _ => this._download(notification)); } } @@ -1700,15 +1732,27 @@ class Notifications { 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'); + + let descriptor; + if (request.header.length === 1) { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); + } else { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); + } + + let title = Localization.getTranslation("notifications.request-title", null, { + name: displayName, + count: request.header.length, + descriptor: descriptor.toLowerCase() + }); + + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-show")); } } @@ -1720,10 +1764,9 @@ class Notifications { _copyText(message, notification) { if (navigator.clipboard.writeText(message)) { notification.close(); - this._notify('Copied text to clipboard'); + this._notify(Localization.getTranslation("notifications.copied-text")); } else { - this._notify('Writing to clipboard failed. Copy manually!'); - + this._notify(Localization.getTranslation("notifications.copied-text-error")); } } @@ -1747,11 +1790,11 @@ class NetworkStatusUI { } _showOfflineMessage() { - Events.fire('notify-user', 'You are offline'); + Events.fire('notify-user', Localization.getTranslation("notifications.offline")); } _showOnlineMessage() { - Events.fire('notify-user', 'You are back online'); + Events.fire('notify-user', Localization.getTranslation("notifications.online")); } } @@ -2209,7 +2252,7 @@ class BrowserTabsConnector { class PairDrop { constructor() { - Events.on('load', _ => { + Events.on('translation-loaded', _ => { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); @@ -2233,6 +2276,7 @@ class PairDrop { const persistentStorage = new PersistentStorage(); const pairDrop = new PairDrop(); +const localization = new Localization(); if ('serviceWorker' in navigator) { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index e384b51..2e8fbb8 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1345,11 +1345,11 @@ x-peers:empty~x-instructions { transition: opacity 300ms; } -#websocket-fallback > span { +#websocket-fallback { margin: 2px; } -#websocket-fallback > span > span { +#websocket-fallback > span:nth-child(2) { border-bottom: solid 4px var(--ws-peer-color); } From ba46befde4d3e3657254690cfa6d7a3cc307614a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 7 Jul 2023 14:58:15 +0200 Subject: [PATCH 028/519] include translations for about buttons and implement translation fallback if used translation is not complete --- public/index.html | 14 ++++----- public/lang/en.json | 8 +++-- public/scripts/localization.js | 30 +++++++++++++------ public_included_ws_fallback/index.html | 14 ++++----- public_included_ws_fallback/lang/en.json | 8 +++-- .../scripts/localization.js | 30 +++++++++++++------ 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/public/index.html b/public/index.html index 15b82da..21f6005 100644 --- a/public/index.html +++ b/public/index.html @@ -83,7 +83,7 @@
- +
@@ -161,7 +161,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public/lang/en.json b/public/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public/scripts/localization.js b/public/scripts/localization.js index d09d5c0..c7d9716 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -2,8 +2,8 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); @@ -29,13 +29,13 @@ class Localization { static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const firstTranslation = !Localization.locale + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; @@ -65,18 +65,20 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i
- +
@@ -166,7 +166,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index d09d5c0..c7d9716 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -2,8 +2,8 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); @@ -29,13 +29,13 @@ class Localization { static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const firstTranslation = !Localization.locale + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; @@ -65,18 +65,20 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i Date: Fri, 7 Jul 2023 00:06:38 +0200 Subject: [PATCH 029/519] Added translation using Weblate (German) --- public/lang/de.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/de.json diff --git a/public/lang/de.json b/public/lang/de.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/de.json @@ -0,0 +1 @@ +{} From 1d333c850c37e6df5b97764b432d6078ce3380e4 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 7 Jul 2023 00:07:04 +0200 Subject: [PATCH 030/519] Added translation using Weblate (Russian) --- public/lang/ru.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/ru.json diff --git a/public/lang/ru.json b/public/lang/ru.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/ru.json @@ -0,0 +1 @@ +{} From 410936dcd888aaa4c140f71b9faf499e7720561c Mon Sep 17 00:00:00 2001 From: kek Date: Fri, 7 Jul 2023 00:04:00 +0000 Subject: [PATCH 031/519] Translated using Weblate (Russian) Currently translated at 100.0% (118 of 118 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/ --- public/lang/ru.json | 137 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/public/lang/ru.json b/public/lang/ru.json index 0967ef4..bcd0103 100644 --- a/public/lang/ru.json +++ b/public/lang/ru.json @@ -1 +1,136 @@ -{} +{ + "header": { + "about_aria-label": "Открыть страницу \"О сервисе\"", + "pair-device_title": "Подключить устройство", + "install_title": "Установить PairDrop", + "cancel-paste-mode": "Выполнено", + "edit-paired-devices_title": "Редактировать сопряженные устройства", + "notification_title": "Включить уведомления", + "about_title": "О сервисе", + "theme-auto_title": "Адаптировать тему к системной", + "theme-dark_title": "Всегда использовать темную тему", + "theme-light_title": "Всегда использовать светлую тему" + }, + "instructions": { + "x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение", + "no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "click-to-send": "Нажмите, чтобы отправить", + "x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "tap-to-send": "Нажмите, чтобы отправить", + "x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу", + "x-instructions_mobile": "Нажмите, чтобы отправить файлы, или долго нажмите, чтобы отправить сообщение", + "no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы", + "no-peers-subtitle": "Сопрягите устройства из разных сетей." + }, + "footer": { + "discovery-everyone": "О вас может узнать любой", + "display-name_placeholder": "Загрузка...", + "routed": "направляется через сервер", + "webrtc": "есть WebRTC недоступен.", + "traffic": "Трафик:", + "and-by": "и от", + "paired-devices": "сопряженные устройства", + "known-as": "Вы известны под именем:", + "on-this-network": "в этой сети", + "display-name_title": "Изменить имя вашего устройства навсегда" + }, + "dialogs": { + "activate-paste-mode-and-other-files": "и {{count}} других файлов", + "activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить", + "activate-paste-mode-activate-paste-mode-shared-text": "общий текст", + "edit-paired-devices-title": "Редактировать сопряженные устройства", + "auto-accept": "автоприем", + "close": "Закрыть", + "decline": "Отклонить", + "share": "Поделиться", + "would-like-to-share": "хотел бы поделиться", + "has-sent": "отправил:", + "paired-devices-wrapper_data-empty": "Нет сопряженных устройств.", + "download": "Скачать", + "receive-text-title": "Сообщение получено", + "send": "Отправить", + "send-message-to": "Отправить сообщение", + "send-message-title": "Отправить сообщение", + "copy": "Копировать", + "base64-files": "файлы", + "base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}", + "base64-processing": "Обработка...", + "base64-tap-to-paste": "Нажмите здесь, чтобы вставить {{type}}", + "base64-text": "текст", + "title-file": "Файл", + "title-file-plural": "Файлы", + "title-image": "Изображение", + "title-image-plural": "Изображения", + "download-again": "Скачать снова", + "auto-accept-instructions-2": "чтобы автоматически принимать все файлы, отправленные с этого устройства.", + "enter-key-from-another-device": "Для продолжения введите ключ с другого устройства.", + "pair-devices-title": "Сопрягите устройства", + "input-key-on-this-device": "Введите этот ключ на другом устройстве", + "scan-qr-code": "или отсканируйте QR-код.", + "cancel": "Отменить", + "pair": "Подключить", + "accept": "Принять", + "auto-accept-instructions-1": "Активировать", + "file-other-description-file": "и 1 другой файл", + "file-other-description-image-plural": "и {{count}} других изображений", + "file-other-description-image": "и 1 другое изображение", + "file-other-description-file-plural": "и {{count}} других файлов", + "receive-title": "{{descriptor}} получен" + }, + "about": { + "close-about-aria-label": "Закрыть страницу \"О сервисе\"", + "claim": "Самый простой способ передачи файлов между устройствами" + }, + "notifications": { + "display-name-changed-permanently": "Отображаемое имя изменено навсегда.", + "display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.", + "pairing-success": "Устройства сопряжены успешно.", + "pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.", + "copied-to-clipboard": "Скопировано в буфер обмена", + "pairing-not-persistent": "Сопряженные устройства непостоянны.", + "link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть", + "notifications-enabled": "Уведомления включены.", + "text-content-incorrect": "Содержание текста неверно.", + "message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать", + "connected": "Подключено.", + "copied-text": "Текст скопирован в буфер обмена", + "online": "Вы снова в сети", + "offline": "Вы находитесь вне сети", + "online-requirement": "Для сопряжения устройств вам нужно быть в сети.", + "files-incorrect": "Файлы неверны.", + "message-transfer-completed": "Передача сообщения завершена.", + "ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз", + "selected-peer-left": "Выбранный узел вышел.", + "request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}", + "rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.", + "unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть?", + "copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!", + "pairing-cleared": "Все устройства не сопряжены.", + "pairing-key-invalid": "Ключ недействителен", + "pairing-key-invalidated": "Ключ {{key}} признан недействительным.", + "click-to-download": "Нажмите, чтобы скачать", + "clipboard-content-incorrect": "Содержание буфера обмена неверно.", + "click-to-show": "Нажмите, чтобы показать", + "connecting": "Подключение...", + "download-successful": "{{descriptor}} успешно загружен", + "display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.", + "file-content-incorrect": "Содержимое файла неверно.", + "file-transfer-completed": "Передача файла завершена." + }, + "peer-ui": { + "click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}", + "preparing": "Подготовка...", + "transferring": "Передача...", + "processing": "Обработка...", + "waiting": "Ожидание...", + "connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах", + "click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение" + }, + "document-titles": { + "file-received-plural": "{{count}} файлов получено", + "message-received-plural": "{{count}} сообщений получено", + "file-received": "Файл получен", + "file-transfer-requested": "Запрошена передача файлов", + "message-received": "Сообщение получено" + } +} From 525fd295b7e264ed332c12bb9a06ca39595fa1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 7 Jul 2023 16:08:09 +0200 Subject: [PATCH 032/519] =?UTF-8?q?Added=20translation=20using=20Weblate?= =?UTF-8?q?=20(Norwegian=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/lang/nb-NO.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/nb-NO.json diff --git a/public/lang/nb-NO.json b/public/lang/nb-NO.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/nb-NO.json @@ -0,0 +1 @@ +{} From 99faa6bbfd9f7982a0abd9bac1440b5d7761fc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 7 Jul 2023 14:25:46 +0000 Subject: [PATCH 033/519] Translated using Weblate (English) Currently translated at 100.0% (122 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/ --- public/lang/en.json | 276 ++++++++++++++++++++++---------------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/public/lang/en.json b/public/lang/en.json index 8ad7b7c..ff8294d 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -1,140 +1,140 @@ { - "header": { - "about_title": "About PairDrop", - "about_aria-label": "Open About PairDrop", - "theme-auto_title": "Adapt Theme to System", - "theme-light_title": "Always Use Light-Theme", - "theme-dark_title": "Always Use Dark-Theme", - "notification_title": "Enable Notifications", - "install_title": "Install PairDrop", - "pair-device_title": "Pair Device", - "edit-paired-devices_title": "Edit Paired Devices", - "cancel-paste-mode": "Done" - }, - "instructions": { - "no-peers_data-drop-bg": "Release to select recipient", - "no-peers-title": "Open PairDrop on other devices to send files", - "no-peers-subtitle": "Pair devices to be discoverable on other networks", - "x-instructions_desktop": "Click to send files or right click to send a message", - "x-instructions_mobile": "Tap to send files or long tap to send a message", - "x-instructions_data-drop-peer": "Release to send to peer", - "x-instructions_data-drop-bg": "Release to select recipient", - "click-to-send": "Click to send", - "tap-to-send": "Tap to send" - }, - "footer": { - "known-as": "You are known as:", - "display-name_placeholder": "Loading...", - "display-name_title": "Edit your device name permanently", - "discovery-everyone": "You can be discovered by everyone", - "on-this-network": "on this network", - "and-by": "and by", - "paired-devices": "paired devices", - "traffic": "Traffic is", - "routed": "routed through the server", - "webrtc": "if WebRTC is not available." - }, - "dialogs": { - "activate-paste-mode-base": "Open PairDrop on other devices to send", - "activate-paste-mode-and-other-files": "and {{count}} other files", - "activate-paste-mode-activate-paste-mode-shared-text": "shared text", - "pair-devices-title": "Pair Devices", - "input-key-on-this-device": "Input this key on another device", - "scan-qr-code": "or scan the QR-Code.", - "enter-key-from-another-device": "Enter key from another device to continue.", - "pair": "Pair", - "cancel": "Cancel", - "edit-paired-devices-title": "Edit Paired Devices", - "paired-devices-wrapper_data-empty": "No paired devices.", - "auto-accept-instructions-1": "Activate", - "auto-accept": "auto-accept", - "auto-accept-instructions-2": "to automatically accept all files sent from that device.", - "close": "Close", - "would-like-to-share": "would like to share", - "accept": "Accept", - "decline": "Decline", - "has-sent": "has sent:", - "share": "Share", - "download": "Download", - "send-message-title": "Send Message", - "send-message-to": "Send a Message to", - "send": "Send", - "receive-text-title": "Message Received", - "copy": "Copy", - "base64-processing": "Processing...", - "base64-tap-to-paste": "Tap here to paste {{type}}", - "base64-paste-to-send": "Paste here to send {{type}}", - "base64-text": "text", - "base64-files": "files", - "file-other-description-image": "and 1 other image", - "file-other-description-file": "and 1 other file", - "file-other-description-image-plural": "and {{count}} other images", - "file-other-description-file-plural": "and {{count}} other files", - "title-image": "Image", - "title-file": "File", - "title-image-plural": "Images", - "title-file-plural": "Files", - "receive-title": "{{descriptor}} Received", - "download-again": "Download again" - }, - "about": { - "close-about_aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices", - "github_title": "PairDrop on Github", - "buy-me-a-coffee_title": "Buy me a coffee!", - "tweet_title": "Tweet about PairDrop", - "faq_title": "Frequently asked questions" - }, - "notifications": { - "display-name-changed-permanently": "Display name is changed permanently.", - "display-name-changed-temporarily": "Display name is changed only for this session.", - "display-name-random-again": "Display name is randomly generated again.", - "download-successful": "{{descriptor}} downloaded successfully", - "pairing-tabs-error": "Pairing of two browser tabs is not possible.", - "pairing-success": "Devices paired successfully.", - "pairing-not-persistent": "Paired devices are not persistent.", - "pairing-key-invalid": "Key not valid", - "pairing-key-invalidated": "Key {{key}} invalidated.", - "pairing-cleared": "All Devices unpaired.", - "copied-to-clipboard": "Copied to clipboard", - "text-content-incorrect": "Text content is incorrect.", - "file-content-incorrect": "File content is incorrect.", - "clipboard-content-incorrect": "Clipboard content is incorrect.", - "notifications-enabled": "Notifications enabled.", - "link-received": "Link received by {{name}} - Click to open", - "message-received": "Message received by {{name}} - Click to copy", - "click-to-download": "Click to download", - "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", - "click-to-show": "Click to show", - "copied-text": "Copied text to clipboard", - "copied-text-error": "Writing to clipboard failed. Copy manually!", - "offline": "You are offline", - "online": "You are back online", - "connected": "Connected.", - "online-requirement": "You need to be online to pair devices.", - "connecting": "Connecting...", - "files-incorrect": "Files are incorrect.", - "file-transfer-completed": "File transfer completed.", - "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", - "message-transfer-completed": "Message transfer completed.", - "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", - "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", - "selected-peer-left": "Selected peer left." - }, - "document-titles": { - "file-received": "File Received", - "file-received-plural": "{{count}} Files Received", - "file-transfer-requested": "File Transfer Requested", - "message-received": "Message Received", - "message-received-plural": "{{count}} Messages Received" - }, - "peer-ui": { - "click-to-send-paste-mode": "Click to send {{descriptor}}", - "click-to-send": "Click to send files or right click to send a message", - "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", - "preparing": "Preparing...", - "waiting": "Waiting...", - "processing": "Processing...", - "transferring": "Transferring..." - } + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading…", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on GitHub", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded", + "pairing-tabs-error": "Pairing two web browser tabs is impossible.", + "pairing-success": "Devices paired.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Invalid key", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting…", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing…", + "waiting": "Waiting…", + "processing": "Processing…", + "transferring": "Transferring…" + } } From 65ec416646b4e58e1a27bfb775d4f11b5b65db3a Mon Sep 17 00:00:00 2001 From: kek Date: Fri, 7 Jul 2023 23:20:33 +0000 Subject: [PATCH 034/519] Translated using Weblate (Russian) Currently translated at 100.0% (122 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/ --- public/lang/ru.json | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/public/lang/ru.json b/public/lang/ru.json index bcd0103..8617ec2 100644 --- a/public/lang/ru.json +++ b/public/lang/ru.json @@ -16,19 +16,19 @@ "no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя", "click-to-send": "Нажмите, чтобы отправить", "x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя", - "tap-to-send": "Нажмите, чтобы отправить", + "tap-to-send": "Прикоснитесь, чтобы отправить", "x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу", - "x-instructions_mobile": "Нажмите, чтобы отправить файлы, или долго нажмите, чтобы отправить сообщение", + "x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение", "no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы", "no-peers-subtitle": "Сопрягите устройства из разных сетей." }, "footer": { - "discovery-everyone": "О вас может узнать любой", - "display-name_placeholder": "Загрузка...", + "discovery-everyone": "О вас может узнать каждый", + "display-name_placeholder": "Загрузка…", "routed": "направляется через сервер", - "webrtc": "есть WebRTC недоступен.", - "traffic": "Трафик:", - "and-by": "и от", + "webrtc": ", если WebRTC недоступен.", + "traffic": "Трафик", + "and-by": "и", "paired-devices": "сопряженные устройства", "known-as": "Вы известны под именем:", "on-this-network": "в этой сети", @@ -55,13 +55,13 @@ "base64-files": "файлы", "base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}", "base64-processing": "Обработка...", - "base64-tap-to-paste": "Нажмите здесь, чтобы вставить {{type}}", + "base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}", "base64-text": "текст", "title-file": "Файл", "title-file-plural": "Файлы", "title-image": "Изображение", "title-image-plural": "Изображения", - "download-again": "Скачать снова", + "download-again": "Скачать еще раз", "auto-accept-instructions-2": "чтобы автоматически принимать все файлы, отправленные с этого устройства.", "enter-key-from-another-device": "Для продолжения введите ключ с другого устройства.", "pair-devices-title": "Сопрягите устройства", @@ -79,12 +79,17 @@ }, "about": { "close-about-aria-label": "Закрыть страницу \"О сервисе\"", - "claim": "Самый простой способ передачи файлов между устройствами" + "claim": "Самый простой способ передачи файлов между устройствами", + "close-about_aria-label": "Закрыть страницу \"О сервисе\"", + "buy-me-a-coffee_title": "Купить мне кофе!", + "github_title": "PairDrop на GitHub", + "tweet_title": "Твит о PairDrop", + "faq_title": "Часто задаваемые вопросы" }, "notifications": { - "display-name-changed-permanently": "Отображаемое имя изменено навсегда.", + "display-name-changed-permanently": "Отображаемое имя было изменено навсегда.", "display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.", - "pairing-success": "Устройства сопряжены успешно.", + "pairing-success": "Устройства сопряжены.", "pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.", "copied-to-clipboard": "Скопировано в буфер обмена", "pairing-not-persistent": "Сопряженные устройства непостоянны.", @@ -106,23 +111,23 @@ "unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть?", "copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!", "pairing-cleared": "Все устройства не сопряжены.", - "pairing-key-invalid": "Ключ недействителен", + "pairing-key-invalid": "Неверный ключ", "pairing-key-invalidated": "Ключ {{key}} признан недействительным.", "click-to-download": "Нажмите, чтобы скачать", "clipboard-content-incorrect": "Содержание буфера обмена неверно.", "click-to-show": "Нажмите, чтобы показать", - "connecting": "Подключение...", - "download-successful": "{{descriptor}} успешно загружен", + "connecting": "Подключение…", + "download-successful": "{{descriptor}} загружен", "display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.", "file-content-incorrect": "Содержимое файла неверно.", "file-transfer-completed": "Передача файла завершена." }, "peer-ui": { "click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}", - "preparing": "Подготовка...", - "transferring": "Передача...", - "processing": "Обработка...", - "waiting": "Ожидание...", + "preparing": "Подготовка…", + "transferring": "Передача…", + "processing": "Обработка…", + "waiting": "Ожидание…", "connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах", "click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение" }, From 9b71d93dd3dff789c77456a9ff3691dfbb3d8230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 7 Jul 2023 14:09:16 +0000 Subject: [PATCH 035/519] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 80.3% (98 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/ --- public/lang/nb-NO.json | 130 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/public/lang/nb-NO.json b/public/lang/nb-NO.json index 0967ef4..52f1d3d 100644 --- a/public/lang/nb-NO.json +++ b/public/lang/nb-NO.json @@ -1 +1,129 @@ -{} +{ + "header": { + "edit-paired-devices_title": "Rediger sammenkoblede enheter", + "about_title": "Om PairDrop", + "about_aria-label": "Åpne «Om PairDrop»", + "theme-auto_title": "Juster drakt til system", + "theme-light_title": "Alltid bruk lys drakt", + "theme-dark_title": "Alltid bruk mørk drakt", + "notification_title": "Skru på merknader", + "cancel-paste-mode": "Ferdig", + "install_title": "Installer PairDrop", + "pair-device_title": "Sammenkoble enhet" + }, + "footer": { + "discovery-everyone": "Du kan oppdages av alle", + "and-by": "og av", + "webrtc": "hvis WebRTC ikke er tilgjengelig.", + "display-name_placeholder": "Laster inn …", + "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", + "traffic": "Trafikken", + "on-this-network": "på dette nettverket", + "known-as": "Du er kjent som:", + "paired-devices": "sammenkoblede enheter", + "routed": "Sendes gjennom tjeneren" + }, + "instructions": { + "x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding", + "x-instructions_data-drop-bg": "Slipp for å velge mottager", + "click-to-send": "Klikk for å sende", + "no-peers_data-drop-bg": "Slipp for å velge mottager", + "no-peers-title": "Åpne PairDrop på andre enheter for å sende filer", + "no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk", + "x-instructions_data-drop-peer": "Slipp for å sende til likemann", + "tap-to-send": "Trykk for å sende" + }, + "dialogs": { + "input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet", + "pair-devices-title": "Sammenkoble enheter", + "would-like-to-share": "ønsker å dele", + "auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.", + "paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter", + "enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.", + "edit-paired-devices-title": "Rediger sammenkoblede enheter", + "accept": "Godta", + "has-sent": "har sendt:", + "base64-paste-to-send": "Trykk her for å sende {{type}}", + "base64-text": "tekst", + "base64-files": "filer", + "file-other-description-image-plural": "og {{count}} andre bilder", + "receive-title": "{{descriptor}} mottatt", + "send-message-title": "Send melding", + "base64-processing": "Behandler …", + "close": "Lukk", + "decline": "Avslå", + "download": "Last ned", + "copy": "Kopier", + "pair": "Sammenkoble", + "cancel": "Avbryt", + "scan-qr-code": "eller skann QR-koden.", + "auto-accept-instructions-1": "Aktiver", + "receive-text-title": "Melding mottatt", + "auto-accept": "auto-godkjenn", + "share": "Del", + "send-message-to": "Send en melding til", + "send": "Send", + "base64-tap-to-paste": "Trykk her for å lime inn {{type]]", + "file-other-description-image": "og ett annet bilde", + "file-other-description-file-plural": "og {{count}} andre filer", + "title-file-plural": "Filer", + "download-again": "Last ned igjen", + "file-other-description-file": "og én annen fil", + "title-image": "Bilde", + "title-file": "Fil", + "title-image-plural": "Bilder", + "activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende", + "activate-paste-mode-and-other-files": "og {{count}} andre filer", + "activate-paste-mode-activate-paste-mode-shared-text": "delt tekst" + }, + "about": { + "close-about_aria-label": "Lukk «Om PairDrop»", + "faq_title": "Ofte stilte spørsmål", + "claim": "Den enkleste måten å overføre filer mellom enheter", + "buy-me-a-coffee_title": "Spander drikke.", + "tweet_title": "Tvitre om PairDrop", + "github_title": "PairDrop på GitHub" + }, + "notifications": { + "copied-to-clipboard": "Kopiert til utklippstavlen", + "pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.", + "notifications-enabled": "Merknader påskrudd.", + "click-to-show": "Klikk for å vise", + "copied-text": "Tekst kopiert til utklippstavlen", + "connected": "Tilkoblet.", + "online": "Du er tilbake på nett", + "file-transfer-completed": "Filoverføring utført.", + "selected-peer-left": "Valgt likemann dro.", + "pairing-key-invalid": "Ugyldig nøkkel", + "connecting": "Kobler til …", + "pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.", + "offline": "Du er frakoblet", + "online-requirement": "Du må være på nett for å koble sammen enheter.", + "display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.", + "display-name-changed-permanently": "Visningsnavnet er endret for godt.", + "display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.", + "text-content-incorrect": "Tekstinnholdet er uriktig.", + "file-content-incorrect": "Filinnholdet er uriktig.", + "click-to-download": "Klikk for å laste ned", + "message-transfer-completed": "Meldingsoverføring utført.", + "download-successful": "{{descriptor}} nedlastet", + "pairing-success": "Enheter sammenkoblet.", + "pairing-cleared": "Sammenkobling av alle enheter opphevet." + }, + "document-titles": { + "file-received": "Fil mottatt", + "file-received-plural": "{{count}} filer mottatt", + "message-received": "Melding mottatt", + "file-transfer-requested": "Filoverføring forespurt", + "message-received-plural": "{{count}} meldinger mottatt" + }, + "peer-ui": { + "preparing": "Forbereder …", + "waiting": "Venter", + "processing": "Behandler …", + "transferring": "Overfører …", + "click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}" + } +} From 044d7aa20da61538448d8f2c586add8b38959062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 12:59:58 +0000 Subject: [PATCH 036/519] docker-swarm-usage reworked --- docs/docker-swarm-usage.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/docker-swarm-usage.md b/docs/docker-swarm-usage.md index ae2c97e..218ae82 100644 --- a/docs/docker-swarm-usage.md +++ b/docs/docker-swarm-usage.md @@ -2,42 +2,46 @@ ## Healthcheck -The [Docker Image](../Dockerfile) includes a Healthcheck with the following options: +The [Docker Image](../Dockerfile) includes a health check with the following options: ``` --interval=30s ``` -> Specifies the time interval at which the health check should be performed. In this case, the health check will be performed every 30 seconds. - +> Specifies the time interval to run the health check. \ +> In this case, the health check is performed every 30 seconds.
``` --timeout=10s ``` -> Specifies the amount of time to wait for a response from the health check command. If the response does not arrive within 10 seconds, the health check will be considered a failure. - +> Specifies the amount of time to wait for a response from the \"healthcheck\" command. \ +> If the response does not arrive within 10 seconds, the health check fails.
``` --start-period=5s ``` -> Specifies the amount of time to wait before starting the health check process. In this case, the health check process will begin 5 seconds after the container is started. - +> Specifies the amount of time to wait before starting the health check process. \ +> In this case, the health check process will begin 5 seconds after the container is started.
``` --retries=3 ``` -> Specifies the number of times Docker should retry the health check before considering the container to be unhealthy. - +> Specifies the number of times Docker should retry the health check \ +> before considering the container to be unhealthy.
-The CMD instruction is used to define the command that will be run as part of the health check. -In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. This command will attempt to connect to `http://localhost:3000/` -and if it fails it will exit with a status code of `1`. If this command returns a status code other than `0`, the health check will be considered a failure. +The CMD instruction is used to define the command that will be run as part of the health check. \ +In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. \ +This command will attempt to connect to `http://localhost:3000/` \ +and if it fails it will exit with a status code of `1`. \ +If this command returns a status code other than `0`, the health check fails. -Overall, this HEALTHCHECK instruction is defining a health check process that will run every 30 seconds, wait up to 10 seconds for a response, -begin 5 seconds after the container is started, and retry up to 3 times. -The health check will consist of attempting to connect to http://localhost:3000/ and will consider the container to be unhealthy if it is unable to connect. +Overall, this HEALTHCHECK instruction is defining a health check process \ +that runs every 30 seconds, and waits up to 10 seconds for a response, \ +begins 5 seconds after the container is started, and retries up to 3 times. \ +The health check attempts to connect to http://localhost:3000/ \ +and will considers the container unhealthy if unable to connect. From 9424f704bf6eae38fed9b13ec985703feb7c3f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:14:07 +0000 Subject: [PATCH 037/519] host-your-own reworked --- docs/host-your-own.md | 171 +++++++++++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 60 deletions(-) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 41ebe9b..5816a95 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -3,9 +3,10 @@ The easiest way to get PairDrop up and running is by using Docker. > TURN server for Internet Transfer > -> Beware that you have to host your own TURN server in order to enable transfers between different networks. +> Beware that you have to host your own TURN server to enable transfers between different networks. > -> You can follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) or deploy it via docker-compose (Step 5). +> Follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) \ +> or deploy it via docker-compose (Step 5). ## Deployment with Docker @@ -15,9 +16,11 @@ The easiest way to get PairDrop up and running is by using Docker. docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop ``` -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the docker container directly, \ +> `127.0.0.1` is specified in the run command. #### Options / Flags Set options by using the following flags in the `docker run` command: @@ -39,21 +42,30 @@ Set options by using the following flags in the `docker run` command: ```bash -e IPV6_LOCALIZE=4 ``` -> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments of the client IPv6 address to be evaluated as the peer's IP. This can be especially useful when using Cloudflare as a proxy. +> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments \ +> of the client IPv6 address to be evaluated as the peer's IP. \ +> This can be especially useful when using Cloudflare as a proxy. > -> The flag must be set to an **integer** between `1` and `7`. The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) to match the client IP against. The most common value would be `4`, which will group peers within the same `/64` subnet. +> The flag must be set to an **integer** between `1` and `7`. \ +> The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) \ +> to match the client IP against. The most common value would be `4`, \ +> which will group peers within the same `/64` subnet. ##### Websocket Fallback (for VPN) ```bash -e WS_FALLBACK=true ``` -> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. +> Provides PairDrop to clients with an included websocket fallback \ +> if the peer to peer WebRTC connection is not available to the client. > -> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance. -> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). +> This is not used on the official https://pairdrop.net website, \ +> but you can activate it on your self-hosted instance. +> This is especially useful if you connect to your instance via a VPN (as most VPN services block WebRTC completely in order to hide your real IP address). ([Read more here](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). > -> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer! -> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. +> **Warning:** All traffic sent between devices using this fallback \ +> is routed through the server and therefor not peer to peer! \ +> Beware that the traffic routed via this fallback is readable by the server. \ +> Only ever use this on instances you can trust. \ > Additionally, beware that all traffic using this fallback debits the servers data plan. ##### Specify STUN/TURN Servers @@ -61,7 +73,8 @@ Set options by using the following flags in the `docker run` command: -e RTC_CONFIG="rtc_config.json" ``` -> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> Specify the STUN/TURN servers PairDrop clients use by setting \ +> `RTC_CONFIG` to a JSON file including the configuration. \ > You can use `pairdrop/rtc_config_example.json` as a starting point. > > To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ @@ -83,8 +96,10 @@ Set options by using the following flags in the `docker run` command: -e DEBUG_MODE="true" ``` -> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) -> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> Use this flag to enable debugging information about the connecting peers IP addresses. \ +> This is quite useful to check whether the [#HTTP-Server](#http-server) \ +> is configured correctly, so the auto-discovery feature works correctly. \ +> Otherwise, all clients discover each other mutually, independently of their network status. > > If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this: > ``` @@ -97,7 +112,7 @@ Set options by using the following flags in the `docker run` command: > if IP is private, '127.0.0.1' is used instead > ----DEBUGGING-PEER-IP-END---- > ``` -> If the IP PairDrop uses is the public IP of your device everything is correctly setup. +> If the IP PairDrop uses is the public IP of your device, everything is set up correctly. \ >To find out your devices public IP visit https://www.whatismyip.com/. > > To preserve your clients' privacy, **never use this flag in production!** @@ -109,13 +124,17 @@ Set options by using the following flags in the `docker run` command: ```bash docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod ``` -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For to prevent \ +> all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the Docker container directly, \ +> `127.0.0.1` is specified in the run command. > -> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) +> To specify options replace `npm run start:prod` \ +> according to [the documentation below.](#options--flags-1) -> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage) +> The Docker Image includes a healthcheck. \ +> Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage). ### Docker Image self-built #### Build the image @@ -130,13 +149,17 @@ docker build --pull . -f Dockerfile -t pairdrop ```bash docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod ``` -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the Docker container \ +> directly, `127.0.0.1` is specified in the run command. > -> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) +> To specify options replace `npm run start:prod` \ +> according to [the documentation below.](#options--flags-1) -> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage) +> The Docker Image includes a Healthcheck. \ +Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
@@ -162,9 +185,11 @@ services: Run the compose file with `docker compose up -d`. -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the Docker container \ +> directly, `127.0.0.1` is specified in the run command.
@@ -190,7 +215,7 @@ or npm start ``` -> Remember to check your IP Address using your OS command to see where you can access the server. +> Remember to check your IP address using your OS command to see where you can access the server. > By default, the node server listens on port 3000. @@ -212,7 +237,8 @@ $env:PORT=3010; npm start ```bash IPV6_LOCALIZE=4 ``` -> Truncate a portion of the client IPv6 address to make peers more discoverable. See [Options/Flags](#options--flags) above. +> Truncate a portion of the client IPv6 address to make peers more discoverable. \ +> See [Options/Flags](#options--flags) above. #### Specify STUN/TURN Server On Unix based systems @@ -223,10 +249,12 @@ On Windows ```bash $env:RTC_CONFIG="rtc_config.json"; npm start ``` -> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` \ +> to a JSON file including the configuration. \ > You can use `pairdrop/rtc_config_example.json` as a starting point. > -> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ +> To host your own TURN server you can follow this guide: \ +> https://gabrieltanner.org/blog/turn-server/ > > Default configuration: > ```json @@ -250,10 +278,13 @@ On Windows $env:DEBUG_MODE="true"; npm start ``` -> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) -> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> Use this flag to enable debugging info about the connecting peers IP addresses. \ +> This is quite useful to check whether the [#HTTP-Server](#http-server) \ +> is configured correctly, so the auto discovery feature works correctly. \ +> Otherwise, all clients discover each other mutually, independently of their network status. > -> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this: +> If this flag is set to `"true"` each peer that connects to the \ +> PairDrop server will produce a log to STDOUT like this: > ``` > ----DEBUGGING-PEER-IP-START---- > remoteAddress: ::ffff:172.17.0.1 @@ -264,10 +295,10 @@ $env:DEBUG_MODE="true"; npm start > if IP is private, '127.0.0.1' is used instead > ----DEBUGGING-PEER-IP-END---- > ``` -> If the IP PairDrop uses is the public IP of your device everything is correctly setup. ->To find out your devices public IP visit https://www.whatismyip.com/. +> If the IP PairDrop uses is the public IP of your device everything is set up correctly. \ +>Find your devices public IP by visiting https://www.whatismyip.com/. > -> To preserve your clients' privacy, **never use this flag in production!** +> Preserve your clients' privacy. **Never use this flag in production!** ### Options / Flags @@ -277,9 +308,11 @@ npm start -- --localhost-only ``` > Only allow connections from localhost. > -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly. +> Use this when deploying PairDrop with node to prevent \ +> bypassing the proxy by reaching the Docker container directly. #### Automatic restart on error ```bash @@ -301,13 +334,19 @@ npm start -- --rate-limit ```bash npm start -- --include-ws-fallback ``` -> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. +> Provides PairDrop to clients with an included websocket fallback \ +> if the peer to peer WebRTC connection is not available to the client. > -> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance. -> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). +> This is not used on the official https://pairdrop.net, \ +but you can activate it on your self-hosted instance. \ +> This is especially useful if you connect to your instance \ +> via a VPN as most VPN services block WebRTC completely in order to hide your real IP address. +> ([Read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). > -> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer! -> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. +> **Warning:** All traffic sent between devices using this fallback \ +> is routed through the server and therefor not peer to peer! \ +> Beware that the traffic routed via this fallback is readable by the server. \ +> Only ever use this on instances you can trust. \ > Additionally, beware that all traffic using this fallback debits the servers data plan.
@@ -321,10 +360,12 @@ npm run start:prod ```bash npm run start:prod -- --localhost-only --include-ws-fallback ``` -> To prevent connections to the node server from bypassing the proxy server you should always use "--localhost-only" on production. +> To prevent connections to the node server from bypassing \ +> the proxy server you should always use "--localhost-only" on production. ## HTTP-Server -When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible. +When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. \ +Otherwise, all clients will be mutually visible. To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode). @@ -405,10 +446,10 @@ a2enmod proxy_wstunnel
-Create a new configuration file under `/etc/apache2/sites-available` (on debian) +Create a new configuration file under `/etc/apache2/sites-available` (on Debian) **pairdrop.conf** -#### Allow http and https requests +#### Allow HTTP and HTTPS requests ```apacheconf ProxyPass / http://127.0.0.1:3000/ @@ -425,7 +466,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian) RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L] ``` -#### Automatic http to https redirect: +#### Automatic HTTP to HTTPS redirect: ```apacheconf Redirect permanent / https://127.0.0.1:3000/ @@ -438,7 +479,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian) RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L] ``` -Activate the new virtual host and reload apache: +Activate the new virtual host and reload Apache: ```bash a2ensite pairdrop ``` @@ -462,28 +503,38 @@ Then, clone the repository and run docker-compose: docker-compose up -d ``` -Now point your browser to `http://localhost:8080`. +Now point your web browser to `http://localhost:8080`. -- To restart the containers run `docker-compose restart`. -- To stop the containers run `docker-compose stop`. -- To debug the NodeJS server run `docker logs pairdrop_node_1`. +- To restart the containers, run `docker-compose restart`. +- To stop the containers, run `docker-compose stop`. +- To debug the NodeJS server, run `docker logs pairdrop_node_1`.
## Testing PWA related features -PWAs require that the app is served under a correctly set up and trusted TLS endpoint. +PWAs requires the app to be served under a correctly set up and trusted TLS endpoint. -The nginx container creates a CA certificate and a website certificate for you. To correctly set the common name of the certificate, you need to change the FQDN environment variable in `docker/fqdn.env` to the fully qualified domain name of your workstation. +The NGINX container creates a CA certificate and a website certificate for you. \ +To correctly set the common name of the certificate, \ +you need to change the FQDN environment variable in `docker/fqdn.env` \ +to the fully qualified domain name of your workstation. -If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://:8080/ca.crt`. Install that certificate to the trust store of your operating system. -- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. -- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL. -- Firefox uses its own trust store. To install the CA, point Firefox at `http://:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK. -- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data). +If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \ +For your convenience, you can download the crt file from `http://:8080/ca.crt`. \ +Install that certificate to the trust store of your operating system. \ +- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. \ +- On macOS, double-click the installed CA certificate in `Keychain Access`, \ +- expand `Trust`, and select `Always Trust` for SSL. \ +- Firefox uses its own trust store. To install the CA, \ +- point Firefox at `http://:8080/ca.crt`. \ +- When prompted, select `Trust this CA to identify websites` and click \"OK\". \ +- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). \ +- Additionally, after installing a new cert, \ +- you need to clear the Storage (DevTools → Application → Clear storage → Clear site data). Please note that the certificates (CA and webserver cert) expire after a day. -Also, whenever you restart the nginx docker, container new certificates are created. +Also, whenever you restart the NGINX Docker, container new certificates are created. The site is served on `https://:8443`. From 913b60b71270919f66923a74d855bc11938e8c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:17:35 +0000 Subject: [PATCH 038/519] how-to reworked --- docs/how-to.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/how-to.md b/docs/how-to.md index a764816..f38d5f2 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -5,7 +5,7 @@ The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progres This is still experimental and must be enabled via a flag **before** the PWA is installed to Windows. 1. [Enabled feature in Edge](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files#enable-the-file-handling-api) -2. Install PairDrop by visiting https://pairdrop.net/ with the Edge browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-). +2. Install PairDrop by visiting https://pairdrop.net/ with the Edge web browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-). 3. You are done! You can now send most files one at a time via PairDrop: _context menu > Open with > PairDrop_ @@ -13,7 +13,8 @@ This is still experimental and must be enabled via a flag **before** the PWA is [//]: # (Todo: add screenshots) ### Sending multiple files to PairDrop -Outstandingly, it is also possible to send multiple files to PairDrop via the context menu by adding PairDrop to the `Send to` menu: +Outstandingly, it is also possible to send multiple files to PairDrop \ +via the context menu by adding PairDrop to the `Send to` menu: 1. [Register PairDrop as file handler](#registering-to-open-files-with-pairdrop) 2. Hit Windows Key+R, type: `shell:programs` and hit Enter. 3. Copy the PairDrop shortcut from the directory @@ -26,7 +27,8 @@ Outstandingly, it is also possible to send multiple files to PairDrop via the co [//]: # (Todo: add screenshots) ## Send directly from share menu on iOS -I created an iOS shortcut to send images, files, folder, URLs or text directly from the share-menu +I created an iOS shortcut to send images, files, folder, URLs \ +or text directly from the share-menu https://routinehub.co/shortcut/13990/ [//]: # (Todo: add doku with screenshots) @@ -63,7 +65,7 @@ On Windows Command Prompt you need to use bash: `bash pairdrop -h` Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop). #### Linux -1. Put file in a preferred folder e.g. `/usr/local/bin` +1. Put the file in a preferred folder e.g. `/usr/local/bin` 2. Make sure the bash file is executable. Otherwise, use `chmod +x pairdrop` 3. Add absolute path of the folder to PATH variable to make `pairdrop` available globally by executing `export PATH=$PATH:/opt/pairdrop-cli` @@ -74,7 +76,7 @@ Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop). #### Windows 1. Put file in a preferred folder e.g. `C:\Users\Public\pairdrop-cli` 2. Search for and open `Edit environment variables for your account` -3. Click `Environment Variables...` +3. Click `Environment Variables…` 4. Under *System Variables* select `Path` and click *Edit...* 5. Click *New*, insert the preferred folder (`C:\Users\Public\pairdrop-cli`), click *OK* until all windows are closed 6. Reopen Command prompt window From 17a12baa2a48643f7c015c4b2dcbb67e31bbdfa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:24:42 +0000 Subject: [PATCH 039/519] technical-documentation reworked --- docs/technical-documentation.md | 80 +++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/docs/technical-documentation.md b/docs/technical-documentation.md index bf050ae..b8783c0 100644 --- a/docs/technical-documentation.md +++ b/docs/technical-documentation.md @@ -3,48 +3,80 @@ Encryption is mandatory for WebRTC connections and completely done by the browser itself. -When the peers are first connecting, a channel is created by exchanging their signaling information. -This signaling information includes some sort of public key and is specific to the clients ip address. -That is what the STUN Server is used for: it simply returns your public IP address as you only know your local ip address +When the peers are first connecting, \ +a channel is created by exchanging their signaling info. \ +This signaling information includes some sort of public key \ +and is specific to the clients IP address. \ +That is what the STUN Server is used for: \ +it simply returns your public IP address \ +as you only know your local ip address \ if behind a NAT (router). -The transfer of the signaling information is done by the PairDrop / Snapdrop server using secure websockets. -After that the channel itself is completely peer-2-peer and all information can only be decrypted by the receiver. -When the two peers are on the same network or when they are not behind any NAT system (which they are always for classic -Snapdrop and for not paired users on PairDrop) the files are send directly peer to peer. +The transfer of the signaling info is done by the \ +PairDrop / Snapdrop server using secure websockets. \ +After that the channel itself is completely peer-to-peer \ +and all info can only be decrypted by the receiver. \ +When the two peers are on the same network \ +or when they are not behind any NAT system \ +(which they are always for classic \ +Snapdrop and for not paired users on PairDrop) \ +the files are send directly peer-to-peer. -When a user is behind a NAT (behind a router) the contents are channeled through a TURN server. -But again, the contents send via the channel can only be decrypted by the receiver. So a rogue TURN server could only -see that there is a connection, but not what is sent. Obviously, connections which are channeled through a TURN server -are not as fast as peer to peer. +When a user is behind a NAT (behind a router) \ +the contents are channeled through a TURN server. \ +But again, the contents send via the channel \ +can only be decrypted by the receiver. \ +So a rogue TURN server could only \ +see that there is a connection, but not what is sent. \ +Obviously, connections which are channeled through a TURN server \ +are not as fast as peer-to-peer. -The selection whether a TURN server is needed or not is also done automatically by the browser. -It simply iterated through the configured RTC iceServers and checks what works. Only if the STUN server is not sufficient, +The selection whether a TURN server is needed \ +or not is also done automatically by the web browser. \ +It simply iterated through the configured \ +RTC iceServers and checks what works. \ +Only if the STUN server is not sufficient, \ the TURN server is used. ![img](https://www.wowza.com/wp-content/uploads/WeRTC-Encryption-Diagrams-01.jpg) _Diagram created by wowza.com_ -Good thing: if your device has an IPv6 address it is uniquely reachable by that address. As I understand it, when both devices are using IPv6 addresses there is no need for a TURN server in any scenario. +Good thing: if your device has an IPv6 address \ +it is uniquely reachable by that address. \ +As I understand it, when both devices are using \ +IPv6 addresses there is no need for a TURN server in any scenario. -To learn more take a look at https://www.wowza.com/blog/webrtc-encryption-and-security which gives a good insight into stun, turn and webrtc +Learn more by reading https://www.wowza.com/blog/webrtc-encryption-and-security \ +which gives a good insight into STUN, TURN and WebRTC. ## Device Pairing The pairing functionality uses the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). -It works by creating long secrets that are served by the server to the initiating and requesting pair peer, -when the inserted key is correct. These long secrets are then saved to an indexedDB database in the browser. -IndexedDB is somewhat the successor of localStorage as saved data is shared between all tabs. -It goes one step further by making the data persistent and available offline if implemented to a PWA. +It works by creating long secrets that are served \ +by the server to the initiating and requesting pair peer, \ +when the inserted key is correct. \ +These long secrets are then saved to an \ +indexedDB database in the web browser. \ +IndexedDB is somewhat the successor of localStorage \ +as saved data is shared between all tabs. \ +It goes one step further by making the data persistent \ +and available offline if implemented to a PWA. -All secrets a client has saved to its database are send to the PairDrop server. Peers with a common secret are discoverable -to each other analog to peers with the same ip-address are discoverable to each other. +All secrets a client has saved to its database \ +are sent to the PairDrop server. \ +Peers with a common secret are discoverable \ +to each other analog to peers with the same \ +IP address are discoverable by each other. -What I really like about this approach, and the reason why I implemented it, is that devices on the same network are always -visible regardless whether any devices are paired or not. The main user flow is never obstructed. Paired devices are simply -shown additionally. This makes it in my idea better than the idea of using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214). +What I really like about this approach (and the reason I implemented it) \ +is that devices on the same network are always \ +visible regardless whether any devices are paired or not. \ +The main user flow is never obstructed. \ +Paired devices are simply shown additionally. \ +This makes it in my idea better than the idea of \ +using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214). [< Back](/README.md) From 6563ec98b39fdf4aa3abcdd34c3fad60577d74ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:26:48 +0000 Subject: [PATCH 040/519] Bold button scheme --- docs/host-your-own.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 5816a95..2a63446 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -528,7 +528,7 @@ Install that certificate to the trust store of your operating system. \ - expand `Trust`, and select `Always Trust` for SSL. \ - Firefox uses its own trust store. To install the CA, \ - point Firefox at `http://:8080/ca.crt`. \ -- When prompted, select `Trust this CA to identify websites` and click \"OK\". \ +- When prompted, select `Trust this CA to identify websites` and click *OK*. \ - When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). \ - Additionally, after installing a new cert, \ - you need to clear the Storage (DevTools → Application → Clear storage → Clear site data). From dccc17400cb493581bef27ffd51e916e3bf020da Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 19 Jul 2023 10:50:01 +0200 Subject: [PATCH 041/519] Added translation using Weblate (Chinese (Simplified)) --- public/lang/zh-Hans.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/zh-Hans.json diff --git a/public/lang/zh-Hans.json b/public/lang/zh-Hans.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/zh-Hans.json @@ -0,0 +1 @@ +{} From 6d7c13775f2a2ccbc0a16fc5499983ac94ddd811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8D=E5=BB=BA=E5=85=B4?= Date: Wed, 19 Jul 2023 09:18:40 +0000 Subject: [PATCH 042/519] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (122 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/ --- public/lang/zh-Hans.json | 141 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/public/lang/zh-Hans.json b/public/lang/zh-Hans.json index 0967ef4..4191c7d 100644 --- a/public/lang/zh-Hans.json +++ b/public/lang/zh-Hans.json @@ -1 +1,140 @@ -{} +{ + "header": { + "about_title": "关于 PairDrop", + "about_aria-label": "打开 关于 PairDrop", + "theme-light_title": "总是使用明亮主题", + "install_title": "安装 PairDrop", + "pair-device_title": "配对新设备", + "theme-auto_title": "主题适应系统", + "theme-dark_title": "总是使用暗黑主题", + "notification_title": "开启通知", + "edit-paired-devices_title": "管理已配对设备", + "cancel-paste-mode": "完成" + }, + "instructions": { + "x-instructions_data-drop-peer": "释放以发送到此设备", + "no-peers_data-drop-bg": "释放来选择接收者", + "no-peers-subtitle": "配对新设备使在其他网络上可见", + "no-peers-title": "在其他设备上打开 PairDrop 来发送文件", + "x-instructions_desktop": "点击以发送文件 或 右键来发送信息", + "x-instructions_mobile": "轻触以发送文件 或 长按来发送信息", + "x-instructions_data-drop-bg": "释放来选择接收者", + "click-to-send": "点击发送", + "tap-to-send": "轻触发送" + }, + "footer": { + "routed": "途径服务器", + "webrtc": "如果 WebRTC 不可用。", + "known-as": "你的名字是:", + "display-name_placeholder": "加载中…", + "and-by": "和", + "display-name_title": "长久修改你的设备名", + "discovery-everyone": "你对所有人可见", + "on-this-network": "在此网络上", + "paired-devices": "已配对的设备", + "traffic": "流量将" + }, + "dialogs": { + "activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送", + "activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件", + "activate-paste-mode-activate-paste-mode-shared-text": "分享文本", + "pair-devices-title": "配对新设备", + "input-key-on-this-device": "在另一个设备上输入这串数字", + "base64-text": "信息", + "enter-key-from-another-device": "输入从另一个设备上获得的数字以继续。", + "edit-paired-devices-title": "管理已配对的设备", + "pair": "配对", + "cancel": "取消", + "scan-qr-code": "或者 扫描二维码。", + "paired-devices-wrapper_data-empty": "无已配对设备。", + "auto-accept-instructions-1": "启用", + "auto-accept": "自动接收", + "decline": "拒绝", + "base64-processing": "处理中...", + "base64-tap-to-paste": "轻触此处粘贴{{type}}", + "base64-paste-to-send": "粘贴到此处以发送 {{type}}", + "auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。", + "would-like-to-share": "想要分享", + "accept": "接收", + "close": "关闭", + "share": "分享", + "download": "保存", + "send": "发送", + "receive-text-title": "收到信息", + "copy": "复制", + "send-message-title": "发送信息", + "send-message-to": "发了一条信息给", + "has-sent": "发送了:", + "base64-files": "文件", + "file-other-description-file": "和 1 个其他的文件", + "file-other-description-image": "和 1 个其他的图片", + "file-other-description-image-plural": "和 {{count}} 个其他的图片", + "file-other-description-file-plural": "和 {{count}} 个其他的文件", + "title-image-plural": "图片", + "receive-title": "收到 {{descriptor}}", + "title-image": "图片", + "title-file": "文件", + "title-file-plural": "文件", + "download-again": "再次保存" + }, + "about": { + "faq_title": "常见问题", + "close-about_aria-label": "关闭 关于 PairDrop", + "github_title": "PairDrop 在 GitHub 上开源", + "claim": "最简单的跨设备传输方案", + "buy-me-a-coffee_title": "帮我买杯咖啡!", + "tweet_title": "关于 PairDrop 的推特" + }, + "notifications": { + "display-name-changed-permanently": "展示的名字已经长久变更。", + "display-name-changed-temporarily": "展示的名字已经变更 仅在此会话中。", + "display-name-random-again": "展示的名字再次随机生成。", + "download-successful": "{{descriptor}} 已下载", + "pairing-tabs-error": "无法配对两个浏览器标签页。", + "pairing-success": "新设备已配对。", + "pairing-not-persistent": "配对的设备不是持久的。", + "pairing-key-invalid": "无效配对码", + "pairing-key-invalidated": "配对码 {{key}} 已失效。", + "text-content-incorrect": "文本内容不合法。", + "file-content-incorrect": "文件内容不合法。", + "clipboard-content-incorrect": "剪贴板内容不合法。", + "link-received": "收到来自 {{name}} 的链接 - 点击打开", + "message-received": "收到来自 {{name}} 的信息 - 点击复制", + "request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}", + "click-to-show": "点击展示", + "copied-text": "复制到剪贴板", + "selected-peer-left": "选择的设备已离开。", + "pairing-cleared": "所有设备已解除配对。", + "copied-to-clipboard": "已复制到剪贴板", + "notifications-enabled": "通知已启用。", + "copied-text-error": "写入剪贴板失败。请手动复制!", + "click-to-download": "点击以保存", + "unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭吗?", + "message-transfer-completed": "信息传输已完成。", + "offline": "你未连接到网络", + "online": "你已重新连接到网络", + "connected": "已连接。", + "online-requirement": "你需要连接网络来配对新设备。", + "files-incorrect": "文件不合法。", + "file-transfer-completed": "文件传输已完成。", + "connecting": "连接中…", + "ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB", + "rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。" + }, + "document-titles": { + "message-received": "收到信息", + "message-received-plural": "收到 {{count}} 条信息", + "file-transfer-requested": "文件传输请求", + "file-received-plural": "收到 {{count}} 个文件", + "file-received": "收到文件" + }, + "peer-ui": { + "click-to-send-paste-mode": "点击发送 {{descriptor}}", + "click-to-send": "点击以发送文件 或 右键来发送信息", + "connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号", + "preparing": "准备中…", + "waiting": "请等待…", + "transferring": "传输中…", + "processing": "处理中…" + } +} From 445a29540498f7bc8fa764e099dde8af5528cccc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 04:58:27 +0000 Subject: [PATCH 043/519] Bump express-rate-limit from 6.7.0 to 6.8.0 Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 6.7.0 to 6.8.0. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v6.7.0...v6.8.0) --- updated-dependencies: - dependency-name: express-rate-limit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index c49aca5..b96bb32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.7.0", + "express-rate-limit": "^6.8.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" @@ -204,11 +204,11 @@ } }, "node_modules/express-rate-limit": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", - "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", + "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", "engines": { - "node": ">= 12.9.0" + "node": ">= 14.0.0" }, "peerDependencies": { "express": "^4 || ^5" @@ -801,9 +801,9 @@ } }, "express-rate-limit": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", - "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", + "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", "requires": {} }, "finalhandler": { diff --git a/package.json b/package.json index aae3823..0b8860a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.7.0", + "express-rate-limit": "^6.8.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" From 471278f7b0727b3ec9476e88be7422db301101ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Mon, 24 Jul 2023 16:02:13 +0000 Subject: [PATCH 044/519] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 81.9% (100 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/ --- public/lang/nb-NO.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/lang/nb-NO.json b/public/lang/nb-NO.json index 52f1d3d..b11b664 100644 --- a/public/lang/nb-NO.json +++ b/public/lang/nb-NO.json @@ -109,7 +109,17 @@ "message-transfer-completed": "Meldingsoverføring utført.", "download-successful": "{{descriptor}} nedlastet", "pairing-success": "Enheter sammenkoblet.", - "pairing-cleared": "Sammenkobling av alle enheter opphevet." + "pairing-cleared": "Sammenkobling av alle enheter opphevet.", + "pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.", + "copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt.", + "clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.", + "link-received": "Lenke mottatt av {{name}}. Klikk for å åpne.", + "request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}", + "message-received": "Melding mottatt av {{name}}. Klikk for å åpne.", + "files-incorrect": "Filene er uriktige.", + "ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.", + "unfinished-transfers-warning": "Lukk med ufullførte overføringer?", + "rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen." }, "document-titles": { "file-received": "Fil mottatt", @@ -124,6 +134,7 @@ "processing": "Behandler …", "transferring": "Overfører …", "click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding", - "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}" + "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}", + "connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen." } } From 714608ce978abf568d3334320e6143c89b8b6fe6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 25 Jul 2023 18:56:56 +0200 Subject: [PATCH 045/519] Added translation using Weblate (Turkish) --- public/lang/tr.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/tr.json diff --git a/public/lang/tr.json b/public/lang/tr.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/tr.json @@ -0,0 +1 @@ +{} From b319fbe1560bbd147a615261275d3977a43f1de6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 25 Jul 2023 22:58:58 +0000 Subject: [PATCH 046/519] Translated using Weblate (Turkish) Currently translated at 12.2% (15 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/tr/ --- public/lang/tr.json | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/public/lang/tr.json b/public/lang/tr.json index 0967ef4..783e25b 100644 --- a/public/lang/tr.json +++ b/public/lang/tr.json @@ -1 +1,25 @@ -{} +{ + "header": { + "about_title": "PairDrop Hakkında", + "about_aria-label": "PairDrop Hakkında Aç", + "theme-auto_title": "Temayı Sisteme Uyarla", + "theme-light_title": "Daima Açık Tema Kullan", + "theme-dark_title": "Daima Koyu Tema Kullan", + "notification_title": "Bildirimleri Etkinleştir", + "install_title": "PairDrop'u Yükle", + "pair-device_title": "Cihaz Eşleştir", + "edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle", + "cancel-paste-mode": "Bitti" + }, + "instructions": { + "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" + }, + "footer": { + "display-name_placeholder": "Yükleniyor…", + "display-name_title": "Cihazının adını kalıcı olarak düzenle" + }, + "dialogs": { + "cancel": "İptal", + "edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle" + } +} From da5038a51a015057c925ee7762e192f3dfd91fd5 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 7 Jul 2023 14:58:15 +0200 Subject: [PATCH 047/519] include translations for about buttons and implement translation fallback if used translation is not complete --- public/index.html | 14 +++--- public/lang/en.json | 8 +++- public/scripts/localization.js | 43 +++++++++++-------- public_included_ws_fallback/index.html | 14 +++--- public_included_ws_fallback/lang/en.json | 8 +++- .../scripts/localization.js | 43 +++++++++++-------- 6 files changed, 78 insertions(+), 52 deletions(-) diff --git a/public/index.html b/public/index.html index 15b82da..21f6005 100644 --- a/public/index.html +++ b/public/index.html @@ -83,7 +83,7 @@
- +
@@ -161,7 +161,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public/lang/en.json b/public/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public/scripts/localization.js b/public/scripts/localization.js index d09d5c0..5f2730b 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -2,10 +2,10 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + const initialLocale = Localization.supportedOrDefault(navigator.languages); Localization.setLocale(initialLocale) .then(_ => { @@ -21,25 +21,21 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } - static browserLocales() { - return navigator.languages.map(locale => - locale.split("-")[0] - ); - } - static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const isFirstTranslation = !Localization.locale + + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); + const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; - if (firstTranslation) { + if (isFirstTranslation) { Events.fire("translation-loaded"); } } @@ -65,30 +61,43 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i
- +
@@ -166,7 +166,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index d09d5c0..5f2730b 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -2,10 +2,10 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + const initialLocale = Localization.supportedOrDefault(navigator.languages); Localization.setLocale(initialLocale) .then(_ => { @@ -21,25 +21,21 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } - static browserLocales() { - return navigator.languages.map(locale => - locale.split("-")[0] - ); - } - static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const isFirstTranslation = !Localization.locale + + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); + const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; - if (firstTranslation) { + if (isFirstTranslation) { Events.fire("translation-loaded"); } } @@ -65,30 +61,43 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i Date: Sun, 30 Jul 2023 17:55:35 +0200 Subject: [PATCH 048/519] Add Weblate and a mention of translations to the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0602ff0..f2a093b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop) * Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101)) * To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558) * When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers) +* Built-in translations ## Screenshots
@@ -82,6 +83,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop) * [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) * [zip.js](https://gildas-lormeau.github.io/zip.js/) * [cyrb53](https://github.com/bryc) super fast hash function +* [Weblate](https://weblate.org/) Web based localization tool Have any questions? Read our [FAQ](/docs/faq.md). From 8869c3c27e68fb3e0ea6cac28f85888923775df7 Mon Sep 17 00:00:00 2001 From: zhongbing Date: Sun, 6 Aug 2023 00:47:01 +0800 Subject: [PATCH 049/519] revise the command line tool --- pairdrop-cli/pairdrop | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pairdrop-cli/pairdrop b/pairdrop-cli/pairdrop index 9172412..b5e80bd 100644 --- a/pairdrop-cli/pairdrop +++ b/pairdrop-cli/pairdrop @@ -38,7 +38,10 @@ openPairDrop() else xdg-open "$url" fi + + exit + } setOs() @@ -98,13 +101,19 @@ sendFiles() [[ -a "$zipPath" ]] && echo "Cannot overwrite $zipPath. Please remove first." && exit if [[ -d $path ]]; then - zipPathTemp="temp_${zipPath}" + zipPathTemp="${path}_pairdrop_temp.zip" [[ -a "$zipPathTemp" ]] && echo "Cannot overwrite $zipPathTemp. Please remove first." && exit echo "Processing directory..." # Create zip files temporarily to send directory - zip -q -b /tmp/ -r "$zipPath" "$path" - zip -q -b /tmp/ "$zipPathTemp" "$zipPath" + if [[ $OS == "Windows" ]];then + powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath}" + echo "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}" + powershell.exe -Command "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}" + else + zip -q -b /tmp/ -r "$zipPath" "$path" + zip -q -b /tmp/ "$zipPathTemp" "$zipPath" + fi if [[ $OS == "Mac" ]];then hash=$(base64 -i "$zipPathTemp") @@ -118,8 +127,12 @@ sendFiles() echo "Processing file..." # Create zip file temporarily to send file - zip -q -b /tmp/ "$zipPath" "$path" + if [[ $OS == "Windows" ]];then + powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath} -CompressionLevel Optimal" + else + zip -q -b /tmp/ "$zipPath" "$path" + fi if [[ $OS == "Mac" ]];then hash=$(base64 -i "$zipPath") else @@ -142,6 +155,7 @@ sendFiles() hash= fi + openPairDrop exit } From 395c3e00a4c23320ced4849ceabc12b220cef80e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 04:24:11 +0000 Subject: [PATCH 050/519] Bump express-rate-limit from 6.8.0 to 6.9.0 Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 6.8.0 to 6.9.0. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v6.8.0...v6.9.0) --- updated-dependencies: - dependency-name: express-rate-limit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b96bb32..d5b6737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.8.0", + "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" @@ -204,9 +204,9 @@ } }, "node_modules/express-rate-limit": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", - "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.9.0.tgz", + "integrity": "sha512-AnISR3V8qy4gpKM62/TzYdoFO9NV84fBx0POXzTryHU/qGUJBWuVGd+JhbvtVmKBv37t8/afmqdnv16xWoQxag==", "engines": { "node": ">= 14.0.0" }, @@ -801,9 +801,9 @@ } }, "express-rate-limit": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", - "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.9.0.tgz", + "integrity": "sha512-AnISR3V8qy4gpKM62/TzYdoFO9NV84fBx0POXzTryHU/qGUJBWuVGd+JhbvtVmKBv37t8/afmqdnv16xWoQxag==", "requires": {} }, "finalhandler": { diff --git a/package.json b/package.json index 0b8860a..015e006 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.8.0", + "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" From 43824d0de20d1fecf6525ff5e79b99a429b8936a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 10 Aug 2023 17:09:51 +0200 Subject: [PATCH 051/519] increase version to v1.7.7 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5b6737..6ea5f15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.6", + "version": "1.7.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.6", + "version": "1.7.7", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 015e006..baca71c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.6", + "version": "1.7.7", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 755673a..ab0c9a2 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.6
+
v1.7.7
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index b409118..88f2a13 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.6'; +const cacheVersion = 'v1.7.7'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 6beae65..838d8d2 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.6
+
v1.7.7
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 78249b2..51c320d 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.6'; +const cacheVersion = 'v1.7.7'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 69f1688dfe7ae6c703b71e91e22fc2f112f241f4 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 11 Aug 2023 13:11:51 +0200 Subject: [PATCH 052/519] Update bug-report.md --- .github/ISSUE_TEMPLATE/bug-report.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 39a6624..f5aa0fb 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,8 +1,8 @@ --- name: Bug Report about: Create a report to help us improve. Please check the FAQ first. -title: 'Bug:/Enhancement:/Feature Request: ' -labels: '' +title: '[Bug] ' +labels: 'bug' assignees: '' --- @@ -34,12 +34,17 @@ If applicable, add screenshots to help explain your problem. - Browser [e.g. stock browser, safari] - Version [e.g. 22] -**Self-Hosted** +**Bug occurs on official PairDrop instance https://pairdrop.net/** +No | Yes +Version: v1.7.7 + +**Bug occurs on self-hosted PairDrop instance** No | Yes **Self-Hosted Setup** Proxy: Nginx | Apache2 Deployment: docker run | docker-compose | npm run start:prod +Version: v1.7.7 **Additional context** Add any other context about the problem here. From f95181c057e75722ef3de61ffe083edb52d663a0 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 11 Aug 2023 13:20:38 +0200 Subject: [PATCH 053/519] Create enhancement issue template --- .github/ISSUE_TEMPLATE/enhancement | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/enhancement diff --git a/.github/ISSUE_TEMPLATE/enhancement b/.github/ISSUE_TEMPLATE/enhancement new file mode 100644 index 0000000..ba85fef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement @@ -0,0 +1,20 @@ +--- +name: Enhancement +about: Enhancements and feature requests are always welcome. See discussions regarding central topics. +title: '[Enhancement] ' +labels: 'enhancement' +assignees: '' + +--- + +**What problem is solved by the new feature** +What's the motivation for this topic + +**Describe the feature** +A clear and concise description of what the new feature/enhancement is. + +**Drafts** +Screenshots of Draw.io graph or drawn sketch. + +**Additional context** +Add any other context here. From e0210b030793e91e3c75763d249c74856c93b07a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 11 Aug 2023 13:21:10 +0200 Subject: [PATCH 054/519] Rename enhancement to enhancement.md --- .github/ISSUE_TEMPLATE/{enhancement => enhancement.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{enhancement => enhancement.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/enhancement b/.github/ISSUE_TEMPLATE/enhancement.md similarity index 100% rename from .github/ISSUE_TEMPLATE/enhancement rename to .github/ISSUE_TEMPLATE/enhancement.md From efeff843202cad67cd79d3e960bd90736cdd8c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 11 Aug 2023 18:59:26 +0000 Subject: [PATCH 055/519] "HEALTHCHECK" --- docs/docker-swarm-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docker-swarm-usage.md b/docs/docker-swarm-usage.md index 218ae82..4d1abf2 100644 --- a/docs/docker-swarm-usage.md +++ b/docs/docker-swarm-usage.md @@ -14,7 +14,7 @@ The [Docker Image](../Dockerfile) includes a health check with the following opt ``` --timeout=10s ``` -> Specifies the amount of time to wait for a response from the \"healthcheck\" command. \ +> Specifies the amount of time to wait for a response from the \"HEALTHCHECK\" command. \ > If the response does not arrive within 10 seconds, the health check fails.
@@ -39,7 +39,7 @@ This command will attempt to connect to `http://localhost:3000/` \ and if it fails it will exit with a status code of `1`. \ If this command returns a status code other than `0`, the health check fails. -Overall, this HEALTHCHECK instruction is defining a health check process \ +Overall, this \"HEALTHCHECK\" instruction is defining a health check process \ that runs every 30 seconds, and waits up to 10 seconds for a response, \ begins 5 seconds after the container is started, and retries up to 3 times. \ The health check attempts to connect to http://localhost:3000/ \ From 22bf1be2b711c3be31146fd4d716177addd68dfb Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 28 Aug 2023 13:41:12 +0200 Subject: [PATCH 056/519] Add TLS requirement to host-your-own.md --- docs/host-your-own.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 2a63446..e0e33f7 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -8,6 +8,10 @@ The easiest way to get PairDrop up and running is by using Docker. > Follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) \ > or deploy it via docker-compose (Step 5). +> PairDrop via HTTPS +> +> On some browsers PairDrop must be served over TLS in order for some feautures to work properly. These may include copying an incoming message via the 'copy' button, installing PairDrop as PWA, persistent pairing of devices and changing of the display name, and notifications. Naturally, this is also recommended to increase security. + ## Deployment with Docker ### Docker Image from Docker Hub From 161bd2be84f1f03b713a1c6221aa30021cb9fe52 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 01:49:09 +0200 Subject: [PATCH 057/519] rename language files to map language codes properly --- public/lang/{nb-NO.json => nb.json} | 0 public/lang/{zh-Hans.json => zh-CN.json} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename public/lang/{nb-NO.json => nb.json} (100%) rename public/lang/{zh-Hans.json => zh-CN.json} (100%) diff --git a/public/lang/nb-NO.json b/public/lang/nb.json similarity index 100% rename from public/lang/nb-NO.json rename to public/lang/nb.json diff --git a/public/lang/zh-Hans.json b/public/lang/zh-CN.json similarity index 100% rename from public/lang/zh-Hans.json rename to public/lang/zh-CN.json From 72f0aff60e541c273d3d71c905c597bb89b5a82e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 01:49:32 +0200 Subject: [PATCH 058/519] copy lang files to ws_fallback version --- public_included_ws_fallback/lang/de.json | 1 + public_included_ws_fallback/lang/en.json | 276 ++++++++++---------- public_included_ws_fallback/lang/nb.json | 140 ++++++++++ public_included_ws_fallback/lang/ru.json | 141 ++++++++++ public_included_ws_fallback/lang/tr.json | 25 ++ public_included_ws_fallback/lang/zh-CN.json | 140 ++++++++++ 6 files changed, 585 insertions(+), 138 deletions(-) create mode 100644 public_included_ws_fallback/lang/de.json create mode 100644 public_included_ws_fallback/lang/nb.json create mode 100644 public_included_ws_fallback/lang/ru.json create mode 100644 public_included_ws_fallback/lang/tr.json create mode 100644 public_included_ws_fallback/lang/zh-CN.json diff --git a/public_included_ws_fallback/lang/de.json b/public_included_ws_fallback/lang/de.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public_included_ws_fallback/lang/de.json @@ -0,0 +1 @@ +{} diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 8ad7b7c..ff8294d 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -1,140 +1,140 @@ { - "header": { - "about_title": "About PairDrop", - "about_aria-label": "Open About PairDrop", - "theme-auto_title": "Adapt Theme to System", - "theme-light_title": "Always Use Light-Theme", - "theme-dark_title": "Always Use Dark-Theme", - "notification_title": "Enable Notifications", - "install_title": "Install PairDrop", - "pair-device_title": "Pair Device", - "edit-paired-devices_title": "Edit Paired Devices", - "cancel-paste-mode": "Done" - }, - "instructions": { - "no-peers_data-drop-bg": "Release to select recipient", - "no-peers-title": "Open PairDrop on other devices to send files", - "no-peers-subtitle": "Pair devices to be discoverable on other networks", - "x-instructions_desktop": "Click to send files or right click to send a message", - "x-instructions_mobile": "Tap to send files or long tap to send a message", - "x-instructions_data-drop-peer": "Release to send to peer", - "x-instructions_data-drop-bg": "Release to select recipient", - "click-to-send": "Click to send", - "tap-to-send": "Tap to send" - }, - "footer": { - "known-as": "You are known as:", - "display-name_placeholder": "Loading...", - "display-name_title": "Edit your device name permanently", - "discovery-everyone": "You can be discovered by everyone", - "on-this-network": "on this network", - "and-by": "and by", - "paired-devices": "paired devices", - "traffic": "Traffic is", - "routed": "routed through the server", - "webrtc": "if WebRTC is not available." - }, - "dialogs": { - "activate-paste-mode-base": "Open PairDrop on other devices to send", - "activate-paste-mode-and-other-files": "and {{count}} other files", - "activate-paste-mode-activate-paste-mode-shared-text": "shared text", - "pair-devices-title": "Pair Devices", - "input-key-on-this-device": "Input this key on another device", - "scan-qr-code": "or scan the QR-Code.", - "enter-key-from-another-device": "Enter key from another device to continue.", - "pair": "Pair", - "cancel": "Cancel", - "edit-paired-devices-title": "Edit Paired Devices", - "paired-devices-wrapper_data-empty": "No paired devices.", - "auto-accept-instructions-1": "Activate", - "auto-accept": "auto-accept", - "auto-accept-instructions-2": "to automatically accept all files sent from that device.", - "close": "Close", - "would-like-to-share": "would like to share", - "accept": "Accept", - "decline": "Decline", - "has-sent": "has sent:", - "share": "Share", - "download": "Download", - "send-message-title": "Send Message", - "send-message-to": "Send a Message to", - "send": "Send", - "receive-text-title": "Message Received", - "copy": "Copy", - "base64-processing": "Processing...", - "base64-tap-to-paste": "Tap here to paste {{type}}", - "base64-paste-to-send": "Paste here to send {{type}}", - "base64-text": "text", - "base64-files": "files", - "file-other-description-image": "and 1 other image", - "file-other-description-file": "and 1 other file", - "file-other-description-image-plural": "and {{count}} other images", - "file-other-description-file-plural": "and {{count}} other files", - "title-image": "Image", - "title-file": "File", - "title-image-plural": "Images", - "title-file-plural": "Files", - "receive-title": "{{descriptor}} Received", - "download-again": "Download again" - }, - "about": { - "close-about_aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices", - "github_title": "PairDrop on Github", - "buy-me-a-coffee_title": "Buy me a coffee!", - "tweet_title": "Tweet about PairDrop", - "faq_title": "Frequently asked questions" - }, - "notifications": { - "display-name-changed-permanently": "Display name is changed permanently.", - "display-name-changed-temporarily": "Display name is changed only for this session.", - "display-name-random-again": "Display name is randomly generated again.", - "download-successful": "{{descriptor}} downloaded successfully", - "pairing-tabs-error": "Pairing of two browser tabs is not possible.", - "pairing-success": "Devices paired successfully.", - "pairing-not-persistent": "Paired devices are not persistent.", - "pairing-key-invalid": "Key not valid", - "pairing-key-invalidated": "Key {{key}} invalidated.", - "pairing-cleared": "All Devices unpaired.", - "copied-to-clipboard": "Copied to clipboard", - "text-content-incorrect": "Text content is incorrect.", - "file-content-incorrect": "File content is incorrect.", - "clipboard-content-incorrect": "Clipboard content is incorrect.", - "notifications-enabled": "Notifications enabled.", - "link-received": "Link received by {{name}} - Click to open", - "message-received": "Message received by {{name}} - Click to copy", - "click-to-download": "Click to download", - "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", - "click-to-show": "Click to show", - "copied-text": "Copied text to clipboard", - "copied-text-error": "Writing to clipboard failed. Copy manually!", - "offline": "You are offline", - "online": "You are back online", - "connected": "Connected.", - "online-requirement": "You need to be online to pair devices.", - "connecting": "Connecting...", - "files-incorrect": "Files are incorrect.", - "file-transfer-completed": "File transfer completed.", - "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", - "message-transfer-completed": "Message transfer completed.", - "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", - "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", - "selected-peer-left": "Selected peer left." - }, - "document-titles": { - "file-received": "File Received", - "file-received-plural": "{{count}} Files Received", - "file-transfer-requested": "File Transfer Requested", - "message-received": "Message Received", - "message-received-plural": "{{count}} Messages Received" - }, - "peer-ui": { - "click-to-send-paste-mode": "Click to send {{descriptor}}", - "click-to-send": "Click to send files or right click to send a message", - "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", - "preparing": "Preparing...", - "waiting": "Waiting...", - "processing": "Processing...", - "transferring": "Transferring..." - } + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading…", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on GitHub", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded", + "pairing-tabs-error": "Pairing two web browser tabs is impossible.", + "pairing-success": "Devices paired.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Invalid key", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting…", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing…", + "waiting": "Waiting…", + "processing": "Processing…", + "transferring": "Transferring…" + } } diff --git a/public_included_ws_fallback/lang/nb.json b/public_included_ws_fallback/lang/nb.json new file mode 100644 index 0000000..b11b664 --- /dev/null +++ b/public_included_ws_fallback/lang/nb.json @@ -0,0 +1,140 @@ +{ + "header": { + "edit-paired-devices_title": "Rediger sammenkoblede enheter", + "about_title": "Om PairDrop", + "about_aria-label": "Åpne «Om PairDrop»", + "theme-auto_title": "Juster drakt til system", + "theme-light_title": "Alltid bruk lys drakt", + "theme-dark_title": "Alltid bruk mørk drakt", + "notification_title": "Skru på merknader", + "cancel-paste-mode": "Ferdig", + "install_title": "Installer PairDrop", + "pair-device_title": "Sammenkoble enhet" + }, + "footer": { + "discovery-everyone": "Du kan oppdages av alle", + "and-by": "og av", + "webrtc": "hvis WebRTC ikke er tilgjengelig.", + "display-name_placeholder": "Laster inn …", + "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", + "traffic": "Trafikken", + "on-this-network": "på dette nettverket", + "known-as": "Du er kjent som:", + "paired-devices": "sammenkoblede enheter", + "routed": "Sendes gjennom tjeneren" + }, + "instructions": { + "x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding", + "x-instructions_data-drop-bg": "Slipp for å velge mottager", + "click-to-send": "Klikk for å sende", + "no-peers_data-drop-bg": "Slipp for å velge mottager", + "no-peers-title": "Åpne PairDrop på andre enheter for å sende filer", + "no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk", + "x-instructions_data-drop-peer": "Slipp for å sende til likemann", + "tap-to-send": "Trykk for å sende" + }, + "dialogs": { + "input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet", + "pair-devices-title": "Sammenkoble enheter", + "would-like-to-share": "ønsker å dele", + "auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.", + "paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter", + "enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.", + "edit-paired-devices-title": "Rediger sammenkoblede enheter", + "accept": "Godta", + "has-sent": "har sendt:", + "base64-paste-to-send": "Trykk her for å sende {{type}}", + "base64-text": "tekst", + "base64-files": "filer", + "file-other-description-image-plural": "og {{count}} andre bilder", + "receive-title": "{{descriptor}} mottatt", + "send-message-title": "Send melding", + "base64-processing": "Behandler …", + "close": "Lukk", + "decline": "Avslå", + "download": "Last ned", + "copy": "Kopier", + "pair": "Sammenkoble", + "cancel": "Avbryt", + "scan-qr-code": "eller skann QR-koden.", + "auto-accept-instructions-1": "Aktiver", + "receive-text-title": "Melding mottatt", + "auto-accept": "auto-godkjenn", + "share": "Del", + "send-message-to": "Send en melding til", + "send": "Send", + "base64-tap-to-paste": "Trykk her for å lime inn {{type]]", + "file-other-description-image": "og ett annet bilde", + "file-other-description-file-plural": "og {{count}} andre filer", + "title-file-plural": "Filer", + "download-again": "Last ned igjen", + "file-other-description-file": "og én annen fil", + "title-image": "Bilde", + "title-file": "Fil", + "title-image-plural": "Bilder", + "activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende", + "activate-paste-mode-and-other-files": "og {{count}} andre filer", + "activate-paste-mode-activate-paste-mode-shared-text": "delt tekst" + }, + "about": { + "close-about_aria-label": "Lukk «Om PairDrop»", + "faq_title": "Ofte stilte spørsmål", + "claim": "Den enkleste måten å overføre filer mellom enheter", + "buy-me-a-coffee_title": "Spander drikke.", + "tweet_title": "Tvitre om PairDrop", + "github_title": "PairDrop på GitHub" + }, + "notifications": { + "copied-to-clipboard": "Kopiert til utklippstavlen", + "pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.", + "notifications-enabled": "Merknader påskrudd.", + "click-to-show": "Klikk for å vise", + "copied-text": "Tekst kopiert til utklippstavlen", + "connected": "Tilkoblet.", + "online": "Du er tilbake på nett", + "file-transfer-completed": "Filoverføring utført.", + "selected-peer-left": "Valgt likemann dro.", + "pairing-key-invalid": "Ugyldig nøkkel", + "connecting": "Kobler til …", + "pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.", + "offline": "Du er frakoblet", + "online-requirement": "Du må være på nett for å koble sammen enheter.", + "display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.", + "display-name-changed-permanently": "Visningsnavnet er endret for godt.", + "display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.", + "text-content-incorrect": "Tekstinnholdet er uriktig.", + "file-content-incorrect": "Filinnholdet er uriktig.", + "click-to-download": "Klikk for å laste ned", + "message-transfer-completed": "Meldingsoverføring utført.", + "download-successful": "{{descriptor}} nedlastet", + "pairing-success": "Enheter sammenkoblet.", + "pairing-cleared": "Sammenkobling av alle enheter opphevet.", + "pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.", + "copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt.", + "clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.", + "link-received": "Lenke mottatt av {{name}}. Klikk for å åpne.", + "request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}", + "message-received": "Melding mottatt av {{name}}. Klikk for å åpne.", + "files-incorrect": "Filene er uriktige.", + "ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.", + "unfinished-transfers-warning": "Lukk med ufullførte overføringer?", + "rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen." + }, + "document-titles": { + "file-received": "Fil mottatt", + "file-received-plural": "{{count}} filer mottatt", + "message-received": "Melding mottatt", + "file-transfer-requested": "Filoverføring forespurt", + "message-received-plural": "{{count}} meldinger mottatt" + }, + "peer-ui": { + "preparing": "Forbereder …", + "waiting": "Venter", + "processing": "Behandler …", + "transferring": "Overfører …", + "click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}", + "connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen." + } +} diff --git a/public_included_ws_fallback/lang/ru.json b/public_included_ws_fallback/lang/ru.json new file mode 100644 index 0000000..8617ec2 --- /dev/null +++ b/public_included_ws_fallback/lang/ru.json @@ -0,0 +1,141 @@ +{ + "header": { + "about_aria-label": "Открыть страницу \"О сервисе\"", + "pair-device_title": "Подключить устройство", + "install_title": "Установить PairDrop", + "cancel-paste-mode": "Выполнено", + "edit-paired-devices_title": "Редактировать сопряженные устройства", + "notification_title": "Включить уведомления", + "about_title": "О сервисе", + "theme-auto_title": "Адаптировать тему к системной", + "theme-dark_title": "Всегда использовать темную тему", + "theme-light_title": "Всегда использовать светлую тему" + }, + "instructions": { + "x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение", + "no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "click-to-send": "Нажмите, чтобы отправить", + "x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "tap-to-send": "Прикоснитесь, чтобы отправить", + "x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу", + "x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение", + "no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы", + "no-peers-subtitle": "Сопрягите устройства из разных сетей." + }, + "footer": { + "discovery-everyone": "О вас может узнать каждый", + "display-name_placeholder": "Загрузка…", + "routed": "направляется через сервер", + "webrtc": ", если WebRTC недоступен.", + "traffic": "Трафик", + "and-by": "и", + "paired-devices": "сопряженные устройства", + "known-as": "Вы известны под именем:", + "on-this-network": "в этой сети", + "display-name_title": "Изменить имя вашего устройства навсегда" + }, + "dialogs": { + "activate-paste-mode-and-other-files": "и {{count}} других файлов", + "activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить", + "activate-paste-mode-activate-paste-mode-shared-text": "общий текст", + "edit-paired-devices-title": "Редактировать сопряженные устройства", + "auto-accept": "автоприем", + "close": "Закрыть", + "decline": "Отклонить", + "share": "Поделиться", + "would-like-to-share": "хотел бы поделиться", + "has-sent": "отправил:", + "paired-devices-wrapper_data-empty": "Нет сопряженных устройств.", + "download": "Скачать", + "receive-text-title": "Сообщение получено", + "send": "Отправить", + "send-message-to": "Отправить сообщение", + "send-message-title": "Отправить сообщение", + "copy": "Копировать", + "base64-files": "файлы", + "base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}", + "base64-processing": "Обработка...", + "base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}", + "base64-text": "текст", + "title-file": "Файл", + "title-file-plural": "Файлы", + "title-image": "Изображение", + "title-image-plural": "Изображения", + "download-again": "Скачать еще раз", + "auto-accept-instructions-2": "чтобы автоматически принимать все файлы, отправленные с этого устройства.", + "enter-key-from-another-device": "Для продолжения введите ключ с другого устройства.", + "pair-devices-title": "Сопрягите устройства", + "input-key-on-this-device": "Введите этот ключ на другом устройстве", + "scan-qr-code": "или отсканируйте QR-код.", + "cancel": "Отменить", + "pair": "Подключить", + "accept": "Принять", + "auto-accept-instructions-1": "Активировать", + "file-other-description-file": "и 1 другой файл", + "file-other-description-image-plural": "и {{count}} других изображений", + "file-other-description-image": "и 1 другое изображение", + "file-other-description-file-plural": "и {{count}} других файлов", + "receive-title": "{{descriptor}} получен" + }, + "about": { + "close-about-aria-label": "Закрыть страницу \"О сервисе\"", + "claim": "Самый простой способ передачи файлов между устройствами", + "close-about_aria-label": "Закрыть страницу \"О сервисе\"", + "buy-me-a-coffee_title": "Купить мне кофе!", + "github_title": "PairDrop на GitHub", + "tweet_title": "Твит о PairDrop", + "faq_title": "Часто задаваемые вопросы" + }, + "notifications": { + "display-name-changed-permanently": "Отображаемое имя было изменено навсегда.", + "display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.", + "pairing-success": "Устройства сопряжены.", + "pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.", + "copied-to-clipboard": "Скопировано в буфер обмена", + "pairing-not-persistent": "Сопряженные устройства непостоянны.", + "link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть", + "notifications-enabled": "Уведомления включены.", + "text-content-incorrect": "Содержание текста неверно.", + "message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать", + "connected": "Подключено.", + "copied-text": "Текст скопирован в буфер обмена", + "online": "Вы снова в сети", + "offline": "Вы находитесь вне сети", + "online-requirement": "Для сопряжения устройств вам нужно быть в сети.", + "files-incorrect": "Файлы неверны.", + "message-transfer-completed": "Передача сообщения завершена.", + "ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз", + "selected-peer-left": "Выбранный узел вышел.", + "request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}", + "rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.", + "unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть?", + "copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!", + "pairing-cleared": "Все устройства не сопряжены.", + "pairing-key-invalid": "Неверный ключ", + "pairing-key-invalidated": "Ключ {{key}} признан недействительным.", + "click-to-download": "Нажмите, чтобы скачать", + "clipboard-content-incorrect": "Содержание буфера обмена неверно.", + "click-to-show": "Нажмите, чтобы показать", + "connecting": "Подключение…", + "download-successful": "{{descriptor}} загружен", + "display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.", + "file-content-incorrect": "Содержимое файла неверно.", + "file-transfer-completed": "Передача файла завершена." + }, + "peer-ui": { + "click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}", + "preparing": "Подготовка…", + "transferring": "Передача…", + "processing": "Обработка…", + "waiting": "Ожидание…", + "connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах", + "click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение" + }, + "document-titles": { + "file-received-plural": "{{count}} файлов получено", + "message-received-plural": "{{count}} сообщений получено", + "file-received": "Файл получен", + "file-transfer-requested": "Запрошена передача файлов", + "message-received": "Сообщение получено" + } +} diff --git a/public_included_ws_fallback/lang/tr.json b/public_included_ws_fallback/lang/tr.json new file mode 100644 index 0000000..783e25b --- /dev/null +++ b/public_included_ws_fallback/lang/tr.json @@ -0,0 +1,25 @@ +{ + "header": { + "about_title": "PairDrop Hakkında", + "about_aria-label": "PairDrop Hakkında Aç", + "theme-auto_title": "Temayı Sisteme Uyarla", + "theme-light_title": "Daima Açık Tema Kullan", + "theme-dark_title": "Daima Koyu Tema Kullan", + "notification_title": "Bildirimleri Etkinleştir", + "install_title": "PairDrop'u Yükle", + "pair-device_title": "Cihaz Eşleştir", + "edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle", + "cancel-paste-mode": "Bitti" + }, + "instructions": { + "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" + }, + "footer": { + "display-name_placeholder": "Yükleniyor…", + "display-name_title": "Cihazının adını kalıcı olarak düzenle" + }, + "dialogs": { + "cancel": "İptal", + "edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle" + } +} diff --git a/public_included_ws_fallback/lang/zh-CN.json b/public_included_ws_fallback/lang/zh-CN.json new file mode 100644 index 0000000..4191c7d --- /dev/null +++ b/public_included_ws_fallback/lang/zh-CN.json @@ -0,0 +1,140 @@ +{ + "header": { + "about_title": "关于 PairDrop", + "about_aria-label": "打开 关于 PairDrop", + "theme-light_title": "总是使用明亮主题", + "install_title": "安装 PairDrop", + "pair-device_title": "配对新设备", + "theme-auto_title": "主题适应系统", + "theme-dark_title": "总是使用暗黑主题", + "notification_title": "开启通知", + "edit-paired-devices_title": "管理已配对设备", + "cancel-paste-mode": "完成" + }, + "instructions": { + "x-instructions_data-drop-peer": "释放以发送到此设备", + "no-peers_data-drop-bg": "释放来选择接收者", + "no-peers-subtitle": "配对新设备使在其他网络上可见", + "no-peers-title": "在其他设备上打开 PairDrop 来发送文件", + "x-instructions_desktop": "点击以发送文件 或 右键来发送信息", + "x-instructions_mobile": "轻触以发送文件 或 长按来发送信息", + "x-instructions_data-drop-bg": "释放来选择接收者", + "click-to-send": "点击发送", + "tap-to-send": "轻触发送" + }, + "footer": { + "routed": "途径服务器", + "webrtc": "如果 WebRTC 不可用。", + "known-as": "你的名字是:", + "display-name_placeholder": "加载中…", + "and-by": "和", + "display-name_title": "长久修改你的设备名", + "discovery-everyone": "你对所有人可见", + "on-this-network": "在此网络上", + "paired-devices": "已配对的设备", + "traffic": "流量将" + }, + "dialogs": { + "activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送", + "activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件", + "activate-paste-mode-activate-paste-mode-shared-text": "分享文本", + "pair-devices-title": "配对新设备", + "input-key-on-this-device": "在另一个设备上输入这串数字", + "base64-text": "信息", + "enter-key-from-another-device": "输入从另一个设备上获得的数字以继续。", + "edit-paired-devices-title": "管理已配对的设备", + "pair": "配对", + "cancel": "取消", + "scan-qr-code": "或者 扫描二维码。", + "paired-devices-wrapper_data-empty": "无已配对设备。", + "auto-accept-instructions-1": "启用", + "auto-accept": "自动接收", + "decline": "拒绝", + "base64-processing": "处理中...", + "base64-tap-to-paste": "轻触此处粘贴{{type}}", + "base64-paste-to-send": "粘贴到此处以发送 {{type}}", + "auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。", + "would-like-to-share": "想要分享", + "accept": "接收", + "close": "关闭", + "share": "分享", + "download": "保存", + "send": "发送", + "receive-text-title": "收到信息", + "copy": "复制", + "send-message-title": "发送信息", + "send-message-to": "发了一条信息给", + "has-sent": "发送了:", + "base64-files": "文件", + "file-other-description-file": "和 1 个其他的文件", + "file-other-description-image": "和 1 个其他的图片", + "file-other-description-image-plural": "和 {{count}} 个其他的图片", + "file-other-description-file-plural": "和 {{count}} 个其他的文件", + "title-image-plural": "图片", + "receive-title": "收到 {{descriptor}}", + "title-image": "图片", + "title-file": "文件", + "title-file-plural": "文件", + "download-again": "再次保存" + }, + "about": { + "faq_title": "常见问题", + "close-about_aria-label": "关闭 关于 PairDrop", + "github_title": "PairDrop 在 GitHub 上开源", + "claim": "最简单的跨设备传输方案", + "buy-me-a-coffee_title": "帮我买杯咖啡!", + "tweet_title": "关于 PairDrop 的推特" + }, + "notifications": { + "display-name-changed-permanently": "展示的名字已经长久变更。", + "display-name-changed-temporarily": "展示的名字已经变更 仅在此会话中。", + "display-name-random-again": "展示的名字再次随机生成。", + "download-successful": "{{descriptor}} 已下载", + "pairing-tabs-error": "无法配对两个浏览器标签页。", + "pairing-success": "新设备已配对。", + "pairing-not-persistent": "配对的设备不是持久的。", + "pairing-key-invalid": "无效配对码", + "pairing-key-invalidated": "配对码 {{key}} 已失效。", + "text-content-incorrect": "文本内容不合法。", + "file-content-incorrect": "文件内容不合法。", + "clipboard-content-incorrect": "剪贴板内容不合法。", + "link-received": "收到来自 {{name}} 的链接 - 点击打开", + "message-received": "收到来自 {{name}} 的信息 - 点击复制", + "request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}", + "click-to-show": "点击展示", + "copied-text": "复制到剪贴板", + "selected-peer-left": "选择的设备已离开。", + "pairing-cleared": "所有设备已解除配对。", + "copied-to-clipboard": "已复制到剪贴板", + "notifications-enabled": "通知已启用。", + "copied-text-error": "写入剪贴板失败。请手动复制!", + "click-to-download": "点击以保存", + "unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭吗?", + "message-transfer-completed": "信息传输已完成。", + "offline": "你未连接到网络", + "online": "你已重新连接到网络", + "connected": "已连接。", + "online-requirement": "你需要连接网络来配对新设备。", + "files-incorrect": "文件不合法。", + "file-transfer-completed": "文件传输已完成。", + "connecting": "连接中…", + "ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB", + "rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。" + }, + "document-titles": { + "message-received": "收到信息", + "message-received-plural": "收到 {{count}} 条信息", + "file-transfer-requested": "文件传输请求", + "file-received-plural": "收到 {{count}} 个文件", + "file-received": "收到文件" + }, + "peer-ui": { + "click-to-send-paste-mode": "点击发送 {{descriptor}}", + "click-to-send": "点击以发送文件 或 右键来发送信息", + "connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号", + "preparing": "准备中…", + "waiting": "请等待…", + "transferring": "传输中…", + "processing": "处理中…" + } +} From c2a746d69cb16f1e790a9a9031034fc3788dd436 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 02:30:01 +0200 Subject: [PATCH 059/519] fix html attribute translation --- public/scripts/localization.js | 2 +- public_included_ws_fallback/scripts/localization.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index 1619676..8f30f26 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -63,7 +63,7 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.attr = Localization.getTranslation(key, attr); + element.setAttribute(attr, Localization.getTranslation(key, attr)); } } diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index 1619676..8f30f26 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -63,7 +63,7 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.attr = Localization.getTranslation(key, attr); + element.setAttribute(attr, Localization.getTranslation(key, attr)); } } From abc06fcc21bb73fede96e6cae3487bbf315d3dd6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 02:32:54 +0200 Subject: [PATCH 060/519] fix translation fallback for sparely translated languages when complete categories are missing --- public/scripts/localization.js | 22 ++++++++++++------- .../scripts/localization.js | 22 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index 8f30f26..a59f30a 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -76,18 +76,24 @@ class Localization { ? Localization.defaultTranslations : Localization.translations; - for (let i=0; i Date: Tue, 29 Aug 2023 02:33:54 +0200 Subject: [PATCH 061/519] enable Norwegian, Russian, and Chinese --- public/scripts/localization.js | 2 +- public_included_ws_fallback/scripts/localization.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index a59f30a..a833993 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -1,7 +1,7 @@ class Localization { constructor() { Localization.defaultLocale = "en"; - Localization.supportedLocales = ["en"]; + Localization.supportedLocales = ["en", "nb", "ru", "zh-CN"]; Localization.translations = {}; Localization.defaultTranslations = {}; diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index a59f30a..a833993 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -1,7 +1,7 @@ class Localization { constructor() { Localization.defaultLocale = "en"; - Localization.supportedLocales = ["en"]; + Localization.supportedLocales = ["en", "nb", "ru", "zh-CN"]; Localization.translations = {}; Localization.defaultTranslations = {}; From d738258869169d9207a3a311b1756c2381479ec7 Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Tue, 29 Aug 2023 09:10:05 -0400 Subject: [PATCH 062/519] Add alternate TURN server option Added note about using an alternate TURN server like OpenRelay. --- docs/host-your-own.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index e0e33f7..c81afa1 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -82,6 +82,7 @@ Set options by using the following flags in the `docker run` command: > You can use `pairdrop/rtc_config_example.json` as a starting point. > > To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ +> Alternatively, use a free, pre-configured TURN server like [OpenRelay]([url](https://www.metered.ca/tools/openrelay/)) > > Default configuration: > ```json From 17afa18d84f0bffdc3cf7b4b9b9de5dfd0a7461e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 30 Aug 2023 14:57:40 +0200 Subject: [PATCH 063/519] add translation selector and fix translation of data-attributes --- public/index.html | 43 ++++++++++++--- public/lang/en.json | 7 ++- public/lang/nb.json | 2 +- public/lang/ru.json | 2 +- public/lang/tr.json | 2 +- public/lang/zh-CN.json | 2 +- public/scripts/localization.js | 49 +++++++++++++---- public/scripts/ui.js | 55 +++++++++++++++++++ public/styles.css | 24 +++++--- public_included_ws_fallback/index.html | 43 ++++++++++++--- public_included_ws_fallback/lang/en.json | 2 +- public_included_ws_fallback/lang/nb.json | 2 +- public_included_ws_fallback/lang/ru.json | 2 +- public_included_ws_fallback/lang/tr.json | 2 +- public_included_ws_fallback/lang/zh-CN.json | 2 +- .../scripts/localization.js | 49 +++++++++++++---- public_included_ws_fallback/scripts/ui.js | 55 +++++++++++++++++++ public_included_ws_fallback/styles.css | 37 +++++++++---- 18 files changed, 312 insertions(+), 68 deletions(-) diff --git a/public/index.html b/public/index.html index cd81663..9625637 100644 --- a/public/index.html +++ b/public/index.html @@ -44,6 +44,11 @@
+
+ + + +
@@ -109,7 +114,7 @@
You are known as: -
+
@@ -125,6 +130,25 @@
+ + + + +

Select Language

+
+
+ + + + + +
+
+ +
+
+
+
@@ -147,7 +171,7 @@
Enter key from another device to continue.
-
+
@@ -173,7 +197,7 @@

-
+
@@ -199,7 +223,7 @@
-
+
@@ -224,7 +248,7 @@
-
+
@@ -244,7 +268,7 @@
-
+
@@ -263,7 +287,7 @@
-
+
@@ -392,6 +416,11 @@ + + + + + diff --git a/public/lang/en.json b/public/lang/en.json index ff8294d..de26740 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -1,6 +1,7 @@ { "header": { "about_title": "About PairDrop", + "language-selector_title": "Select Language", "about_aria-label": "Open About PairDrop", "theme-auto_title": "Adapt Theme to System", "theme-light_title": "Always Use Light-Theme", @@ -24,7 +25,7 @@ }, "footer": { "known-as": "You are known as:", - "display-name_placeholder": "Loading…", + "display-name_data-placeholder": "Loading…", "display-name_title": "Edit your device name permanently", "discovery-everyone": "You can be discovered by everyone", "on-this-network": "on this network", @@ -75,7 +76,9 @@ "title-image-plural": "Images", "title-file-plural": "Files", "receive-title": "{{descriptor}} Received", - "download-again": "Download again" + "download-again": "Download again", + "language-selector-title": "Select Language", + "system-language": "System Language" }, "about": { "close-about_aria-label": "Close About PairDrop", diff --git a/public/lang/nb.json b/public/lang/nb.json index b11b664..ee2bd64 100644 --- a/public/lang/nb.json +++ b/public/lang/nb.json @@ -15,7 +15,7 @@ "discovery-everyone": "Du kan oppdages av alle", "and-by": "og av", "webrtc": "hvis WebRTC ikke er tilgjengelig.", - "display-name_placeholder": "Laster inn …", + "display-name_data-placeholder": "Laster inn…", "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", "traffic": "Trafikken", "on-this-network": "på dette nettverket", diff --git a/public/lang/ru.json b/public/lang/ru.json index 8617ec2..1c67504 100644 --- a/public/lang/ru.json +++ b/public/lang/ru.json @@ -24,7 +24,7 @@ }, "footer": { "discovery-everyone": "О вас может узнать каждый", - "display-name_placeholder": "Загрузка…", + "display-name_data-placeholder": "Загрузка…", "routed": "направляется через сервер", "webrtc": ", если WebRTC недоступен.", "traffic": "Трафик", diff --git a/public/lang/tr.json b/public/lang/tr.json index 783e25b..87608f2 100644 --- a/public/lang/tr.json +++ b/public/lang/tr.json @@ -15,7 +15,7 @@ "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" }, "footer": { - "display-name_placeholder": "Yükleniyor…", + "display-name_data-placeholder": "Yükleniyor…", "display-name_title": "Cihazının adını kalıcı olarak düzenle" }, "dialogs": { diff --git a/public/lang/zh-CN.json b/public/lang/zh-CN.json index 4191c7d..d6af684 100644 --- a/public/lang/zh-CN.json +++ b/public/lang/zh-CN.json @@ -26,7 +26,7 @@ "routed": "途径服务器", "webrtc": "如果 WebRTC 不可用。", "known-as": "你的名字是:", - "display-name_placeholder": "加载中…", + "display-name_data-placeholder": "加载中…", "and-by": "和", "display-name_title": "长久修改你的设备名", "discovery-everyone": "你对所有人可见", diff --git a/public/scripts/localization.js b/public/scripts/localization.js index a833993..4510682 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -5,12 +5,19 @@ class Localization { Localization.translations = {}; Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(navigator.languages); + Localization.systemLocale = Localization.supportedOrDefault(navigator.languages); - Localization.setLocale(initialLocale) + let storedLanguageCode = localStorage.getItem("language-code"); + + Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) + ? storedLanguageCode + : Localization.systemLocale; + + Localization.setTranslation(Localization.initialLocale) .then(_ => { - Localization.translatePage(); - }) + console.log("Initial translation successful."); + Events.fire("translation-loaded"); + }); } static isSupported(locale) { @@ -21,11 +28,21 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } + static async setTranslation(locale) { + if (!locale) locale = Localization.systemLocale; + + await Localization.setLocale(locale) + await Localization.translatePage(); + + console.log("Page successfully translated", + `System language: ${Localization.systemLocale}`, + `Selected language: ${locale}` + ); + } + static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; - const isFirstTranslation = !Localization.locale - Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); @@ -34,10 +51,14 @@ class Localization { Localization.locale = newLocale; Localization.translations = newTranslations; + } - if (isFirstTranslation) { - Events.fire("translation-loaded"); - } + static getLocale() { + return Localization.locale; + } + + static isSystemLocale() { + return !localStorage.getItem('language-code'); } static async fetchTranslationsFor(newLocale) { @@ -48,7 +69,7 @@ class Localization { return await response.json(); } - static translatePage() { + static async translatePage() { document .querySelectorAll("[data-i18n-key]") .forEach(element => Localization.translateElement(element)); @@ -63,10 +84,14 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.setAttribute(attr, Localization.getTranslation(key, attr)); + if (attr.startsWith("data-")) { + let dataAttr = attr.substring(5); + element.dataset.dataAttr = Localization.getTranslation(key, attr); + } { + element.setAttribute(attr, Localization.getTranslation(key, attr)); + } } } - } static getTranslation(key, attr, data, useDefault=false) { diff --git a/public/scripts/ui.js b/public/scripts/ui.js index f3d08d8..0ab425f 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -45,6 +45,8 @@ class PeersUI { this.$displayName = $('display-name'); + this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder); + 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)); @@ -613,6 +615,58 @@ class Dialog { } } +class LanguageSelectDialog extends Dialog { + + constructor() { + super('language-select-dialog'); + + this.$languageSelectBtn = $('language-selector'); + this.$languageSelectBtn.addEventListener('click', _ => this.show()); + + this.$languageButtons = this.$el.querySelectorAll(".language-buttons button"); + this.$languageButtons.forEach($btn => { + $btn.addEventListener("click", e => this.selectLanguage(e)); + }) + Events.on('keydown', e => this._onKeyDown(e)); + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this.hide(); + } + } + + show() { + if (Localization.isSystemLocale()) { + this.$languageButtons[0].focus(); + } else { + let locale = Localization.getLocale(); + for (let i=0; i this.hide()); + } +} + class ReceiveDialog extends Dialog { constructor(id) { super(id); @@ -2255,6 +2309,7 @@ class PairDrop { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); + const languageSelectDialog = new LanguageSelectDialog(); const receiveFileDialog = new ReceiveFileDialog(); const receiveRequestDialog = new ReceiveRequestDialog(); const sendTextDialog = new SendTextDialog(); diff --git a/public/styles.css b/public/styles.css index 1375b46..4b23974 100644 --- a/public/styles.css +++ b/public/styles.css @@ -23,6 +23,7 @@ body { -webkit-user-select: none; -moz-user-select: none; user-select: none; + transition: color 300ms; } body { @@ -40,6 +41,10 @@ html { min-height: fill-available; } +.fw { + width: 100%; +} + .row-reverse { display: flex; flex-direction: row-reverse; @@ -591,7 +596,6 @@ footer { align-items: center; padding: 0 0 16px 0; text-align: center; - transition: color 300ms; cursor: default; } @@ -683,7 +687,6 @@ x-dialog x-paper { top: max(50%, 350px); margin-top: -328.5px; width: calc(100vw - 20px); - height: 625px; } #pair-device-dialog ::-moz-selection, @@ -761,7 +764,7 @@ x-dialog a { } x-dialog hr { - margin: 40px -24px 30px -24px; + margin: 20px -24px 20px -24px; border: solid 1.25px var(--border-color); } @@ -868,18 +871,18 @@ x-dialog .row { } /* button row*/ -x-paper > div:last-child { - margin: auto -24px -15px; +x-paper > .button-row { + margin: 25px -24px -15px; border-top: solid 2.5px var(--border-color); height: 50px; } -x-paper > div:last-child > .button { +x-paper > .button-row > .button { height: 100%; width: 100%; } -x-paper > div:last-child > .button:not(:last-child) { +x-paper > .button-row > .button:not(:last-child) { border-left: solid 2.5px var(--border-color); } @@ -1044,6 +1047,11 @@ x-dialog .dialog-subheader { opacity: 0.1; } +.button[selected], +.icon-button[selected] { + opacity: 0.1; +} + #cancel-paste-mode { z-index: 2; margin: 0; @@ -1301,7 +1309,7 @@ x-peers:empty~x-instructions { x-dialog x-paper { padding: 15px; } - x-paper > div:last-child { + x-paper > .button-row { margin: auto -15px -15px; } } diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index a233aab..529bc1a 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -44,6 +44,11 @@ +
+ + + +
@@ -109,7 +114,7 @@
You are known as: -
+
@@ -130,6 +135,25 @@ if WebRTC is not available.
+ + + + +

Select Language

+
+
+ + + + + +
+
+ +
+
+
+
@@ -152,7 +176,7 @@
Enter key from another device to continue.
-
+
@@ -178,7 +202,7 @@

-
+
@@ -204,7 +228,7 @@
-
+
@@ -229,7 +253,7 @@
-
+
@@ -249,7 +273,7 @@
-
+
@@ -268,7 +292,7 @@
-
+
@@ -397,6 +421,11 @@ + + + + + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index ff8294d..4c88dae 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -24,7 +24,7 @@ }, "footer": { "known-as": "You are known as:", - "display-name_placeholder": "Loading…", + "display-name_data-placeholder": "Loading…", "display-name_title": "Edit your device name permanently", "discovery-everyone": "You can be discovered by everyone", "on-this-network": "on this network", diff --git a/public_included_ws_fallback/lang/nb.json b/public_included_ws_fallback/lang/nb.json index b11b664..ee2bd64 100644 --- a/public_included_ws_fallback/lang/nb.json +++ b/public_included_ws_fallback/lang/nb.json @@ -15,7 +15,7 @@ "discovery-everyone": "Du kan oppdages av alle", "and-by": "og av", "webrtc": "hvis WebRTC ikke er tilgjengelig.", - "display-name_placeholder": "Laster inn …", + "display-name_data-placeholder": "Laster inn…", "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", "traffic": "Trafikken", "on-this-network": "på dette nettverket", diff --git a/public_included_ws_fallback/lang/ru.json b/public_included_ws_fallback/lang/ru.json index 8617ec2..1c67504 100644 --- a/public_included_ws_fallback/lang/ru.json +++ b/public_included_ws_fallback/lang/ru.json @@ -24,7 +24,7 @@ }, "footer": { "discovery-everyone": "О вас может узнать каждый", - "display-name_placeholder": "Загрузка…", + "display-name_data-placeholder": "Загрузка…", "routed": "направляется через сервер", "webrtc": ", если WebRTC недоступен.", "traffic": "Трафик", diff --git a/public_included_ws_fallback/lang/tr.json b/public_included_ws_fallback/lang/tr.json index 783e25b..87608f2 100644 --- a/public_included_ws_fallback/lang/tr.json +++ b/public_included_ws_fallback/lang/tr.json @@ -15,7 +15,7 @@ "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" }, "footer": { - "display-name_placeholder": "Yükleniyor…", + "display-name_data-placeholder": "Yükleniyor…", "display-name_title": "Cihazının adını kalıcı olarak düzenle" }, "dialogs": { diff --git a/public_included_ws_fallback/lang/zh-CN.json b/public_included_ws_fallback/lang/zh-CN.json index 4191c7d..d6af684 100644 --- a/public_included_ws_fallback/lang/zh-CN.json +++ b/public_included_ws_fallback/lang/zh-CN.json @@ -26,7 +26,7 @@ "routed": "途径服务器", "webrtc": "如果 WebRTC 不可用。", "known-as": "你的名字是:", - "display-name_placeholder": "加载中…", + "display-name_data-placeholder": "加载中…", "and-by": "和", "display-name_title": "长久修改你的设备名", "discovery-everyone": "你对所有人可见", diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index a833993..a447669 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -5,12 +5,19 @@ class Localization { Localization.translations = {}; Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(navigator.languages); + Localization.systemLocale = Localization.supportedOrDefault(navigator.languages); - Localization.setLocale(initialLocale) + let storedLanguageCode = localStorage.getItem("language-code"); + + Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) + ? storedLanguageCode + : Localization.systemLocale; + + Localization.setTranslation(Localization.initialLocale) .then(_ => { - Localization.translatePage(); - }) + console.log("Initial translation successful."); + Events.fire("translation-loaded"); + }); } static isSupported(locale) { @@ -21,10 +28,20 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } + static async setTranslation(locale) { + if (!locale) locale = Localization.systemLocale; + + await Localization.setLocale(locale) + await Localization.translatePage(); + + console.log("Page successfully translated", + `System language: ${Localization.systemLocale}`, + `Selected language: ${locale}` + ); + } + static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; - - const isFirstTranslation = !Localization.locale Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); @@ -34,10 +51,14 @@ class Localization { Localization.locale = newLocale; Localization.translations = newTranslations; + } - if (isFirstTranslation) { - Events.fire("translation-loaded"); - } + static getLocale() { + return Localization.locale; + } + + static isSystemLocale() { + return !localStorage.getItem('language-code'); } static async fetchTranslationsFor(newLocale) { @@ -48,7 +69,7 @@ class Localization { return await response.json(); } - static translatePage() { + static async translatePage() { document .querySelectorAll("[data-i18n-key]") .forEach(element => Localization.translateElement(element)); @@ -63,10 +84,14 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.setAttribute(attr, Localization.getTranslation(key, attr)); + if (attr.startsWith("data-")) { + let dataAttr = attr.substring(5); + element.dataset.dataAttr = Localization.getTranslation(key, attr); + } { + element.setAttribute(attr, Localization.getTranslation(key, attr)); + } } } - } static getTranslation(key, attr, data, useDefault=false) { diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index b3afac4..edba81e 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -45,6 +45,8 @@ class PeersUI { this.$displayName = $('display-name'); + this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder); + 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)); @@ -614,6 +616,58 @@ class Dialog { } } +class LanguageSelectDialog extends Dialog { + + constructor() { + super('language-select-dialog'); + + this.$languageSelectBtn = $('language-selector'); + this.$languageSelectBtn.addEventListener('click', _ => this.show()); + + this.$languageButtons = this.$el.querySelectorAll(".language-buttons button"); + this.$languageButtons.forEach($btn => { + $btn.addEventListener("click", e => this.selectLanguage(e)); + }) + Events.on('keydown', e => this._onKeyDown(e)); + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this.hide(); + } + } + + show() { + if (Localization.isSystemLocale()) { + this.$languageButtons[0].focus(); + } else { + let locale = Localization.getLocale(); + for (let i=0; i this.hide()); + } +} + class ReceiveDialog extends Dialog { constructor(id) { super(id); @@ -2256,6 +2310,7 @@ class PairDrop { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); + const languageSelectDialog = new LanguageSelectDialog(); const receiveFileDialog = new ReceiveFileDialog(); const receiveRequestDialog = new ReceiveRequestDialog(); const sendTextDialog = new SendTextDialog(); diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 2e8fbb8..b36dd69 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -24,6 +24,7 @@ body { -webkit-user-select: none; -moz-user-select: none; user-select: none; + transition: color 300ms; } body { @@ -41,6 +42,10 @@ html { min-height: fill-available; } +.fw { + width: 100%; +} + .row-reverse { display: flex; flex-direction: row-reverse; @@ -452,7 +457,7 @@ x-no-peers::before { } x-no-peers[drop-bg]::before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-no-peers[drop-bg] * { @@ -652,11 +657,13 @@ footer .font-body2 { #on-this-network { border-bottom: solid 4px var(--primary-color); padding-bottom: 1px; + word-break: keep-all; } #paired-devices { border-bottom: solid 4px var(--paired-device-color); padding-bottom: 1px; + word-break: keep-all; } #display-name { @@ -723,7 +730,6 @@ x-dialog x-paper { top: max(50%, 350px); margin-top: -328.5px; width: calc(100vw - 20px); - height: 625px; } #pair-device-dialog ::-moz-selection, @@ -800,8 +806,12 @@ x-dialog .font-subheading { margin: 16px; } +#pair-instructions { + flex-direction: column; +} + x-dialog hr { - margin: 40px -24px 30px -24px; + margin: 20px -24px 20px -24px; border: solid 1.25px var(--border-color); } @@ -811,7 +821,7 @@ x-dialog hr { /* Edit Paired Devices Dialog */ .paired-devices-wrapper:empty:before { - content: "No paired devices."; + content: attr(data-empty); } .paired-devices-wrapper:empty { @@ -908,18 +918,18 @@ x-dialog .row { } /* button row*/ -x-paper > div:last-child { - margin: auto -24px -15px; +x-paper > .button-row { + margin: 25px -24px -15px; border-top: solid 2.5px var(--border-color); height: 50px; } -x-paper > div:last-child > .button { +x-paper > .button-row > .button { height: 100%; width: 100%; } -x-paper > div:last-child > .button:not(:last-child) { +x-paper > .button-row > .button:not(:last-child) { border-left: solid 2.5px var(--border-color); } @@ -1084,6 +1094,11 @@ x-dialog .dialog-subheader { opacity: 0.1; } +.button[selected], +.icon-button[selected] { + opacity: 0.1; +} + #cancel-paste-mode { z-index: 2; margin: 0; @@ -1314,11 +1329,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before { } x-instructions[drop-peer]:before { - content: "Release to send to peer"; + content: attr(data-drop-peer); } x-instructions[drop-bg]:not([drop-peer]):before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-instructions p { @@ -1358,7 +1373,7 @@ x-peers:empty~x-instructions { x-dialog x-paper { padding: 15px; } - x-paper > div:last-child { + x-paper > .button-row { margin: auto -15px -15px; } } From 1c946fc78ef6014c443c54f4e9432f12eb6007e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 04:32:13 +0000 Subject: [PATCH 064/519] Bump ws from 8.13.0 to 8.14.1 Bumps [ws](https://github.com/websockets/ws) from 8.13.0 to 8.14.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.13.0...8.14.1) --- updated-dependencies: - dependency-name: ws dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ea5f15..b35dc56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", - "ws": "^8.13.0" + "ws": "^8.14.1" }, "engines": { "node": ">=15" @@ -633,9 +633,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "engines": { "node": ">=10.0.0" }, @@ -1095,9 +1095,9 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "requires": {} } } diff --git a/package.json b/package.json index baca71c..a7df332 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", - "ws": "^8.13.0" + "ws": "^8.14.1" }, "engines": { "node": ">=15" From d689fe28e54cc2636c3bd61231e9db51b1c43686 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 18:31:42 +0200 Subject: [PATCH 065/519] add English language names next to native language names to language select dialog --- public/index.html | 6 +++--- public_included_ws_fallback/index.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index 9625637..155f511 100644 --- a/public/index.html +++ b/public/index.html @@ -139,9 +139,9 @@
- - - + + +
diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 529bc1a..9a1523b 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -144,9 +144,9 @@
- - - + + +
From 02911804cb696a73e36254b9806e39551c2254cd Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:08:57 +0200 Subject: [PATCH 066/519] add default values to Localization.getTranslation function --- public/scripts/localization.js | 4 ++-- public_included_ws_fallback/scripts/localization.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index 4510682..879deae 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -42,7 +42,7 @@ class Localization { static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; - + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); @@ -94,7 +94,7 @@ class Localization { } } - static getTranslation(key, attr, data, useDefault=false) { + static getTranslation(key, attr=null, data={}, useDefault=false) { const keys = key.split("."); let translationCandidates = useDefault diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index a447669..879deae 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -94,7 +94,7 @@ class Localization { } } - static getTranslation(key, attr, data, useDefault=false) { + static getTranslation(key, attr=null, data={}, useDefault=false) { const keys = key.split("."); let translationCandidates = useDefault From 6679ef75293be94e0a40175a57e5e8dd97e586a5 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:23:47 +0200 Subject: [PATCH 067/519] fix keepAliveTimers not correctly cleared on reconnect --- index.js | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 029a955..176519e 100644 --- a/index.js +++ b/index.js @@ -130,8 +130,10 @@ class PairDropServer { this._wss = new WebSocket.Server({ server }); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); - this._rooms = {}; - this._roomSecrets = {}; + this._rooms = {}; // { roomId: peers[] } + this._roomSecrets = {}; // { pairKey: roomSecret } + + this._keepAliveTimers = {}; console.log('PairDrop is running on port', port); } @@ -139,7 +141,9 @@ class PairDropServer { _onConnection(peer) { peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.onerror = e => console.error(e); + this._keepAlive(peer); + this._send(peer, { type: 'rtc-config', config: rtcConfig @@ -170,7 +174,7 @@ class PairDropServer { this._onDisconnect(sender); break; case 'pong': - sender.lastBeat = Date.now(); + this._keepAliveTimers[sender.id].lastBeat = Date.now(); break; case 'join-ip-room': this._joinRoom(sender); @@ -223,10 +227,15 @@ class PairDropServer { } _disconnect(sender) { - this._leaveRoom(sender, 'ip', '', true); - this._leaveAllSecretRooms(sender, true); this._removeRoomKey(sender.roomKey); sender.roomKey = null; + + this._cancelKeepAlive(sender); + delete this._keepAliveTimers[sender.id]; + + this._leaveRoom(sender, 'ip', '', true); + this._leaveAllSecretRooms(sender, true); + sender.socket.terminate(); } @@ -465,23 +474,29 @@ class PairDropServer { _keepAlive(peer) { this._cancelKeepAlive(peer); - let timeout = 500; - if (!peer.lastBeat) { - peer.lastBeat = Date.now(); + let timeout = 1000; + + if (!this._keepAliveTimers[peer.id]) { + this._keepAliveTimers[peer.id] = { + timer: 0, + lastBeat: Date.now() + }; } - if (Date.now() - peer.lastBeat > 2 * timeout) { + + if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 2 * timeout) { + // Disconnect peer if unresponsive for 10s this._disconnect(peer); return; } this._send(peer, { type: 'ping' }); - peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); + this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout); } _cancelKeepAlive(peer) { - if (peer && peer.timerId) { - clearTimeout(peer.timerId); + if (this._keepAliveTimers[peer.id]?.timer) { + clearTimeout(this._keepAliveTimers[peer.id].timer); } } } @@ -506,10 +521,6 @@ class Peer { // set name this._setName(request); - // for keepalive - this.timerId = 0; - this.lastBeat = Date.now(); - this.roomSecrets = []; this.roomKey = null; this.roomKeyRate = 0; From c71bf456e3a71d6539f0ade58c9e0cf5342979c6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:44:49 +0200 Subject: [PATCH 068/519] fix "and 2 other files" div not cleared properly --- public/scripts/ui.js | 24 +++++++++++------------ public_included_ws_fallback/scripts/ui.js | 24 +++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 0ab425f..69cc23a 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -695,20 +695,20 @@ class ReceiveDialog extends Dialog { } _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { - if (files.length > 1) { - let fileOther; - if (files.length === 2) { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image") - : Localization.getTranslation("dialogs.file-other-description-file"); - } else { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) - : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); - } - this.$fileOther.innerText = fileOther; + let fileOther = ""; + + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else if (files.length >= 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } + this.$fileOther.innerText = fileOther; + const fileName = files[0].name; const fileNameSplit = fileName.split('.'); const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index edba81e..c5eb462 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -696,20 +696,20 @@ class ReceiveDialog extends Dialog { } _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { - if (files.length > 1) { - let fileOther; - if (files.length === 2) { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image") - : Localization.getTranslation("dialogs.file-other-description-file"); - } else { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) - : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); - } - this.$fileOther.innerText = fileOther; + let fileOther = ""; + + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else if (files.length >= 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } + this.$fileOther.innerText = fileOther; + const fileName = files[0].name; const fileNameSplit = fileName.split('.'); const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; From bd7b3c6d28d23004bd2181b112a3c55922e6c220 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:50:46 +0200 Subject: [PATCH 069/519] show warning to user if navigator.clipboard.writeText fails --- public/lang/en.json | 1 + public/scripts/ui.js | 11 ++++++++--- public_included_ws_fallback/lang/en.json | 1 + public_included_ws_fallback/scripts/ui.js | 11 ++++++++--- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/public/lang/en.json b/public/lang/en.json index de26740..75a4d60 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -100,6 +100,7 @@ "pairing-key-invalidated": "Key {{key}} invalidated.", "pairing-cleared": "All Devices unpaired.", "copied-to-clipboard": "Copied to clipboard", + "copied-to-clipboard-error": "Copying not possible. Copy manually.", "text-content-incorrect": "Text content is incorrect.", "file-content-incorrect": "File content is incorrect.", "clipboard-content-incorrect": "Clipboard content is incorrect.", diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 69cc23a..4cebb96 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1497,9 +1497,14 @@ class ReceiveTextDialog extends Dialog { async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); - await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); - this.hide(); + navigator.clipboard.writeText(sanitizedText) + .then(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); + this.hide(); + }) + .catch(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard-error")); + }); } hide() { diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 4c88dae..a607219 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -97,6 +97,7 @@ "pairing-key-invalidated": "Key {{key}} invalidated.", "pairing-cleared": "All Devices unpaired.", "copied-to-clipboard": "Copied to clipboard", + "copied-to-clipboard-error": "Copying not possible. Copy manually.", "text-content-incorrect": "Text content is incorrect.", "file-content-incorrect": "File content is incorrect.", "clipboard-content-incorrect": "Clipboard content is incorrect.", diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index c5eb462..460f30f 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1498,9 +1498,14 @@ class ReceiveTextDialog extends Dialog { async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); - await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); - this.hide(); + navigator.clipboard.writeText(sanitizedText) + .then(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); + this.hide(); + }) + .catch(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard-error")); + }); } hide() { From 8d2584fa6933c3209bb7096f18b686e4a60b7e29 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 18:15:01 +0200 Subject: [PATCH 070/519] implement temporary public rooms, tidy up index.js, rework UI dialogs and change colors slightly --- index.js | 283 ++++++++++------ public/index.html | 255 ++++++++++---- public/lang/en.json | 30 +- public/scripts/network.js | 235 +++++++++---- public/scripts/ui.js | 677 ++++++++++++++++++++++++++++---------- public/styles.css | 253 +++++++++----- 6 files changed, 1237 insertions(+), 496 deletions(-) diff --git a/index.js b/index.js index 176519e..1d08d7d 100644 --- a/index.js +++ b/index.js @@ -177,7 +177,7 @@ class PairDropServer { this._keepAliveTimers[sender.id].lastBeat = Date.now(); break; case 'join-ip-room': - this._joinRoom(sender); + this._joinIpRoom(sender); break; case 'room-secrets': this._onRoomSecrets(sender, message); @@ -196,9 +196,15 @@ class PairDropServer { break; case 'regenerate-room-secret': this._onRegenerateRoomSecret(sender, message); - break - case 'resend-peers': - this._notifyPeers(sender); + break; + case 'create-public-room': + this._onCreatePublicRoom(sender); + break; + case 'join-public-room': + this._onJoinPublicRoom(sender, message); + break; + case 'leave-public-room': + this._onLeavePublicRoom(sender); break; case 'signal': default: @@ -207,7 +213,9 @@ class PairDropServer { } _signalAndRelay(sender, message) { - const room = message.roomType === 'ip' ? sender.ip : message.roomSecret; + const room = message.roomType === 'ip' + ? sender.ip + : message.roomId; // relay message to recipient if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) { @@ -227,14 +235,15 @@ class PairDropServer { } _disconnect(sender) { - this._removeRoomKey(sender.roomKey); - sender.roomKey = null; + this._removePairKey(sender.pairKey); + sender.pairKey = null; this._cancelKeepAlive(sender); delete this._keepAliveTimers[sender.id]; - this._leaveRoom(sender, 'ip', '', true); + this._leaveIpRoom(sender, true); this._leaveAllSecretRooms(sender, true); + this._leavePublicRoom(sender, true); sender.socket.terminate(); } @@ -264,7 +273,7 @@ class PairDropServer { for (const peerId in room) { const peer = room[peerId]; - this._leaveRoom(peer, 'secret', roomSecret); + this._leaveSecretRoom(peer, roomSecret, true); this._send(peer, { type: 'secret-room-deleted', @@ -275,34 +284,35 @@ class PairDropServer { _onPairDeviceInitiate(sender) { let roomSecret = randomizer.getRandomString(256); - let roomKey = this._createRoomKey(sender, roomSecret); - if (sender.roomKey) this._removeRoomKey(sender.roomKey); - sender.roomKey = roomKey; + let pairKey = this._createPairKey(sender, roomSecret); + + if (sender.pairKey) { + this._removePairKey(sender.pairKey); + } + sender.pairKey = pairKey; + this._send(sender, { type: 'pair-device-initiated', roomSecret: roomSecret, - roomKey: roomKey + pairKey: pairKey }); - this._joinRoom(sender, 'secret', roomSecret); + this._joinSecretRoom(sender, roomSecret); } _onPairDeviceJoin(sender, message) { - // rate limit implementation: max 10 attempts every 10s - if (sender.roomKeyRate >= 10) { - this._send(sender, { type: 'pair-device-join-key-rate-limit' }); + if (sender.rateLimitReached()) { + this._send(sender, { type: '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) { + if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].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); + const roomSecret = this._roomSecrets[message.pairKey].roomSecret; + const creator = this._roomSecrets[message.pairKey].creator; + this._removePairKey(message.pairKey); this._send(sender, { type: 'pair-device-joined', roomSecret: roomSecret, @@ -313,22 +323,53 @@ class PairDropServer { roomSecret: roomSecret, peerId: sender.id }); - this._joinRoom(sender, 'secret', roomSecret); - this._removeRoomKey(sender.roomKey); + this._joinSecretRoom(sender, roomSecret); + this._removePairKey(sender.pairKey); } _onPairDeviceCancel(sender) { - const roomKey = sender.roomKey + const pairKey = sender.pairKey - if (!roomKey) return; + if (!pairKey) return; - this._removeRoomKey(roomKey); + this._removePairKey(pairKey); this._send(sender, { type: 'pair-device-canceled', - roomKey: roomKey, + pairKey: pairKey, }); } + _onCreatePublicRoom(sender) { + let publicRoomId = randomizer.getRandomString(5, true).toLowerCase(); + + this._send(sender, { + type: 'public-room-created', + roomId: publicRoomId + }); + + this._joinPublicRoom(sender, publicRoomId); + } + + _onJoinPublicRoom(sender, message) { + if (sender.rateLimitReached()) { + this._send(sender, { type: 'join-key-rate-limit' }); + return; + } + + if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) { + this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId }); + return; + } + + this._leavePublicRoom(sender); + this._joinPublicRoom(sender, message.publicRoomId); + } + + _onLeavePublicRoom(sender) { + this._leavePublicRoom(sender, true); + this._send(sender, { type: 'public-room-left' }); + } + _onRegenerateRoomSecret(sender, message) { const oldRoomSecret = message.roomSecret; const newRoomSecret = randomizer.getRandomString(256); @@ -346,122 +387,158 @@ class PairDropServer { delete this._rooms[oldRoomSecret]; } - _createRoomKey(creator, roomSecret) { - let roomKey; + _createPairKey(creator, roomSecret) { + let pairKey; 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) + pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s + } while (pairKey in this._roomSecrets) - this._roomSecrets[roomKey] = { + this._roomSecrets[pairKey] = { roomSecret: roomSecret, creator: creator } - return roomKey; + return pairKey; } - _removeRoomKey(roomKey) { + _removePairKey(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; + _joinIpRoom(peer) { + this._joinRoom(peer, 'ip', peer.ip); + } - if (this._rooms[room] && this._rooms[room][peer.id]) { + _joinSecretRoom(peer, roomSecret) { + this._joinRoom(peer, 'secret', roomSecret); + + // add secret to peer + peer.addRoomSecret(roomSecret); + } + + _joinPublicRoom(peer, publicRoomId) { + // prevent joining of 2 public rooms simultaneously + this._leavePublicRoom(peer); + + this._joinRoom(peer, 'public-id', publicRoomId); + + peer.publicRoomId = publicRoomId; + } + + _joinRoom(peer, roomType, roomId) { + // roomType: 'ip', 'secret' or 'public-id' + if (this._rooms[roomId] && this._rooms[roomId][peer.id]) { // ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect. - this._leaveRoom(peer, roomType, roomSecret); + this._leaveRoom(peer, roomType, roomId); } // if room doesn't exist, create it - if (!this._rooms[room]) { - this._rooms[room] = {}; + if (!this._rooms[roomId]) { + this._rooms[roomId] = {}; } - this._notifyPeers(peer, roomType, roomSecret); + this._notifyPeers(peer, roomType, roomId); // add peer to room - this._rooms[room][peer.id] = peer; - // add secret to peer - if (roomType === 'secret') { - peer.addRoomSecret(roomSecret); - } + this._rooms[roomId][peer.id] = peer; } - _leaveRoom(peer, roomType = 'ip', roomSecret = '', disconnect = false) { - const room = roomType === 'ip' ? peer.ip : roomSecret; - if (!this._rooms[room] || !this._rooms[room][peer.id]) return; - this._cancelKeepAlive(this._rooms[room][peer.id]); + _leaveIpRoom(peer, disconnect = false) { + this._leaveRoom(peer, 'ip', peer.ip, disconnect); + } - // delete the peer - delete this._rooms[room][peer.id]; + _leaveSecretRoom(peer, roomSecret, disconnect = false) { + this._leaveRoom(peer, 'secret', roomSecret, disconnect) - //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, - disconnect: disconnect - }); - } - } //remove secret from peer - if (roomType === 'secret') { - peer.removeRoomSecret(roomSecret); + peer.removeRoomSecret(roomSecret); + } + + _leavePublicRoom(peer, disconnect = false) { + if (!peer.publicRoomId) return; + + this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect); + + peer.publicRoomId = null; + } + + _leaveRoom(peer, roomType, roomId, disconnect = false) { + if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return; + + // remove peer from room + delete this._rooms[roomId][peer.id]; + + // delete room if empty and abort + if (!Object.keys(this._rooms[roomId]).length) { + delete this._rooms[roomId]; + return; + } + + // notify all other peers that remain in room that peer left + for (const otherPeerId in this._rooms[roomId]) { + const otherPeer = this._rooms[roomId][otherPeerId]; + + let msg = { + type: 'peer-left', + peerId: peer.id, + roomType: roomType, + roomId: roomId, + disconnect: disconnect + }; + + this._send(otherPeer, msg); } } - _notifyPeers(peer, roomType = 'ip', roomSecret = '') { - const room = roomType === 'ip' ? peer.ip : roomSecret; - if (!this._rooms[room]) return; + _notifyPeers(peer, roomType, roomId) { + if (!this._rooms[roomId]) return; - // notify all other peers - for (const otherPeerId in this._rooms[room]) { + // notify all other peers that peer joined + for (const otherPeerId in this._rooms[roomId]) { if (otherPeerId === peer.id) continue; - const otherPeer = this._rooms[room][otherPeerId]; - this._send(otherPeer, { + const otherPeer = this._rooms[roomId][otherPeerId]; + + let msg = { type: 'peer-joined', peer: peer.getInfo(), roomType: roomType, - roomSecret: roomSecret - }); + roomId: roomId + }; + + this._send(otherPeer, msg); } - // notify peer about the other peers + // notify peer about peers already in the room const otherPeers = []; - for (const otherPeerId in this._rooms[room]) { + for (const otherPeerId in this._rooms[roomId]) { if (otherPeerId === peer.id) continue; - otherPeers.push(this._rooms[room][otherPeerId].getInfo()); + otherPeers.push(this._rooms[roomId][otherPeerId].getInfo()); } - this._send(peer, { + let msg = { type: 'peers', peers: otherPeers, roomType: roomType, - roomSecret: roomSecret - }); + roomId: roomId + }; + + this._send(peer, msg); } _joinSecretRooms(peer, roomSecrets) { for (let i=0; i 2 * timeout) { + if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) { // Disconnect peer if unresponsive for 10s this._disconnect(peer); return; @@ -521,9 +598,22 @@ class Peer { // set name this._setName(request); + this.requestRate = 0; + this.roomSecrets = []; this.roomKey = null; - this.roomKeyRate = 0; + + this.publicRoomId = null; + } + + rateLimitReached() { + // rate limit implementation: max 10 attempts every 10s + if (this.requestRate >= 10) { + return true; + } + this.requestRate += 1; + setTimeout(_ => this.requestRate -= 1, 10000); + return false; } _setIP(request) { @@ -699,8 +789,15 @@ const hasher = (() => { })() const randomizer = (() => { + let charCodeLettersOnly = r => 65 <= r && r <= 90; + let charCodeAllPrintableChars = r => r === 45 || 47 <= r && r <= 57 || 64 <= r && r <= 90 || 97 <= r && r <= 122; + return { - getRandomString(length) { + getRandomString(length, lettersOnly = false) { + const charCodeCondition = lettersOnly + ? charCodeLettersOnly + : charCodeAllPrintableChars; + let string = ""; while (string.length < length) { let arr = new Uint16Array(length); @@ -711,7 +808,7 @@ const randomizer = (() => { }) arr = arr.filter(function (r) { /* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */ - return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122; + return charCodeCondition(r); }); string += String.fromCharCode.apply(String, arr); } diff --git a/public/index.html b/public/index.html index 155f511..0ea2fd1 100644 --- a/public/index.html +++ b/public/index.html @@ -78,7 +78,7 @@
-