implement file receive request with image-thumbnail-preview, share-menu on click additional to download and multiple file support by zipping file(s) to prepare for sending; add status "waiting.." and "preparing..." to UX; lock pointer-input when peer-node busy; tidy-up paste-mode deactivation
This commit is contained in:
parent
6707021e04
commit
5525caa766
8 changed files with 581 additions and 204 deletions
|
@ -131,17 +131,28 @@
|
||||||
<x-dialog id="receiveDialog">
|
<x-dialog id="receiveDialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h3>File Received</h3>
|
<h2 class="center">Pairdrop</h2>
|
||||||
<div class="font-subheading" id="fileName">Filename</div>
|
<div class="text-center file-description"></div>
|
||||||
<div class="font-body2" id="fileSize"></div>
|
<div class="font-body2 text-center file-size"></div>
|
||||||
<div class='preview' style="visibility: hidden;"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="row">
|
<div class="row-reverse space-between">
|
||||||
<label for="autoDownload" class="grow">Ask to save each file before downloading</label>
|
<a class="button" id="shareOrDownload" autofocus></a>
|
||||||
<input type="checkbox" id="autoDownload" checked="">
|
<button class="button" close>Close</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-reverse">
|
</x-paper>
|
||||||
<a class="button" close id="download" title="Download File" autofocus>Save</a>
|
</x-background>
|
||||||
<button class="button" close>Ignore</button>
|
</x-dialog>
|
||||||
|
<!-- Receive Dialog -->
|
||||||
|
<x-dialog id="receiveRequestDialog">
|
||||||
|
<x-background class="full center">
|
||||||
|
<x-paper shadow="2">
|
||||||
|
<h2 class="center">Pairdrop</h2>
|
||||||
|
<div class="text-center file-description"></div>
|
||||||
|
<div class="font-body2 text-center file-size"></div>
|
||||||
|
<div class="center file-preview"></div>
|
||||||
|
<div class="row-reverse space-between">
|
||||||
|
<button class="button" id="acceptRequest" title="Accept Request" close autofocus>Accept</button>
|
||||||
|
<button class="button" id="declineRequest" title="Decline Request" close>Decline</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -272,11 +283,12 @@
|
||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<script src="scripts/zip.min.js" async></script>
|
||||||
|
<script src="scripts/util.js"></script>
|
||||||
<script src="scripts/network.js"></script>
|
<script src="scripts/network.js"></script>
|
||||||
<script src="scripts/qrcode.js" async></script>
|
<script src="scripts/qrcode.js" async></script>
|
||||||
<script src="scripts/ui.js"></script>
|
<script src="scripts/ui.js"></script>
|
||||||
<script src="scripts/theme.js" async></script>
|
<script src="scripts/theme.js" async></script>
|
||||||
<script src="scripts/clipboard.js" async></script>
|
|
||||||
<!-- Sounds -->
|
<!-- Sounds -->
|
||||||
<audio id="blop" autobuffer="true">
|
<audio id="blop" autobuffer="true">
|
||||||
<source src="/sounds/blop.mp3" type="audio/mpeg">
|
<source src="/sounds/blop.mp3" type="audio/mpeg">
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
// Polyfill for Navigator.clipboard.writeText
|
|
||||||
if (!navigator.clipboard) {
|
|
||||||
navigator.clipboard = {
|
|
||||||
writeText: text => {
|
|
||||||
|
|
||||||
// A <span> contains the text to copy
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.textContent = text;
|
|
||||||
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
|
|
||||||
|
|
||||||
// Paint the span outside the viewport
|
|
||||||
span.style.position = 'absolute';
|
|
||||||
span.style.left = '-9999px';
|
|
||||||
span.style.top = '-9999px';
|
|
||||||
|
|
||||||
const win = window;
|
|
||||||
const selection = win.getSelection();
|
|
||||||
win.document.body.appendChild(span);
|
|
||||||
|
|
||||||
const range = win.document.createRange();
|
|
||||||
selection.removeAllRanges();
|
|
||||||
range.selectNode(span);
|
|
||||||
selection.addRange(range);
|
|
||||||
|
|
||||||
let success = false;
|
|
||||||
try {
|
|
||||||
success = win.document.execCommand('copy');
|
|
||||||
} catch (err) {
|
|
||||||
return Promise.error();
|
|
||||||
}
|
|
||||||
|
|
||||||
selection.removeAllRanges();
|
|
||||||
span.remove();
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,8 @@ class ServerConnection {
|
||||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||||
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
||||||
|
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
||||||
|
Events.on('online', _ => this._connect());
|
||||||
}
|
}
|
||||||
|
|
||||||
_connect() {
|
_connect() {
|
||||||
|
@ -50,7 +52,7 @@ class ServerConnection {
|
||||||
|
|
||||||
_onPairDeviceJoin(roomKey) {
|
_onPairDeviceJoin(roomKey) {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 200);
|
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||||
|
@ -143,9 +145,9 @@ class ServerConnection {
|
||||||
|
|
||||||
_onDisconnect() {
|
_onDisconnect() {
|
||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...');
|
Events.fire('notify-user', 'Connection lost. Retrying...');
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,10 +189,114 @@ class Peer {
|
||||||
this._send(JSON.stringify(message));
|
this._send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFiles(files) {
|
async createHeader(file) {
|
||||||
for (let i = 0; i < files.length; i++) {
|
let hashHex = await this.getHashHex(file);
|
||||||
this._filesQueue.push(files[i]);
|
return {
|
||||||
|
name: file.name,
|
||||||
|
mime: file.type,
|
||||||
|
size: file.size,
|
||||||
|
hashHex: hashHex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHashHex(file) {
|
||||||
|
if (!crypto.subtle) {
|
||||||
|
console.error("PairDrop only works in secure contexts.")
|
||||||
}
|
}
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', await file.arrayBuffer());
|
||||||
|
// Convert hex to hash, see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
|
||||||
|
return(hashHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let image = new Image();
|
||||||
|
image.src = URL.createObjectURL(file);
|
||||||
|
image.onload = _ => {
|
||||||
|
let imageWidth = image.width;
|
||||||
|
let imageHeight = image.height;
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
// resize the canvas and draw the image data into it
|
||||||
|
if (width && height) {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
} else if (width) {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = Math.floor(imageHeight * width / imageWidth)
|
||||||
|
} else if (height) {
|
||||||
|
canvas.width = Math.floor(imageWidth * height / imageHeight);
|
||||||
|
canvas.height = height;
|
||||||
|
} else {
|
||||||
|
canvas.width = imageWidth;
|
||||||
|
canvas.height = imageHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx = canvas.getContext("2d");
|
||||||
|
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
let dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||||
|
resolve(dataUrl);
|
||||||
|
}
|
||||||
|
}).then(dataUrl => {
|
||||||
|
return dataUrl;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestFileTransfer(files) {
|
||||||
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'prepare'})
|
||||||
|
|
||||||
|
let header = [];
|
||||||
|
let allFilesAreImages = true;
|
||||||
|
let combinedSize = 0;
|
||||||
|
for (let i=0; i<files.length; i++) {
|
||||||
|
header.push(await this.createHeader(files[i]));
|
||||||
|
if (files[i].type.split('/')[0] !== 'image') {
|
||||||
|
allFilesAreImages = false;
|
||||||
|
}
|
||||||
|
combinedSize += files[i].size;
|
||||||
|
}
|
||||||
|
this._fileHeaderRequested = header;
|
||||||
|
let bytesCompleted = 0;
|
||||||
|
|
||||||
|
for (let i=0; i<files.length; i++) {
|
||||||
|
const entry = await zipper.addFile(files[i], {
|
||||||
|
onprogress: (progress, total) => {
|
||||||
|
Events.fire('set-progress', {
|
||||||
|
peerId: this._peerId,
|
||||||
|
progress: (bytesCompleted + progress) / combinedSize,
|
||||||
|
status: 'prepare'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bytesCompleted += files[i].size;
|
||||||
|
}
|
||||||
|
this.zipFileRequested = await zipper.getZipFile();
|
||||||
|
|
||||||
|
if (allFilesAreImages) {
|
||||||
|
this.getResizedImageDataUrl(files[0], 400, null, 0.9).then(dataUrl => {
|
||||||
|
this.sendJSON({type: 'request',
|
||||||
|
header: header,
|
||||||
|
size: combinedSize,
|
||||||
|
thumbnailDataUrl: dataUrl
|
||||||
|
});
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.sendJSON({type: 'request',
|
||||||
|
header: header,
|
||||||
|
size: combinedSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'})
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendFiles() {
|
||||||
|
console.debug("sendFiles")
|
||||||
|
console.debug(this.zipFileRequested);
|
||||||
|
this._filesQueue.push({zipFile: this.zipFileRequested, fileHeader: this._fileHeaderRequested});
|
||||||
|
this._fileHeaderRequested = null
|
||||||
if (this._busy) return;
|
if (this._busy) return;
|
||||||
this._dequeueFile();
|
this._dequeueFile();
|
||||||
}
|
}
|
||||||
|
@ -202,14 +308,13 @@ class Peer {
|
||||||
this._sendFile(file);
|
this._sendFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendFile(file) {
|
async _sendFile(file) {
|
||||||
this.sendJSON({
|
this.sendJSON({
|
||||||
type: 'header',
|
type: 'header',
|
||||||
name: file.name,
|
size: file.zipFile.size,
|
||||||
mime: file.type,
|
fileHeader: file.fileHeader
|
||||||
size: file.size
|
|
||||||
});
|
});
|
||||||
this._chunker = new FileChunker(file,
|
this._chunker = new FileChunker(file.zipFile,
|
||||||
chunk => this._send(chunk),
|
chunk => this._send(chunk),
|
||||||
offset => this._onPartitionEnd(offset));
|
offset => this._onPartitionEnd(offset));
|
||||||
this._chunker.nextPartition();
|
this._chunker.nextPartition();
|
||||||
|
@ -240,8 +345,11 @@ class Peer {
|
||||||
message = JSON.parse(message);
|
message = JSON.parse(message);
|
||||||
console.log('RTC:', message);
|
console.log('RTC:', message);
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
|
case 'request':
|
||||||
|
this._onFilesTransferRequest(message);
|
||||||
|
break;
|
||||||
case 'header':
|
case 'header':
|
||||||
this._onFileHeader(message);
|
this._onFilesHeader(message);
|
||||||
break;
|
break;
|
||||||
case 'partition':
|
case 'partition':
|
||||||
this._onReceivedPartitionEnd(message);
|
this._onReceivedPartitionEnd(message);
|
||||||
|
@ -252,6 +360,9 @@ class Peer {
|
||||||
case 'progress':
|
case 'progress':
|
||||||
this._onDownloadProgress(message.progress);
|
this._onDownloadProgress(message.progress);
|
||||||
break;
|
break;
|
||||||
|
case 'files-transfer-response':
|
||||||
|
this._onFileTransferResponded(message);
|
||||||
|
break;
|
||||||
case 'file-transfer-complete':
|
case 'file-transfer-complete':
|
||||||
this._onFileTransferCompleted();
|
this._onFileTransferCompleted();
|
||||||
break;
|
break;
|
||||||
|
@ -264,17 +375,37 @@ class Peer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFileHeader(header) {
|
_onFilesTransferRequest(request) {
|
||||||
this._lastProgress = 0;
|
if (this._requestPending) {
|
||||||
this._digester = new FileDigester({
|
// Only accept one request at a time
|
||||||
name: header.name,
|
this.sendJSON({type: 'files-transfer-response', accepted: false});
|
||||||
mime: header.mime,
|
return;
|
||||||
size: header.size
|
}
|
||||||
}, file => this._onFileReceived(file));
|
this._requestPending = true;
|
||||||
|
Events.fire('files-transfer-request', {
|
||||||
|
request: request,
|
||||||
|
peerId: this._peerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_respondToFileTransferRequest(header, accepted) {
|
||||||
|
this._requestPending = false;
|
||||||
|
this._acceptedHeader = header;
|
||||||
|
this.sendJSON({type: 'files-transfer-response', accepted: accepted});
|
||||||
|
if (accepted) this._busy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_onFilesHeader(msg) {
|
||||||
|
if (JSON.stringify(this._acceptedHeader) === JSON.stringify(msg.fileHeader)) {
|
||||||
|
this._lastProgress = 0;
|
||||||
|
this._digester = new FileDigester(msg.size, blob => this._onFileReceived(blob, msg.fileHeader));
|
||||||
|
this._acceptedHeader = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChunkReceived(chunk) {
|
_onChunkReceived(chunk) {
|
||||||
if(!(chunk.byteLength || chunk.size)) return;
|
if(!this._digester || !(chunk.byteLength || chunk.size)) return;
|
||||||
|
|
||||||
this._digester.unchunk(chunk);
|
this._digester.unchunk(chunk);
|
||||||
const progress = this._digester.progress;
|
const progress = this._digester.progress;
|
||||||
|
@ -287,24 +418,58 @@ class Peer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDownloadProgress(progress) {
|
_onDownloadProgress(progress) {
|
||||||
Events.fire('file-progress', { sender: this._peerId, progress: progress });
|
if (this._busy) {
|
||||||
|
Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFileReceived(proxyFile) {
|
async _onFileReceived(zipBlob, fileHeader) {
|
||||||
Events.fire('file-received', proxyFile);
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'});
|
||||||
this.sendJSON({ type: 'file-transfer-complete' });
|
|
||||||
|
this._busy = false;
|
||||||
|
this.sendJSON({type: 'file-transfer-complete'});
|
||||||
|
let zipEntries = await zipper.getEntries(zipBlob);
|
||||||
|
let files = [];
|
||||||
|
let hashHexs = [];
|
||||||
|
for (let i=0; i<zipEntries.length; i++) {
|
||||||
|
let fileBlob = await zipper.getData(zipEntries[i]);
|
||||||
|
let hashHex = await this.getHashHex(fileBlob)
|
||||||
|
if (hashHex !== fileHeader[i].hashHex) {
|
||||||
|
Events.fire('notify-user', 'Files are malformed.');
|
||||||
|
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||||
|
throw new Error("Hash of received file differs from hash of requested file. Abort!");
|
||||||
|
}
|
||||||
|
files.push(new File([fileBlob], zipEntries[i].filename, {
|
||||||
|
type: fileHeader[i].mime,
|
||||||
|
lastModified: new Date().getTime()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Events.fire('files-received', {sender: this._peerId, files: files});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFileTransferCompleted() {
|
_onFileTransferCompleted() {
|
||||||
this._onDownloadProgress(1);
|
this._onDownloadProgress(1);
|
||||||
this._reader = null;
|
this._digester = null;
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
this._dequeueFile();
|
this._dequeueFile();
|
||||||
Events.fire('notify-user', 'File transfer completed.');
|
Events.fire('notify-user', 'File transfer completed.');
|
||||||
|
Events.fire('deactivate-paste-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFileTransferResponded(message) {
|
||||||
|
if (!message.accepted) {
|
||||||
|
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||||
|
|
||||||
|
this.zipFile = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Events.fire('file-transfer-accepted');
|
||||||
|
this.sendFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessageTransferCompleted() {
|
_onMessageTransferCompleted() {
|
||||||
Events.fire('notify-user', 'Message transfer completed.');
|
Events.fire('notify-user', 'Message transfer completed.');
|
||||||
|
Events.fire('deactivate-paste-mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
sendText(text) {
|
sendText(text) {
|
||||||
|
@ -477,6 +642,7 @@ class PeersManager {
|
||||||
Events.on('signal', e => this._onMessage(e.detail));
|
Events.on('signal', e => this._onMessage(e.detail));
|
||||||
Events.on('peers', e => this._onPeers(e.detail));
|
Events.on('peers', e => this._onPeers(e.detail));
|
||||||
Events.on('files-selected', e => this._onFilesSelected(e.detail));
|
Events.on('files-selected', e => this._onFilesSelected(e.detail));
|
||||||
|
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||||
Events.on('send-text', e => this._onSendText(e.detail));
|
Events.on('send-text', e => this._onSendText(e.detail));
|
||||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||||
|
@ -522,8 +688,12 @@ class PeersManager {
|
||||||
this.peers[peerId].send(message);
|
this.peers[peerId].send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onRespondToFileTransferRequest(detail) {
|
||||||
|
this.peers[detail.to]._respondToFileTransferRequest(detail.header, detail.accepted);
|
||||||
|
}
|
||||||
|
|
||||||
_onFilesSelected(message) {
|
_onFilesSelected(message) {
|
||||||
this.peers[message.to].sendFiles(message.files);
|
this.peers[message.to].requestFileTransfer(message.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSendText(message) {
|
_onSendText(message) {
|
||||||
|
@ -614,12 +784,10 @@ class FileChunker {
|
||||||
|
|
||||||
class FileDigester {
|
class FileDigester {
|
||||||
|
|
||||||
constructor(meta, callback) {
|
constructor(size, callback) {
|
||||||
this._buffer = [];
|
this._buffer = [];
|
||||||
this._bytesReceived = 0;
|
this._bytesReceived = 0;
|
||||||
this._size = meta.size;
|
this._size = size;
|
||||||
this._mime = meta.mime || 'application/octet-stream';
|
|
||||||
this._name = meta.name;
|
|
||||||
this._callback = callback;
|
this._callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -631,13 +799,7 @@ class FileDigester {
|
||||||
|
|
||||||
if (this._bytesReceived < this._size) return;
|
if (this._bytesReceived < this._size) return;
|
||||||
// we are done
|
// we are done
|
||||||
let blob = new Blob(this._buffer, { type: this._mime });
|
this._callback(new Blob(this._buffer));
|
||||||
this._callback({
|
|
||||||
name: this._name,
|
|
||||||
mime: this._mime,
|
|
||||||
size: this._size,
|
|
||||||
blob: blob
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const $ = query => document.getElementById(query);
|
const $ = query => document.getElementById(query);
|
||||||
const $$ = query => document.body.querySelector(query);
|
const $$ = query => document.body.querySelector(query);
|
||||||
const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase());
|
const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase());
|
||||||
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.android = /android/i.test(navigator.userAgent);
|
||||||
window.pasteMode = {};
|
window.pasteMode = {};
|
||||||
window.pasteMode.activated = false;
|
window.pasteMode.activated = false;
|
||||||
|
|
||||||
|
@ -23,11 +23,22 @@ class PeersUI {
|
||||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail));
|
Events.on('peer-connected', e => this._onPeerConnected(e.detail));
|
||||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
Events.on('peers', e => this._onPeers(e.detail));
|
Events.on('peers', e => this._onPeers(e.detail));
|
||||||
Events.on('file-progress', e => this._onFileProgress(e.detail));
|
Events.on('set-progress', e => this._onSetProgress(e.detail));
|
||||||
Events.on('paste', e => this._onPaste(e));
|
Events.on('paste', e => this._onPaste(e));
|
||||||
Events.on('ws-disconnected', _ => this._clearPeers());
|
Events.on('ws-disconnected', _ => this._clearPeers());
|
||||||
Events.on('secret-room-deleted', _ => this._clearPeers('secret'));
|
Events.on('secret-room-deleted', _ => this._clearPeers('secret'));
|
||||||
this.peers = {};
|
this.peers = {};
|
||||||
|
|
||||||
|
this.$cancelPasteModeBtn = document.getElementById('cancelPasteModeBtn');
|
||||||
|
this.$cancelPasteModeBtn.addEventListener('click', this._cancelPasteMode);
|
||||||
|
|
||||||
|
Events.on('keydown', e => this._onKeyDown(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyDown(e) {
|
||||||
|
if (window.pasteMode.activated && e.code === "Escape") {
|
||||||
|
Events.fire('deactivate-paste-mode');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPeerJoined(msg) {
|
_onPeerJoined(msg) {
|
||||||
|
@ -85,18 +96,16 @@ class PeersUI {
|
||||||
_onSecretRoomDeleted(roomSecret) {
|
_onSecretRoomDeleted(roomSecret) {
|
||||||
for (const peerId in this.peers) {
|
for (const peerId in this.peers) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
console.debug(peer);
|
|
||||||
if (peer.roomSecret === roomSecret) {
|
if (peer.roomSecret === roomSecret) {
|
||||||
this._onPeerLeft(peerId);
|
this._onPeerLeft(peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFileProgress(progress) {
|
_onSetProgress(progress) {
|
||||||
const peerId = progress.sender || progress.recipient;
|
const $peer = $(progress.peerId);
|
||||||
const $peer = $(peerId);
|
|
||||||
if (!$peer) return;
|
if (!$peer) return;
|
||||||
$peer.ui.setProgress(progress.progress);
|
$peer.ui.setProgress(progress.progress, progress.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearPeers(roomType = 'all') {
|
_clearPeers(roomType = 'all') {
|
||||||
|
@ -161,13 +170,9 @@ class PeersUI {
|
||||||
|
|
||||||
const _callback = (e) => this._sendClipboardData(e, files, text);
|
const _callback = (e) => this._sendClipboardData(e, files, text);
|
||||||
Events.on('paste-pointerdown', _callback);
|
Events.on('paste-pointerdown', _callback);
|
||||||
|
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback));
|
||||||
|
|
||||||
const _deactivateCallback = (e) => this._deactivatePasteMode(e, _callback)
|
this.$cancelPasteModeBtn.removeAttribute('hidden');
|
||||||
const cancelPasteModeBtn = document.getElementById('cancelPasteModeBtn');
|
|
||||||
cancelPasteModeBtn.addEventListener('click', this._cancelPasteMode)
|
|
||||||
cancelPasteModeBtn.removeAttribute('hidden');
|
|
||||||
|
|
||||||
Events.on('notify-user', _deactivateCallback);
|
|
||||||
|
|
||||||
window.pasteMode.descriptor = descriptor;
|
window.pasteMode.descriptor = descriptor;
|
||||||
window.pasteMode.activated = true;
|
window.pasteMode.activated = true;
|
||||||
|
@ -179,10 +184,11 @@ class PeersUI {
|
||||||
|
|
||||||
_cancelPasteMode() {
|
_cancelPasteMode() {
|
||||||
Events.fire('notify-user', 'Paste Mode canceled');
|
Events.fire('notify-user', 'Paste Mode canceled');
|
||||||
|
Events.fire('deactivate-paste-mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
_deactivatePasteMode(e, _callback) {
|
_deactivatePasteMode(_callback) {
|
||||||
if (window.pasteMode.activated && ['File transfer completed.', 'Message transfer completed.', 'Paste Mode canceled'].includes(e.detail)) {
|
if (window.pasteMode.activated) {
|
||||||
window.pasteMode.descriptor = undefined;
|
window.pasteMode.descriptor = undefined;
|
||||||
window.pasteMode.activated = false;
|
window.pasteMode.activated = false;
|
||||||
console.log('Paste mode deactivated.')
|
console.log('Paste mode deactivated.')
|
||||||
|
@ -328,24 +334,23 @@ class PeerUI {
|
||||||
files: files,
|
files: files,
|
||||||
to: this._peer.id
|
to: this._peer.id
|
||||||
});
|
});
|
||||||
$input.value = null; // reset input
|
$input.files = null; // reset input
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(progress) {
|
setProgress(progress, status) {
|
||||||
if (progress > 0) {
|
if (0.5 < progress && progress < 1) {
|
||||||
this.$el.setAttribute('transfer', '1');
|
|
||||||
}
|
|
||||||
if (progress > 0.5) {
|
|
||||||
this.$progress.classList.add('over50');
|
this.$progress.classList.add('over50');
|
||||||
} else {
|
} else {
|
||||||
this.$progress.classList.remove('over50');
|
this.$progress.classList.remove('over50');
|
||||||
}
|
}
|
||||||
|
if (progress < 1) {
|
||||||
|
this.$el.setAttribute('status', status);
|
||||||
|
} else {
|
||||||
|
this.$el.removeAttribute('status');
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
const degrees = `rotate(${360 * progress}deg)`;
|
const degrees = `rotate(${360 * progress}deg)`;
|
||||||
this.$progress.style.setProperty('--progress', degrees);
|
this.$progress.style.setProperty('--progress', degrees);
|
||||||
if (progress >= 1) {
|
|
||||||
this.setProgress(0);
|
|
||||||
this.$el.removeAttribute('transfer');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDrop(e) {
|
_onDrop(e) {
|
||||||
|
@ -410,76 +415,12 @@ class Dialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReceiveDialog extends Dialog {
|
class ReceiveDialog extends Dialog {
|
||||||
|
constructor(id, hideOnDisconnect = true) {
|
||||||
|
super(id, hideOnDisconnect);
|
||||||
|
|
||||||
constructor() {
|
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
|
||||||
super('receiveDialog', false);
|
this.$fileSizeNode = this.$el.querySelector('.file-size');
|
||||||
Events.on('file-received', e => {
|
this.$previewBox = this.$el.querySelector('.file-preview')
|
||||||
this._nextFile(e.detail);
|
|
||||||
window.blop.play();
|
|
||||||
});
|
|
||||||
this._filesQueue = [];
|
|
||||||
this.$previewBox = this.$el.querySelector('.preview')
|
|
||||||
}
|
|
||||||
|
|
||||||
_nextFile(nextFile) {
|
|
||||||
if (nextFile) this._filesQueue.push(nextFile);
|
|
||||||
if (this._busy) return;
|
|
||||||
this._busy = true;
|
|
||||||
const file = this._filesQueue.shift();
|
|
||||||
this._displayFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
_dequeueFile() {
|
|
||||||
if (!this._filesQueue.length) { // nothing to do
|
|
||||||
this._busy = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// dequeue next file
|
|
||||||
setTimeout(_ => {
|
|
||||||
this._busy = false;
|
|
||||||
this._nextFile();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
_displayFile(file) {
|
|
||||||
const $a = this.$el.querySelector('#download');
|
|
||||||
const url = URL.createObjectURL(file.blob);
|
|
||||||
$a.href = url;
|
|
||||||
$a.download = file.name;
|
|
||||||
|
|
||||||
if(this._autoDownload()){
|
|
||||||
$a.click()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let mime = file.mime.split('/')[0]
|
|
||||||
let previewElement = {
|
|
||||||
image: 'img',
|
|
||||||
audio: 'audio',
|
|
||||||
video: 'video'
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Object.keys(previewElement).indexOf(mime) !== -1){
|
|
||||||
console.log('the file is able to preview');
|
|
||||||
let element = document.createElement(previewElement[mime]);
|
|
||||||
element.src = url;
|
|
||||||
element.controls = true;
|
|
||||||
element.classList = 'element-preview'
|
|
||||||
|
|
||||||
this.$previewBox.style.visibility = 'inherit';
|
|
||||||
this.$previewBox.appendChild(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$el.querySelector('#fileName').textContent = file.name;
|
|
||||||
this.$el.querySelector('#fileSize').textContent = this._formatFileSize(file.size);
|
|
||||||
this.show();
|
|
||||||
|
|
||||||
if (window.isDownloadSupported) return;
|
|
||||||
// fallback for iOS
|
|
||||||
$a.target = '_blank';
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = _ => $a.href = reader.result;
|
|
||||||
reader.readAsDataURL(file.blob);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_formatFileSize(bytes) {
|
_formatFileSize(bytes) {
|
||||||
|
@ -493,17 +434,221 @@ class ReceiveDialog extends Dialog {
|
||||||
return bytes + ' Bytes';
|
return bytes + ' Bytes';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
class ReceiveFileDialog extends ReceiveDialog {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('receiveDialog', false);
|
||||||
|
|
||||||
|
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload');
|
||||||
|
|
||||||
|
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files));
|
||||||
|
this._filesQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFilesReceived(sender, files) {
|
||||||
|
this._nextFiles(sender, files);
|
||||||
|
window.blop.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextFiles(sender, nextFiles) {
|
||||||
|
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles});
|
||||||
|
if (this._busy) return;
|
||||||
|
this._busy = true;
|
||||||
|
const {peerId, files} = this._filesQueue.shift();
|
||||||
|
this._displayFiles(peerId, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
_dequeueFile() {
|
||||||
|
if (!this._filesQueue.length) { // nothing to do
|
||||||
|
this._busy = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// dequeue next file
|
||||||
|
setTimeout(_ => {
|
||||||
|
this._busy = false;
|
||||||
|
this._nextFiles();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
createPreviewElement(file) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let mime = file.type.split('/')[0]
|
||||||
|
let previewElement = {
|
||||||
|
image: 'img',
|
||||||
|
audio: 'audio',
|
||||||
|
video: 'video'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(previewElement).indexOf(mime) === -1) {
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
console.log('the file is able to preview');
|
||||||
|
let element = document.createElement(previewElement[mime]);
|
||||||
|
element.src = URL.createObjectURL(file);
|
||||||
|
element.controls = true;
|
||||||
|
element.classList = 'element-preview'
|
||||||
|
|
||||||
|
this.$previewBox.style.display = 'block';
|
||||||
|
this.$previewBox.appendChild(element)
|
||||||
|
element.onload = _ => resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _displayFiles(peerId, files) {
|
||||||
|
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
|
||||||
|
|
||||||
|
let url;
|
||||||
|
let description;
|
||||||
|
let size;
|
||||||
|
let filename;
|
||||||
|
let shareTitle
|
||||||
|
|
||||||
|
if (files.length === 1) {
|
||||||
|
shareTitle = "PairDrop File"
|
||||||
|
description = files[0].name;
|
||||||
|
size = this._formatFileSize(files[0].size);
|
||||||
|
filename = files[0].name;
|
||||||
|
url = URL.createObjectURL(files[0])
|
||||||
|
} else {
|
||||||
|
shareTitle = "PairDrop Files";
|
||||||
|
let completeSize = 0
|
||||||
|
for (let i=0; i<files.length; i++) {
|
||||||
|
completeSize += files[0].size;
|
||||||
|
}
|
||||||
|
description = `${files[0].name} and ${files.length-1} more ${files.length>2 ? "files" : "file"}`;
|
||||||
|
size = this._formatFileSize(completeSize);
|
||||||
|
|
||||||
|
for (let i=0; i<files.length; i++) {
|
||||||
|
await zipper.addFile(files[i]);
|
||||||
|
}
|
||||||
|
url = await zipper.getBlobURL();
|
||||||
|
|
||||||
|
let now = new Date(Date.now());
|
||||||
|
let year = now.getFullYear().toString();
|
||||||
|
let month = (now.getMonth()+1).toString();
|
||||||
|
month = month.length < 2 ? "0" + month : month;
|
||||||
|
let date = now.getDate().toString();
|
||||||
|
date = date.length < 2 ? "0" + date : date;
|
||||||
|
let hours = now.getHours().toString();
|
||||||
|
hours = hours.length < 2 ? "0" + hours : hours;
|
||||||
|
let minutes = now.getMinutes().toString();
|
||||||
|
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
||||||
|
filename = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$fileDescriptionNode.textContent = description;
|
||||||
|
this.$fileSizeNode.textContent = size;
|
||||||
|
this.$shareOrDownloadBtn.download = filename;
|
||||||
|
|
||||||
|
if ((window.iOS || window.android) && !!navigator.share && navigator.canShare({files})) {
|
||||||
|
this.$shareOrDownloadBtn.innerText = "Share";
|
||||||
|
this.continueCallback = async _ => {
|
||||||
|
navigator.share({
|
||||||
|
title: shareTitle,
|
||||||
|
text: description,
|
||||||
|
files: files
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
|
||||||
|
} else {
|
||||||
|
this.$shareOrDownloadBtn.innerText = "Download";
|
||||||
|
this.$shareOrDownloadBtn.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createPreviewElement(files[0]).then(_ => {
|
||||||
|
this.show()
|
||||||
|
Events.fire('set-progress', {
|
||||||
|
peerId: peerId,
|
||||||
|
progress: 1,
|
||||||
|
status: 'wait'
|
||||||
|
})
|
||||||
|
this.$shareOrDownloadBtn.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.$previewBox.style.visibility = 'hidden';
|
this.$shareOrDownloadBtn.href = '';
|
||||||
|
this.$previewBox.style.display = 'none';
|
||||||
this.$previewBox.innerHTML = '';
|
this.$previewBox.innerHTML = '';
|
||||||
super.hide();
|
super.hide();
|
||||||
this._dequeueFile();
|
this._dequeueFile();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReceiveRequestDialog extends ReceiveDialog {
|
||||||
|
|
||||||
_autoDownload(){
|
constructor() {
|
||||||
return !this.$el.querySelector('#autoDownload').checked
|
super('receiveRequestDialog', true);
|
||||||
|
|
||||||
|
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest');
|
||||||
|
this.$declineRequestBtn = this.$el.querySelector('#declineRequest');
|
||||||
|
|
||||||
|
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
||||||
|
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
|
||||||
|
|
||||||
|
Events.on('files-transfer-request', e => this._onRequestFileTransfer(e.detail.request, e.detail.peerId))
|
||||||
|
Events.on('peer-left', e => this._onPeerDisconnectedOrLeft(e.detail))
|
||||||
|
Events.on('peer-disconnected', e => this._onPeerDisconnectedOrLeft(e.detail))
|
||||||
|
Events.on('keydown', e => this._onKeyDown(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyDown(e) {
|
||||||
|
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||||
|
this._respondToFileTransferRequest(false)
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPeerDisconnectedOrLeft(peerId) {
|
||||||
|
if (peerId === this.requestingPeerId) {
|
||||||
|
this._respondToFileTransferRequest(false)
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRequestFileTransfer(request, peerId) {
|
||||||
|
this.requestingPeerId = peerId;
|
||||||
|
this.requestedHeader = request.header;
|
||||||
|
|
||||||
|
const peer = $(peerId);
|
||||||
|
let peerDisplayName = peer.ui._displayName();
|
||||||
|
let fileDesc = request.header.length === 1
|
||||||
|
? "a file"
|
||||||
|
: `${request.header.length} files`
|
||||||
|
|
||||||
|
this.$fileDescriptionNode.innerText = `${peerDisplayName} would like to share ${fileDesc}`;
|
||||||
|
this.$fileSizeNode.innerText = this._formatFileSize(request.size);
|
||||||
|
|
||||||
|
if (request.thumbnailDataUrl) {
|
||||||
|
let element = document.createElement('img');
|
||||||
|
element.src = request.thumbnailDataUrl;
|
||||||
|
element.classList = 'element-preview'
|
||||||
|
|
||||||
|
this.$previewBox.style.display = 'block';
|
||||||
|
this.$previewBox.appendChild(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
_respondToFileTransferRequest(accepted) {
|
||||||
|
Events.fire('respond-to-files-transfer-request', {
|
||||||
|
to: this.requestingPeerId,
|
||||||
|
header: this.requestedHeader,
|
||||||
|
accepted: accepted
|
||||||
|
})
|
||||||
|
this.requestingPeerId = null;
|
||||||
|
if (accepted) {
|
||||||
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.$previewBox.style.display = 'none';
|
||||||
|
this.$previewBox.innerHTML = '';
|
||||||
|
super.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -554,12 +699,14 @@ class PairDeviceDialog extends Dialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown(e) {
|
_onKeyDown(e) {
|
||||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
if (this.$el.attributes["show"]) {
|
||||||
this.hide();
|
if (e.code === "Escape") {
|
||||||
this._pairDeviceCancel();
|
this.hide();
|
||||||
}
|
this._pairDeviceCancel();
|
||||||
if (this.$el.attributes["show"] && e.code === "keyO") {
|
}
|
||||||
this._onRoomSecretDelete()
|
if (e.code === "keyO") {
|
||||||
|
this._onRoomSecretDelete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -701,7 +848,6 @@ class PairDeviceDialog extends Dialog {
|
||||||
|
|
||||||
_onRoomSecretDelete(roomSecret) {
|
_onRoomSecretDelete(roomSecret) {
|
||||||
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
|
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
|
||||||
console.debug("then secret: " + roomSecret)
|
|
||||||
Events.fire('room-secret-deleted', roomSecret)
|
Events.fire('room-secret-deleted', roomSecret)
|
||||||
this._evaluateNumberRoomSecrets();
|
this._evaluateNumberRoomSecrets();
|
||||||
}).catch((e) => console.error(e));
|
}).catch((e) => console.error(e));
|
||||||
|
@ -867,7 +1013,7 @@ class Notifications {
|
||||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||||
}
|
}
|
||||||
Events.on('text-received', e => this._messageNotification(e.detail.text));
|
Events.on('text-received', e => this._messageNotification(e.detail.text));
|
||||||
Events.on('file-received', e => this._downloadNotification(e.detail.name));
|
Events.on('files-received', _ => this._downloadNotification());
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestPermission() {
|
_requestPermission() {
|
||||||
|
@ -919,7 +1065,7 @@ class Notifications {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadNotification(message) {
|
_downloadNotification() {
|
||||||
if (document.visibilityState !== 'visible') {
|
if (document.visibilityState !== 'visible') {
|
||||||
const notification = this._notify(message, 'Click to download');
|
const notification = this._notify(message, 'Click to download');
|
||||||
this._bind(notification, _ => this._download(notification));
|
this._bind(notification, _ => this._download(notification));
|
||||||
|
@ -927,7 +1073,7 @@ class Notifications {
|
||||||
}
|
}
|
||||||
|
|
||||||
_download(notification) {
|
_download(notification) {
|
||||||
document.querySelector('x-dialog [download]').click();
|
$('shareOrDownload').click();
|
||||||
notification.close();
|
notification.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1178,7 +1324,8 @@ class Pairdrop {
|
||||||
const server = new ServerConnection();
|
const server = new ServerConnection();
|
||||||
const peers = new PeersManager(server);
|
const peers = new PeersManager(server);
|
||||||
const peersUI = new PeersUI();
|
const peersUI = new PeersUI();
|
||||||
const receiveDialog = new ReceiveDialog();
|
const receiveFileDialog = new ReceiveFileDialog();
|
||||||
|
const receiveRequestDialog = new ReceiveRequestDialog();
|
||||||
const sendTextDialog = new SendTextDialog();
|
const sendTextDialog = new SendTextDialog();
|
||||||
const receiveTextDialog = new ReceiveTextDialog();
|
const receiveTextDialog = new ReceiveTextDialog();
|
||||||
const pairDeviceDialog = new PairDeviceDialog();
|
const pairDeviceDialog = new PairDeviceDialog();
|
||||||
|
|
79
public/scripts/util.js
Normal file
79
public/scripts/util.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// Polyfill for Navigator.clipboard.writeText
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
navigator.clipboard = {
|
||||||
|
writeText: text => {
|
||||||
|
|
||||||
|
// A <span> contains the text to copy
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = text;
|
||||||
|
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
|
||||||
|
|
||||||
|
// Paint the span outside the viewport
|
||||||
|
span.style.position = 'absolute';
|
||||||
|
span.style.left = '-9999px';
|
||||||
|
span.style.top = '-9999px';
|
||||||
|
|
||||||
|
const win = window;
|
||||||
|
const selection = win.getSelection();
|
||||||
|
win.document.body.appendChild(span);
|
||||||
|
|
||||||
|
const range = win.document.createRange();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
range.selectNode(span);
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
|
success = win.document.execCommand('copy');
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.error();
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.removeAllRanges();
|
||||||
|
span.remove();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipper = (() => {
|
||||||
|
|
||||||
|
let zipWriter;
|
||||||
|
return {
|
||||||
|
addFile(file, options) {
|
||||||
|
if (!zipWriter) {
|
||||||
|
zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, level: 0 });
|
||||||
|
}
|
||||||
|
return zipWriter.add(file.name, new zip.BlobReader(file), options);
|
||||||
|
},
|
||||||
|
async getBlobURL() {
|
||||||
|
if (zipWriter) {
|
||||||
|
const blobURL = URL.createObjectURL(await zipWriter.close());
|
||||||
|
zipWriter = null;
|
||||||
|
return blobURL;
|
||||||
|
} else {
|
||||||
|
throw new Error("Zip file closed");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
specifyOnProgress(onprogressCallback) {
|
||||||
|
zipWriter.onprogress = onprogressCallback;
|
||||||
|
},
|
||||||
|
async getZipFile(filename = "archive.zip") {
|
||||||
|
if (zipWriter) {
|
||||||
|
const file = new File([await zipWriter.close()], filename, {type: "application/zip"});
|
||||||
|
zipWriter = null;
|
||||||
|
return file;
|
||||||
|
} else {
|
||||||
|
throw new Error("Zip file closed");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getEntries(file, options) {
|
||||||
|
return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);
|
||||||
|
},
|
||||||
|
async getData(entry, options) {
|
||||||
|
return await entry.getData(new zip.BlobWriter(), options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
1
public/scripts/zip.min.js
vendored
Normal file
1
public/scripts/zip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,11 +1,13 @@
|
||||||
var CACHE_NAME = 'pairdrop-cache-v2';
|
var CACHE_NAME = 'pairdrop-cache-v3';
|
||||||
var urlsToCache = [
|
var urlsToCache = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'./',
|
'./',
|
||||||
'styles.css',
|
'styles.css',
|
||||||
'scripts/network.js',
|
'scripts/network.js',
|
||||||
'scripts/ui.js',
|
'scripts/ui.js',
|
||||||
'scripts/clipboard.js',
|
'scripts/util.js',
|
||||||
|
'scripts/qrcode.js',
|
||||||
|
'scripts/zip.min.js',
|
||||||
'scripts/theme.js',
|
'scripts/theme.js',
|
||||||
'sounds/blop.mp3',
|
'sounds/blop.mp3',
|
||||||
'images/favicon-96x96.png'
|
'images/favicon-96x96.png'
|
||||||
|
|
|
@ -105,7 +105,7 @@ h2 {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: -.012em;
|
letter-spacing: -.012em;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
color: var(--primary-color);}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
@ -269,12 +269,12 @@ x-peer:not(.type-ip) x-icon {
|
||||||
background: #00a69c;
|
background: #00a69c;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer:not([transfer]):hover x-icon,
|
x-peer:not([status]):hover x-icon,
|
||||||
x-peer:not([transfer]):focus x-icon {
|
x-peer:not([status]):focus x-icon {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer[transfer] x-icon {
|
x-peer[status] x-icon {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
|
@ -291,15 +291,27 @@ x-peer[transfer] x-icon {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer[transfer] .status:before {
|
x-peer[status=transfer] .status:before {
|
||||||
content: 'Transferring...';
|
content: 'Transferring...';
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer:not([transfer]) .status,
|
x-peer[status=prepare] .status:before {
|
||||||
x-peer[transfer] .device-name {
|
content: 'Preparing...';
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer[status=wait] .status:before {
|
||||||
|
content: 'Waiting...';
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer:not([status]) .status,
|
||||||
|
x-peer[status] .device-name {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x-peer[status] {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
x-peer x-icon {
|
x-peer x-icon {
|
||||||
animation: pop 600ms ease-out 1;
|
animation: pop 600ms ease-out 1;
|
||||||
}
|
}
|
||||||
|
@ -437,7 +449,7 @@ x-dialog .font-subheading {
|
||||||
height: 80px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pairDeviceDialog>*>*>*>hr {
|
#pairDeviceDialog hr {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue