PairDrop/client/scripts/ui.js

649 lines
17 KiB
JavaScript
Raw Normal View History

2021-06-16 07:13:19 +02:00
const $ = (query) => document.getElementById(query);
const $$ = (query) => document.body.querySelector(query);
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");
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
2021-06-16 07:13:19 +02:00
Events.on("display-name", (e) => {
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 {
2021-06-16 07:13:19 +02:00
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));
Events.on("paste", (e) => this._onPaste(e));
}
_onPeerJoined(peer) {
if ($(peer.id)) return; // peer already exists
const peerUI = new PeerUI(peer);
$$("x-peers").appendChild(peerUI.$el);
setTimeout((e) => window.animateBackground(false), 1750); // Stop animation
}
_onPeers(peers) {
this._clearPeers();
peers.forEach((peer) => this._onPeerJoined(peer));
}
_onPeerLeft(peerId) {
const $peer = $(peerId);
if (!$peer) return;
$peer.remove();
}
_onFileProgress(progress) {
const peerId = progress.sender || progress.recipient;
const $peer = $(peerId);
if (!$peer) return;
$peer.ui.setProgress(progress.progress);
}
_clearPeers() {
const $peers = ($$("x-peers").innerHTML = "");
}
_onPaste(e) {
const files =
e.clipboardData.files ||
e.clipboardData.items
.filter((i) => i.type.indexOf("image") > -1)
.map((i) => i.getAsFile());
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
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 {
2021-06-16 07:13:19 +02:00
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>
2021-06-16 07:13:19 +02:00
</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());
el.querySelector(".name").textContent = this._displayName();
el.querySelector(".device-name").textContent = this._deviceName();
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());
}
_displayName() {
return this._peer.name.displayName;
}
_deviceName() {
return this._peer.name.deviceName;
}
_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);
2018-09-21 16:05:03 +02:00
}
2021-06-16 07:13:19 +02:00
}
2018-09-21 16:05:03 +02:00
}
class Dialog {
2021-06-16 07:13:19 +02:00
constructor(id) {
this.$el = $(id);
this.$el.querySelectorAll("[close]").forEach((el) => {
el.addEventListener("click", (e) => this.hide());
});
this.$el.querySelectorAll("[role=\"textbox\"]").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();
}
2018-09-21 16:05:03 +02:00
}
class ReceiveDialog extends Dialog {
2021-06-16 07:13:19 +02:00
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;
$a.download = file.name;
if (this._autoDownload()) {
$a.click();
return;
}
if (file.mime.split("/")[0] === "image") {
console.log("the file is image");
this.$el.querySelector(".preview").style.visibility = "inherit";
this.$el.querySelector("#img-preview").src = url;
}
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 = (e) => ($a.href = reader.result);
reader.readAsDataURL(file.blob);
}
_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";
2018-09-21 16:05:03 +02:00
}
2021-06-16 07:13:19 +02:00
}
2021-06-16 07:13:19 +02:00
hide() {
this.$el.querySelector(".preview").style.visibility = "hidden";
this.$el.querySelector("#img-preview").src = "";
super.hide();
this._dequeueFile();
}
2021-06-16 07:13:19 +02:00
_autoDownload() {
return !this.$el.querySelector("#autoDownload").checked;
}
2018-09-21 16:05:03 +02:00
}
class SendTextDialog extends Dialog {
2021-06-16 07:13:19 +02:00
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;
this._handleShareTargetText();
this.show();
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(this.$text);
sel.removeAllRanges();
sel.addRange(range);
}
_handleShareTargetText() {
if (!window.shareTargetText) return;
this.$text.textContent = window.shareTargetText;
window.shareTargetText = "";
}
_send(e) {
e.preventDefault();
Events.fire("send-text", {
to: this._recipient,
text: this.$text.innerText,
});
}
2018-09-21 16:05:03 +02:00
}
class ReceiveTextDialog extends Dialog {
2021-06-16 07:13:19 +02:00
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;
2018-09-21 16:05:03 +02:00
}
2021-06-16 07:13:19 +02:00
this.show();
window.blop.play();
}
2018-09-21 16:05:03 +02:00
2021-06-16 07:13:19 +02:00
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
Events.fire("notify-user", "Copied to clipboard");
}
2018-09-21 16:05:03 +02:00
}
class Toast extends Dialog {
2021-06-16 07:13:19 +02:00
constructor() {
super("toast");
Events.on("notify-user", (e) => this._onNotfiy(e.detail));
}
_onNotfiy(message) {
this.$el.textContent = message;
this.show();
setTimeout((_) => this.hide(), 3000);
}
2018-09-21 16:05:03 +02:00
}
class Notifications {
2021-06-16 07:13:19 +02:00
constructor() {
// Check if the browser supports notifications
if (!("Notification" in window)) return;
// 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);
});
}
_notify(message, body, closeTimeout = 20000) {
const config = {
body: body,
icon: "/images/logo_transparent_128x128.png",
};
let notification;
try {
notification = new Notification(message, config);
} catch (e) {
// Android doesn't support "new Notification" if service worker is installed
if (!serviceWorker || !serviceWorker.showNotification) return;
notification = serviceWorker.showNotification(message, config);
2018-09-21 22:31:46 +02:00
}
2021-06-16 07:13:19 +02:00
// Notification is persistent on Android. We have to close it manually
if (closeTimeout) {
setTimeout((_) => notification.close(), closeTimeout);
2018-09-21 16:05:03 +02:00
}
2021-06-16 07:13:19 +02:00
return notification;
}
2018-09-21 23:19:54 +02:00
2021-06-16 07:13:19 +02:00
_messageNotification(message) {
if (isURL(message)) {
const notification = this._notify(message, "Click to open link");
this._bind(notification, (e) =>
window.open(message, "_blank", null, true)
);
} else {
const notification = this._notify(message, "Click to copy text");
this._bind(notification, (e) => this._copyText(message, notification));
}
}
_downloadNotification(message) {
const notification = this._notify(message, "Click to download");
if (!window.isDownloadSupported) return;
this._bind(notification, (e) => this._download(notification));
}
_download(notification) {
document.querySelector("x-dialog [download]").click();
notification.close();
}
_copyText(message, notification) {
notification.close();
if (!navigator.clipboard.writeText(message)) return;
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
}
2021-06-16 07:13:19 +02:00
}
2018-09-21 16:05:03 +02:00
}
2019-03-13 01:21:44 +01:00
class NetworkStatusUI {
2021-06-16 07:13:19 +02:00
constructor() {
window.addEventListener(
"offline",
(e) => this._showOfflineMessage(),
false
);
window.addEventListener("online", (e) => this._showOnlineMessage(), false);
if (!navigator.onLine) this._showOfflineMessage();
}
_showOfflineMessage() {
Events.fire("notify-user", "You are offline");
}
_showOnlineMessage() {
Events.fire("notify-user", "You are back online");
}
}
2019-03-13 01:48:53 +01:00
class WebShareTargetUI {
2021-06-16 07:13:19 +02:00
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) : "";
if (url) shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable.
if (!shareTargetText) return;
window.shareTargetText = shareTargetText;
history.pushState({}, "URL Rewrite", "/");
console.log("Shared Target Text:", '"' + shareTargetText + '"');
}
2019-03-13 01:48:53 +01:00
}
2018-09-21 16:05:03 +02:00
class Snapdrop {
2021-06-16 07:13:19 +02:00
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();
const webShareTargetUI = new WebShareTargetUI();
});
}
2018-09-21 16:05:03 +02:00
}
const snapdrop = new Snapdrop();
2021-06-16 07:13:19 +02:00
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then((serviceWorker) => {
console.log("Service Worker registered");
window.serviceWorker = serviceWorker;
});
2018-09-21 16:05:03 +02:00
}
2021-06-16 07:13:19 +02: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();
}
2019-03-14 21:37:44 +01:00
});
2018-09-21 16:05:03 +02:00
// Background Animation
2021-06-16 07:13:19 +02:00
Events.on("load", () => {
let c = document.createElement("canvas");
document.body.appendChild(c);
let style = c.style;
style.width = "100%";
style.position = "absolute";
style.zIndex = -1;
style.top = 0;
style.left = 0;
let ctx = c.getContext("2d");
let x0, y0, w, h, dw;
function init() {
w = window.innerWidth;
h = window.innerHeight;
c.width = w;
c.height = h;
let offset = h > 380 ? 100 : 65;
offset = h > 800 ? 116 : offset;
x0 = w / 2;
y0 = h - offset;
dw = Math.max(w, h, 1000) / 13;
drawCircles();
}
window.onresize = init;
function drawCircle(radius) {
ctx.beginPath();
let color = Math.round(255 * (1 - radius / Math.max(w, h)));
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;
function drawCircles() {
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < 8; i++) {
drawCircle(dw * i + (step % dw));
}
step += 1;
}
let loading = true;
function animate() {
if (loading || step % dw < dw - 5) {
requestAnimationFrame(function () {
2018-09-21 16:05:03 +02:00
drawCircles();
animate();
2021-06-16 07:13:19 +02:00
});
}
}
window.animateBackground = function (l) {
loading = l;
2018-09-21 16:05:03 +02:00
animate();
2021-06-16 07:13:19 +02:00
};
init();
animate();
2018-09-21 16:05:03 +02:00
});
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.`;
2021-06-16 07:13:19 +02:00
document.body.onclick = (e) => {
// safari hack to fix audio
document.body.onclick = null;
if (!/.*Version.*Safari.*/.test(navigator.userAgent)) return;
blop.play();
};