snapdrop

A Progressive Web App for local file sharing
git clone http://git.hanabi.in/repos/snapdrop.git
Log | Files | Refs | README | LICENSE

commit 87a2dec99269631af924c74d472753f9f3852450
parent 44bd3edd7b664b97b5356a89a9bd3e19a158b019
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Mon, 24 Sep 2018 13:14:11 +0200

Squashed commit of the following:

commit c04cdad7db20fcd66a8e191c99282d21aa1a4ca2
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Sat Sep 22 08:47:40 2018 +0200

    Cleanup

commit 891859680a1565cead8fe3dca771449b5e1e3035
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Sat Sep 22 05:55:09 2018 +0200

    Refactor about page

commit 04415ef28f8e7281c13546f168f2582a82bcb34f
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Sat Sep 22 04:44:17 2018 +0200

    Cleanup

commit 52bd7692e951c5fafdcdb182a69646b78f03884d
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 23:19:54 2018 +0200

    Notifications Android & Desktop

commit f537b9621350fd1ea6694e7a2a8d3eca1edbf012
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 22:32:39 2018 +0200

    Notifications

commit 476cb0ae6525177d7ea0519c9f0c4de67d2bf0f3
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 22:31:46 2018 +0200

    Notifications

commit 5a631d3833a63e76d3611dc97ec073e471af58ba
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 21:21:44 2018 +0200

    Notfication API on android

commit 600d3551f4765d65a4348a33012b57ee370b3da3
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 21:12:11 2018 +0200

    Add will-change

commit 3ac40fb3d7467df6c0f66b0d156202cbfbf80f09
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 21:04:48 2018 +0200

    Don’t reconnect if already connecting

commit 9c9ca70d05fb2894d4e8113acc3a99a7b582ec78
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 20:55:15 2018 +0200

    Reconnect on rejoin room

commit 7194c65c74681883089d21c574275dfc01864d50
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 20:51:56 2018 +0200

    Reconnect on rejoin room

commit 0ede41f8d5397e8b7beb62ba6abe2571296f4690
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 20:34:49 2018 +0200

    Cancel keep alive on join room

commit 1d9581632fe3a99c2068d8c0cece9345c9397cc8
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 20:25:54 2018 +0200

    Cancel keep alive on join room

commit e71564a97cc083b606f144db4de3e35e5e172bfb
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 20:15:55 2018 +0200

    Cancel keep alive on join room

commit 0731a21d685c35e45985a39e3df32549730953db
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 20:09:49 2018 +0200

    Cancel keep alive on join room

commit 61697d3abc5430ad58478b12a9b7fc36d8978881
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 20:01:59 2018 +0200

    Cancel keep alive on join room

commit b0fd89eb96f69279a01b64ce5db29c020838f87d
Merge: 4cf2bed b67afca
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:55:32 2018 +0200

    Merge remote-tracking branch 'origin/master' into dev

    # Conflicts:
    #	server/index.js

commit 4cf2beda9075d75e1efc6dca3dc0e390612882fd
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:49:16 2018 +0200

    Fix beforeunload on iphone

commit 728aabd449ed8f70231c643d756488fef7431aae
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:32:59 2018 +0200

    Fix typo in server

commit 96e37aef40c7a69203b172655b290732dc236dc3
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:25:01 2018 +0200

    Fix typo

commit 31e5f635d19a69ebe719194aeb8465368e9f672d
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:24:01 2018 +0200

    Add connection state handler

commit e573d5741979fa48fdbfebed4f3cedd7f82f9ea5
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:17:07 2018 +0200

    STUN server tests

commit 6a1de2926782ad0ca861b078c31ad3de2bc6e172
Merge: 6317c25 92a5f3b
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:15:31 2018 +0200

    Merge branch 'dev' of github.com:RobinLinus/snapdrop into dev

    # Conflicts:
    #	client/scripts/network.js

commit 6317c25b10f3d885c8ccc3374fb1e873e2c18f44
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:14:25 2018 +0200

    Cleanup; fix STUN servers

commit 92a5f3b782b4037beaf9b404683239cf71105586
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 19:14:25 2018 +0200

    Cleanup; fix STUN servers

