PairDrop/client/scripts/ui.js

630 lines
18 KiB
JavaScript
Raw Normal View History

2018-09-21 16:05:03 +02:00
const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(query);
const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase());
2018-09-21 23:19:54 +02:00
window.isDownloadSupported = (typeof document.createElement('a').download !== 'undefined');
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
2018-10-11 00:08:07 +02:00
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
2018-09-21 16:05:03 +02:00
2020-07-12 19:23:07 +02:00
// set display name
2020-12-16 04:16:53 +01:00
Events.on('display-name', e => {
2020-12-19 21:05:48 +01:00
const me = e.detail.message;
const $displayName = $('displayName')
$displayName.textContent = 'You are known as ' + me.displayName;
$displayName.title = me.deviceName;
2020-07-12 19:23:07 +02:00
});
2018-09-21 16:05:03 +02:00
class PeersUI {
constructor() {
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peers', e => this._onPeers(e.detail));
Events.on('file-progress', e => this._onFileProgress(e.detail));
2020-03-09 22:40:57 +01:00
Events.on('paste', e => this._onPaste(e));
2018-09-21 16:05:03 +02:00
}
_onPeerJoined(peer) {
if ($(peer.id)) return; // peer already exists
2018-09-21 16:05:03 +02:00
const peerUI = new PeerUI(peer);
$$('x-peers').appendChild(peerUI.$el);
}
_onPeers(peers) {
this._clearPeers();
peers.forEach(peer => this._onPeerJoined(peer));
}
_onPeerLeft(peerId) {
2018-10-09 15:45:07 +02:00
const $peer = $(peerId);
if (!$peer) return;
$peer.remove();
2018-09-21 16:05:03 +02:00
}
_onFileProgress(progress) {
const peerId = progress.sender || progress.recipient;
2018-10-09 15:45:07 +02:00
const $peer = $(peerId);
if (!$peer) return;
$peer.ui.setProgress(progress.progress);
2018-09-21 16:05:03 +02:00
}
_clearPeers() {
const $peers = $$('x-peers').innerHTML = '';
}
_onPaste(e) {
2020-03-09 22:40:57 +01:00
const files = e.clipboardData.files || e.clipboardData.items
.filter(i => i.type.indexOf('image') > -1)
.map(i => i.getAsFile());
2020-03-09 22:49:59 +01:00
const peers = document.querySelectorAll('x-peer');
// send the pasted image content to the only peer if there is one
// otherwise, select the peer somehow by notifying the client that
// "image data has been pasted, click the client to which to send it"
// not implemented
2020-03-09 22:47:35 +01:00
if (files.length > 0 && peers.length === 1) {
Events.fire('files-selected', {
files: files,
to: $$('x-peer').id
});
}
2018-09-21 16:05:03 +02:00
}
}
class PeerUI {
html() {
return `
2020-12-16 05:48:54 +01:00
<label class="column center" title="Click to send files or right click to send a text">
2018-09-21 16:05:03 +02:00
<input type="file" multiple>
<x-icon shadow="1">
<svg class="icon"><use xlink:href="#"/></svg>
</x-icon>
<div class="progress">
<div class="circle"></div>
<div class="circle right"></div>
</div>
<div class="name font-subheading"></div>
2020-12-19 21:05:48 +01:00
<div class="device-name font-body2"></div>
2018-09-21 16:05:03 +02:00
<div class="status font-body2"></div>
</label>`
}
constructor(peer) {
this._peer = peer;
this._initDom();
this._bindListeners(this.$el);
}
_initDom() {
const el = document.createElement('x-peer');
el.id = this._peer.id;
el.innerHTML = this.html();
el.ui = this;
el.querySelector('svg use').setAttribute('xlink:href', this._icon());
2020-12-19 21:05:48 +01:00
el.querySelector('.name').textContent = this._displayName();
el.querySelector('.device-name').textContent = this._deviceName();
2018-09-21 16:05:03 +02:00
this.$el = el;
this.$progress = el.querySelector('.progress');
}
_bindListeners(el) {
el.querySelector('input').addEventListener('change', e => this._onFilesSelected(e));
el.addEventListener('drop', e => this._onDrop(e));
el.addEventListener('dragend', e => this._onDragEnd(e));
el.addEventListener('dragleave', e => this._onDragEnd(e));
el.addEventListener('dragover', e => this._onDragOver(e));
el.addEventListener('contextmenu', e => this._onRightClick(e));
el.addEventListener('touchstart', e => this._onTouchStart(e));
el.addEventListener('touchend', e => this._onTouchEnd(e));
// prevent browser's default file drop behavior
Events.on('dragover', e => e.preventDefault());
Events.on('drop', e => e.preventDefault());
}
2020-12-19 21:05:48 +01:00
_displayName() {
return this._peer.name.displayName;
2018-09-21 16:05:03 +02:00
}
2020-12-19 21:05:48 +01:00
_deviceName() {
return this._peer.name.deviceName;
}
2018-09-21 16:05:03 +02:00
_icon() {
const device = this._peer.name.device || this._peer.name;
if (device.type === 'mobile') {
return '#phone-iphone';
}
if (device.type === 'tablet') {
return '#tablet-mac';
}
return '#desktop-mac';
}
_onFilesSelected(e) {
const $input = e.target;
const files = $input.files;
Events.fire('files-selected', {
files: files,
to: this._peer.id
});
$input.value = null; // reset input
}
setProgress(progress) {
if (progress > 0) {
this.$el.setAttribute('transfer', '1');
}
if (progress > 0.5) {
this.$progress.classList.add('over50');
} else {
this.$progress.classList.remove('over50');
}
const degrees = `rotate(${360 * progress}deg)`;
this.$progress.style.setProperty('--progress', degrees);
if (progress >= 1) {
this.setProgress(0);
this.$el.removeAttribute('transfer');
}
}
_onDrop(e) {
e.preventDefault();
const files = e.dataTransfer.files;
Events.fire('files-selected', {
files: files,
to: this._peer.id
});
this._onDragEnd();
}
_onDragOver() {
this.$el.setAttribute('drop', 1);
}
_onDragEnd() {
this.$el.removeAttribute('drop');
}
_onRightClick(e) {
e.preventDefault();
Events.fire('text-recipient', this._peer.id);
}
_onTouchStart(e) {
this._touchStart = Date.now();
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
}
_onTouchEnd(e) {
if (Date.now() - this._touchStart < 500) {
clearTimeout(this._touchTimer);
} else { // this was a long tap
if (e) e.preventDefault();
Events.fire('text-recipient', this._peer.id);
}
}
}
class Dialog {
constructor(id) {
this.$el = $(id);
this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', e => this.hide()))
this.$autoFocus = this.$el.querySelector('[autofocus]');
}
show() {
this.$el.setAttribute('show', 1);
if (this.$autoFocus) this.$autoFocus.focus();
}
hide() {
this.$el.removeAttribute('show');
document.activeElement.blur();
window.blur();
}
}
class ReceiveDialog extends Dialog {
constructor() {
super('receiveDialog');
Events.on('file-received', e => {
this._nextFile(e.detail);
window.blop.play();
});
this._filesQueue = [];
}
_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;
2018-09-21 16:05:03 +02:00
$a.download = file.name;
if(this._autoDownload()){
$a.click()
return
}
2018-09-21 16:05:03 +02:00
this.$el.querySelector('#fileName').textContent = file.name;
this.$el.querySelector('#fileSize').textContent = this._formatFileSize(file.size);
this.show();
2018-09-21 23:19:54 +02:00
if (window.isDownloadSupported) return;
// fallback for iOS
$a.target = '_blank';
const reader = new FileReader();
reader.onload = e => $a.href = reader.result;
reader.readAsDataURL(file.blob);
2018-09-21 16:05:03 +02:00
}
_formatFileSize(bytes) {
if (bytes >= 1e9) {
return (Math.round(bytes / 1e8) / 10) + ' GB';
} else if (bytes >= 1e6) {
return (Math.round(bytes / 1e5) / 10) + ' MB';
} else if (bytes > 1000) {
return Math.round(bytes / 1000) + ' KB';
} else {
return bytes + ' Bytes';
}
}
hide() {
super.hide();
this._dequeueFile();
}
_autoDownload(){
return !this.$el.querySelector('#autoDownload').checked
}
2018-09-21 16:05:03 +02:00
}
class SendTextDialog extends Dialog {
constructor() {
super('sendTextDialog');
Events.on('text-recipient', e => this._onRecipient(e.detail))
this.$text = this.$el.querySelector('#textInput');
const button = this.$el.querySelector('form');
button.addEventListener('submit', e => this._send(e));
}
_onRecipient(recipient) {
this._recipient = recipient;
2019-03-13 01:48:53 +01:00
this._handleShareTargetText();
2018-09-21 16:05:03 +02:00
this.show();
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(this.$text);
sel.removeAllRanges();
sel.addRange(range);
2018-09-21 16:05:03 +02:00
}
2019-03-14 21:37:44 +01:00
_handleShareTargetText() {
if (!window.shareTargetText) return;
this.$text.textContent = window.shareTargetText;
2019-03-13 01:48:53 +01:00
window.shareTargetText = '';
}
2018-09-21 16:05:03 +02:00
_send(e) {
e.preventDefault();
Events.fire('send-text', {
to: this._recipient,
2020-12-28 20:21:50 +01:00
text: this.$text.innerText
2018-09-21 16:05:03 +02:00
});
}
}
class ReceiveTextDialog extends Dialog {
constructor() {
super('receiveTextDialog');
Events.on('text-received', e => this._onText(e.detail))
this.$text = this.$el.querySelector('#text');
const $copy = this.$el.querySelector('#copy');
copy.addEventListener('click', _ => this._onCopy());
}
_onText(e) {
this.$text.innerHTML = '';
const text = e.text;
if (isURL(text)) {
const $a = document.createElement('a');
$a.href = text;
$a.target = '_blank';
$a.textContent = text;
this.$text.appendChild($a);
} else {
this.$text.textContent = text;
}
this.show();
window.blop.play();
}
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
2018-09-21 16:05:03 +02:00
Events.fire('notify-user', 'Copied to clipboard');
}
}
class Toast extends Dialog {
constructor() {
super('toast');
Events.on('notify-user', e => this._onNotfiy(e.detail));
}
_onNotfiy(message) {
this.$el.textContent = message;
this.show();
setTimeout(_ => this.hide(), 3000);
}
}
class Notifications {
constructor() {
// Check if the browser supports notifications
if (!('Notification' in window)) return;
2018-09-21 22:31:46 +02:00
2018-09-21 16:05:03 +02:00
// Check whether notification permissions have already been granted
if (Notification.permission !== 'granted') {
this.$button = $('notification');
this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', e => this._requestPermission());
}
Events.on('text-received', e => this._messageNotification(e.detail.text));
Events.on('file-received', e => this._downloadNotification(e.detail.name));
}
_requestPermission() {
Notification.requestPermission(permission => {
if (permission !== 'granted') {
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
return;
}
this._notify('Even more snappy sharing!');
this.$button.setAttribute('hidden', 1);
});
}
2019-03-13 01:21:44 +01:00
_notify(message, body, closeTimeout = 20000) {
2018-09-21 18:54:52 +02:00
const config = {
2018-09-21 16:05:03 +02:00
body: body,
2018-09-21 18:54:52 +02:00
icon: '/images/logo_transparent_128x128.png',
}
let notification;
2018-09-21 23:19:54 +02:00
try {
notification = new Notification(message, config);
2018-09-21 23:19:54 +02:00
} catch (e) {
// Android doesn't support "new Notification" if service worker is installed
2018-09-21 23:19:54 +02:00
if (!serviceWorker || !serviceWorker.showNotification) return;
notification = serviceWorker.showNotification(message, config);
2018-09-21 18:54:52 +02:00
}
2018-09-21 23:19:54 +02:00
// Notification is persistent on Android. We have to close it manually
2019-03-13 01:21:44 +01:00
if (closeTimeout) {
setTimeout(_ => notification.close(), closeTimeout);
}
return notification;
2018-09-21 16:05:03 +02:00
}
_messageNotification(message) {
if (isURL(message)) {
const notification = this._notify(message, 'Click to open link');
2018-09-21 23:19:54 +02:00
this._bind(notification, e => window.open(message, '_blank', null, true));
2018-09-21 16:05:03 +02:00
} else {
const notification = this._notify(message, 'Click to copy text');
2018-09-21 23:19:54 +02:00
this._bind(notification, e => this._copyText(message, notification));
2018-09-21 16:05:03 +02:00
}
}
_downloadNotification(message) {
const notification = this._notify(message, 'Click to download');
2018-09-21 23:19:54 +02:00
if (!window.isDownloadSupported) return;
this._bind(notification, e => this._download(notification));
2018-09-21 22:31:46 +02:00
}
2018-09-21 23:19:54 +02:00
_download(notification) {
2018-09-21 22:31:46 +02:00
document.querySelector('x-dialog [download]').click();
2018-09-21 23:19:54 +02:00
notification.close();
2018-09-21 16:05:03 +02:00
}
2018-09-21 23:19:54 +02:00
_copyText(message, notification) {
2018-09-21 22:31:46 +02:00
notification.close();
if (!navigator.clipboard.writeText(message)) return;
2018-09-21 23:19:54 +02:00
this._notify('Copied text to clipboard');
}
_bind(notification, handler) {
if (notification.then) {
notification.then(e => serviceWorker.getNotifications().then(notifications => {
serviceWorker.addEventListener('notificationclick', handler);
}));
} else {
notification.onclick = handler;
}
2018-09-21 22:31:46 +02:00
}
2018-09-21 16:05:03 +02:00
}
2019-03-13 01:21:44 +01:00
class NetworkStatusUI {
constructor() {
window.addEventListener('offline', e => this._showOfflineMessage(), false);
window.addEventListener('online', e => this._showOnlineMessage(), false);
2019-03-13 01:21:44 +01:00
if (!navigator.onLine) this._showOfflineMessage();
}
2019-03-13 01:21:44 +01:00
_showOfflineMessage() {
Events.fire('notify-user', 'You are offline');
}
2019-03-13 01:21:44 +01:00
_showOnlineMessage() {
Events.fire('notify-user', 'You are back online');
}
}
2019-03-13 01:48:53 +01:00
class WebShareTargetUI {
constructor() {
const parsedUrl = new URL(window.location);
const title = parsedUrl.searchParams.get('title');
const text = parsedUrl.searchParams.get('text');
const url = parsedUrl.searchParams.get('url');
let shareTargetText = title ? title : '';
shareTargetText += text ? shareTargetText ? ' ' + text : text : '';
2020-12-16 06:15:25 +01:00
if(url) shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable.
2019-03-14 21:37:44 +01:00
if (!shareTargetText) return;
2019-03-13 01:48:53 +01:00
window.shareTargetText = shareTargetText;
2019-03-14 21:37:44 +01:00
history.pushState({}, 'URL Rewrite', '/');
2019-03-13 01:48:53 +01:00
console.log('Shared Target Text:', '"' + shareTargetText + '"');
}
}
2019-03-13 01:21:44 +01:00
2018-09-21 16:05:03 +02:00
class Snapdrop {
constructor() {
const server = new ServerConnection();
const peers = new PeersManager(server);
const peersUI = new PeersUI();
Events.on('load', e => {
const receiveDialog = new ReceiveDialog();
const sendTextDialog = new SendTextDialog();
const receiveTextDialog = new ReceiveTextDialog();
const toast = new Toast();
const notifications = new Notifications();
const networkStatusUI = new NetworkStatusUI();
2019-03-13 01:21:44 +01:00
const webShareTargetUI = new WebShareTargetUI();
2019-08-28 17:14:51 +02:00
});
2018-09-21 16:05:03 +02:00
}
}
const snapdrop = new Snapdrop();
2018-09-21 18:54:52 +02:00
2018-10-24 17:43:50 +02:00
if ('serviceWorker' in navigator) {
2019-03-14 21:37:44 +01:00
navigator.serviceWorker.register('/service-worker.js')
2018-09-21 18:54:52 +02:00
.then(serviceWorker => {
console.log('Service Worker registered');
window.serviceWorker = serviceWorker
});
2018-09-21 16:05:03 +02:00
}
2019-03-14 21:37:44 +01:00
window.addEventListener('beforeinstallprompt', e => {
if (window.matchMedia('(display-mode: standalone)').matches) {
// don't display install banner when installed
return e.preventDefault();
} else {
const btn = document.querySelector('#install')
btn.hidden = false;
btn.onclick = _ => e.prompt();
return e.preventDefault();
}
});
2018-09-21 16:05:03 +02:00
// Background Animation
Events.on('load', () => {
let c = document.createElement('canvas');
2018-09-21 16:05:03 +02:00
document.body.appendChild(c);
let style = c.style;
2018-09-21 16:05:03 +02:00
style.width = '100%';
style.position = 'absolute';
style.zIndex = -1;
2020-05-19 22:14:10 +02:00
style.top = 0;
style.left = 0;
let ctx = c.getContext('2d');
let x0, y0, w, h, dw;
2018-09-21 16:05:03 +02:00
function init() {
w = window.innerWidth;
h = window.innerHeight;
c.width = w;
c.height = h;
let offset = h > 380 ? 100 : 65;
2020-12-20 22:13:22 +01:00
offset = h > 800 ? 116 : offset;
2018-09-21 16:05:03 +02:00
x0 = w / 2;
y0 = h - offset;
dw = Math.max(w, h, 1000) / 13;
drawCircles();
}
window.onresize = init;
2021-01-11 06:40:51 +01:00
function drawCircle(radius) {
2018-09-21 16:05:03 +02:00
ctx.beginPath();
let color = Math.round(255 * (1 - radius / Math.max(w, h)));
2018-09-21 16:05:03 +02:00
ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)';
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.lineWidth = 2;
}
let step = 0;
2018-09-21 16:05:03 +02:00
function drawCircles() {
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < 8; i++) {
2021-01-11 06:40:51 +01:00
drawCircle(dw * i + step % dw);
2018-09-21 16:05:03 +02:00
}
step += 1;
}
let loading = true;
2018-09-21 16:05:03 +02:00
function animate() {
if (loading || step % dw < dw - 5) {
requestAnimationFrame(function() {
2018-09-21 16:05:03 +02:00
drawCircles();
animate();
});
}
}
window.animateBackground = function(l) {
loading = l;
animate();
};
init();
animate();
setTimeout(e => window.animateBackground(false), 3000);
});
Notifications.PERMISSION_ERROR = `
Notifications permission has been blocked
as the user has dismissed the permission prompt several times.
This can be reset in Page Info
2018-09-21 16:05:03 +02:00
which can be accessed by clicking the lock icon next to the URL.`;
document.body.onclick = e => { // safari hack to fix audio
2018-09-21 16:05:03 +02:00
document.body.onclick = null;
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
blop.play();
}