Compare commits

..

21 Commits

Author SHA1 Message Date
schlagmichdoch
a9d7960a59 increase version to v1.2.1 2023-03-03 13:12:06 +01:00
schlagmichdoch
39ca5b2d21 ws-fallback: remove all WSPeers when server connection disconnects + fix onPeerLeft 2023-03-03 13:10:14 +01:00
schlagmichdoch
cf715b2872 stability on reconnect: prevent "peer-left" signal after "peer-joined" by leaving rooms first before reentering them, clear _keepAlive timeout before joining ip room and not manually terminating sockets 2023-03-03 13:10:14 +01:00
schlagmichdoch
bbb8c1b10f ws-fallback: prevent signaling from stopping on reconnect. Do not stop to signal until both devices have sent event "peer-connected" 2023-03-03 13:10:13 +01:00
schlagmichdoch
d6ef5887dd move logging of rtc message from class Peer class to overwritten method in class RTCPeer 2023-03-03 12:38:34 +01:00
schlagmichdoch
f9f1abef7a Replace all urls in received messages with links. Center the message if it does not include any whitespace. 2023-03-03 12:28:50 +01:00
schlagmichdoch
d244f5fa47 fix circles position on ios safari are shifted by url bar 2023-03-03 12:03:20 +01:00
schlagmichdoch
3a2d8c75f7 - restructure and unify dialogs to use less space on mobile and be clearer
- give user option both options "share" and "download" on mobile
- add fallback if zipper fails that downloads files individually
- fix dequeuing of message queue not possible if sending peer has left
2023-03-03 12:01:43 +01:00
schlagmichdoch
545cdc2459 Fix browser reloading when first message is sent by preventing event default on submit 2023-03-02 16:30:47 +01:00
schlagmichdoch
a1fdd81629 increase version to v1.2.0 2023-03-02 15:33:30 +01:00
schlagmichdoch
7220e85422 document/tab title: Show number of received messages and move '- PairDrop' to the end 2023-03-02 15:31:06 +01:00
schlagmichdoch
a3b348d9b6 refactor all missing html ids to kebap-case 2023-03-01 21:38:13 +01:00
schlagmichdoch
4566528179 - restructure UI to use flexbox everywhere
- structure peers on desktop responsively
- make peer box scrollable when peers are overflowing + shadow
- add highlight badge to differentiate local peers into paired and not paired
- change websocket fallback warning and move to the bottom
2023-03-01 21:38:13 +01:00
schlagmichdoch
7b08973cef remove safari audio blop "hack" as it should not completely stop music that is playing in the background 2023-03-01 21:38:12 +01:00
schlagmichdoch
eda60a3d78 Merge pull request #52 from kylethedeveloper/docs
add docker-compose instructions to docs
2023-02-26 21:29:44 +01:00
schlagmichdoch
e96ca53aa4 Fix rate limit docs and set header hierarchy correctly 2023-02-26 21:28:17 +01:00
kylethedeveloper
11d6a8a372 Merge 'upstream/master' into docs 2023-02-26 01:40:59 -06:00
kylethedeveloper
75726ae5f4 resolve comments on documentation 2023-02-26 01:35:19 -06:00
kylethedeveloper
80dc36c00a merge commit 2023-02-26 01:34:37 -06:00
schlagmichdoch
765b4e65b1 Update GHCR docker docs 2023-02-25 17:32:19 +01:00
kylethedeveloper
b6238b05ae add docker-compose instructions to docs 2023-02-21 22:42:41 -06:00
14 changed files with 1415 additions and 879 deletions

View File

@@ -2,39 +2,34 @@
The easiest way to get PairDrop up and running is by using Docker.
## Deployment with Docker
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
### Image from Docker Hub
### Docker Image from Docker Hub
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
```
### Image from GHCR
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop
```
### Options / Flags
#### Options / Flags
Set options by using the following flags in the `docker run` command:
#### Port
##### Port
```
-p 127.0.0.1:8080:3000
```
> Specify the port used by the docker image
> - 3000 -> `-p 127.0.0.1:3000:3000`
> - 8080 -> `-p 127.0.0.1:8080:3000`
#### Rate limiting requests
##### Rate limiting requests
```
-e RATE_LIMIT=true
```
> Limits clients to 100 requests per 5 min
> Limits clients to 1000 requests per 5 min
#### Websocket Fallback (for VPN)
##### Websocket Fallback (for VPN)
```
-e WS_FALLBACK=true
```
@@ -47,10 +42,18 @@ Set options by using the following flags in the `docker run` command:
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
> Additionally, beware that all traffic using this fallback debits the servers data plan.
<br>
### Docker Image from GHCR
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
```
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
>
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
## Deployment with Docker with self-built image
### Build the image
### Docker Image self-built
#### Build the image
```bash
docker build --pull . -f Dockerfile -t pairdrop
```
@@ -58,15 +61,45 @@ docker build --pull . -f Dockerfile -t pairdrop
>
> `--pull` ensures always the latest node image is used.
### Run the image
#### Run the image
```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
```
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
>
> To specify options replace `npm run start:prod` according to [the documentation above.](#options--flags)
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
<br>
## Deployment with Docker Compose
Here's an example docker-compose file:
```yaml
version: "2"
services:
pairdrop:
image: lscr.io/linuxserver/pairdrop:latest
container_name: pairdrop
restart: unless-stopped
environment:
- PUID=1000 # UID to run the application as
- PGID=1000 # GID to run the application as
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
- TZ=Etc/UTC # Time Zone
ports:
- 127.0.0.1:3000:3000 # Web UI
```
Run the compose file with `docker compose up -d`.
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
<br>
## Deployment with node
@@ -117,7 +150,7 @@ npm start -- --localhost-only
>
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
>
> Use this when deploying PairDrop with node to prevent bypassing the proxy and reach the docker container directly.
> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly.
#### Automatic restart on error
```bash
@@ -131,7 +164,7 @@ npm start -- --auto-restart
```bash
npm start -- --rate-limit
```
> Limits clients to 100 requests per 5 min
> Limits clients to 1000 requests per 5 min
<br>

View File

@@ -1,6 +1,7 @@
const process = require('process')
const crypto = require('crypto')
const {spawn} = require('child_process')
const WebSocket = require('ws');
// Handle SIGINT
process.on('SIGINT', () => {
@@ -99,7 +100,6 @@ const { uniqueNamesGenerator, animals, colors } = require('unique-names-generato
class PairDropServer {
constructor() {
const WebSocket = require('ws');
this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
@@ -110,10 +110,10 @@ class PairDropServer {
}
_onConnection(peer) {
this._joinRoom(peer);
peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e);
this._keepAlive(peer);
this._joinRoom(peer);
// send displayName
this._send(peer, {
@@ -317,6 +317,10 @@ class PairDropServer {
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.ip : roomSecret;
if (this._rooms[room] && this._rooms[room][peer.id]) {
this._leaveRoom(peer, roomType, roomSecret);
}
// if room doesn't exist, create it
if (!this._rooms[room]) {
this._rooms[room] = {};
@@ -341,10 +345,6 @@ class PairDropServer {
// delete the peer
delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room];

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "pairdrop",
"version": "1.1.3",
"version": "1.2.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.1.3",
"version": "1.2.1",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.1.3",
"version": "1.2.1",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -69,162 +69,176 @@
<use xlink:href="#clear-pair-devices-icon" />
</svg>
</a>
<a id="cancel-paste-mode" class="button" hidden>Done</a>
</header>
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
<!-- Peers -->
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div>
</x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
<p id="pasteFilename"></p>
</x-instructions>
<!-- Center -->
<div id="center">
<!-- Peers -->
<div class="x-peers-filler"></div>
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div>
</x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
<p id="paste-filename"></p>
</x-instructions>
</div>
<!-- Footer -->
<footer class="column">
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<div id="displayName" placeholder="&nbsp;"></div>
<div id="display-name" placeholder="&nbsp;"></div>
<div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
</div>
</footer>
<!-- Pair Device Dialog -->
<x-dialog id="pairDeviceDialog">
<x-dialog id="pair-device-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Pair Devices</h2>
<div class="center" id="roomKeyQrCode"></div>
<h1 class="center" id="roomKey">000 000</h1>
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<div id="room-key-qr-code" class="center"></div>
<h1 id="room-key" class="center">000 000</h1>
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr>
<div id="keyInputContainer">
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<div id="key-input-container">
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between">
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clearDevicesDialog">
<x-dialog id="clear-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="row-reverse space-between">
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Receive Request Dialog -->
<x-dialog id="receiveRequestDialog">
<x-dialog id="receive-request-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center">PairDrop</h2>
<div class="text-center file-description">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span id="requestingPeerDisplayName"></span>
<span class="display-name"></span>
<span>would like to share</span>
</div>
<div class="row" id="fileName">
<span id="fileStem"></span>
<span id="fileExtension"></span>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row">
<span id="fileOther"></span>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div>
<div class="row-reverse space-between">
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
<div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Receive File Dialog -->
<x-dialog id="receiveFileDialog">
<x-dialog id="receive-file-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center" id="receiveTitle"></h2>
<div class="text-center file-description"></div>
<div class="font-body2 text-center file-size"></div>
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div>
<div class="row-reverse space-between">
<a class="button" id="shareOrDownload" autofocus></a>
<div class="separator"></div>
<div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Send Text Dialog -->
<x-dialog id="sendTextDialog">
<x-dialog id="send-text-dialog">
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<h2>PairDrop - Send a Message</h2>
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse">
<h2 class="text-center">Send Message</h2>
<div class="dialog-subheader text-center">
<span>Send a Message to</span>
<span class="display-name"></span>
</div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<div class="separator"></div>
<a class="button" title="ESCAPE" close>Cancel</a>
<button class="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Receive Text Dialog -->
<x-dialog id="receiveTextDialog">
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2>PairDrop - Message Received</h2>
<div id="receiveTextDescriptionContainer">
<span id="receiveTextPeerDisplayName"></span>
<span>sent the following message:</span>
<h2 class="text-center">Message Received</h2>
<div class="text-center dialog-subheader">
<span class="display-name"></span>
<span>has sent:</span>
</div>
<div class="row-separator"></div>
<div id="text"></div>
<div class="row-reverse">
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button class="button" id="close" title="ESCAPE">Close</button>
<div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<button id="close" class="button" title="ESCAPE">Close</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- base64PasteDialog Dialog -->
<x-dialog id="base64PasteDialog">
<!-- base64 Paste Dialog -->
<x-dialog id="base64-paste-dialog">
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<button class="button center" close>Close</button>
</x-paper>
</x-background>
</x-dialog>
<!-- Toast -->
<div class="toast-container full center">
<x-toast class="row" shadow="1" id="toast"></x-toast>
<x-toast id="toast" class="row" shadow="1"></x-toast>
</div>
<!-- About Page -->
<x-about id="about" class="full center column">

View File

@@ -320,7 +320,6 @@ class Peer {
return;
}
message = JSON.parse(message);
console.log('RTC:', message);
switch (message.type) {
case 'request':
this._onFilesTransferRequest(message);
@@ -441,7 +440,7 @@ class Peer {
if (!this._requestAccepted.header.length) {
this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = [];
this._requestAccepted = null;
}
@@ -492,6 +491,7 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
}
@@ -568,6 +568,14 @@ class RTCPeer extends Peer {
this._channel = channel;
}
_onMessage(message) {
if (typeof message === 'string') {
message = JSON.parse(message);
console.log('RTC:', message);
}
super._onMessage(message);
}
getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");

View File

@@ -1,6 +1,5 @@
const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(query);
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
@@ -10,7 +9,7 @@ window.pasteMode.activated = false;
// set display name
Events.on('display-name', e => {
const me = e.detail.message;
const $displayName = $('displayName')
const $displayName = $('display-name')
$displayName.textContent = 'You are known as ' + me.displayName;
$displayName.title = me.deviceName;
});
@@ -28,7 +27,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {};
this.$cancelPasteModeBtn = $('cancelPasteModeBtn');
this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e));
@@ -38,8 +37,12 @@ class PeersUI {
Events.on('drop', e => this._onDrop(e));
Events.on('keydown', e => this._onKeyDown(e));
this.$xPeers = $$('x-peers');
this.$xNoPeers = $$('x-no-peers');
this.$xInstructions = $$('x-instructions');
Events.on('peer-added', _ => this.evaluateOverflowing());
Events.on('bg-resize', _ => this.evaluateOverflowing());
}
_onKeyDown(e) {
@@ -53,11 +56,11 @@ class PeersUI {
}
_joinPeer(peer, roomType, roomSecret) {
peer.roomType = roomType;
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
if (this.peers[peer.id]) {
this.peers[peer.id].roomType = peer.roomType;
this._redrawPeer(peer);
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(this.peers[peer.id]);
return; // peer already exists
}
this.peers[peer.id] = peer;
@@ -72,7 +75,15 @@ class PeersUI {
const peerNode = $(peer.id);
if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret');
peerNode.classList.add(`type-${peer.roomType}`)
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
}
evaluateOverflowing() {
if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {
this.$xPeers.classList.add('overflowing');
} else {
this.$xPeers.classList.remove('overflowing');
}
}
_onPeers(msg) {
@@ -83,6 +94,7 @@ class PeersUI {
const $peer = $(peerId);
if (!$peer) return;
$peer.remove();
this.evaluateOverflowing();
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
}
@@ -213,6 +225,18 @@ class PeersUI {
class PeerUI {
constructor(peer, connectionHash) {
this._peer = peer;
this._connectionHash = connectionHash;
this._initDom();
this._bindListeners();
$$('x-peers').appendChild(this.$el)
Events.fire('peer-added');
this.$xInstructions = $$('x-instructions');
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
}
html() {
let title;
let input = '';
@@ -225,17 +249,24 @@ class PeerUI {
this.$el.innerHTML = `
<label class="column center" title="${title}">
${input}
<x-icon shadow="1">
<svg class="icon"><use xlink:href="#"/></svg>
<x-icon>
<div class="icon-wrapper" shadow="1">
<svg class="icon"><use xlink:href="#"/></svg>
</div>
<div class="highlight-wrapper center">
<div class="highlight" shadow="1"></div>
</div>
</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>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
<div class="device-descriptor">
<div class="name font-subheading"></div>
<div class="device-name font-body2"></div>
<div class="status font-body2"></div>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
</div>
</label>`;
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
@@ -245,23 +276,12 @@ class PeerUI {
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
}
constructor(peer, connectionHash) {
this._peer = peer;
this._roomType = peer.roomType;
this._roomSecret = peer.roomSecret;
this._connectionHash = connectionHash;
this._initDom();
this._bindListeners();
$$('x-peers').appendChild(this.$el);
this.$xInstructions = $$('x-instructions');
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
}
_initDom() {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this.$el.classList.add(`type-${this._roomType}`);
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.$el.classList.add('center');
this.html();
this._callbackInput = e => this._onFilesSelected(e)
@@ -272,7 +292,7 @@ class PeerUI {
this._callbackDragLeave = e => this._onDragEnd(e)
this._callbackDragOver = e => this._onDragOver(e)
this._callbackContextMenu = e => this._onRightClick(e)
this._callbackTouchStart = _ => this._onTouchStart()
this._callbackTouchStart = e => this._onTouchStart(e)
this._callbackTouchEnd = e => this._onTouchEnd(e)
this._callbackPointerDown = e => this._onPointerDown(e)
// PasteMode
@@ -393,21 +413,28 @@ class PeerUI {
_onRightClick(e) {
e.preventDefault();
Events.fire('text-recipient', this._peer.id);
Events.fire('text-recipient', {
peerId: this._peer.id,
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
});
}
_onTouchStart() {
_onTouchStart(e) {
this._touchStart = Date.now();
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 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);
} else if (this._touchTimer) { // this was a long tap
e.preventDefault();
Events.fire('text-recipient', {
peerId: this._peer.id,
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
});
}
this._touchTimer = null;
}
}
@@ -445,10 +472,14 @@ class Dialog {
class ReceiveDialog extends Dialog {
constructor(id) {
super(id);
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
this.$fileSizeNode = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview')
this.$fileDescription = this.$el.querySelector('.file-description');
this.$displayName = this.$el.querySelector('.display-name');
this.$fileStem = this.$el.querySelector('.file-stem');
this.$fileExtension = this.$el.querySelector('.file-extension');
this.$fileOther = this.$el.querySelector('.file-other');
this.$fileSize = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview');
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
}
_formatFileSize(bytes) {
@@ -464,31 +495,52 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes';
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file';
} else {
fileOtherText += imagesOnly ? 'images' : 'files';
}
this.$fileOther.innerText = fileOtherText;
}
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
class ReceiveFileDialog extends ReceiveDialog {
constructor() {
super('receiveFileDialog');
super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload');
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle')
this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$shareBtn = this.$el.querySelector('#share-btn');
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
this._filesQueue = [];
}
_onFilesReceived(sender, files, request) {
this._nextFiles(sender, files, request);
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
_nextFiles(sender, nextFiles, nextRequest) {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peerId, files, request} = this._filesQueue.shift();
this._displayFiles(peerId, files, request);
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
}
_dequeueFile() {
@@ -519,7 +571,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.classList.add('element-preview');
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
@@ -530,30 +581,32 @@ class ReceiveFileDialog extends ReceiveDialog {
});
}
async _displayFiles(peerId, files, request) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
let url;
let title;
let filenameDownload;
let descriptor = request.imagesOnly ? "Image" : "File";
let size = this._formatFileSize(request.totalSize);
let description = files[0].name;
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
url = URL.createObjectURL(files[0])
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
descriptor = imagesOnly ? 'Image' : 'File';
} else {
title = `PairDrop - ${files.length} ${descriptor}s Received`
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
if(files.length>2) description += "s";
descriptor = imagesOnly ? 'Images' : 'Files';
}
this.$receiveTitle.innerText = `${descriptor} Received`;
if(!shareInsteadOfDownload) {
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) {
this.$shareBtn.removeAttribute('hidden');
this.$shareBtn.onclick = _ => {
navigator.share({files: files})
.catch(err => {
console.error(err);
});
}
}
let downloadZipped = false;
if (files.length > 1) {
downloadZipped = true;
try {
let bytesCompleted = 0;
zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) {
@@ -561,7 +614,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => {
Events.fire('set-progress', {
peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize,
progress: (bytesCompleted + progress) / totalSize,
status: 'process'
})
}
@@ -581,47 +634,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
}
}
this.$receiveTitleNode.textContent = title;
this.$fileDescriptionNode.textContent = description;
this.$fileSizeNode.textContent = size;
if (shareInsteadOfDownload) {
this.$shareOrDownloadBtn.innerText = "Share";
this.continue = _ => {
navigator.share({files: files})
.catch(err => console.error(err));
this.$downloadBtn.innerText = "Download";
this.$downloadBtn.onclick = _ => {
if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
tmpZipBtn.download = filenameDownload;
tmpZipBtn.href = url;
tmpZipBtn.click();
} else {
this._downloadFilesIndividually(files);
}
this.continueCallback = _ => this.continue();
} else {
this.$shareOrDownloadBtn.innerText = "Download again";
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
if (!canShare) {
this.$downloadBtn.innerText = "Download again";
}
Events.fire('notify-user', `${descriptor} downloaded successfully`);
this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = `PairDrop - ${files.length} Files received`;
document.title = files.length === 1
? 'File received - PairDrop'
: `(${files.length}) Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.continue();
this.show();
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() {
this.$shareOrDownloadBtn.removeAttribute('href');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$shareBtn.setAttribute('hidden', '');
this.$previewBox.innerHTML = '';
super.hide();
this._dequeueFile();
@@ -631,15 +695,10 @@ class ReceiveFileDialog extends ReceiveDialog {
class ReceiveRequestDialog extends ReceiveDialog {
constructor() {
super('receiveRequestDialog');
super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName');
this.$fileStemNode = this.$el.querySelector('#fileStem');
this.$fileExtensionNode = this.$el.querySelector('#fileExtension');
this.$fileOtherNode = this.$el.querySelector('#fileOther');
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest');
this.$declineRequestBtn = this.$el.querySelector('#declineRequest');
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
@@ -669,32 +728,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
const fileName = request.header[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtensionNode.innerText = fileExtension
if (request.header.length >= 2) {
let fileOtherText = ` and ${request.header.length - 1} other `;
fileOtherText += request.imagesOnly ? 'image' : 'file';
if (request.header.length > 2) fileOtherText += "s";
this.$fileOtherNode.innerText = fileOtherText;
}
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element)
}
document.title = 'PairDrop - File Transfer Requested';
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
@@ -720,16 +765,16 @@ class ReceiveRequestDialog extends ReceiveDialog {
class PairDeviceDialog extends Dialog {
constructor() {
super('pairDeviceDialog');
super('pair-device-dialog');
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
this.$roomKey = this.$el.querySelector('#roomKey');
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', _ => this._onSubmit());
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@@ -799,7 +844,7 @@ class PairDeviceDialog extends Dialog {
}
evaluateRoomKeyChars() {
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) {
this.$submitBtn.setAttribute("disabled", "");
} else {
this.inputRoomKey = "";
@@ -808,7 +853,7 @@ class PairDeviceDialog extends Dialog {
})
this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit();
this._pairDeviceJoin(this.inputRoomKey);
}
}
}
@@ -843,7 +888,7 @@ class PairDeviceDialog extends Dialog {
height: 150,
padding: 0,
background: "transparent",
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
color: `rgb(var(--text-color))`,
ecl: "L",
join: true
});
@@ -858,7 +903,8 @@ class PairDeviceDialog extends Dialog {
return url.href;
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey);
}
@@ -935,23 +981,29 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
}
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable());
}
}
class ClearDevicesDialog extends Dialog {
constructor() {
super('clearDevicesDialog');
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
}
_onClearPairDevices() {
this.show();
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
}
@@ -959,12 +1011,13 @@ class ClearDevicesDialog extends Dialog {
class SendTextDialog extends Dialog {
constructor() {
super('sendTextDialog');
Events.on('text-recipient', e => this._onRecipient(e.detail));
this.$text = this.$el.querySelector('#textInput');
super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('.display-name');
this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send());
this.$form.addEventListener('submit', e => this._onSubmit(e));
this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e));
}
@@ -992,8 +1045,9 @@ class SendTextDialog extends Dialog {
}
}
_onRecipient(peerId) {
_onRecipient(peerId, deviceName) {
this.correspondingPeerId = peerId;
this.$peerDisplayName.innerText = deviceName;
this.show();
const range = document.createRange();
@@ -1005,6 +1059,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range);
}
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() {
Events.fire('send-text', {
to: this.correspondingPeerId,
@@ -1017,7 +1076,7 @@ class SendTextDialog extends Dialog {
class ReceiveTextDialog extends Dialog {
constructor() {
super('receiveTextDialog');
super('receive-text-dialog');
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
this.$text = this.$el.querySelector('#text');
this.$copy = this.$el.querySelector('#copy');
@@ -1028,7 +1087,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName');
this.$displayNameNode = this.$el.querySelector('.display-name');
this._receiveTextQueue = [];
}
@@ -1046,6 +1105,7 @@ class ReceiveTextDialog extends Dialog {
_onText(text, peerId) {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return;
this._dequeueRequests();
}
@@ -1057,23 +1117,35 @@ class ReceiveTextDialog extends Dialog {
}
_showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) {
const $a = document.createElement('a');
$a.href = text;
$a.target = '_blank';
$a.textContent = text;
this.$text.innerHTML = '';
this.$text.appendChild($a);
} else {
this.$text.textContent = text;
this.$text.innerText = text;
this.$text.classList.remove('text-center');
// Beautify text if text is short
if (text.length < 2000) {
// replace urls with actual links
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
return `<a href="${url}" target="_blank">${url}</a>`;
});
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
}
}
document.title = 'PairDrop - Message Received';
this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
_setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop'
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`;
}
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
Events.fire('notify-user', 'Copied to clipboard');
@@ -1089,13 +1161,13 @@ class ReceiveTextDialog extends Dialog {
class Base64ZipDialog extends Dialog {
constructor() {
super('base64PasteDialog');
super('base64-paste-dialog');
const urlParams = new URL(window.location).searchParams;
const base64Text = urlParams.get('base64text');
const base64Zip = urlParams.get('base64zip');
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
if (base64Text) {
this.show();
@@ -1146,7 +1218,7 @@ class Base64ZipDialog extends Dialog {
}
_setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
}
@@ -1246,6 +1318,7 @@ class Notifications {
this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission());
}
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files));
}
@@ -1290,7 +1363,7 @@ class Notifications {
_messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName();
if (isURL(message)) {
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true));
} else {
@@ -1321,7 +1394,7 @@ class Notifications {
}
_download(notification) {
$('shareOrDownload').click();
$('share-or-download').click();
notification.close();
}
@@ -1710,23 +1783,19 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset;
function init() {
w = window.innerWidth;
h = window.innerHeight;
w = document.documentElement.clientWidth;
h = document.documentElement.clientHeight;
c.width = w;
c.height = h;
offset = h > 800
? 116
: h > 380
? 100
: 65;
if (w < 420) offset += 20;
offset = $$('footer').offsetHeight - 32;
if (h > 800) offset += 16;
x0 = w / 2;
y0 = h - offset;
dw = Math.max(w, h, 1000) / 13;
drawCircles();
}
window.onresize = init;
Events.on('bg-resize', _ => init());
window.onresize = _ => Events.fire('bg-resize');
function drawCircle(radius) {
ctx.beginPath();
@@ -1791,9 +1860,3 @@ 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 = _ => { // safari hack to fix audio
document.body.onclick = null;
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
blop.play();
}

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.1.3';
const cacheVersion = 'v1.2.1';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',

View File

@@ -10,28 +10,25 @@
/* Layout */
html {
min-height: 100%;
height: -webkit-fill-available;
}
html,
body {
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
width: 100vw;
overflow-x: hidden;
overscroll-behavior-y: none;
overscroll-behavior: none;
overflow-y: hidden;
}
body {
min-height: 100%;
min-height: 100vh;
/* mobile viewport bug fix */
min-height: -webkit-fill-available;
flex-grow: 1;
align-items: center;
justify-content: center;
overflow-y: hidden;
}
html {
height: -webkit-fill-available;
}
.row-reverse {
@@ -73,10 +70,7 @@ body {
}
header {
position: absolute;
top: 0;
left: 0;
right: 0;
position: relative;
height: 56px;
align-items: center;
padding: 16px;
@@ -119,9 +113,9 @@ h3 {
}
.font-subheading {
font-size: 16px;
font-size: 14px;
font-weight: 400;
line-height: 24px;
line-height: 18px;
word-break: normal;
}
@@ -199,20 +193,151 @@ body>header a {
margin-left: 8px;
}
#center {
position: relative;
display: flex;
flex-direction: column-reverse;
flex-grow: 1;
--footer-height: 132px;
max-height: calc(100vh - 56px - var(--footer-height));
justify-content: space-around;
align-items: center;
overflow-x: hidden;
overflow-y: scroll;
overscroll-behavior-x: none;
}
@media screen and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center {
--footer-height: 150px;
}
}
/* Peers List */
#x-peers-filler {
display: flex;
flex-grow: 1;
}
x-peers {
width: 100%;
overflow: hidden;
position: relative;
display: flex;
flex-flow: row wrap;
flex-grow: 1;
align-items: start !important;
justify-content: center;
z-index: 2;
transition: color 300ms;
transition: --bg-color 0.5s ease;
overflow-y: scroll;
overflow-x: hidden;
overscroll-behavior-x: none;
scrollbar-width: none;
--peers-per-row: 6; /* default if browser does not support :has selector */
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
width: var(--x-peers-width);
margin-right: 20px;
margin-left: 20px;
}
x-peers.overflowing {
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
x-peers:has(> x-peer) {
--peers-per-row: 10;
}
@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px),
screen and (min-height: 486px) and (max-height: 631px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(7)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 10;
}
}
@media screen and (min-height: 649px) and (max-width: 425px),
screen and (min-height: 631px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(28)) {
--peers-per-row: 10;
}
}
::-webkit-scrollbar {
display: none;
}
/* Empty Peers List */
x-no-peers {
height: 114px;
display: flex;
flex-direction: column;
padding: 8px;
text-align: center;
/* prevent flickering on load */
@@ -254,25 +379,19 @@ x-no-peers[drop-bg] * {
x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
}
x-peer label {
width: var(--peer-width);
padding: 8px;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative;
}
x-peer .name {
width: var(--peer-width);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
input[type="file"] {
visibility: hidden;
position: absolute;
@@ -280,21 +399,45 @@ input[type="file"] {
x-peer x-icon {
--icon-size: 40px;
margin-bottom: 4px;
transition: transform 150ms;
will-change: transform;
display: flex;
flex-direction: column;
}
x-peer .icon-wrapper {
width: var(--icon-size);
padding: 12px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
margin-bottom: 8px;
transition: transform 150ms;
will-change: transform;
}
x-peer:not(.type-ip) x-icon {
x-peer:not(.type-ip).type-secret .icon-wrapper {
background: var(--paired-device-color);
}
x-peer x-icon > .highlight-wrapper {
align-self: center;
align-items: center;
margin: 7px auto 0;
height: 6px;
}
x-peer x-icon > .highlight-wrapper > .highlight {
width: 6px;
height: 6px;
border-radius: 50%;
display: none;
}
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
background-color: var(--paired-device-color);
display: inline;
}
x-peer:not([status]):hover x-icon,
x-peer:not([status]):focus x-icon {
transform: scale(1.05);
@@ -306,6 +449,18 @@ x-peer[status] x-icon {
transform: scale(1);
}
.device-descriptor {
text-align: center;
}
.name {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.status,
.device-name,
.connection-hash {
@@ -371,10 +526,9 @@ x-peer[drop] x-icon {
/* Footer */
footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
position: relative;
margin-top: auto;
z-index: 2;
align-items: center;
padding: 0 0 16px 0;
text-align: center;
@@ -385,6 +539,7 @@ footer .logo {
--icon-size: 80px;
margin-bottom: 8px;
color: var(--primary-color);
margin-top: -10px;
}
footer .font-body2 {
@@ -409,7 +564,7 @@ x-dialog x-background {
z-index: 10;
transition: opacity 300ms;
will-change: opacity;
padding: 35px;
padding: 15px;
overflow: overlay;
}
@@ -420,16 +575,20 @@ x-dialog x-paper {
padding: 16px 24px;
width: 100%;
max-width: 400px;
overflow: hidden;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
#pairDeviceDialog x-paper {
#pair-device-dialog x-paper {
display: flex;
flex-direction: column;
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
}
x-dialog:not([show]) {
@@ -444,12 +603,6 @@ x-dialog:not([show]) x-background {
opacity: 0;
}
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a {
color: var(--primary-color);
@@ -461,13 +614,13 @@ x-dialog .font-subheading {
/* PairDevicesDialog */
#keyInputContainer {
#key-input-container {
width: 100%;
display: flex;
justify-content: center;
}
#keyInputContainer>input {
#key-input-container>input {
width: 45px;
height: 45px;
font-size: 30px;
@@ -483,15 +636,15 @@ x-dialog .font-subheading {
justify-content: center;
}
#keyInputContainer>input + * {
#key-input-container>input + * {
margin-left: 6px;
}
#keyInputContainer>input:nth-of-type(4) {
margin-left: 18px;
#key-input-container>input:nth-of-type(4) {
margin-left: 5%;
}
#roomKey {
#room-key {
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block;
@@ -499,19 +652,15 @@ x-dialog .font-subheading {
margin: 15px -15px;
}
#roomKeyQrCode {
padding: inherit;
margin: auto;
width: 150px;
height: 150px;
#room-key-qr-code {
margin: 16px;
}
#pairDeviceDialog hr {
margin-top: 40px;
margin-bottom: 40px;
#pair-device-dialog hr {
margin: 40px -24px;
}
#pairDeviceDialog x-background {
#pair-device-dialog x-background {
padding: 16px!important;
}
@@ -522,29 +671,24 @@ x-dialog .row {
margin-bottom: 8px;
}
x-dialog h2 {
margin-top: 1rem;
}
#receiveRequestDialog h2,
#receiveFileDialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px auto;
/* button row*/
x-paper > div:last-child {
margin: auto -24px -15px;
border-top: solid 2.5px var(--border-color);
height: 50px;
}
.separator {
border: solid 1.25px var(--border-color);
margin-bottom: -16px;
x-paper > div:last-child > .button {
height: 100%;
width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
}
.file-description {
word-break: break-word;
width: 80%;
margin: auto;
margin-bottom: 25px;
}
.file-description .row {
@@ -556,52 +700,52 @@ x-dialog .row-reverse {
word-break: normal;
}
#fileName {
.file-name {
font-style: italic;
max-width: 100%;
}
#fileStem {
max-width: 80%;
.file-stem {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
white-space: nowrap;
}
/* Send Text Dialog */
#textInput {
min-height: 120px;
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input {
min-height: 200px;
margin: 14px auto;
}
/* Receive Text Dialog */
#receiveTextDialog #text {
#receive-text-dialog #text {
width: 100%;
word-break: break-all;
max-height: 300px;
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
white-space: pre-wrap;
margin-top:36px;
padding: 15px 0;
}
#receiveTextDialog #text a {
#receive-text-dialog #text a {
cursor: pointer;
}
#receiveTextDialog #text a:hover {
#receive-text-dialog #text a:hover {
text-decoration: underline;
}
#receiveTextDialog h3 {
#receive-text-dialog h3 {
/* Select the received text when double-clicking the dialog */
user-select: none;
pointer-events: none;
@@ -609,29 +753,25 @@ x-dialog .row-reverse {
.row-separator {
border-bottom: solid 2.5px var(--border-color);
margin: auto -25px;
margin: auto -24px;
}
#receiveTextDescriptionContainer {
margin-bottom: 25px;
}
#base64PasteBtn {
#base64-paste-btn {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
}
#base64PasteDialog button {
#base64-paste-dialog button {
margin: auto;
border-radius: 8px;
}
#base64PasteDialog button[close] {
#base64-paste-dialog button[close] {
margin-top: 20px;
}
#base64PasteDialog button[close]:before {
#base64-paste-dialog button[close]:before {
border-radius: 8px;
}
@@ -641,7 +781,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0;
box-sizing: border-box;
min-height: 36px;
min-width: 100px;
font-size: 14px;
line-height: 24px;
font-weight: 700;
@@ -652,6 +791,7 @@ x-dialog .row-reverse {
user-select: none;
background: inherit;
color: var(--primary-color);
overflow: hidden;
}
.button[disabled] {
@@ -689,16 +829,18 @@ x-dialog .row-reverse {
opacity: 0.1;
}
#cancelPasteModeBtn {
#cancel-paste-mode {
z-index: 2;
margin-top: 0;
margin: 0;
padding: 0;
position: absolute;
top: 0;
right: 0;
left: 0;
width: 100%;
width: 100vw;
height: 56px;
border-bottom: solid 2.5px var(--border-color);
background-color: var(--primary-color);
color: rgb(238, 238, 238);
}
.button:focus:before,
@@ -714,7 +856,6 @@ button::-moz-focus-inner {
/* Icon Button */
.icon-button {
width: 40px;
height: 40px;
@@ -724,10 +865,7 @@ button::-moz-focus-inner {
border-radius: 50%;
}
/* Text Input */
.textarea {
box-sizing: border-box;
border: none;
@@ -741,9 +879,8 @@ button::-moz-focus-inner {
display: block;
overflow: auto;
resize: none;
min-height: 40px;
line-height: 16px;
max-height: 300px;
max-height: calc(100vh - 254px);
white-space: pre;
}
@@ -809,7 +946,7 @@ button::-moz-focus-inner {
width: 80px;
height: 80px;
position: absolute;
top: 0;
top: -8px;
clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg);
transition: transform 200ms;
@@ -876,13 +1013,16 @@ x-toast:not([show]):not(:hover) {
/* Instructions */
x-instructions {
position: absolute;
top: 120px;
position: relative;
opacity: 0.5;
transition: opacity 300ms;
z-index: -1;
text-align: center;
width: 80%;
margin-left: 10px;
margin-right: 10px;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
}
x-instructions:not([drop-peer]):not([drop-bg]):before {
@@ -899,88 +1039,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
x-instructions p {
display: none;
margin: 0 auto auto;
max-width: 80%;
}
x-peers:empty~x-instructions {
opacity: 0;
}
@media (hover: none) and (pointer: coarse) {
x-peer {
transform: scale(0.95);
padding: 4px 0;
}
}
#websocket-fallback {
margin-left: 5px;
margin-right: 5px;
padding: 5px;
text-align: center;
opacity: 0.5;
transition: opacity 300ms;
}
#websocket-fallback>span {
margin: 2px;
}
#websocket-fallback > span > span {
border-bottom: solid 4px var(--ws-peer-color);
}
/* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
padding: 15px;
}
x-paper > div:last-child {
margin: auto -15px -15px;
}
}
@media (min-height: 800px) {
@media screen and (min-height: 800px) {
footer {
margin-bottom: 16px;
}
}
@media screen and (min-height: 800px),
screen and (min-width: 1100px) {
@media (hover: hover) and (pointer: fine) {
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(desktop);
}
}
@media (max-height: 420px) {
x-instructions {
top: 24px;
}
footer .logo {
--icon-size: 40px;
}
}
/*
iOS specific styles
*/
@supports (-webkit-overflow-scrolling: touch) {
html {
position: fixed;
}
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(mobile);
}
}
/*
Color Themes
*/
/* Default colors */
body {
--text-color: #333;
--bg-color: #fff;
--text-color: 51,51,51;
--bg-color: 250,250,250; /*rgb code*/
--bg-color-test: 18,18,18;
--bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8;
}
/* Dark theme colors */
body.dark-theme {
--text-color: #eee;
--bg-color: #121212;
--text-color: 238,238,238;
--bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333;
--border-color: #252525;
}
/* Colored Elements */
body {
color: var(--text-color);
background-color: var(--bg-color);
color: rgb(var(--text-color));
background-color: rgb(var(--bg-color));
transition: background-color 0.5s ease;
}
x-dialog x-paper {
background-color: var(--bg-color);
background-color: rgb(var(--bg-color));
}
.textarea {
color: var(--text-color) !important;
color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important;
}
@@ -1006,7 +1150,9 @@ x-dialog x-paper {
display: none;
}
.element-preview {
.file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%;
max-height: 40vh;
margin: auto;
@@ -1018,16 +1164,16 @@ x-dialog x-paper {
/* defaults to dark theme */
body {
--text-color: #eee;
--bg-color: #121212;
--text-color: 238,238,238;
--bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333;
--border-color: #252525;
}
/* Override dark mode with light mode styles if the user decides to swap */
body.light-theme {
--text-color: #333;
--bg-color: #fafafa;
--text-color: 51,51,51;
--bg-color: 250,250,250; /*rgb code*/
--bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8;
}
@@ -1045,6 +1191,15 @@ x-dialog x-paper {
}
}
/*
iOS specific styles
*/
@supports (-webkit-overflow-scrolling: touch) {
html {
min-height: -webkit-fill-available;
}
}
/* webkit scrollbar style*/
::-webkit-scrollbar{

View File

@@ -69,165 +69,179 @@
<use xlink:href="#clear-pair-devices-icon" />
</svg>
</a>
<a id="cancel-paste-mode" class="button" hidden>Done</a>
</header>
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
<!-- Peers -->
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div>
<br>
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
</x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
<p id="pasteFilename"></p>
</x-instructions>
<!-- Center -->
<div id="center">
<!-- Peers -->
<div class="x-peers-filler"></div>
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div>
</x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
<p id="paste-filename"></p>
</x-instructions>
</div>
<!-- Footer -->
<footer class="column">
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<div id="displayName" placeholder="&nbsp;"></div>
<div id="display-name" placeholder="&nbsp;"></div>
<div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
</div>
<div id="websocket-fallback">
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span>
</div>
</footer>
<!-- Pair Device Dialog -->
<x-dialog id="pairDeviceDialog">
<x-dialog id="pair-device-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Pair Devices</h2>
<div class="center" id="roomKeyQrCode"></div>
<h1 class="center" id="roomKey">000 000</h1>
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<div id="room-key-qr-code" class="center"></div>
<h1 id="room-key" class="center">000 000</h1>
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr>
<div id="keyInputContainer">
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<div id="key-input-container">
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between">
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clearDevicesDialog">
<x-dialog id="clear-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="row-reverse space-between">
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Receive Request Dialog -->
<x-dialog id="receiveRequestDialog">
<x-dialog id="receive-request-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center">PairDrop</h2>
<div class="text-center file-description">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span id="requestingPeerDisplayName"></span>
<span class="display-name"></span>
<span>would like to share</span>
</div>
<div class="row" id="fileName">
<span id="fileStem"></span>
<span id="fileExtension"></span>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row">
<span id="fileOther"></span>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div>
<div class="row-reverse space-between">
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
<div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Receive File Dialog -->
<x-dialog id="receiveFileDialog">
<x-dialog id="receive-file-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center" id="receiveTitle"></h2>
<div class="text-center file-description"></div>
<div class="font-body2 text-center file-size"></div>
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div>
<div class="row-reverse space-between">
<a class="button" id="shareOrDownload" autofocus></a>
<div class="separator"></div>
<div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Send Text Dialog -->
<x-dialog id="sendTextDialog">
<x-dialog id="send-text-dialog">
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<h2>PairDrop - Send a Message</h2>
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse">
<h2 class="text-center">Send Message</h2>
<div class="dialog-subheader text-center">
<span>Send a Message to</span>
<span class="display-name"></span>
</div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<div class="separator"></div>
<a class="button" title="ESCAPE" close>Cancel</a>
<button class="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Receive Text Dialog -->
<x-dialog id="receiveTextDialog">
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2>PairDrop - Message Received</h2>
<div id="receiveTextDescriptionContainer">
<span id="receiveTextPeerDisplayName"></span>
<span>sent the following message:</span>
<h2 class="text-center">Message Received</h2>
<div class="text-center dialog-subheader">
<span class="display-name"></span>
<span>has sent:</span>
</div>
<div class="row-separator"></div>
<div id="text"></div>
<div class="row-reverse">
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button class="button" id="close" title="ESCAPE">Close</button>
<div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<button id="close" class="button" title="ESCAPE">Close</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- base64PasteDialog Dialog -->
<x-dialog id="base64PasteDialog">
<!-- base64 Paste Dialog -->
<x-dialog id="base64-paste-dialog">
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<button class="button center" close>Close</button>
</x-paper>
</x-background>
</x-dialog>
<!-- Toast -->
<div class="toast-container full center">
<x-toast class="row" shadow="1" id="toast"></x-toast>
<x-toast id="toast" class="row" shadow="1"></x-toast>
</div>
<!-- About Page -->
<x-about id="about" class="full center column">

View File

@@ -324,13 +324,12 @@ class Peer {
this.sendJSON({ type: 'progress', progress: progress });
}
_onMessage(message, logMessage = true) {
_onMessage(message) {
if (typeof message !== 'string') {
this._onChunkReceived(message);
return;
}
message = JSON.parse(message);
if (logMessage) console.log('RTC:', message);
switch (message.type) {
case 'request':
this._onFilesTransferRequest(message);
@@ -451,7 +450,7 @@ class Peer {
if (!this._requestAccepted.header.length) {
this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = [];
this._requestAccepted = null;
}
@@ -502,6 +501,7 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
}
@@ -578,6 +578,14 @@ class RTCPeer extends Peer {
this._channel = channel;
}
_onMessage(message) {
if (typeof message === 'string') {
message = JSON.parse(message);
console.log('RTC:', message);
}
super._onMessage(message);
}
getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
@@ -682,6 +690,7 @@ class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller
this._sendSignal();
}
@@ -700,15 +709,15 @@ class WSPeer extends Peer {
this._server.send(message);
}
_sendSignal() {
this.sendJSON({type: 'signal'});
_sendSignal(connected = false) {
this.sendJSON({type: 'signal', connected: connected});
}
onServerMessage(message) {
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._peerId) return;
if (message.connected) return;
this._peerId = message.sender.id;
this._sendSignal();
this._sendSignal(true);
}
getConnectionHash() {
@@ -730,6 +739,7 @@ class PeersManager {
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail));
}
@@ -749,7 +759,7 @@ class PeersManager {
_onWsRelay(message) {
const messageJSON = JSON.parse(message)
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message, false)
this.peers[messageJSON.sender.id]._onMessage(message)
}
_onPeers(msg) {
@@ -797,15 +807,24 @@ class PeersManager {
}
_onPeerLeft(msg) {
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) {
console.log('WSPeer left:', msg.peerId)
Events.fire('peer-disconnected', msg.peerId)
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId);
Events.fire('peer-disconnected', msg.peerId);
} else if (msg.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
}
}
_onWsDisconnected() {
for (const peerId in this.peers) {
console.debug(this.peers[peerId].rtcSupported);
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
Events.fire('peer-disconnected', peerId);
}
}
}
_onPeerDisconnected(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];

View File

@@ -1,6 +1,5 @@
const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(query);
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
@@ -10,7 +9,7 @@ window.pasteMode.activated = false;
// set display name
Events.on('display-name', e => {
const me = e.detail.message;
const $displayName = $('displayName')
const $displayName = $('display-name')
$displayName.textContent = 'You are known as ' + me.displayName;
$displayName.title = me.deviceName;
});
@@ -28,7 +27,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {};
this.$cancelPasteModeBtn = $('cancelPasteModeBtn');
this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e));
@@ -38,8 +37,12 @@ class PeersUI {
Events.on('drop', e => this._onDrop(e));
Events.on('keydown', e => this._onKeyDown(e));
this.$xPeers = $$('x-peers');
this.$xNoPeers = $$('x-no-peers');
this.$xInstructions = $$('x-instructions');
Events.on('peer-added', _ => this.evaluateOverflowing());
Events.on('bg-resize', _ => this.evaluateOverflowing());
}
_onKeyDown(e) {
@@ -53,11 +56,11 @@ class PeersUI {
}
_joinPeer(peer, roomType, roomSecret) {
peer.roomType = roomType;
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
if (this.peers[peer.id]) {
this.peers[peer.id].roomType = peer.roomType;
this._redrawPeer(peer);
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(this.peers[peer.id]);
return; // peer already exists
}
this.peers[peer.id] = peer;
@@ -72,7 +75,15 @@ class PeersUI {
const peerNode = $(peer.id);
if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret');
peerNode.classList.add(`type-${peer.roomType}`)
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
}
evaluateOverflowing() {
if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {
this.$xPeers.classList.add('overflowing');
} else {
this.$xPeers.classList.remove('overflowing');
}
}
_onPeers(msg) {
@@ -83,6 +94,7 @@ class PeersUI {
const $peer = $(peerId);
if (!$peer) return;
$peer.remove();
this.evaluateOverflowing();
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
}
@@ -213,6 +225,18 @@ class PeersUI {
class PeerUI {
constructor(peer, connectionHash) {
this._peer = peer;
this._connectionHash = connectionHash;
this._initDom();
this._bindListeners();
$$('x-peers').appendChild(this.$el)
Events.fire('peer-added');
this.$xInstructions = $$('x-instructions');
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
}
html() {
let title;
let input = '';
@@ -225,17 +249,24 @@ class PeerUI {
this.$el.innerHTML = `
<label class="column center" title="${title}">
${input}
<x-icon shadow="1">
<svg class="icon"><use xlink:href="#"/></svg>
<x-icon>
<div class="icon-wrapper" shadow="1">
<svg class="icon"><use xlink:href="#"/></svg>
</div>
<div class="highlight-wrapper center">
<div class="highlight" shadow="1"></div>
</div>
</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>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
<div class="device-descriptor">
<div class="name font-subheading"></div>
<div class="device-name font-body2"></div>
<div class="status font-body2"></div>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
</div>
</label>`;
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
@@ -245,23 +276,12 @@ class PeerUI {
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
}
constructor(peer, connectionHash) {
this._peer = peer;
this._roomType = peer.roomType;
this._roomSecret = peer.roomSecret;
this._connectionHash = connectionHash;
this._initDom();
this._bindListeners();
$$('x-peers').appendChild(this.$el);
this.$xInstructions = $$('x-instructions');
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
}
_initDom() {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this.$el.classList.add(`type-${this._roomType}`);
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.$el.classList.add('center');
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer')
this.html();
@@ -273,7 +293,7 @@ class PeerUI {
this._callbackDragLeave = e => this._onDragEnd(e)
this._callbackDragOver = e => this._onDragOver(e)
this._callbackContextMenu = e => this._onRightClick(e)
this._callbackTouchStart = _ => this._onTouchStart()
this._callbackTouchStart = e => this._onTouchStart(e)
this._callbackTouchEnd = e => this._onTouchEnd(e)
this._callbackPointerDown = e => this._onPointerDown(e)
// PasteMode
@@ -394,21 +414,28 @@ class PeerUI {
_onRightClick(e) {
e.preventDefault();
Events.fire('text-recipient', this._peer.id);
Events.fire('text-recipient', {
peerId: this._peer.id,
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
});
}
_onTouchStart() {
_onTouchStart(e) {
this._touchStart = Date.now();
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 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);
} else if (this._touchTimer) { // this was a long tap
e.preventDefault();
Events.fire('text-recipient', {
peerId: this._peer.id,
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
});
}
this._touchTimer = null;
}
}
@@ -446,10 +473,14 @@ class Dialog {
class ReceiveDialog extends Dialog {
constructor(id) {
super(id);
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
this.$fileSizeNode = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview')
this.$fileDescription = this.$el.querySelector('.file-description');
this.$displayName = this.$el.querySelector('.display-name');
this.$fileStem = this.$el.querySelector('.file-stem');
this.$fileExtension = this.$el.querySelector('.file-extension');
this.$fileOther = this.$el.querySelector('.file-other');
this.$fileSize = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview');
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
}
_formatFileSize(bytes) {
@@ -465,31 +496,52 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes';
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file';
} else {
fileOtherText += imagesOnly ? 'images' : 'files';
}
this.$fileOther.innerText = fileOtherText;
}
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
class ReceiveFileDialog extends ReceiveDialog {
constructor() {
super('receiveFileDialog');
super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload');
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle')
this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$shareBtn = this.$el.querySelector('#share-btn');
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
this._filesQueue = [];
}
_onFilesReceived(sender, files, request) {
this._nextFiles(sender, files, request);
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
_nextFiles(sender, nextFiles, nextRequest) {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peerId, files, request} = this._filesQueue.shift();
this._displayFiles(peerId, files, request);
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
}
_dequeueFile() {
@@ -520,7 +572,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.classList.add('element-preview');
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
@@ -531,30 +582,32 @@ class ReceiveFileDialog extends ReceiveDialog {
});
}
async _displayFiles(peerId, files, request) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
let url;
let title;
let filenameDownload;
let descriptor = request.imagesOnly ? "Image" : "File";
let size = this._formatFileSize(request.totalSize);
let description = files[0].name;
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
url = URL.createObjectURL(files[0])
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
descriptor = imagesOnly ? 'Image' : 'File';
} else {
title = `PairDrop - ${files.length} ${descriptor}s Received`
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
if(files.length>2) description += "s";
descriptor = imagesOnly ? 'Images' : 'Files';
}
this.$receiveTitle.innerText = `${descriptor} Received`;
if(!shareInsteadOfDownload) {
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) {
this.$shareBtn.removeAttribute('hidden');
this.$shareBtn.onclick = _ => {
navigator.share({files: files})
.catch(err => {
console.error(err);
});
}
}
let downloadZipped = false;
if (files.length > 1) {
downloadZipped = true;
try {
let bytesCompleted = 0;
zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) {
@@ -562,7 +615,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => {
Events.fire('set-progress', {
peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize,
progress: (bytesCompleted + progress) / totalSize,
status: 'process'
})
}
@@ -582,47 +635,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
}
}
this.$receiveTitleNode.textContent = title;
this.$fileDescriptionNode.textContent = description;
this.$fileSizeNode.textContent = size;
if (shareInsteadOfDownload) {
this.$shareOrDownloadBtn.innerText = "Share";
this.continue = _ => {
navigator.share({files: files})
.catch(err => console.error(err));
this.$downloadBtn.innerText = "Download";
this.$downloadBtn.onclick = _ => {
if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
tmpZipBtn.download = filenameDownload;
tmpZipBtn.href = url;
tmpZipBtn.click();
} else {
this._downloadFilesIndividually(files);
}
this.continueCallback = _ => this.continue();
} else {
this.$shareOrDownloadBtn.innerText = "Download again";
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
if (!canShare) {
this.$downloadBtn.innerText = "Download again";
}
Events.fire('notify-user', `${descriptor} downloaded successfully`);
this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = `PairDrop - ${files.length} Files received`;
document.title = files.length === 1
? 'File received - PairDrop'
: `(${files.length}) Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.continue();
this.show();
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() {
this.$shareOrDownloadBtn.removeAttribute('href');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$shareBtn.setAttribute('hidden', '');
this.$previewBox.innerHTML = '';
super.hide();
this._dequeueFile();
@@ -632,15 +696,10 @@ class ReceiveFileDialog extends ReceiveDialog {
class ReceiveRequestDialog extends ReceiveDialog {
constructor() {
super('receiveRequestDialog');
super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName');
this.$fileStemNode = this.$el.querySelector('#fileStem');
this.$fileExtensionNode = this.$el.querySelector('#fileExtension');
this.$fileOtherNode = this.$el.querySelector('#fileOther');
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest');
this.$declineRequestBtn = this.$el.querySelector('#declineRequest');
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
@@ -670,32 +729,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
const fileName = request.header[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtensionNode.innerText = fileExtension
if (request.header.length >= 2) {
let fileOtherText = ` and ${request.header.length - 1} other `;
fileOtherText += request.imagesOnly ? 'image' : 'file';
if (request.header.length > 2) fileOtherText += "s";
this.$fileOtherNode.innerText = fileOtherText;
}
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element)
}
document.title = 'PairDrop - File Transfer Requested';
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
@@ -721,16 +766,16 @@ class ReceiveRequestDialog extends ReceiveDialog {
class PairDeviceDialog extends Dialog {
constructor() {
super('pairDeviceDialog');
super('pair-device-dialog');
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
this.$roomKey = this.$el.querySelector('#roomKey');
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', _ => this._onSubmit());
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@@ -800,7 +845,7 @@ class PairDeviceDialog extends Dialog {
}
evaluateRoomKeyChars() {
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) {
this.$submitBtn.setAttribute("disabled", "");
} else {
this.inputRoomKey = "";
@@ -809,7 +854,7 @@ class PairDeviceDialog extends Dialog {
})
this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit();
this._pairDeviceJoin(this.inputRoomKey);
}
}
}
@@ -844,7 +889,7 @@ class PairDeviceDialog extends Dialog {
height: 150,
padding: 0,
background: "transparent",
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
color: `rgb(var(--text-color))`,
ecl: "L",
join: true
});
@@ -859,7 +904,8 @@ class PairDeviceDialog extends Dialog {
return url.href;
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey);
}
@@ -936,23 +982,29 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
}
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable());
}
}
class ClearDevicesDialog extends Dialog {
constructor() {
super('clearDevicesDialog');
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
}
_onClearPairDevices() {
this.show();
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
}
@@ -960,12 +1012,13 @@ class ClearDevicesDialog extends Dialog {
class SendTextDialog extends Dialog {
constructor() {
super('sendTextDialog');
Events.on('text-recipient', e => this._onRecipient(e.detail));
this.$text = this.$el.querySelector('#textInput');
super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('.display-name');
this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send());
this.$form.addEventListener('submit', e => this._onSubmit(e));
this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e));
}
@@ -993,8 +1046,9 @@ class SendTextDialog extends Dialog {
}
}
_onRecipient(peerId) {
_onRecipient(peerId, deviceName) {
this.correspondingPeerId = peerId;
this.$peerDisplayName.innerText = deviceName;
this.show();
const range = document.createRange();
@@ -1006,6 +1060,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range);
}
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() {
Events.fire('send-text', {
to: this.correspondingPeerId,
@@ -1018,7 +1077,7 @@ class SendTextDialog extends Dialog {
class ReceiveTextDialog extends Dialog {
constructor() {
super('receiveTextDialog');
super('receive-text-dialog');
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
this.$text = this.$el.querySelector('#text');
this.$copy = this.$el.querySelector('#copy');
@@ -1029,7 +1088,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName');
this.$displayNameNode = this.$el.querySelector('.display-name');
this._receiveTextQueue = [];
}
@@ -1047,6 +1106,7 @@ class ReceiveTextDialog extends Dialog {
_onText(text, peerId) {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return;
this._dequeueRequests();
}
@@ -1058,23 +1118,35 @@ class ReceiveTextDialog extends Dialog {
}
_showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) {
const $a = document.createElement('a');
$a.href = text;
$a.target = '_blank';
$a.textContent = text;
this.$text.innerHTML = '';
this.$text.appendChild($a);
} else {
this.$text.textContent = text;
this.$text.innerText = text;
this.$text.classList.remove('text-center');
// Beautify text if text is short
if (text.length < 2000) {
// replace urls with actual links
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
return `<a href="${url}" target="_blank">${url}</a>`;
});
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
}
}
document.title = 'PairDrop - Message Received';
this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
_setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop'
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`;
}
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
Events.fire('notify-user', 'Copied to clipboard');
@@ -1090,13 +1162,13 @@ class ReceiveTextDialog extends Dialog {
class Base64ZipDialog extends Dialog {
constructor() {
super('base64PasteDialog');
super('base64-paste-dialog');
const urlParams = new URL(window.location).searchParams;
const base64Text = urlParams.get('base64text');
const base64Zip = urlParams.get('base64zip');
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
if (base64Text) {
this.show();
@@ -1147,7 +1219,7 @@ class Base64ZipDialog extends Dialog {
}
_setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
}
@@ -1247,6 +1319,7 @@ class Notifications {
this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission());
}
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files));
}
@@ -1291,7 +1364,7 @@ class Notifications {
_messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName();
if (isURL(message)) {
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true));
} else {
@@ -1322,7 +1395,7 @@ class Notifications {
}
_download(notification) {
$('shareOrDownload').click();
$('share-or-download').click();
notification.close();
}
@@ -1711,23 +1784,18 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset;
function init() {
w = window.innerWidth;
h = window.innerHeight;
w = document.documentElement.clientWidth;
h = document.documentElement.clientHeight;
c.width = w;
c.height = h;
offset = h > 800
? 116
: h > 380
? 100
: 65;
if (w < 420) offset += 20;
offset = $$('footer').offsetHeight - 32;
x0 = w / 2;
y0 = h - offset;
dw = Math.max(w, h, 1000) / 13;
drawCircles();
}
window.onresize = init;
Events.on('bg-resize', _ => init());
window.onresize = _ => Events.fire('bg-resize');
function drawCircle(radius) {
ctx.beginPath();
@@ -1792,9 +1860,3 @@ 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 = _ => { // safari hack to fix audio
document.body.onclick = null;
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
blop.play();
}

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.1.3';
const cacheVersion = 'v1.2.1';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',

View File

@@ -11,28 +11,25 @@
/* Layout */
html {
min-height: 100%;
height: -webkit-fill-available;
}
html,
body {
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
width: 100vw;
overflow-x: hidden;
overscroll-behavior-y: none;
overscroll-behavior: none;
overflow-y: hidden;
}
body {
min-height: 100%;
min-height: 100vh;
/* mobile viewport bug fix */
min-height: -webkit-fill-available;
flex-grow: 1;
align-items: center;
justify-content: center;
overflow-y: hidden;
}
html {
height: -webkit-fill-available;
}
.row-reverse {
@@ -74,10 +71,7 @@ body {
}
header {
position: absolute;
top: 0;
left: 0;
right: 0;
position: relative;
height: 56px;
align-items: center;
padding: 16px;
@@ -120,9 +114,9 @@ h3 {
}
.font-subheading {
font-size: 16px;
font-size: 14px;
font-weight: 400;
line-height: 24px;
line-height: 18px;
word-break: normal;
}
@@ -200,20 +194,160 @@ body>header a {
margin-left: 8px;
}
#center {
position: relative;
display: flex;
flex-direction: column-reverse;
flex-grow: 1;
--footer-height: 146px;
max-height: calc(100vh - 56px - var(--footer-height));
justify-content: space-around;
align-items: center;
overflow-x: hidden;
overflow-y: scroll;
overscroll-behavior-x: none;
}
@media screen and (min-width: 402px) and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center {
--footer-height: 164px;
}
}
@media screen and (max-width: 402px) {
#center {
--footer-height: 184px;
}
}
/* Peers List */
#x-peers-filler {
display: flex;
flex-grow: 1;
}
x-peers {
width: 100%;
overflow: hidden;
position: relative;
display: flex;
flex-flow: row wrap;
flex-grow: 1;
align-items: start !important;
justify-content: center;
z-index: 2;
transition: color 300ms;
transition: --bg-color 0.5s ease;
overflow-y: scroll;
overflow-x: hidden;
overscroll-behavior-x: none;
scrollbar-width: none;
--peers-per-row: 6; /* default if browser does not support :has selector */
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
width: var(--x-peers-width);
margin-right: 20px;
margin-left: 20px;
}
x-peers.overflowing {
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
x-peers:has(> x-peer) {
--peers-per-row: 10;
}
/* peers-per-row if height is too small for 2 rows */
@media screen and (min-height: 538px) and (max-height: 683px) and (max-width: 402px),
screen and (min-height: 517px) and (max-height: 664px) and (max-width: 426px),
screen and (min-height: 501px) and (max-height: 647px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(7)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 10;
}
}
/* peers-per-row if height is too small for 3 rows */
@media screen and (min-height: 683px) and (max-width: 402px),
screen and (min-height: 664px) and (max-width: 426px),
screen and (min-height: 647px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(28)) {
--peers-per-row: 10;
}
}
::-webkit-scrollbar {
display: none;
}
/* Empty Peers List */
x-no-peers {
height: 114px;
display: flex;
flex-direction: column;
padding: 8px;
text-align: center;
/* prevent flickering on load */
@@ -255,25 +389,19 @@ x-no-peers[drop-bg] * {
x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
}
x-peer label {
width: var(--peer-width);
padding: 8px;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative;
}
x-peer .name {
width: var(--peer-width);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
input[type="file"] {
visibility: hidden;
position: absolute;
@@ -281,27 +409,43 @@ input[type="file"] {
x-peer x-icon {
--icon-size: 40px;
margin-bottom: 4px;
transition: transform 150ms;
will-change: transform;
display: flex;
flex-direction: column;
}
x-peer .icon-wrapper {
width: var(--icon-size);
padding: 12px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
margin-bottom: 8px;
transition: transform 150ms;
will-change: transform;
}
x-peer:not(.type-ip) x-icon {
x-peer:not(.type-ip).type-secret .icon-wrapper {
background: var(--paired-device-color);
}
x-peer.ws-peer x-icon {
border: solid 4px var(--ws-peer-color);
x-peer x-icon > .highlight-wrapper {
align-self: center;
align-items: center;
margin: 7px auto 0;
height: 6px;
}
x-peer.ws-peer .progress {
margin-top: 4px;
x-peer x-icon > .highlight-wrapper > .highlight {
width: 6px;
height: 6px;
border-radius: 50%;
display: none;
}
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
background-color: var(--paired-device-color);
display: inline;
}
x-peer:not([status]):hover x-icon,
@@ -315,6 +459,35 @@ x-peer[status] x-icon {
transform: scale(1);
}
x-peer.ws-peer {
margin-top: -1.5px;
}
x-peer.ws-peer .progress {
margin-top: 3px;
}
x-peer.ws-peer .icon-wrapper{
border: solid 3px var(--ws-peer-color);
}
x-peer.ws-peer .highlight-wrapper {
margin-top: 3px;
}
.device-descriptor {
text-align: center;
}
.name {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.status,
.device-name,
.connection-hash {
@@ -380,12 +553,10 @@ x-peer[drop] x-icon {
/* Footer */
footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
position: relative;
margin-top: auto;
z-index: 2;
align-items: center;
padding: 0 0 16px 0;
text-align: center;
transition: color 300ms;
}
@@ -394,6 +565,7 @@ footer .logo {
--icon-size: 80px;
margin-bottom: 8px;
color: var(--primary-color);
margin-top: -10px;
}
footer .font-body2 {
@@ -418,7 +590,7 @@ x-dialog x-background {
z-index: 10;
transition: opacity 300ms;
will-change: opacity;
padding: 35px;
padding: 15px;
overflow: overlay;
}
@@ -429,16 +601,20 @@ x-dialog x-paper {
padding: 16px 24px;
width: 100%;
max-width: 400px;
overflow: hidden;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
#pairDeviceDialog x-paper {
#pair-device-dialog x-paper {
display: flex;
flex-direction: column;
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
}
x-dialog:not([show]) {
@@ -453,12 +629,6 @@ x-dialog:not([show]) x-background {
opacity: 0;
}
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a {
color: var(--primary-color);
@@ -470,13 +640,13 @@ x-dialog .font-subheading {
/* PairDevicesDialog */
#keyInputContainer {
#key-input-container {
width: 100%;
display: flex;
justify-content: center;
}
#keyInputContainer>input {
#key-input-container>input {
width: 45px;
height: 45px;
font-size: 30px;
@@ -492,15 +662,15 @@ x-dialog .font-subheading {
justify-content: center;
}
#keyInputContainer>input + * {
#key-input-container>input + * {
margin-left: 6px;
}
#keyInputContainer>input:nth-of-type(4) {
margin-left: 18px;
#key-input-container>input:nth-of-type(4) {
margin-left: 5%;
}
#roomKey {
#room-key {
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block;
@@ -508,19 +678,15 @@ x-dialog .font-subheading {
margin: 15px -15px;
}
#roomKeyQrCode {
padding: inherit;
margin: auto;
width: 150px;
height: 150px;
#room-key-qr-code {
margin: 16px;
}
#pairDeviceDialog hr {
margin-top: 40px;
margin-bottom: 40px;
#pair-device-dialog hr {
margin: 40px -24px;
}
#pairDeviceDialog x-background {
#pair-device-dialog x-background {
padding: 16px!important;
}
@@ -531,29 +697,24 @@ x-dialog .row {
margin-bottom: 8px;
}
x-dialog h2 {
margin-top: 1rem;
}
#receiveRequestDialog h2,
#receiveFileDialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px auto;
/* button row*/
x-paper > div:last-child {
margin: auto -24px -15px;
border-top: solid 2.5px var(--border-color);
height: 50px;
}
.separator {
border: solid 1.25px var(--border-color);
margin-bottom: -16px;
x-paper > div:last-child > .button {
height: 100%;
width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
}
.file-description {
word-break: break-word;
width: 80%;
margin: auto;
margin-bottom: 25px;
}
.file-description .row {
@@ -565,52 +726,52 @@ x-dialog .row-reverse {
word-break: normal;
}
#fileName {
.file-name {
font-style: italic;
max-width: 100%;
}
#fileStem {
max-width: 80%;
.file-stem {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
white-space: nowrap;
}
/* Send Text Dialog */
#textInput {
min-height: 120px;
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input {
min-height: 200px;
margin: 14px auto;
}
/* Receive Text Dialog */
#receiveTextDialog #text {
#receive-text-dialog #text {
width: 100%;
word-break: break-all;
max-height: 300px;
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
white-space: pre-wrap;
margin-top:36px;
padding: 15px 0;
}
#receiveTextDialog #text a {
#receive-text-dialog #text a {
cursor: pointer;
}
#receiveTextDialog #text a:hover {
#receive-text-dialog #text a:hover {
text-decoration: underline;
}
#receiveTextDialog h3 {
#receive-text-dialog h3 {
/* Select the received text when double-clicking the dialog */
user-select: none;
pointer-events: none;
@@ -618,29 +779,25 @@ x-dialog .row-reverse {
.row-separator {
border-bottom: solid 2.5px var(--border-color);
margin: auto -25px;
margin: auto -24px;
}
#receiveTextDescriptionContainer {
margin-bottom: 25px;
}
#base64PasteBtn {
#base64-paste-btn {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
}
#base64PasteDialog button {
#base64-paste-dialog button {
margin: auto;
border-radius: 8px;
}
#base64PasteDialog button[close] {
#base64-paste-dialog button[close] {
margin-top: 20px;
}
#base64PasteDialog button[close]:before {
#base64-paste-dialog button[close]:before {
border-radius: 8px;
}
@@ -650,7 +807,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0;
box-sizing: border-box;
min-height: 36px;
min-width: 100px;
font-size: 14px;
line-height: 24px;
font-weight: 700;
@@ -661,6 +817,7 @@ x-dialog .row-reverse {
user-select: none;
background: inherit;
color: var(--primary-color);
overflow: hidden;
}
.button[disabled] {
@@ -698,16 +855,18 @@ x-dialog .row-reverse {
opacity: 0.1;
}
#cancelPasteModeBtn {
#cancel-paste-mode {
z-index: 2;
margin-top: 0;
margin: 0;
padding: 0;
position: absolute;
top: 0;
right: 0;
left: 0;
width: 100%;
width: 100vw;
height: 56px;
border-bottom: solid 2.5px var(--border-color);
background-color: var(--primary-color);
color: rgb(238, 238, 238);
}
.button:focus:before,
@@ -723,7 +882,6 @@ button::-moz-focus-inner {
/* Icon Button */
.icon-button {
width: 40px;
height: 40px;
@@ -733,10 +891,7 @@ button::-moz-focus-inner {
border-radius: 50%;
}
/* Text Input */
.textarea {
box-sizing: border-box;
border: none;
@@ -750,9 +905,8 @@ button::-moz-focus-inner {
display: block;
overflow: auto;
resize: none;
min-height: 40px;
line-height: 16px;
max-height: 300px;
max-height: calc(100vh - 254px);
white-space: pre;
}
@@ -818,7 +972,7 @@ button::-moz-focus-inner {
width: 80px;
height: 80px;
position: absolute;
top: 0;
top: -8px;
clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg);
transition: transform 200ms;
@@ -885,13 +1039,16 @@ x-toast:not([show]):not(:hover) {
/* Instructions */
x-instructions {
position: absolute;
top: 120px;
position: relative;
opacity: 0.5;
transition: opacity 300ms;
z-index: -1;
text-align: center;
width: 80%;
margin-left: 10px;
margin-right: 10px;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
}
x-instructions:not([drop-peer]):not([drop-bg]):before {
@@ -908,92 +1065,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
x-instructions p {
display: none;
margin: 0 auto auto;
max-width: 80%;
}
x-peers:empty~x-instructions {
opacity: 0;
}
.websocket-fallback {
@media (hover: none) and (pointer: coarse) {
x-peer {
transform: scale(0.95);
padding: 4px 0;
}
}
#websocket-fallback {
margin-left: 5px;
margin-right: 5px;
padding: 5px;
text-align: center;
opacity: 0.5;
transition: opacity 300ms;
}
#websocket-fallback>span {
margin: 2px;
}
#websocket-fallback > span > span {
border-bottom: solid 4px var(--ws-peer-color);
padding-bottom: 1px;
}
/* Responsive Styles */
@media (min-height: 800px) {
footer {
margin-bottom: 16px;
@media screen and (max-width: 360px) {
x-dialog x-paper {
padding: 15px;
}
x-paper > div:last-child {
margin: auto -15px -15px;
}
}
@media screen and (min-height: 800px),
screen and (min-width: 1100px) {
@media screen and (min-height: 800px) {
#websocket-fallback {
padding-bottom: 15px;
}
}
@media (hover: hover) and (pointer: fine) {
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(desktop);
}
}
@media (max-height: 420px) {
x-instructions {
top: 24px;
}
footer .logo {
--icon-size: 40px;
}
}
/*
iOS specific styles
*/
@supports (-webkit-overflow-scrolling: touch) {
html {
position: fixed;
}
x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(mobile);
}
}
/*
Color Themes
*/
/* Default colors */
body {
--text-color: #333;
--bg-color: #fff;
--text-color: 51,51,51;
--bg-color: 250,250,250; /*rgb code*/
--bg-color-test: 18,18,18;
--bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8;
}
/* Dark theme colors */
body.dark-theme {
--text-color: #eee;
--bg-color: #121212;
--text-color: 238,238,238;
--bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333;
--border-color: #252525;
}
/* Colored Elements */
body {
color: var(--text-color);
background-color: var(--bg-color);
color: rgb(var(--text-color));
background-color: rgb(var(--bg-color));
transition: background-color 0.5s ease;
}
x-dialog x-paper {
background-color: var(--bg-color);
background-color: rgb(var(--bg-color));
}
.textarea {
color: var(--text-color) !important;
color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important;
}
@@ -1019,7 +1176,9 @@ x-dialog x-paper {
display: none;
}
.element-preview {
.file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%;
max-height: 40vh;
margin: auto;
@@ -1031,16 +1190,16 @@ x-dialog x-paper {
/* defaults to dark theme */
body {
--text-color: #eee;
--bg-color: #121212;
--text-color: 238,238,238;
--bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333;
--border-color: #252525;
}
/* Override dark mode with light mode styles if the user decides to swap */
body.light-theme {
--text-color: #333;
--bg-color: #fafafa;
--text-color: 51,51,51;
--bg-color: 250,250,250; /*rgb code*/
--bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8;
}
@@ -1058,6 +1217,15 @@ x-dialog x-paper {
}
}
/*
iOS specific styles
*/
@supports (-webkit-overflow-scrolling: touch) {
html {
min-height: -webkit-fill-available;
}
}
/* webkit scrollbar style*/
::-webkit-scrollbar{