commit e9eeea48e5ea76214971daa1a879b0ae2ba1a196
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 18:54:52 2018 +0200

    Fix notifications on android

commit 36ec13d4285b1d8633cc3bddd26df8fddbb8e59e
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 18:53:31 2018 +0200

    Fix uncaught error in server

commit abf96c02282c5171793f5487880918f4719e8f15
Author: RobinLinus <robinlinus@users.noreply.github.com>
Date:   Fri Sep 21 16:36:59 2018 +0200

    Test

Diffstat:
MREADME.md | 16+++++++++-------
Mclient/index.html | 90++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mclient/scripts/network.js | 132++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mclient/scripts/ui.js | 2+-
Mclient/styles.css | 114++++++++++++++++++++++++++++++++++++-------------------------------------------
5 files changed, 177 insertions(+), 177 deletions(-)

diff --git a/README.md b/README.md @@ -2,7 +2,7 @@ [Snapdrop](https://snapdrop.net): local file sharing in your browser - inspired by Apple's Airdrop. -#### Snapdrop Version 2 is built with the following awesome technologies: +#### Snapdrop (Version 2) is built with the following awesome technologies: * Vanilla HTML5 / ES6 / CSS3 * Progressive Web App * [WebRTC](http://webrtc.org/) @@ -18,12 +18,12 @@ * [idownloadblog](http://www.idownloadblog.com/2015/12/29/snapdrop/) * [thenextweb](http://thenextweb.com/insider/2015/12/27/snapdrop-is-a-handy-web-based-replacement-for-apples-fiddly-airdrop-file-transfer-tool/) * [winboard](http://www.winboard.org/artikel-ratgeber/6253-dateien-vom-desktop-pc-mit-anderen-plattformen-teilen-mit-snapdrop.html) -* [免費資源網路社群](https://free.com.tw/snapdrop/?utm_content=buffere6987&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer) +* [免費資源網路社群](https://free.com.tw/snapdrop/) ##### What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? It uses a P2P connection if WebRTC is supported by the browser. (WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer). -If WebRTC isn’t supported (Safari, IE) it uses a Web Sockets fallback for the file transfer. The server connects the clients with a stream. +If WebRTC isn’t supported (Safari, IE) it uses a Web Sockets fallback for the file transfer. The server connects the clients with each other. ##### What about privacy? Will files be saved on third-party-servers? None of your files are ever saved on any server. @@ -33,15 +33,15 @@ But it does use Google Analytics. ##### Is SnapDrop a fork of ShareDrop? No. ShareDrop is built with Ember. Snapdrop is built with vanilla ES6. I wanted to play around with Progressive Web Apps and then I got the idea of a local file sharing app. By doing research on this idea I found and analysed ShareDrop. I liked it and thought about how to improve it. -ShareDrop uses WebRTC only and isn't compatible with Safari Browsers. Snapdrop uses a Websocket fallback and some hacks to make Snapdrop work due to the download restrictions on iDevices. +ShareDrop uses WebRTC only and isn't compatible with Safari browsers. Snapdrop uses a Websocket fallback and some hacks to make Snapdrop work due to the download restrictions on iDevices. ### Snapdrop is awesome! How can I support it? * [File bugs, give feedback, submit suggestions](https://github.com/RobinLinus/snapdrop/issues) * Share Snapdrop on your social media. * [Buy me a cup of coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R9C5E42UYEQCN) -* Fix bugs and make a Pull Request. This is my first open source project, so I am not very used to the common workflow, but we'll figure it out! -* Do Security Analysis and suggestions +* Fix bugs and make a pull request. +* Do security analysis and suggestions ## Local Development ``` @@ -49,7 +49,9 @@ ShareDrop uses WebRTC only and isn't compatible with Safari Browsers. Snapdrop u cd snapdrop/server npm install node index.js - cd ../client + + # open a second shell: + cd snapdrop/client python -m SimpleHTTPServer ``` diff --git a/client/index.html b/client/index.html @@ -36,6 +36,18 @@ </head> <body> + <header class="row-reverse"> + <a href="#about" class="icon-button" title="About Snapdrop"> + <svg class="icon"> + <use xlink:href="#info-outline" /> + </svg> + </a> + <a href="#" id="notification" class="icon-button" title="Enable Notifications" hidden> + <svg class="icon"> + <use xlink:href="#notifications" /> + </svg> + </a> + </header> <!-- Peers --> <x-peers class="center"></x-peers> <x-no-peers> @@ -97,47 +109,41 @@ <div class="toast-container full center"> <x-toast class="row" shadow="1" id="toast">File Transfer Completed</x-toast> </div> - <!-- Info Page --> - <div id="info" class="full center column"> - <a href="#" class="close icon-button"> - <svg class="icon"> - <use xlink:href="#close" /> + <!-- About Page --> + <x-about id="about" class="full center column"> + <section class="center column fade-in"> + <header class="row-reverse"> + <a href="#" class="close icon-button"> + <svg class="icon"> + <use xlink:href="#close" /> + </svg> + </a> + </header> + <svg class="icon logo"> + <use xlink:href="#wifi-tethering" /> </svg> - </a> - <svg class="icon logo"> - <use xlink:href="#wifi-tethering" /> - </svg> - <h1>Snapdrop</h1> - <div class="font-subheading">The easiest way to transfer files across devices.</div> - <div class="row"> - <a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop" title="Snapdrop on Github"> - <svg class="icon"> - <use xlink:href="#github" /> - </svg> - </a> - <a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https://snapdrop.net%20by%20@robin_linus%20&" title="tweet about Snapdrop"> - <svg class="icon"> - <use xlink:href="#twitter" /> - </svg> - </a> - <a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop#frequently-asked-questions" title="frequently asked questions"> - <svg class="icon"> - <use xlink:href="#help-outline" /> - </svg> - </a> - </div> - </div> - <a href="#info" class="icon-button" title="about Snapdrop"> - <svg class="icon"> - <use xlink:href="#info-outline" /> - </svg> - <div class="info-background"></div> - </a> - <a id="notification" class="icon-button" hidden title="enable Notifications"> - <svg class="icon"> - <use xlink:href="#notifications" /> - </svg> - </a> + <h1>Snapdrop</h1> + <div class="font-subheading">The easiest way to transfer files across devices.</div> + <div class="row"> + <a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop" title="Snapdrop on Github"> + <svg class="icon"> + <use xlink:href="#github" /> + </svg> + </a> + <a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https://snapdrop.net%20by%20@robin_linus%20&" title="Tweet about Snapdrop"> + <svg class="icon"> + <use xlink:href="#twitter" /> + </svg> + </a> + <a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop#frequently-asked-questions" title="Frequently asked questions"> + <svg class="icon"> + <use xlink:href="#help-outline" /> + </svg> + </a> + </div> + </section> + <x-background></x-background> + </x-about> <!-- SVG Icon Library --> <svg style="display: none;"> <symbol id=wifi-tethering viewBox="0 0 24 24"> @@ -172,8 +178,8 @@ </g> </svg> <!-- Scripts --> - <script type="text/javascript" src="scripts/network.js"></script> - <script type="text/javascript" src="scripts/ui.js"></script> + <script src="scripts/network.js"></script> + <script src="scripts/ui.js"></script> <!-- Sounds --> <audio id="blop" preload="auto" autobuffer="true"> <source src="/sounds/blop.mp3" type="audio/mpeg"> diff --git a/client/scripts/network.js b/client/scripts/network.js @@ -2,8 +2,8 @@ class ServerConnection { constructor() { this._connect(); - Events.on('beforeunload', e => this._disconnect(), false); - Events.on('pagehide', e => this._disconnect(), false); + Events.on('beforeunload', e => this._disconnect()); + Events.on('pagehide', e => this._disconnect()); document.addEventListener('visibilitychange', e => this._onVisibilityChange()); } @@ -20,14 +20,6 @@ class ServerConnection { this._socket = ws; } - _isConnected() { - return this._socket && this._socket.readyState === this._socket.OPEN; - } - - _isConnecting() { - return this._socket && this._socket.readyState === this._socket.CONNECTING; - } - _onMessage(msg) { msg = JSON.parse(msg); console.log('WS:', msg); @@ -68,6 +60,7 @@ class ServerConnection { _disconnect() { this.send({ type: 'disconnect' }); + this._socket.onclose = null; this._socket.close(); } @@ -82,6 +75,14 @@ class ServerConnection { if (document.hidden) return; this._connect(); } + + _isConnected() { + return this._socket && this._socket.readyState === this._socket.OPEN; + } + + _isConnecting() { + return this._socket && this._socket.readyState === this._socket.CONNECTING; + } } class Peer { @@ -130,7 +131,7 @@ class Peer { } _onReceivedPartitionEnd(offset) { - this.sendJSON({ type: 'partition_received', offset: offset }); + this.sendJSON({ type: 'partition-received', offset: offset }); } _sendNextPartition() { @@ -156,7 +157,7 @@ class Peer { case 'partition': this._onReceivedPartitionEnd(message); break; - case 'partition_received': + case 'partition-received': this._sendNextPartition(); break; case 'progress': @@ -192,16 +193,12 @@ class Peer { } _onDownloadProgress(progress) { - Events.fire('file-progress', { - sender: this._peerId, - progress: progress - }); + Events.fire('file-progress', { sender: this._peerId, progress: progress }); } _onFileReceived(proxyFile) { Events.fire('file-received', proxyFile); this.sendJSON({ type: 'transfer-complete' }); - // this._digester = null; } _onTransferCompleted() { @@ -213,17 +210,13 @@ class Peer { } sendText(text) { - this.sendJSON({ - type: 'text', - text: btoa(unescape(encodeURIComponent(text))) - }); + const unescaped = btoa(unescape(encodeURIComponent(text))); + this.sendJSON({ type: 'text', text: unescaped }); } _onTextReceived(message) { - Events.fire('text-received', { - text: decodeURIComponent(escape(atob(message.text))), - sender: this._peerId - }); + const escaped = decodeURIComponent(escape(atob(message.text))); + Events.fire('text-received', { text: escaped, sender: this._peerId }); } } @@ -232,35 +225,37 @@ class RTCPeer extends Peer { constructor(serverConnection, peerId) { super(serverConnection, peerId); if (!peerId) return; // we will listen for a caller - this._start(peerId, true); + this._connect(peerId, true); } - _start(peerId, isCaller) { - if (!this._peer) { - this._isCaller = isCaller; - this._peerId = peerId; - this._peer = new RTCPeerConnection(RTCPeer.config); - this._peer.onicecandidate = e => this._onIceCandidate(e); - this._peer.onconnectionstatechange = e => this._onConnectionStateChange(e); - } + _connect(peerId, isCaller) { + if (!this._conn) this._openConnection(peerId, isCaller); if (isCaller) { - this._createChannel(); + this._openChannel(); } else { - this._peer.ondatachannel = e => this._onChannelOpened(e); + this._conn.ondatachannel = e => this._onChannelOpened(e); } } - _createChannel() { - const channel = this._peer.createDataChannel('data-channel', { reliable: true }); + _openConnection(peerId, isCaller) { + this._isCaller = isCaller; + this._peerId = peerId; + this._conn = new RTCPeerConnection(RTCPeer.config); + this._conn.onicecandidate = e => this._onIceCandidate(e); + this._conn.onconnectionstatechange = e => this._onConnectionStateChange(e); + } + + _openChannel() { + const channel = this._conn.createDataChannel('data-channel', { reliable: true }); channel.binaryType = 'arraybuffer'; channel.onopen = e => this._onChannelOpened(e); - this._peer.createOffer(d => this._onDescription(d), e => this._onError(e)); + this._conn.createOffer(d => this._onDescription(d), e => this._onError(e)); } _onDescription(description) { // description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400'); - this._peer.setLocalDescription(description, + this._conn.setLocalDescription(description, _ => this._sendSignal({ sdp: description }), e => this._onError(e)); } @@ -270,23 +265,16 @@ class RTCPeer extends Peer { this._sendSignal({ ice: event.candidate }); } - _sendSignal(signal) { - signal.type = 'signal'; - signal.to = this._peerId; - this._server.send(signal); - } - onServerMessage(message) { - if (!this._peer) this._start(message.sender, false); - const conn = this._peer; + if (!this._conn) this._connect(message.sender, false); if (message.sdp) { - this._peer.setRemoteDescription(new RTCSessionDescription(message.sdp), () => { + this._conn.setRemoteDescription(new RTCSessionDescription(message.sdp), () => { if (message.sdp.type !== 'offer') return; - this._peer.createAnswer(d => this._onDescription(d), e => this._onError(e)); + this._conn.createAnswer(d => this._onDescription(d), e => this._onError(e)); }, e => this._onError(e)); } else if (message.ice) { - this._peer.addIceCandidate(new RTCIceCandidate(message.ice)); + this._conn.addIceCandidate(new RTCIceCandidate(message.ice)); } } @@ -301,34 +289,48 @@ class RTCPeer extends Peer { _onChannelClosed() { console.log('RTC: channel closed', this._peerId); if (!this.isCaller) return; - this._start(this._peerId, true); // reopen the channel + this._connect(this._peerId, true); // reopen the channel } _onConnectionStateChange(e) { - console.log('RTC: state changed:', this._peer.connectionState); - switch (this._peer.connectionState) { + console.log('RTC: state changed:', this._conn.connectionState); + switch (this._conn.connectionState) { case 'disconnected': this._onChannelClosed(); break; case 'failed': - this._peer = null; + this._conn = null; this._onChannelClosed(); break; } } + _onError(error) { + console.error(error); + } + _send(message) { this._channel.send(message); } - _onError(error) { - console.error(error); + _sendSignal(signal) { + signal.type = 'signal'; + signal.to = this._peerId; + this._server.send(signal); } refresh() { - // check if channel open. otherwise create one - if (this._peer && this._channel && this._channel.readyState !== 'open') return; - this._start(this._peerId, this._isCaller); + // check if channel is open. otherwise create one + if (this._isConnected() || this._isConnecting()) return; + this._connect(this._peerId, this._isCaller); + } + + _isConnected() { + return this._channel && this._channel.readyState === 'open'; + } + + _isConnecting() { + return this._channel && this._channel.readyState === 'connecting'; } } @@ -422,7 +424,7 @@ class FileChunker { this._partitionSize += chunk.byteLength; this._onChunk(chunk); if (this._isPartitionEnd() || this.isFileEnd()) { - this._onPartitionEnd(this._partitionSize); + this._onPartitionEnd(this._offset); return; } this._readChunk(); @@ -447,6 +449,7 @@ class FileChunker { } class FileDigester { + constructor(meta, callback) { this._buffer = []; this._bytesReceived = 0; @@ -462,8 +465,8 @@ class FileDigester { const totalChunks = this._buffer.length; this.progress = this._bytesReceived / this._size; if (this._bytesReceived < this._size) return; - - let received = new Blob(this._buffer, { type: this._mime }); // pass a useful mime type here + // we are done + let received = new Blob(this._buffer, { type: this._mime }); let url = URL.createObjectURL(received); this._callback({ name: this._name, @@ -471,8 +474,8 @@ class FileDigester { size: this._size, url: url }); - this._callback = null; } + } class Events { @@ -485,7 +488,6 @@ class Events { } } - window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); RTCPeer.config = { diff --git a/client/scripts/ui.js b/client/scripts/ui.js @@ -392,8 +392,8 @@ class Notifications { } _copyText(message, notification) { - document.copy(message); notification.close(); + if(!document.copy(message)) return; this._notify('Copied text to clipboard'); } diff --git a/client/styles.css b/client/styles.css @@ -6,6 +6,7 @@ --peer-width: 120px; } + /* Layout */ html { @@ -57,10 +58,22 @@ body { bottom: 0; } +header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 56px; + align-items: center; + padding: 16px; + box-sizing: border-box; +} + [hidden] { display: none !important; } + /* Typography */ body { @@ -112,7 +125,7 @@ body { a { text-decoration: none; - color: var(--primary-color); + color: currentColor; cursor: pointer; } @@ -153,7 +166,11 @@ a { } } +/* Main Header */ +body>header a { + margin-left: 8px; +} /* Peers List */ @@ -164,8 +181,6 @@ x-peers { z-index: 2; } - - /* Empty Peers List */ x-no-peers { @@ -177,7 +192,8 @@ x-no-peers { animation-fill-mode: backwards; } -x-no-peers h2 { +x-no-peers h2, +x-no-peers a { color: var(--primary-color); } @@ -266,6 +282,8 @@ x-peer[drop] x-icon { transform: scale(1.1); } + + /* Footer */ footer { @@ -294,7 +312,6 @@ footer .font-body2 { } - /* Dialog */ x-dialog x-background { @@ -335,7 +352,6 @@ x-dialog .row-reverse>.button { } - /* Receive Dialog */ #receiveTextDialog #text { @@ -351,15 +367,12 @@ x-dialog .row-reverse>.button { - /* Button */ .button { padding: 0 16px; box-sizing: border-box; min-height: 36px; - border: none; - outline: none; min-width: 100px; font-size: 14px; line-height: 24px; @@ -381,6 +394,8 @@ x-dialog .row-reverse>.button { justify-content: center; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); touch-action: manipulation; + border: none; + outline: none; } .button:before, @@ -405,16 +420,18 @@ x-dialog .row-reverse>.button { border-radius: 8px; } -.button:focus:before { +.button:focus:before, +.icon-button:focus:before { opacity: 0.2; } + + button::-moz-focus-inner { border: 0; } - /* Icon Button */ .icon-button { @@ -444,74 +461,56 @@ input { } - /* Info Animation */ -#info { - text-align: center; +#about { color: white; + z-index: 11; + overflow: hidden; + pointer-events: none; +} + +#about .fade-in { transition: opacity 300ms; will-change: opacity; - z-index: 11; transition-delay: 300ms; + z-index: 11; + pointer-events: all; } -#info:not(:target) { +#about:not(:target) .fade-in { opacity: 0; pointer-events: none; transition-delay: 0; } -#info .logo { +#about .logo { --icon-size: 96px; } -#info .close { - position: absolute; - top: 12px; - right: 12px; - color: white; - border-radius: 50%; -} - -.info-background { - position: relative; -} - -.info-background:before { - content: ''; +#about x-background { position: absolute; - width: 40px; - height: 40px; - top: -20px; - left: -32px; + top: calc(32px - 200px); + right: calc(32px - 200px); + width: 400px; + height: 400px; border-radius: 50%; background: var(--primary-color); transform: scale(0); - transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1); will-change: transform; + transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1); + z-index: -1; } -#info:target+a>.info-background:before { - transform: scale(100); -} - -a[href="#info"] { - position: absolute; - top: 12px; - right: 12px; - color: #333; - z-index: 10; +#about:target x-background { + transform: scale(10); } -#info .row a { - color: currentColor; +#about .row a { margin: 8px 8px -16px; } - - /* Loading Indicator */ .progress { @@ -544,7 +543,6 @@ a[href="#info"] { } - /* Toast */ .toast-container { @@ -559,7 +557,6 @@ x-toast { bottom: 24px; width: 100%; max-width: 344px; - border-radius: 8px; background-color: #323232; color: rgba(255, 255, 255, 0.95); align-items: center; @@ -569,7 +566,7 @@ x-toast { transition: opacity 200ms, transform 300ms ease-out; cursor: default; line-height: 24px; - border-radius: 6px; + border-radius: 8px; pointer-events: all; } @@ -578,12 +575,6 @@ x-toast:not([show]):not(:hover) { transform: translateY(100px); } -#notification { - position: absolute; - right: 56px; - top: 12px; - color: #333; -} /* Instructions */ @@ -603,12 +594,10 @@ x-peers:empty~x-instructions { opacity: 0; } + /* Responsive Styles */ @media (min-height: 800px) { - x-toast { - right: 24px; - } footer { margin-bottom: 16px; } @@ -625,6 +614,7 @@ screen and (min-width: 1100px) { x-instructions { top: 24px; } + footer .logo { --icon-size: 40px; }