123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636 |
- 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');
- window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
- // set display name
- 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;
- });
- 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));
- 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
- });
- }
- }
- }
- class PeerUI {
- html() {
- return `
- <label class="column center" title="Click to send files or right click to send a text">
- <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>
- <div class="device-name font-body2"></div>
- <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());
- 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);
- }
- }
- }
- 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;
- $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';
- }
- }
- hide() {
- this.$el.querySelector('.preview').style.visibility = 'hidden';
- this.$el.querySelector("#img-preview").src = "";
- super.hide();
- this._dequeueFile();
- }
- _autoDownload(){
- return !this.$el.querySelector('#autoDownload').checked
- }
- }
- 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;
- 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
- });
- }
- }
- 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);
- 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;
- // 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);
- }
- // Notification is persistent on Android. We have to close it manually
- if (closeTimeout) {
- setTimeout(_ => notification.close(), closeTimeout);
- }
- return notification;
- }
- _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;
- }
- }
- }
- class NetworkStatusUI {
- 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');
- }
- }
- 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 : '';
- 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 + '"');
- }
- }
- 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();
- const webShareTargetUI = new WebShareTargetUI();
- });
- }
- }
- const snapdrop = new Snapdrop();
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('/service-worker.js')
- .then(serviceWorker => {
- console.log('Service Worker registered');
- window.serviceWorker = serviceWorker
- });
- }
- 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();
- }
- });
- // Background Animation
- 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() {
- drawCircles();
- animate();
- });
- }
- }
- window.animateBackground = function(l) {
- loading = l;
- animate();
- };
- init();
- animate();
- });
- 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
- which can be accessed by clicking the lock icon next to the URL.`;
- document.body.onclick = e => { // safari hack to fix audio
- document.body.onclick = null;
- if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
- blop.play();
- }
|