mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-20 15:06:15 -04:00
implement paste mode and ui for files and text for multiple peers
This commit is contained in:
parent
cc9c2bf088
commit
10276b472d
3 changed files with 172 additions and 31 deletions
|
@ -65,6 +65,7 @@
|
||||||
<h2>Open Snapdrop on other devices to send files</h2>
|
<h2>Open Snapdrop on other devices to send files</h2>
|
||||||
</x-no-peers>
|
</x-no-peers>
|
||||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message"></x-instructions>
|
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message"></x-instructions>
|
||||||
|
<a id="cancelPasteModeBtn" class="button" close hidden style="z-index: 2">Cancel</a>
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="column">
|
<footer class="column">
|
||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
|
|
|
@ -167,8 +167,11 @@ class Peer {
|
||||||
case 'progress':
|
case 'progress':
|
||||||
this._onDownloadProgress(message.progress);
|
this._onDownloadProgress(message.progress);
|
||||||
break;
|
break;
|
||||||
case 'transfer-complete':
|
case 'file-transfer-complete':
|
||||||
this._onTransferCompleted();
|
this._onFileTransferCompleted();
|
||||||
|
break;
|
||||||
|
case 'message-transfer-complete':
|
||||||
|
this._onMessageTransferCompleted();
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
this._onTextReceived(message);
|
this._onTextReceived(message);
|
||||||
|
@ -204,10 +207,10 @@ class Peer {
|
||||||
|
|
||||||
_onFileReceived(proxyFile) {
|
_onFileReceived(proxyFile) {
|
||||||
Events.fire('file-received', proxyFile);
|
Events.fire('file-received', proxyFile);
|
||||||
this.sendJSON({ type: 'transfer-complete' });
|
this.sendJSON({ type: 'file-transfer-complete' });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onTransferCompleted() {
|
_onFileTransferCompleted() {
|
||||||
this._onDownloadProgress(1);
|
this._onDownloadProgress(1);
|
||||||
this._reader = null;
|
this._reader = null;
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
|
@ -215,6 +218,10 @@ class Peer {
|
||||||
Events.fire('notify-user', 'File transfer completed.');
|
Events.fire('notify-user', 'File transfer completed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onMessageTransferCompleted() {
|
||||||
|
Events.fire('notify-user', 'Message transfer completed.');
|
||||||
|
}
|
||||||
|
|
||||||
sendText(text) {
|
sendText(text) {
|
||||||
const unescaped = btoa(unescape(encodeURIComponent(text)));
|
const unescaped = btoa(unescape(encodeURIComponent(text)));
|
||||||
this.sendJSON({ type: 'text', text: unescaped });
|
this.sendJSON({ type: 'text', text: unescaped });
|
||||||
|
@ -223,6 +230,7 @@ class Peer {
|
||||||
_onTextReceived(message) {
|
_onTextReceived(message) {
|
||||||
const escaped = decodeURIComponent(escape(atob(message.text)));
|
const escaped = decodeURIComponent(escape(atob(message.text)));
|
||||||
Events.fire('text-received', { text: escaped, sender: this._peerId });
|
Events.fire('text-received', { text: escaped, sender: this._peerId });
|
||||||
|
this.sendJSON({ type: 'message-transfer-complete' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,7 +310,7 @@ class RTCPeer extends Peer {
|
||||||
|
|
||||||
_onChannelClosed() {
|
_onChannelClosed() {
|
||||||
console.log('RTC: channel closed', this._peerId);
|
console.log('RTC: channel closed', this._peerId);
|
||||||
if (!this.isCaller) return;
|
if (!this._isCaller) return;
|
||||||
this._connect(this._peerId, true); // reopen the channel
|
this._connect(this._peerId, true); // reopen the channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase());
|
||||||
window.isDownloadSupported = (typeof document.createElement('a').download !== 'undefined');
|
window.isDownloadSupported = (typeof document.createElement('a').download !== 'undefined');
|
||||||
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||||
|
window.pasteMode = {};
|
||||||
|
window.pasteMode.activated = false;
|
||||||
|
|
||||||
// set display name
|
// set display name
|
||||||
Events.on('display-name', e => {
|
Events.on('display-name', e => {
|
||||||
|
@ -52,19 +54,123 @@ class PeersUI {
|
||||||
const $peers = $$('x-peers').innerHTML = '';
|
const $peers = $$('x-peers').innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getPeers() {
|
||||||
|
let peers = []
|
||||||
|
const peersNodes = document.querySelectorAll('x-peer');
|
||||||
|
peersNodes.forEach(function(peersNode) {
|
||||||
|
peers.push({
|
||||||
|
id: peersNode.id,
|
||||||
|
name: peersNode.name,
|
||||||
|
rtcSupported: peersNode.rtcSupported
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return peers;
|
||||||
|
}
|
||||||
|
|
||||||
_onPaste(e) {
|
_onPaste(e) {
|
||||||
const files = e.clipboardData.files || e.clipboardData.items
|
const dialogNodes = document.querySelectorAll('x-dialog');
|
||||||
.filter(i => i.type.indexOf('image') > -1)
|
let dialogIsOpen = false
|
||||||
.map(i => i.getAsFile());
|
dialogNodes.forEach(function (dialogNode) {
|
||||||
const peers = document.querySelectorAll('x-peer');
|
for(let i=0; i<dialogNode.attributes.length; i++){
|
||||||
// send the pasted image content to the only peer if there is one
|
if (dialogNode.attributes[i].name === "show") {
|
||||||
// otherwise, select the peer somehow by notifying the client that
|
dialogIsOpen = true;
|
||||||
// "image data has been pasted, click the client to which to send it"
|
break
|
||||||
// not implemented
|
}
|
||||||
if (files.length > 0 && peers.length === 1) {
|
}
|
||||||
|
});
|
||||||
|
if(!dialogIsOpen) {
|
||||||
|
// prevent send on paste when dialog is open
|
||||||
|
e.preventDefault()
|
||||||
|
const files = e.clipboardData.files;
|
||||||
|
const text = e.clipboardData.getData("Text");
|
||||||
|
if (files.length === 0 && text === 0) return;
|
||||||
|
this._activatePasteMode(files, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_activatePasteMode(files, text) {
|
||||||
|
if (!window.pasteMode.activated) {
|
||||||
|
let descriptor;
|
||||||
|
let noPeersMessage;
|
||||||
|
|
||||||
|
if (files.length === 1) {
|
||||||
|
descriptor = files[0].name;
|
||||||
|
noPeersMessage = `Open Snapdrop on other devices to send <i>${descriptor}</i> directly`;
|
||||||
|
} else if (files.length > 1) {
|
||||||
|
console.debug(files);
|
||||||
|
descriptor = `${files.length} files`;
|
||||||
|
noPeersMessage = `Open Snapdrop on other devices to send ${descriptor} directly`;
|
||||||
|
} else if (text.length > 0) {
|
||||||
|
descriptor = `pasted text`;
|
||||||
|
noPeersMessage = `Open Snapdrop on other devices to send ${descriptor} directly`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xInstructions = document.querySelectorAll('x-instructions')[0];
|
||||||
|
xInstructions.setAttribute('desktop', `Click to send ${descriptor} directly`);
|
||||||
|
xInstructions.setAttribute('mobile', `Tap to send ${descriptor} directly`);
|
||||||
|
|
||||||
|
const xNoPeers = document.querySelectorAll('x-no-peers')[0];
|
||||||
|
xNoPeers.getElementsByTagName('h2')[0].innerHTML = noPeersMessage;
|
||||||
|
|
||||||
|
const _callback = (e) => this._sendClipboardData(e, files, text);
|
||||||
|
Events.on('paste-pointerdown', _callback);
|
||||||
|
|
||||||
|
const _deactivateCallback = (e) => this._deactivatePasteMode(e, _callback)
|
||||||
|
const cancelPasteModeBtn = document.getElementById('cancelPasteModeBtn');
|
||||||
|
cancelPasteModeBtn.addEventListener('click', this._cancelPasteMode)
|
||||||
|
cancelPasteModeBtn.removeAttribute('hidden');
|
||||||
|
|
||||||
|
Events.on('notify-user', _deactivateCallback);
|
||||||
|
|
||||||
|
window.pasteMode.descriptor = descriptor;
|
||||||
|
window.pasteMode.activated = true;
|
||||||
|
console.log('Paste mode activated.')
|
||||||
|
|
||||||
|
this._onPeers(this._getPeers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancelPasteMode() {
|
||||||
|
Events.fire('notify-user', 'Paste Mode canceled');
|
||||||
|
}
|
||||||
|
|
||||||
|
_deactivatePasteMode(e, _callback) {
|
||||||
|
if (window.pasteMode.activated && ['File transfer completed.', 'Message transfer completed.', 'Paste Mode canceled'].includes(e.detail)) {
|
||||||
|
window.pasteMode.descriptor = undefined;
|
||||||
|
window.pasteMode.activated = false;
|
||||||
|
console.log('Paste mode deactivated.')
|
||||||
|
|
||||||
|
Events.off('paste-pointerdown', _callback);
|
||||||
|
|
||||||
|
const xInstructions = document.querySelectorAll('x-instructions')[0];
|
||||||
|
xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message');
|
||||||
|
xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message');
|
||||||
|
|
||||||
|
const xNoPeers = document.querySelectorAll('x-no-peers')[0];
|
||||||
|
xNoPeers.getElementsByTagName('h2')[0].innerHTML = 'Open Snapdrop on other devices to send files';
|
||||||
|
|
||||||
|
const cancelPasteModeBtn = document.getElementById('cancelPasteModeBtn');
|
||||||
|
cancelPasteModeBtn.removeEventListener('click', this._cancelPasteMode);
|
||||||
|
cancelPasteModeBtn.setAttribute('hidden', "");
|
||||||
|
|
||||||
|
this._onPeers(this._getPeers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendClipboardData(e, files, text) {
|
||||||
|
// send the pasted file/text content
|
||||||
|
const peerId = e.detail.peerId;
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
Events.fire('files-selected', {
|
Events.fire('files-selected', {
|
||||||
files: files,
|
files: files,
|
||||||
to: $$('x-peer').id
|
to: peerId
|
||||||
|
});
|
||||||
|
} else if (text.length > 0) {
|
||||||
|
Events.fire('send-text', {
|
||||||
|
text: text,
|
||||||
|
to: peerId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,9 +179,20 @@ class PeersUI {
|
||||||
class PeerUI {
|
class PeerUI {
|
||||||
|
|
||||||
html() {
|
html() {
|
||||||
|
let title;
|
||||||
|
let textInput;
|
||||||
|
|
||||||
|
if (window.pasteMode.activated) {
|
||||||
|
title = `Click to send ${window.pasteMode.descriptor} directly`;
|
||||||
|
textInput = '';
|
||||||
|
} else {
|
||||||
|
title = 'Click to send files or right click to send a message';
|
||||||
|
textInput = '<input type="file" multiple>';
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<label class="column center" title="Click to send files or right click to send a text">
|
<label class="column center" title="${title}">
|
||||||
<input type="file" multiple>
|
${textInput}
|
||||||
<x-icon shadow="1">
|
<x-icon shadow="1">
|
||||||
<svg class="icon"><use xlink:href="#"/></svg>
|
<svg class="icon"><use xlink:href="#"/></svg>
|
||||||
</x-icon>
|
</x-icon>
|
||||||
|
@ -86,7 +203,7 @@ class PeerUI {
|
||||||
<div class="name font-subheading"></div>
|
<div class="name font-subheading"></div>
|
||||||
<div class="device-name font-body2"></div>
|
<div class="device-name font-body2"></div>
|
||||||
<div class="status font-body2"></div>
|
<div class="status font-body2"></div>
|
||||||
</label>`
|
</label>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(peer) {
|
constructor(peer) {
|
||||||
|
@ -98,6 +215,8 @@ class PeerUI {
|
||||||
_initDom() {
|
_initDom() {
|
||||||
const el = document.createElement('x-peer');
|
const el = document.createElement('x-peer');
|
||||||
el.id = this._peer.id;
|
el.id = this._peer.id;
|
||||||
|
el.name = this._peer.name;
|
||||||
|
el.rtcSupported = this._peer.rtcSupported;
|
||||||
el.innerHTML = this.html();
|
el.innerHTML = this.html();
|
||||||
el.ui = this;
|
el.ui = this;
|
||||||
el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||||
|
@ -108,17 +227,30 @@ class PeerUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
_bindListeners(el) {
|
_bindListeners(el) {
|
||||||
el.querySelector('input').addEventListener('change', e => this._onFilesSelected(e));
|
if(!window.pasteMode.activated) {
|
||||||
el.addEventListener('drop', e => this._onDrop(e));
|
el.querySelector('input').addEventListener('change', e => this._onFilesSelected(e));
|
||||||
el.addEventListener('dragend', e => this._onDragEnd(e));
|
el.addEventListener('drop', e => this._onDrop(e));
|
||||||
el.addEventListener('dragleave', e => this._onDragEnd(e));
|
el.addEventListener('dragend', e => this._onDragEnd(e));
|
||||||
el.addEventListener('dragover', e => this._onDragOver(e));
|
el.addEventListener('dragleave', e => this._onDragEnd(e));
|
||||||
el.addEventListener('contextmenu', e => this._onRightClick(e));
|
el.addEventListener('dragover', e => this._onDragOver(e));
|
||||||
el.addEventListener('touchstart', e => this._onTouchStart(e));
|
el.addEventListener('contextmenu', e => this._onRightClick(e));
|
||||||
el.addEventListener('touchend', e => this._onTouchEnd(e));
|
el.addEventListener('touchstart', e => this._onTouchStart(e));
|
||||||
// prevent browser's default file drop behavior
|
el.addEventListener('touchend', e => this._onTouchEnd(e));
|
||||||
Events.on('dragover', e => e.preventDefault());
|
// prevent browser's default file drop behavior
|
||||||
Events.on('drop', e => e.preventDefault());
|
Events.on('dragover', e => e.preventDefault());
|
||||||
|
Events.on('drop', e => e.preventDefault());
|
||||||
|
} else {
|
||||||
|
el.addEventListener('pointerdown', (e) => this._onPointerDown(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPointerDown(e) {
|
||||||
|
// Prevents triggering of event twice on touch devices
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
Events.fire('paste-pointerdown', {
|
||||||
|
peerId: this._peer.id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_displayName() {
|
_displayName() {
|
||||||
|
@ -382,10 +514,10 @@ class ReceiveTextDialog extends Dialog {
|
||||||
class Toast extends Dialog {
|
class Toast extends Dialog {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('toast');
|
super('toast');
|
||||||
Events.on('notify-user', e => this._onNotfiy(e.detail));
|
Events.on('notify-user', e => this._onNotifiy(e.detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNotfiy(message) {
|
_onNotifiy(message) {
|
||||||
this.$el.textContent = message;
|
this.$el.textContent = message;
|
||||||
this.show();
|
this.show();
|
||||||
setTimeout(_ => this.hide(), 3000);
|
setTimeout(_ => this.hide(), 3000);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue