diff --git a/client/index.html b/client/index.html index e4752dbe..347cd631 100644 --- a/client/index.html +++ b/client/index.html @@ -38,11 +38,16 @@
- + + + + + + @@ -73,19 +78,46 @@

Open Snapdrop on other devices to send files

You can be discovered by everyone on this network
- - + +
+
+ Files +
    +
+
+
+ + -

File Received

-
Filename
-
+

File Request

+
Filename
+
- + +
+
+ + +
+
+
+
+ + + + +

File Received

+
Filename
+
+ +
+ +
Save @@ -208,9 +240,12 @@

Snapdrop

- + + + + - + diff --git a/client/scripts/network.js b/client/scripts/network.js index eb599014..7e216fc4 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -26,23 +26,23 @@ class ServerConnection { msg = JSON.parse(msg); console.log('WS:', msg); switch (msg.type) { - case 'peers': - Events.fire('peers', msg.peers); + case Events.PEERS: + Events.fire(Events.PEERS, msg.peers); break; - case 'peer-joined': - Events.fire('peer-joined', msg.peer); + case Events.PEER_JOINED: + Events.fire(Events.PEER_JOINED, msg.peer); break; - case 'peer-left': - Events.fire('peer-left', msg.peerId); + case Events.PEER_LEFT: + Events.fire(Events.PEER_LEFT, msg.peerId); break; - case 'signal': - Events.fire('signal', msg); + case Events.SIGNAL: + Events.fire(Events.SIGNAL, msg); break; - case 'ping': - this.send({ type: 'pong' }); + case Events.DISPLAY_NAME: + Events.fire(Events.DISPLAY_NAME, msg); break; - case 'display-name': - Events.fire('display-name', msg); + case Events.PING: + this.send({ type: Events.PONG }); break; default: console.error('WS: unkown message type', msg); @@ -70,7 +70,7 @@ class ServerConnection { _onDisconnect() { console.log('WS: server disconnected'); - Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...'); + Events.fire(Events.NOTIFY_USER, 'Connection lost. Retry in 5 seconds...'); clearTimeout(this._reconnectTimer); this._reconnectTimer = setTimeout(_ => this._connect(), 5000); } @@ -91,11 +91,40 @@ class ServerConnection { class Peer { + // return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + static uuid() { + let uuid = '', + ii; + for (ii = 0; ii < 32; ii += 1) { + switch (ii) { + case 8: + case 20: + uuid += '-'; + uuid += (Math.random() * 16 | 0).toString(16); + break; + case 12: + uuid += '-'; + uuid += '4'; + break; + case 16: + uuid += '-'; + uuid += (Math.random() * 4 | 8).toString(16); + break; + default: + uuid += (Math.random() * 16 | 0).toString(16); + } + } + return uuid; + }; + constructor(serverConnection, peerId) { this._server = serverConnection; this._peerId = peerId; this._filesQueue = []; - this._busy = false; + this._files = {} + this._sendingSimultaneos = 0 + this._totalBytesAccepted = 0; + this._totalBytesReceived = 0; } sendJSON(message) { @@ -104,47 +133,64 @@ class Peer { sendFiles(files) { for (let i = 0; i < files.length; i++) { - this._filesQueue.push(files[i]); + this._sendFile(files[i]) } - if (this._busy) return; - this._dequeueFile(); - } - - _dequeueFile() { - if (!this._filesQueue.length) return; - this._busy = true; - const file = this._filesQueue.shift(); - this._sendFile(file); } _sendFile(file) { + file.uuid = Peer.uuid(); this.sendJSON({ type: 'header', name: file.name, mime: file.type, - size: file.size + size: file.size, + uuid: file.uuid, }); - this._chunker = new FileChunker(file, - chunk => this._send(chunk), - offset => this._onPartitionEnd(offset)); - this._chunker.nextPartition(); + this._files[file.uuid] = { + header: file, + chunker: new FileChunker( + file, + chunk => { + let uuidBytes = new TextEncoder().encode(file.uuid) + let fileBytes = new Uint8Array(chunk); + let bytes = new Uint8Array(uuidBytes.byteLength + fileBytes.byteLength); + bytes.set(uuidBytes); + bytes.set(fileBytes, uuidBytes.byteLength); + this._send(bytes) + }, + offset => this._onPartitionEnd(file.uuid, offset) + ) + } + } + + static MAX_SIMULTANEOUS_REQUEST = 5 + + _dequeueFile() { + if ( + this._sendingSimultaneos >= Peer.MAX_SIMULTANEOUS_REQUEST | + !this._filesQueue.length + ) return; + this._sendingSimultaneos++ + const uuid = this._filesQueue.shift(); + this._sendNextPartition(uuid); } - _onPartitionEnd(offset) { - this.sendJSON({ type: 'partition', offset: offset }); + _onPartitionEnd(uuid, offset) { + this.sendJSON({ type: 'partition', uuid, offset }); } - _onReceivedPartitionEnd(offset) { - this.sendJSON({ type: 'partition-received', offset: offset }); + _onReceivedPartitionEnd(uuid, offset) { + this.sendJSON({ type: 'partition-received', uuid, offset }); } - _sendNextPartition() { - if (!this._chunker || this._chunker.isFileEnd()) return; - this._chunker.nextPartition(); + _sendNextPartition(uuid) { + let chunker = this._files[uuid].chunker + if (!chunker || chunker.isFileEnd()) return; + chunker.nextPartition(); } - _sendProgress(progress) { - this.sendJSON({ type: 'progress', progress: progress }); + _sendProgress(totalProgress, uuid, fileProgress, ) { + this.sendJSON({ type: 'progress', uuid, totalProgress, fileProgress }); } _onMessage(message) { @@ -159,13 +205,22 @@ class Peer { this._onFileHeader(message); break; case 'partition': - this._onReceivedPartitionEnd(message); + this._onReceivedPartitionEnd(message.uuid, message.offset); break; + case Events.FILE_DENY: + this._dequeueFile(); + break; + case Events.FILE_ACCEPT: + Events.fire(Events.FILE_ACCEPT, this._files[message.uuid].header) + this._filesQueue.push(message.uuid); + this._dequeueFile(); + break case 'partition-received': - this._sendNextPartition(); + this._sendNextPartition(message.uuid); break; case 'progress': - this._onDownloadProgress(message.progress); + const {totalProgress, uuid, fileProgress} = message + this._onDownloadProgress(totalProgress, uuid, fileProgress); break; case 'transfer-complete': this._onTransferCompleted(); @@ -177,42 +232,61 @@ class Peer { } _onFileHeader(header) { - this._lastProgress = 0; - this._digester = new FileDigester({ - name: header.name, - mime: header.mime, - size: header.size - }, file => this._onFileReceived(file)); + this._files[header.uuid] = { + header, + lastProgress: 0, + digester: new FileDigester({ + name: header.name, + mime: header.mime, + size: header.size, + uuid: header.uuid + }, file => this._onFileReceived(header.uuid, file)) + } + Events.fire(Events.FILE_REQUEST, { + file: header, + accept: () => { + this._totalBytesAccepted += header.size + this.sendJSON({type: Events.FILE_ACCEPT, uuid: header.uuid}) + }, + deny: () => this.sendJSON({type: Events.FILE_DENY, uuid: header.uuid}) + }) } - _onChunkReceived(chunk) { - if(!chunk.byteLength) return; + _onChunkReceived(data) { + if(!data.byteLength) return; + + let uuid = new TextDecoder().decode(data.slice(0,36)); + var chunk = data.slice(36); - this._digester.unchunk(chunk); - const progress = this._digester.progress; - this._onDownloadProgress(progress); + let file = this._files[uuid]; + file.digester.unchunk(chunk); + + this._totalBytesReceived += chunk.byteLength + const totalProgress = this._totalBytesReceived / this._totalBytesAccepted + const fileProgress = file.digester.progress; + this._onDownloadProgress(totalProgress, uuid, fileProgress); // occasionally notify sender about our progress - if (progress - this._lastProgress < 0.01) return; - this._lastProgress = progress; - this._sendProgress(progress); + if (fileProgress - file.lastProgress < 0.05) return; + file.lastProgress = fileProgress; + + this._sendProgress(totalProgress, file.header.uuid, file.digester.progress); } - _onDownloadProgress(progress) { - Events.fire('file-progress', { sender: this._peerId, progress: progress }); + _onDownloadProgress(totalProgress, uuid, fileProgress) { + Events.fire(Events.FILE_PROGRESS, { sender: this._peerId, uuid, totalProgress, fileProgress }); } - _onFileReceived(proxyFile) { - Events.fire('file-received', proxyFile); - this.sendJSON({ type: 'transfer-complete' }); + _onFileReceived(uuid, proxyFile) { + Events.fire(Events.FILE_RECEIVED, { file: proxyFile, uuid }); + this.sendJSON({ type: 'transfer-complete', uuid }); } _onTransferCompleted() { - this._onDownloadProgress(1); + this._sendingSimultaneos-- this._reader = null; - this._busy = false; this._dequeueFile(); - Events.fire('notify-user', 'File transfer completed.'); + Events.fire(Events.NOTIFY_USER, 'File transfer completed.'); } sendText(text) { @@ -222,7 +296,7 @@ class Peer { _onTextReceived(message) { const escaped = decodeURIComponent(escape(atob(message.text))); - Events.fire('text-received', { text: escaped, sender: this._peerId }); + Events.fire(Events.TEXT_RECEIVED, { text: escaped, sender: this._peerId }); } } @@ -339,7 +413,7 @@ class RTCPeer extends Peer { } _sendSignal(signal) { - signal.type = 'signal'; + signal.type = Events.SIGNAL; signal.to = this._peerId; this._server.send(signal); } @@ -364,11 +438,11 @@ class PeersManager { constructor(serverConnection) { this.peers = {}; this._server = serverConnection; - Events.on('signal', e => this._onMessage(e.detail)); - Events.on('peers', e => this._onPeers(e.detail)); - Events.on('files-selected', e => this._onFilesSelected(e.detail)); - Events.on('send-text', e => this._onSendText(e.detail)); - Events.on('peer-left', e => this._onPeerLeft(e.detail)); + Events.on(Events.SIGNAL, e => this._onMessage(e.detail)); + Events.on(Events.PEERS, e => this._onPeers(e.detail)); + Events.on(Events.FILES_SELECTED, e => this._onFilesSelected(e.detail)); + Events.on(Events.SEND_TEXT, e => this._onSendText(e.detail)); + Events.on(Events.PEER_LEFT, e => this._onPeerLeft(e.detail)); } _onMessage(message) { @@ -423,7 +497,7 @@ class WSPeer { class FileChunker { constructor(file, onChunk, onPartitionEnd) { - this._chunkSize = 64000; // 64 KB + this._chunkSize = 128000; // 128 KB this._maxPartitionSize = 1e6; // 1 MB this._offset = 0; this._partitionSize = 0; @@ -431,7 +505,7 @@ class FileChunker { this._onChunk = onChunk; this._onPartitionEnd = onPartitionEnd; this._reader = new FileReader(); - this._reader.addEventListener('load', e => this._onChunkRead(e.target.result)); + this._reader.addEventListener(Events.LOAD, e => this._onChunkRead(e.target.result)); } nextPartition() { @@ -512,6 +586,30 @@ class Events { static on(type, callback) { return window.addEventListener(type, callback, false); } + + static LOAD = 'load' + + static PEERS = 'peers' + static PEER_JOINED = 'peer-joined' + static PEER_LEFT = 'peer-left' + static PING = 'ping' + static PONG = 'pong' + + static SIGNAL = 'signal' + static DISPLAY_NAME = 'display-name' + static NOTIFY_USER = 'notify-user' + + static FILES_SELECTED = 'files-selected' + static FILE_PROGRESS = 'file-progress' + static FILE_RECEIVED = 'file-received' + static FILE_REQUEST = 'file-request' + static FILE_ACCEPT = 'file-accept' + static FILE_DENY = 'file-deny' + + static PASTE = 'paste' + static SEND_TEXT = 'send-text' + static TEXT_RECEIVED = 'text-received' + static TEXT_RECIPIENT = 'text-recipient' } @@ -520,4 +618,4 @@ RTCPeer.config = { 'iceServers': [{ urls: 'stun:stun.l.google.com:19302' }] -} +} \ No newline at end of file diff --git a/client/scripts/ui.js b/client/scripts/ui.js index 0c159841..234dca58 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -6,21 +6,66 @@ 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 => { +Events.on(Events.DISPLAY_NAME, e => { const me = e.detail.message; const $displayName = $('displayName') $displayName.textContent = 'You are known as ' + me.displayName; $displayName.title = me.deviceName; }); +Number.prototype.bytesToHumanFileSize = function () { + let bytes = this; + if (bytes >= 1e12) { + return (Math.round(bytes / 1e11) / 10) + ' TB'; + } else 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'; + } +} + +class PreviewView { + + static Elements = { + image: 'img', + audio: 'audio', + video: 'video' + } + + static Keys = Object.keys(PreviewView.Elements) + + constructor(file, parent) { + let mine = file.mime.split('/')[0] + this.url = URL.createObjectURL(file.blob); + this.isPlayable = PreviewView.Keys.indexOf(mine) !== -1 + this.$parent = parent + + if(this.isPlayable){ + console.log('the file is able to preview'); + let element = document.createElement(PreviewView.Elements[mine]); + element.src = this.url; + element.controls = true; + element.classList = 'element-preview' + + this.$parent.appendChild(element) + } + + } + +} + 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)); + Events.on(Events.PEER_JOINED, e => this._onPeerJoined(e.detail)); + Events.on(Events.PEER_LEFT, e => this._onPeerLeft(e.detail)); + Events.on(Events.PEERS, e => this._onPeers(e.detail)); + Events.on(Events.FILE_PROGRESS, e => this._onFileProgress(e.detail)); + Events.on(Events.PASTE, e => this._onPaste(e)); } _onPeerJoined(peer) { @@ -62,7 +107,7 @@ class PeersUI { // "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', { + Events.fire(Events.FILES_SELECTED, { files: files, to: $$('x-peer').id }); @@ -143,7 +188,7 @@ class PeerUI { _onFilesSelected(e) { const $input = e.target; const files = $input.files; - Events.fire('files-selected', { + Events.fire(Events.FILES_SELECTED, { files: files, to: this._peer.id }); @@ -170,7 +215,7 @@ class PeerUI { _onDrop(e) { e.preventDefault(); const files = e.dataTransfer.files; - Events.fire('files-selected', { + Events.fire(Events.FILES_SELECTED, { files: files, to: this._peer.id }); @@ -187,7 +232,7 @@ class PeerUI { _onRightClick(e) { e.preventDefault(); - Events.fire('text-recipient', this._peer.id); + Events.fire(Events.TEXT_RECIPIENT, this._peer.id); } _onTouchStart(e) { @@ -200,7 +245,7 @@ class PeerUI { clearTimeout(this._touchTimer); } else { // this was a long tap if (e) e.preventDefault(); - Events.fire('text-recipient', this._peer.id); + Events.fire(Events.TEXT_RECIPIENT, this._peer.id); } } } @@ -225,15 +270,71 @@ class Dialog { } } +class RequestDialog extends Dialog { + + constructor() { + super('requestDialog'); + Events.on(Events.FILE_REQUEST, e => { + window.blop.play(); + this._acceptQueue.push(e.detail) + this._proccess() + }); + this._acceptQueue = [] + this._showing = false + this.$el.querySelector('.accept').addEventListener('click', () => this._accept()) + this.$el.querySelector('.deny').addEventListener('click', () => this._deny()) + } + + _proccess () { + if(this._showing || this._acceptQueue.length == 0) return; + this.lastDetail = this._acceptQueue.shift() + if(this._autoDownload()) { + this._accept(); + } else { + this._ask(this.lastDetail.file); + } + } + + _ask(file) { + this.$el.querySelector('.fileName').textContent = file.name; + this.$el.querySelector('.fileSize').textContent = file.size.bytesToHumanFileSize(); + this.show() + this._showing = true + } + + _accept() { + this.lastDetail && this.lastDetail.accept(); + this._hide() + } + + _deny() { + this.lastDetail && this.lastDetail.deny(); + this._hide() + } + + _hide() { + this._showing = false + this.lastDetail = null + this.hide() + this._proccess() + } + + _autoDownload(){ + return !this.$el.querySelector('.autoDownload').checked + } + +} + class ReceiveDialog extends Dialog { constructor() { super('receiveDialog'); - Events.on('file-received', e => { - this._nextFile(e.detail); + Events.on(Events.FILE_RECEIVED, e => { + this._nextFile(e.detail.file); window.blop.play(); }); this._filesQueue = []; + this.$previewBox = this.$el.querySelector('.preview') } _nextFile(nextFile) { @@ -257,23 +358,23 @@ class ReceiveDialog extends Dialog { } _displayFile(file) { + const preview = new PreviewView(file, this.$previewBox); + const $a = this.$el.querySelector('#download'); - const url = URL.createObjectURL(file.blob); - $a.href = url; + $a.href = preview.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; + + if(preview.isPlayable) { + this.$previewBox.style.visibility = 'inherit'; } - this.$el.querySelector('#fileName').textContent = file.name; - this.$el.querySelector('#fileSize').textContent = this._formatFileSize(file.size); + this.$el.querySelector('.fileName').textContent = file.name; + this.$el.querySelector('.fileSize').textContent = file.size.bytesToHumanFileSize(); this.show(); if (window.isDownloadSupported) return; @@ -284,28 +385,16 @@ class ReceiveDialog extends Dialog { 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 = ""; + this.$previewBox.style.visibility = 'hidden'; + this.$previewBox.innerHTML = ''; super.hide(); this._dequeueFile(); } _autoDownload(){ - return !this.$el.querySelector('#autoDownload').checked + return !this.$el.querySelector('.autoDownload').checked } } @@ -313,7 +402,7 @@ class ReceiveDialog extends Dialog { class SendTextDialog extends Dialog { constructor() { super('sendTextDialog'); - Events.on('text-recipient', e => this._onRecipient(e.detail)) + Events.on(Events.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)); @@ -341,7 +430,7 @@ class SendTextDialog extends Dialog { _send(e) { e.preventDefault(); - Events.fire('send-text', { + Events.fire(Events.SEND_TEXT, { to: this._recipient, text: this.$text.innerText }); @@ -351,7 +440,7 @@ class SendTextDialog extends Dialog { class ReceiveTextDialog extends Dialog { constructor() { super('receiveTextDialog'); - Events.on('text-received', e => this._onText(e.detail)) + Events.on(Events.TEXT_RECEIVED, e => this._onText(e.detail)) this.$text = this.$el.querySelector('#text'); const $copy = this.$el.querySelector('#copy'); copy.addEventListener('click', _ => this._onCopy()); @@ -375,14 +464,14 @@ class ReceiveTextDialog extends Dialog { async _onCopy() { await navigator.clipboard.writeText(this.$text.textContent); - Events.fire('notify-user', 'Copied to clipboard'); + Events.fire(Events.NOTIFY_USER, 'Copied to clipboard'); } } class Toast extends Dialog { constructor() { super('toast'); - Events.on('notify-user', e => this._onNotfiy(e.detail)); + Events.on(Events.NOTIFY_USER, e => this._onNotfiy(e.detail)); } _onNotfiy(message) { @@ -405,14 +494,14 @@ class Notifications { 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)); + Events.on(Events.TEXT_RECEIVED, e => this._messageNotification(e.detail.text)); + Events.on(Events.FILE_RECEIVED, e => this._downloadNotification(e.detail.file.name)); } _requestPermission() { Notification.requestPermission(permission => { if (permission !== 'granted') { - Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); + Events.fire(Events.NOTIFY_USER, Notifications.PERMISSION_ERROR || 'Error'); return; } this._notify('Even more snappy sharing!'); @@ -490,11 +579,11 @@ class NetworkStatusUI { } _showOfflineMessage() { - Events.fire('notify-user', 'You are offline'); + Events.fire(Events.NOTIFY_USER, 'You are offline'); } _showOnlineMessage() { - Events.fire('notify-user', 'You are back online'); + Events.fire(Events.NOTIFY_USER, 'You are back online'); } } @@ -517,14 +606,86 @@ class WebShareTargetUI { } } +class FilesListView { + constructor() { + this.$el = $('filesListView'); + this.$list = this.$el.querySelector('ul'); + this.$el.querySelectorAll('[toggle]').forEach(el => el.addEventListener('click', e => this.toggle())) + this.$autoFocus = this.$el.querySelector('[autofocus]'); + this._visible = false + this._files = {} + + Events.on(Events.FILE_ACCEPT, ({detail}) => this._addFile(detail)) + Events.on(Events.FILE_REQUEST, ({detail}) => this._addFile(detail.file)) + Events.on(Events.FILE_PROGRESS, ({detail}) => this._files[detail.uuid].setProgress(detail.fileProgress)) + Events.on(Events.FILE_RECEIVED, ({detail}) => this._files[detail.uuid].preview(detail.file) ); + + } + + _addFile(file) { + this._files[file.uuid] = new FilesListItemView(file).root(this.$list) + } + + toggle() { + this._visible = !this._visible + if (this._visible) { + this.$el.setAttribute('show', 1); + } else { + this.$el.removeAttribute('show'); + } + } + +} + +class FilesListItemView { + constructor(file) { + this._file = file + this.$el = document.createElement('li') + this.$el.innerHTML = ` +
+
+ ${file.name} +
+ ${(0).bytesToHumanFileSize()} / ${file.size.bytesToHumanFileSize()} +
+
+ +
+
+ ` + this.$progress = this.$el.querySelector('progress') + this.$progressLabel = this.$el.querySelector('.progressLabel') + this.$previewBox = this.$el.querySelector('.preview') + } + + setProgress(value) { + this.$progress.value = value + this.$progressLabel.innerHTML = (this._file.size * value).bytesToHumanFileSize() + } + + root(root) { + root.appendChild(this.$el) + return this + } + + preview(file) { + let preview = new PreviewView(file, this.$previewBox); + if(preview.isPlayable) { + this.$previewBox.setAttribute('preview','loaded') + } + } + +} class Snapdrop { constructor() { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); - Events.on('load', e => { + Events.on(Events.LOAD, e => { + const filesListView = new FilesListView(); const receiveDialog = new ReceiveDialog(); + const requestDialog = new RequestDialog(); const sendTextDialog = new SendTextDialog(); const receiveTextDialog = new ReceiveTextDialog(); const toast = new Toast(); @@ -560,7 +721,7 @@ window.addEventListener('beforeinstallprompt', e => { }); // Background Animation -Events.on('load', () => { +Events.on(Events.LOAD, () => { let c = document.createElement('canvas'); document.body.appendChild(c); let style = c.style; diff --git a/client/styles.css b/client/styles.css index 0a9aca98..679bbdaa 100644 --- a/client/styles.css +++ b/client/styles.css @@ -322,6 +322,109 @@ footer .font-body2 { } } +/* + Files Listing +*/ + +#filesListView { + box-sizing: border-box; + border-radius: 20px; + position: fixed; + left: 2vw; + top: 2.5vh; + width: 90px; + height: 45px; + max-width: 600px; + padding: 10px; + background-color: var(--bg-color-secondary); + transition-duration: 0.3s; + transition-property: width, height, overflow-y; + overflow-x: hidden; + overflow-y: hidden; + z-index: 15; +} + +#filesListView [toggle] { + display: block; + padding-bottom: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + text-align: center; + cursor: pointer; +} + +#filesListView[show] { + width: 98vw; + height: 95vh; + overflow-y: scroll; + z-index: 3; +} + +#filesListView > div { + width: 98vw; + max-width: 100%; +} + +#filesListView ul { + margin: 0; + padding: 0; + list-style: none; +} + +#filesListView li { + display: flex; + align-items: center; + margin: 10px 0; + padding: 10px 12px; + background-color: rgba(255, 255, 255, 0.08); + border-radius: 8px; +} + +#filesListView li .preview { + display: flex; + align-items: end; + max-width: 0px; + max-height: 100px; + border-radius: 8px; + transition-duration: 0.3s; + transition-property: max-width, opacity, margin; + opacity: 0; + margin-left: 0; + overflow: hidden; +} + +#filesListView li .preview > * { + min-width: 100%; + min-height: 100%; +} + +#filesListView li .preview[preview] { + margin-left: 15px; + max-width: 100px; + max-height: 100px; + opacity: 1; +} + +#filesListView li .content { + flex: 1; +} + +#filesListView li .content > div { + display: flex; + align-items: flex-end; +} + + +#filesListView .details { + text-align: right; + font-size: 0.7em; +} + +#filesListView progress { + display: block; + box-sizing: border-box; + width: 100%; +} + /* Dialog */ @@ -717,8 +820,9 @@ x-dialog x-paper { color: var(--text-color); background-color: var(--bg-color-secondary); } -/* Image Preview */ -#img-preview{ +/* Image/Video/Audio Preview */ +.element-preview { + max-width: 100%; max-height: 50vh; margin: auto; display: block; @@ -754,4 +858,3 @@ x-dialog x-paper { overflow: hidden; } } -