merge master into branch

This commit is contained in:
schlagmichdoch 2023-03-01 21:55:50 +01:00
commit de76da52fe
12 changed files with 952 additions and 499 deletions

View file

@ -16,7 +16,7 @@ on:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository | downcase }} IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
build-and-push-image: build-and-push-image:

View file

@ -1,32 +1,35 @@
# Deployment Notes # Deployment Notes
The easiest way to get PairDrop up and running is by using Docker. The easiest way to get PairDrop up and running is by using Docker.
## Deployment with Docker from Docker Hub ## Deployment with Docker
### Docker Image from Docker Hub
```bash ```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
``` ```
> 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)). > 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.
### Options / Flags #### Options / Flags
Set options by using the following flags in the `docker run` command: Set options by using the following flags in the `docker run` command:
#### Port ##### Port
```bash ```bash
-p 127.0.0.1:8080:3000 -p 127.0.0.1:8080:3000
``` ```
> Specify the port used by the docker image > Specify the port used by the docker image
> - 3000 -> `-p 127.0.0.1:3000:3000` > - 3000 -> `-p 127.0.0.1:3000:3000`
> - 8080 -> `-p 127.0.0.1:8080:3000` > - 8080 -> `-p 127.0.0.1:8080:3000`
#### Rate limiting requests ##### Rate limiting requests
``` ```
-e RATE_LIMIT=true -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)
```bash ```bash
-e WS_FALLBACK=true -e WS_FALLBACK=true
``` ```
@ -69,8 +72,18 @@ Set options by using the following flags in the `docker run` command:
<br> <br>
## Deployment with Docker with self-built image ### Docker Image from GHCR
### Build the image ```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)
### Docker Image self-built
#### Build the image
```bash ```bash
docker build --pull . -f Dockerfile -t pairdrop docker build --pull . -f Dockerfile -t pairdrop
``` ```
@ -78,15 +91,45 @@ docker build --pull . -f Dockerfile -t pairdrop
> >
> `--pull` ensures always the latest node image is used. > `--pull` ensures always the latest node image is used.
### Run the image #### Run the image
```bash ```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod 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)). > 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 ## Deployment with node
@ -169,7 +212,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)). > 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 #### Automatic restart on error
```bash ```bash
@ -183,7 +226,7 @@ npm start -- --auto-restart
```bash ```bash
npm start -- --rate-limit npm start -- --rate-limit
``` ```
> Limits clients to 100 requests per 5 min > Limits clients to 1000 requests per 5 min
<br> <br>
@ -218,7 +261,7 @@ When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Ot
### Using nginx ### Using nginx
#### Allow http and https requests #### Allow http and https requests
```nginx configuration ```
server { server {
listen 80; listen 80;
@ -251,7 +294,7 @@ server {
``` ```
#### Automatic http to https redirect: #### Automatic http to https redirect:
```nginx configuration ```
server { server {
listen 80; listen 80;

4
package-lock.json generated
View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ window.pasteMode.activated = false;
// set display name // set display name
Events.on('display-name', e => { Events.on('display-name', e => {
const me = e.detail.message; const me = e.detail.message;
const $displayName = $('displayName') const $displayName = $('display-name')
$displayName.textContent = 'You are known as ' + me.displayName; $displayName.textContent = 'You are known as ' + me.displayName;
$displayName.title = me.deviceName; $displayName.title = me.deviceName;
}); });
@ -28,7 +28,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {}; this.peers = {};
this.$cancelPasteModeBtn = $('cancelPasteModeBtn'); this.$cancelPasteModeBtn = $('cancel-paste-mode-btn');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e)); Events.on('dragover', e => this._onDragOver(e));
@ -38,8 +38,12 @@ class PeersUI {
Events.on('drop', e => this._onDrop(e)); Events.on('drop', e => this._onDrop(e));
Events.on('keydown', e => this._onKeyDown(e)); Events.on('keydown', e => this._onKeyDown(e));
this.$xPeers = $$('x-peers');
this.$xNoPeers = $$('x-no-peers'); this.$xNoPeers = $$('x-no-peers');
this.$xInstructions = $$('x-instructions'); this.$xInstructions = $$('x-instructions');
Events.on('peer-added', _ => this.evaluateOverflowing());
Events.on('bg-resize', _ => this.evaluateOverflowing());
} }
_onKeyDown(e) { _onKeyDown(e) {
@ -53,11 +57,11 @@ class PeersUI {
} }
_joinPeer(peer, roomType, roomSecret) { _joinPeer(peer, roomType, roomSecret) {
peer.roomType = roomType; peer.roomTypes = [roomType];
peer.roomSecret = roomSecret; peer.roomSecret = roomSecret;
if (this.peers[peer.id]) { if (this.peers[peer.id]) {
this.peers[peer.id].roomType = peer.roomType; if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(peer); this._redrawPeer(this.peers[peer.id]);
return; // peer already exists return; // peer already exists
} }
this.peers[peer.id] = peer; this.peers[peer.id] = peer;
@ -72,7 +76,15 @@ class PeersUI {
const peerNode = $(peer.id); const peerNode = $(peer.id);
if (!peerNode) return; if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret'); 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) { _onPeers(msg) {
@ -83,6 +95,7 @@ class PeersUI {
const $peer = $(peerId); const $peer = $(peerId);
if (!$peer) return; if (!$peer) return;
$peer.remove(); $peer.remove();
this.evaluateOverflowing();
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
} }
@ -213,6 +226,18 @@ class PeersUI {
class PeerUI { 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() { html() {
let title; let title;
let input = ''; let input = '';
@ -225,17 +250,24 @@ class PeerUI {
this.$el.innerHTML = ` this.$el.innerHTML = `
<label class="column center" title="${title}"> <label class="column center" title="${title}">
${input} ${input}
<x-icon shadow="1"> <x-icon>
<div class="icon-wrapper" shadow="1">
<svg class="icon"><use xlink:href="#"/></svg> <svg class="icon"><use xlink:href="#"/></svg>
</div>
<div class="highlight-wrapper center">
<div class="highlight" shadow="1"></div>
</div>
</x-icon> </x-icon>
<div class="progress"> <div class="progress">
<div class="circle"></div> <div class="circle"></div>
<div class="circle right"></div> <div class="circle right"></div>
</div> </div>
<div class="device-descriptor">
<div class="name font-subheading"></div> <div class="name font-subheading"></div>
<div class="device-name font-body2"></div> <div class="device-name font-body2"></div>
<div class="status 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> <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>`; </label>`;
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
@ -245,23 +277,12 @@ class PeerUI {
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16); 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() { _initDom() {
this.$el = document.createElement('x-peer'); this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id; this.$el.id = this._peer.id;
this.$el.ui = this; 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.html();
this._callbackInput = e => this._onFilesSelected(e) this._callbackInput = e => this._onFilesSelected(e)
@ -272,7 +293,7 @@ class PeerUI {
this._callbackDragLeave = e => this._onDragEnd(e) this._callbackDragLeave = e => this._onDragEnd(e)
this._callbackDragOver = e => this._onDragOver(e) this._callbackDragOver = e => this._onDragOver(e)
this._callbackContextMenu = e => this._onRightClick(e) this._callbackContextMenu = e => this._onRightClick(e)
this._callbackTouchStart = _ => this._onTouchStart() this._callbackTouchStart = e => this._onTouchStart(e)
this._callbackTouchEnd = e => this._onTouchEnd(e) this._callbackTouchEnd = e => this._onTouchEnd(e)
this._callbackPointerDown = e => this._onPointerDown(e) this._callbackPointerDown = e => this._onPointerDown(e)
// PasteMode // PasteMode
@ -393,21 +414,28 @@ class PeerUI {
_onRightClick(e) { _onRightClick(e) {
e.preventDefault(); 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._touchStart = Date.now();
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610); this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610);
} }
_onTouchEnd(e) { _onTouchEnd(e) {
if (Date.now() - this._touchStart < 500) { if (Date.now() - this._touchStart < 500) {
clearTimeout(this._touchTimer); clearTimeout(this._touchTimer);
} else { // this was a long tap } else if (this._touchTimer) { // this was a long tap
if (e) e.preventDefault(); 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
});
} }
this._touchTimer = null;
} }
} }
@ -469,10 +497,10 @@ class ReceiveDialog extends Dialog {
class ReceiveFileDialog extends ReceiveDialog { class ReceiveFileDialog extends ReceiveDialog {
constructor() { constructor() {
super('receiveFileDialog'); super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload'); this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download');
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle') this.$receiveTitleNode = this.$el.querySelector('#receive-title')
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.request));
this._filesQueue = []; this._filesQueue = [];
@ -631,15 +659,15 @@ class ReceiveFileDialog extends ReceiveDialog {
class ReceiveRequestDialog extends ReceiveDialog { class ReceiveRequestDialog extends ReceiveDialog {
constructor() { constructor() {
super('receiveRequestDialog'); super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName'); this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name');
this.$fileStemNode = this.$el.querySelector('#fileStem'); this.$fileStemNode = this.$el.querySelector('#file-stem');
this.$fileExtensionNode = this.$el.querySelector('#fileExtension'); this.$fileExtensionNode = this.$el.querySelector('#file-extension');
this.$fileOtherNode = this.$el.querySelector('#fileOther'); this.$fileOtherNode = this.$el.querySelector('#file-other');
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest'); this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#declineRequest'); this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false)); this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
@ -720,12 +748,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
class PairDeviceDialog extends Dialog { class PairDeviceDialog extends Dialog {
constructor() { constructor() {
super('pairDeviceDialog'); super('pair-device-dialog');
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate()); $('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.$submitBtn = this.$el.querySelector('button[type="submit"]');
this.$roomKey = this.$el.querySelector('#roomKey'); this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#roomKeyQrCode'); this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$clearSecretsBtn = $('clear-pair-devices'); this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form'); let createJoinForm = this.$el.querySelector('form');
@ -799,7 +827,7 @@ class PairDeviceDialog extends Dialog {
} }
evaluateRoomKeyChars() { 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", ""); this.$submitBtn.setAttribute("disabled", "");
} else { } else {
this.inputRoomKey = ""; this.inputRoomKey = "";
@ -843,7 +871,7 @@ class PairDeviceDialog extends Dialog {
height: 150, height: 150,
padding: 0, padding: 0,
background: "transparent", background: "transparent",
color: getComputedStyle(document.body).getPropertyValue('--text-color'), color: `rgb(var(--text-color))`,
ecl: "L", ecl: "L",
join: true join: true
}); });
@ -935,13 +963,14 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn.setAttribute('hidden', ''); this.$clearSecretsBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
} }
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable()); }).catch(_ => PersistentStorage.logBrowserNotCapable());
} }
} }
class ClearDevicesDialog extends Dialog { class ClearDevicesDialog extends Dialog {
constructor() { constructor() {
super('clearDevicesDialog'); super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form'); let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
@ -959,9 +988,10 @@ class ClearDevicesDialog extends Dialog {
class SendTextDialog extends Dialog { class SendTextDialog extends Dialog {
constructor() { constructor() {
super('sendTextDialog'); super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail)); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#textInput'); this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name');
this.$form = this.$el.querySelector('form'); this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]'); this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send()); this.$form.addEventListener('submit', _ => this._send());
@ -992,8 +1022,9 @@ class SendTextDialog extends Dialog {
} }
} }
_onRecipient(peerId) { _onRecipient(peerId, deviceName) {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
this.$peerDisplayName.innerText = deviceName;
this.show(); this.show();
const range = document.createRange(); const range = document.createRange();
@ -1017,7 +1048,7 @@ class SendTextDialog extends Dialog {
class ReceiveTextDialog extends Dialog { class ReceiveTextDialog extends Dialog {
constructor() { constructor() {
super('receiveTextDialog'); super('receive-text-dialog');
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
this.$text = this.$el.querySelector('#text'); this.$text = this.$el.querySelector('#text');
this.$copy = this.$el.querySelector('#copy'); this.$copy = this.$el.querySelector('#copy');
@ -1028,7 +1059,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName'); this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name');
this._receiveTextQueue = []; this._receiveTextQueue = [];
} }
@ -1089,13 +1120,13 @@ class ReceiveTextDialog extends Dialog {
class Base64ZipDialog extends Dialog { class Base64ZipDialog extends Dialog {
constructor() { constructor() {
super('base64PasteDialog'); super('base64-paste-dialog');
const urlParams = new URL(window.location).searchParams; const urlParams = new URL(window.location).searchParams;
const base64Text = urlParams.get('base64text'); const base64Text = urlParams.get('base64text');
const base64Zip = urlParams.get('base64zip'); const base64Zip = urlParams.get('base64zip');
const base64Hash = window.location.hash.substring(1); const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
if (base64Text) { if (base64Text) {
this.show(); this.show();
@ -1246,6 +1277,7 @@ class Notifications {
this.$button.removeAttribute('hidden'); this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission()); this.$button.addEventListener('click', _ => this._requestPermission());
} }
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files)); Events.on('files-received', e => this._downloadNotification(e.detail.files));
} }
@ -1321,7 +1353,7 @@ class Notifications {
} }
_download(notification) { _download(notification) {
$('shareOrDownload').click(); $('share-or-download').click();
notification.close(); notification.close();
} }
@ -1714,19 +1746,15 @@ Events.on('load', () => {
h = window.innerHeight; h = window.innerHeight;
c.width = w; c.width = w;
c.height = h; c.height = h;
offset = h > 800 offset = $$('footer').offsetHeight - 32;
? 116 if (h > 800) offset += 16;
: h > 380
? 100
: 65;
if (w < 420) offset += 20;
x0 = w / 2; x0 = w / 2;
y0 = h - offset; y0 = h - offset;
dw = Math.max(w, h, 1000) / 13; dw = Math.max(w, h, 1000) / 13;
drawCircles(); drawCircles();
} }
window.onresize = init; Events.on('bg-resize', _ => init());
window.onresize = _ => Events.fire('bg-resize');
function drawCircle(radius) { function drawCircle(radius) {
ctx.beginPath(); ctx.beginPath();
@ -1791,9 +1819,3 @@ Notifications permission has been blocked
as the user has dismissed the permission prompt several times. as the user has dismissed the permission prompt several times.
This can be reset in Page Info This can be reset in Page Info
which can be accessed by clicking the lock icon next to the URL.`; 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.1'; const cacheVersion = 'v1.1.3';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',

View file

@ -10,28 +10,25 @@
/* Layout */ /* Layout */
html {
min-height: 100%;
height: -webkit-fill-available;
}
html, html,
body { body {
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100vw;
overflow-x: hidden; overflow-x: hidden;
overscroll-behavior-y: none; overscroll-behavior: none;
overflow-y: hidden;
} }
body { body {
min-height: 100%; min-height: 100vh;
/* mobile viewport bug fix */
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
flex-grow: 1; }
align-items: center;
justify-content: center; html {
overflow-y: hidden; height: -webkit-fill-available;
} }
.row-reverse { .row-reverse {
@ -73,10 +70,7 @@ body {
} }
header { header {
position: absolute; position: relative;
top: 0;
left: 0;
right: 0;
height: 56px; height: 56px;
align-items: center; align-items: center;
padding: 16px; padding: 16px;
@ -119,9 +113,9 @@ h3 {
} }
.font-subheading { .font-subheading {
font-size: 16px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 18px;
word-break: normal; word-break: normal;
} }
@ -199,20 +193,151 @@ body>header a {
margin-left: 8px; 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 */ /* Peers List */
#x-peers-filler {
display: flex;
flex-grow: 1;
}
x-peers { x-peers {
width: 100%; position: relative;
overflow: hidden; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
flex-grow: 1;
align-items: start !important;
justify-content: center;
z-index: 2; 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 */ /* Empty Peers List */
x-no-peers { x-no-peers {
height: 114px; display: flex;
flex-direction: column;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
/* prevent flickering on load */ /* prevent flickering on load */
@ -254,25 +379,19 @@ x-no-peers[drop-bg] * {
x-peer { x-peer {
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
} }
x-peer label { x-peer label {
width: var(--peer-width); width: var(--peer-width);
padding: 8px;
cursor: pointer; cursor: pointer;
touch-action: manipulation; touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative; position: relative;
} }
x-peer .name {
width: var(--peer-width);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
input[type="file"] { input[type="file"] {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
@ -280,21 +399,45 @@ input[type="file"] {
x-peer x-icon { x-peer x-icon {
--icon-size: 40px; --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); width: var(--icon-size);
padding: 12px; padding: 12px;
border-radius: 50%; border-radius: 50%;
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
display: flex; 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); 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]):hover x-icon,
x-peer:not([status]):focus x-icon { x-peer:not([status]):focus x-icon {
transform: scale(1.05); transform: scale(1.05);
@ -306,6 +449,18 @@ x-peer[status] x-icon {
transform: scale(1); transform: scale(1);
} }
.device-descriptor {
text-align: center;
}
.name {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.status, .status,
.device-name, .device-name,
.connection-hash { .connection-hash {
@ -371,10 +526,9 @@ x-peer[drop] x-icon {
/* Footer */ /* Footer */
footer { footer {
position: absolute; position: relative;
bottom: 0; margin-top: auto;
left: 0; z-index: 2;
right: 0;
align-items: center; align-items: center;
padding: 0 0 16px 0; padding: 0 0 16px 0;
text-align: center; text-align: center;
@ -385,6 +539,7 @@ footer .logo {
--icon-size: 80px; --icon-size: 80px;
margin-bottom: 8px; margin-bottom: 8px;
color: var(--primary-color); color: var(--primary-color);
margin-top: -10px;
} }
footer .font-body2 { footer .font-body2 {
@ -425,11 +580,14 @@ x-dialog x-paper {
will-change: transform; will-change: transform;
} }
#pairDeviceDialog x-paper { #pair-device-dialog x-paper {
position: absolute; position: absolute;
top: max(50%, 350px); top: max(50%, 350px);
height: 650px; height: 650px;
margin-top: -325px; margin-top: -325px;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
x-dialog:not([show]) { x-dialog:not([show]) {
@ -461,13 +619,13 @@ x-dialog .font-subheading {
/* PairDevicesDialog */ /* PairDevicesDialog */
#keyInputContainer { #key-input-container {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
#keyInputContainer>input { #key-input-container>input {
width: 45px; width: 45px;
height: 45px; height: 45px;
font-size: 30px; font-size: 30px;
@ -483,15 +641,15 @@ x-dialog .font-subheading {
justify-content: center; justify-content: center;
} }
#keyInputContainer>input + * { #key-input-container>input + * {
margin-left: 6px; margin-left: 6px;
} }
#keyInputContainer>input:nth-of-type(4) { #key-input-container>input:nth-of-type(4) {
margin-left: 18px; margin-left: 18px;
} }
#roomKey { #room-key {
font-size: 50px; font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block; display: inline-block;
@ -499,19 +657,20 @@ x-dialog .font-subheading {
margin: 15px -15px; margin: 15px -15px;
} }
#roomKeyQrCode { #room-key-qr-code {
padding: inherit; padding: inherit;
margin: auto; margin: auto;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
#pairDeviceDialog hr { #pair-device-dialog hr {
margin-top: 40px; margin-top: 40px;
margin-bottom: 40px; margin-bottom: 40px;
width: 100%;
} }
#pairDeviceDialog x-background { #pair-device-dialog x-background {
padding: 16px!important; padding: 16px!important;
} }
@ -526,13 +685,13 @@ x-dialog h2 {
margin-top: 1rem; margin-top: 1rem;
} }
#receiveRequestDialog h2, #receive-request-dialog h2,
#receiveFileDialog h2 { #receive-file-dialog h2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
x-dialog .row-reverse { x-dialog .row-reverse {
margin: 40px -24px auto; margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
} }
@ -556,11 +715,11 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#fileName { #file-name {
font-style: italic; font-style: italic;
} }
#fileStem { #file-stem {
max-width: 80%; max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -574,13 +733,13 @@ x-dialog .row-reverse {
/* Send Text Dialog */ /* Send Text Dialog */
#textInput { #text-input {
min-height: 120px; min-height: 120px;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
#receiveTextDialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: 300px;
@ -593,15 +752,15 @@ x-dialog .row-reverse {
margin-top:36px; margin-top:36px;
} }
#receiveTextDialog #text a { #receive-text-dialog #text a {
cursor: pointer; cursor: pointer;
} }
#receiveTextDialog #text a:hover { #receive-text-dialog #text a:hover {
text-decoration: underline; text-decoration: underline;
} }
#receiveTextDialog h3 { #receive-text-dialog h3 {
/* Select the received text when double-clicking the dialog */ /* Select the received text when double-clicking the dialog */
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
@ -612,26 +771,26 @@ x-dialog .row-reverse {
margin: auto -25px; margin: auto -25px;
} }
#receiveTextDescriptionContainer { #receive-text-description-container {
margin-bottom: 25px; margin-bottom: 25px;
} }
#base64PasteBtn { #base64-paste-btn {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; border: solid 12px #438cff;
} }
#base64PasteDialog button { #base64-paste-dialog button {
margin: auto; margin: auto;
border-radius: 8px; border-radius: 8px;
} }
#base64PasteDialog button[close] { #base64-paste-dialog button[close] {
margin-top: 20px; margin-top: 20px;
} }
#base64PasteDialog button[close]:before { #base64-paste-dialog button[close]:before {
border-radius: 8px; border-radius: 8px;
} }
@ -689,16 +848,18 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancelPasteModeBtn { #cancel-paste-mode-btn {
z-index: 2; z-index: 2;
margin-top: 0; margin: 0;
padding: 0;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
left: 0; left: 0;
width: 100%; width: 100vw;
height: 56px; height: 56px;
border-bottom: solid 2.5px var(--border-color); background-color: var(--primary-color);
color: rgb(238, 238, 238);
} }
.button:focus:before, .button:focus:before,
@ -809,7 +970,7 @@ button::-moz-focus-inner {
width: 80px; width: 80px;
height: 80px; height: 80px;
position: absolute; position: absolute;
top: 0; top: -8px;
clip: rect(0px, 80px, 80px, 40px); clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg); --progress: rotate(0deg);
transition: transform 200ms; transition: transform 200ms;
@ -876,13 +1037,16 @@ x-toast:not([show]):not(:hover) {
/* Instructions */ /* Instructions */
x-instructions { x-instructions {
position: absolute; position: relative;
top: 120px;
opacity: 0.5; opacity: 0.5;
transition: opacity 300ms; transition: opacity 300ms;
z-index: -1;
text-align: center; 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 { x-instructions:not([drop-peer]):not([drop-bg]):before {
@ -899,88 +1063,84 @@ x-instructions[drop-bg]:not([drop-peer]):before {
x-instructions p { x-instructions p {
display: none; display: none;
margin: 0 auto auto;
max-width: 80%;
} }
x-peers:empty~x-instructions { x-peers:empty~x-instructions {
opacity: 0; 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 */ /* Responsive Styles */
@media (min-height: 800px) { @media screen and (min-height: 800px) {
footer { footer {
margin-bottom: 16px; margin-bottom: 16px;
} }
} }
@media screen and (min-height: 800px), @media (hover: hover) and (pointer: fine) {
screen and (min-width: 1100px) {
x-instructions:not([drop-peer]):not([drop-bg]):before { x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(desktop); 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 Color Themes
*/ */
/* Default colors */ /* Default colors */
body { body {
--text-color: #333; --text-color: 51,51,51;
--bg-color: #fff; --bg-color: 250,250,250; /*rgb code*/
--bg-color-test: 18,18,18;
--bg-color-secondary: #f1f3f4; --bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8; --border-color: #e7e8e8;
} }
/* Dark theme colors */ /* Dark theme colors */
body.dark-theme { body.dark-theme {
--text-color: #eee; --text-color: 238,238,238;
--bg-color: #121212; --bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333; --bg-color-secondary: #333;
--border-color: #252525; --border-color: #252525;
} }
/* Colored Elements */ /* Colored Elements */
body { body {
color: var(--text-color); color: rgb(var(--text-color));
background-color: var(--bg-color); background-color: rgb(var(--bg-color));
transition: background-color 0.5s ease; transition: background-color 0.5s ease;
} }
x-dialog x-paper { x-dialog x-paper {
background-color: var(--bg-color); background-color: rgb(var(--bg-color));
} }
.textarea { .textarea {
color: var(--text-color) !important; color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important; background-color: var(--bg-color-secondary) !important;
} }
@ -1018,16 +1178,16 @@ x-dialog x-paper {
/* defaults to dark theme */ /* defaults to dark theme */
body { body {
--text-color: #eee; --text-color: 238,238,238;
--bg-color: #121212; --bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333; --bg-color-secondary: #333;
--border-color: #252525; --border-color: #252525;
} }
/* Override dark mode with light mode styles if the user decides to swap */ /* Override dark mode with light mode styles if the user decides to swap */
body.light-theme { body.light-theme {
--text-color: #333; --text-color: 51,51,51;
--bg-color: #fafafa; --bg-color: 250,250,250; /*rgb code*/
--bg-color-secondary: #f1f3f4; --bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8; --border-color: #e7e8e8;
} }
@ -1045,6 +1205,15 @@ x-dialog x-paper {
} }
} }
/*
iOS specific styles
*/
@supports (-webkit-overflow-scrolling: touch) {
html {
min-height: -webkit-fill-available;
}
}
/* webkit scrollbar style*/ /* webkit scrollbar style*/
::-webkit-scrollbar{ ::-webkit-scrollbar{

View file

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

View file

@ -10,7 +10,7 @@ window.pasteMode.activated = false;
// set display name // set display name
Events.on('display-name', e => { Events.on('display-name', e => {
const me = e.detail.message; const me = e.detail.message;
const $displayName = $('displayName') const $displayName = $('display-name')
$displayName.textContent = 'You are known as ' + me.displayName; $displayName.textContent = 'You are known as ' + me.displayName;
$displayName.title = me.deviceName; $displayName.title = me.deviceName;
}); });
@ -28,7 +28,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {}; this.peers = {};
this.$cancelPasteModeBtn = $('cancelPasteModeBtn'); this.$cancelPasteModeBtn = $('cancel-paste-mode-btn');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e)); Events.on('dragover', e => this._onDragOver(e));
@ -38,8 +38,12 @@ class PeersUI {
Events.on('drop', e => this._onDrop(e)); Events.on('drop', e => this._onDrop(e));
Events.on('keydown', e => this._onKeyDown(e)); Events.on('keydown', e => this._onKeyDown(e));
this.$xPeers = $$('x-peers');
this.$xNoPeers = $$('x-no-peers'); this.$xNoPeers = $$('x-no-peers');
this.$xInstructions = $$('x-instructions'); this.$xInstructions = $$('x-instructions');
Events.on('peer-added', _ => this.evaluateOverflowing());
Events.on('bg-resize', _ => this.evaluateOverflowing());
} }
_onKeyDown(e) { _onKeyDown(e) {
@ -53,11 +57,11 @@ class PeersUI {
} }
_joinPeer(peer, roomType, roomSecret) { _joinPeer(peer, roomType, roomSecret) {
peer.roomType = roomType; peer.roomTypes = [roomType];
peer.roomSecret = roomSecret; peer.roomSecret = roomSecret;
if (this.peers[peer.id]) { if (this.peers[peer.id]) {
this.peers[peer.id].roomType = peer.roomType; if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(peer); this._redrawPeer(this.peers[peer.id]);
return; // peer already exists return; // peer already exists
} }
this.peers[peer.id] = peer; this.peers[peer.id] = peer;
@ -72,7 +76,15 @@ class PeersUI {
const peerNode = $(peer.id); const peerNode = $(peer.id);
if (!peerNode) return; if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret'); 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) { _onPeers(msg) {
@ -83,6 +95,7 @@ class PeersUI {
const $peer = $(peerId); const $peer = $(peerId);
if (!$peer) return; if (!$peer) return;
$peer.remove(); $peer.remove();
this.evaluateOverflowing();
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
} }
@ -213,6 +226,18 @@ class PeersUI {
class PeerUI { 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() { html() {
let title; let title;
let input = ''; let input = '';
@ -225,17 +250,24 @@ class PeerUI {
this.$el.innerHTML = ` this.$el.innerHTML = `
<label class="column center" title="${title}"> <label class="column center" title="${title}">
${input} ${input}
<x-icon shadow="1"> <x-icon>
<div class="icon-wrapper" shadow="1">
<svg class="icon"><use xlink:href="#"/></svg> <svg class="icon"><use xlink:href="#"/></svg>
</div>
<div class="highlight-wrapper center">
<div class="highlight" shadow="1"></div>
</div>
</x-icon> </x-icon>
<div class="progress"> <div class="progress">
<div class="circle"></div> <div class="circle"></div>
<div class="circle right"></div> <div class="circle right"></div>
</div> </div>
<div class="device-descriptor">
<div class="name font-subheading"></div> <div class="name font-subheading"></div>
<div class="device-name font-body2"></div> <div class="device-name font-body2"></div>
<div class="status 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> <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>`; </label>`;
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
@ -245,23 +277,12 @@ class PeerUI {
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16); 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() { _initDom() {
this.$el = document.createElement('x-peer'); this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id; this.$el.id = this._peer.id;
this.$el.ui = this; 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') if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer')
this.html(); this.html();
@ -273,7 +294,7 @@ class PeerUI {
this._callbackDragLeave = e => this._onDragEnd(e) this._callbackDragLeave = e => this._onDragEnd(e)
this._callbackDragOver = e => this._onDragOver(e) this._callbackDragOver = e => this._onDragOver(e)
this._callbackContextMenu = e => this._onRightClick(e) this._callbackContextMenu = e => this._onRightClick(e)
this._callbackTouchStart = _ => this._onTouchStart() this._callbackTouchStart = e => this._onTouchStart(e)
this._callbackTouchEnd = e => this._onTouchEnd(e) this._callbackTouchEnd = e => this._onTouchEnd(e)
this._callbackPointerDown = e => this._onPointerDown(e) this._callbackPointerDown = e => this._onPointerDown(e)
// PasteMode // PasteMode
@ -394,21 +415,28 @@ class PeerUI {
_onRightClick(e) { _onRightClick(e) {
e.preventDefault(); 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._touchStart = Date.now();
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610); this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610);
} }
_onTouchEnd(e) { _onTouchEnd(e) {
if (Date.now() - this._touchStart < 500) { if (Date.now() - this._touchStart < 500) {
clearTimeout(this._touchTimer); clearTimeout(this._touchTimer);
} else { // this was a long tap } else if (this._touchTimer) { // this was a long tap
if (e) e.preventDefault(); 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
});
} }
this._touchTimer = null;
} }
} }
@ -470,10 +498,10 @@ class ReceiveDialog extends Dialog {
class ReceiveFileDialog extends ReceiveDialog { class ReceiveFileDialog extends ReceiveDialog {
constructor() { constructor() {
super('receiveFileDialog'); super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload'); this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download');
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle') this.$receiveTitleNode = this.$el.querySelector('#receive-title')
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.request));
this._filesQueue = []; this._filesQueue = [];
@ -632,15 +660,15 @@ class ReceiveFileDialog extends ReceiveDialog {
class ReceiveRequestDialog extends ReceiveDialog { class ReceiveRequestDialog extends ReceiveDialog {
constructor() { constructor() {
super('receiveRequestDialog'); super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName'); this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name');
this.$fileStemNode = this.$el.querySelector('#fileStem'); this.$fileStemNode = this.$el.querySelector('#file-stem');
this.$fileExtensionNode = this.$el.querySelector('#fileExtension'); this.$fileExtensionNode = this.$el.querySelector('#file-extension');
this.$fileOtherNode = this.$el.querySelector('#fileOther'); this.$fileOtherNode = this.$el.querySelector('#file-other');
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest'); this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#declineRequest'); this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false)); this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
@ -721,12 +749,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
class PairDeviceDialog extends Dialog { class PairDeviceDialog extends Dialog {
constructor() { constructor() {
super('pairDeviceDialog'); super('pair-device-dialog');
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate()); $('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.$submitBtn = this.$el.querySelector('button[type="submit"]');
this.$roomKey = this.$el.querySelector('#roomKey'); this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#roomKeyQrCode'); this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$clearSecretsBtn = $('clear-pair-devices'); this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form'); let createJoinForm = this.$el.querySelector('form');
@ -800,7 +828,7 @@ class PairDeviceDialog extends Dialog {
} }
evaluateRoomKeyChars() { 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", ""); this.$submitBtn.setAttribute("disabled", "");
} else { } else {
this.inputRoomKey = ""; this.inputRoomKey = "";
@ -844,7 +872,7 @@ class PairDeviceDialog extends Dialog {
height: 150, height: 150,
padding: 0, padding: 0,
background: "transparent", background: "transparent",
color: getComputedStyle(document.body).getPropertyValue('--text-color'), color: `rgb(var(--text-color))`,
ecl: "L", ecl: "L",
join: true join: true
}); });
@ -936,13 +964,14 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn.setAttribute('hidden', ''); this.$clearSecretsBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
} }
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable()); }).catch(_ => PersistentStorage.logBrowserNotCapable());
} }
} }
class ClearDevicesDialog extends Dialog { class ClearDevicesDialog extends Dialog {
constructor() { constructor() {
super('clearDevicesDialog'); super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form'); let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
@ -960,9 +989,10 @@ class ClearDevicesDialog extends Dialog {
class SendTextDialog extends Dialog { class SendTextDialog extends Dialog {
constructor() { constructor() {
super('sendTextDialog'); super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail)); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#textInput'); this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name');
this.$form = this.$el.querySelector('form'); this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]'); this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send()); this.$form.addEventListener('submit', _ => this._send());
@ -993,8 +1023,9 @@ class SendTextDialog extends Dialog {
} }
} }
_onRecipient(peerId) { _onRecipient(peerId, deviceName) {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
this.$peerDisplayName.innerText = deviceName;
this.show(); this.show();
const range = document.createRange(); const range = document.createRange();
@ -1018,7 +1049,7 @@ class SendTextDialog extends Dialog {
class ReceiveTextDialog extends Dialog { class ReceiveTextDialog extends Dialog {
constructor() { constructor() {
super('receiveTextDialog'); super('receive-text-dialog');
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
this.$text = this.$el.querySelector('#text'); this.$text = this.$el.querySelector('#text');
this.$copy = this.$el.querySelector('#copy'); this.$copy = this.$el.querySelector('#copy');
@ -1029,7 +1060,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName'); this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name');
this._receiveTextQueue = []; this._receiveTextQueue = [];
} }
@ -1090,13 +1121,13 @@ class ReceiveTextDialog extends Dialog {
class Base64ZipDialog extends Dialog { class Base64ZipDialog extends Dialog {
constructor() { constructor() {
super('base64PasteDialog'); super('base64-paste-dialog');
const urlParams = new URL(window.location).searchParams; const urlParams = new URL(window.location).searchParams;
const base64Text = urlParams.get('base64text'); const base64Text = urlParams.get('base64text');
const base64Zip = urlParams.get('base64zip'); const base64Zip = urlParams.get('base64zip');
const base64Hash = window.location.hash.substring(1); const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
if (base64Text) { if (base64Text) {
this.show(); this.show();
@ -1247,6 +1278,7 @@ class Notifications {
this.$button.removeAttribute('hidden'); this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission()); this.$button.addEventListener('click', _ => this._requestPermission());
} }
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files)); Events.on('files-received', e => this._downloadNotification(e.detail.files));
} }
@ -1322,7 +1354,7 @@ class Notifications {
} }
_download(notification) { _download(notification) {
$('shareOrDownload').click(); $('share-or-download').click();
notification.close(); notification.close();
} }
@ -1715,19 +1747,14 @@ Events.on('load', () => {
h = window.innerHeight; h = window.innerHeight;
c.width = w; c.width = w;
c.height = h; c.height = h;
offset = h > 800 offset = $$('footer').offsetHeight - 32;
? 116
: h > 380
? 100
: 65;
if (w < 420) offset += 20;
x0 = w / 2; x0 = w / 2;
y0 = h - offset; y0 = h - offset;
dw = Math.max(w, h, 1000) / 13; dw = Math.max(w, h, 1000) / 13;
drawCircles(); drawCircles();
} }
window.onresize = init; Events.on('bg-resize', _ => init());
window.onresize = _ => Events.fire('bg-resize');
function drawCircle(radius) { function drawCircle(radius) {
ctx.beginPath(); ctx.beginPath();
@ -1792,9 +1819,3 @@ Notifications permission has been blocked
as the user has dismissed the permission prompt several times. as the user has dismissed the permission prompt several times.
This can be reset in Page Info This can be reset in Page Info
which can be accessed by clicking the lock icon next to the URL.`; 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.1'; const cacheVersion = 'v1.1.3';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',

View file

@ -11,28 +11,25 @@
/* Layout */ /* Layout */
html {
min-height: 100%;
height: -webkit-fill-available;
}
html, html,
body { body {
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100vw;
overflow-x: hidden; overflow-x: hidden;
overscroll-behavior-y: none; overscroll-behavior: none;
overflow-y: hidden;
} }
body { body {
min-height: 100%; min-height: 100vh;
/* mobile viewport bug fix */
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
flex-grow: 1; }
align-items: center;
justify-content: center; html {
overflow-y: hidden; height: -webkit-fill-available;
} }
.row-reverse { .row-reverse {
@ -74,10 +71,7 @@ body {
} }
header { header {
position: absolute; position: relative;
top: 0;
left: 0;
right: 0;
height: 56px; height: 56px;
align-items: center; align-items: center;
padding: 16px; padding: 16px;
@ -120,9 +114,9 @@ h3 {
} }
.font-subheading { .font-subheading {
font-size: 16px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 18px;
word-break: normal; word-break: normal;
} }
@ -200,20 +194,160 @@ body>header a {
margin-left: 8px; 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 */ /* Peers List */
#x-peers-filler {
display: flex;
flex-grow: 1;
}
x-peers { x-peers {
width: 100%; position: relative;
overflow: hidden; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
flex-grow: 1;
align-items: start !important;
justify-content: center;
z-index: 2; 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 */ /* Empty Peers List */
x-no-peers { x-no-peers {
height: 114px; display: flex;
flex-direction: column;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
/* prevent flickering on load */ /* prevent flickering on load */
@ -255,25 +389,19 @@ x-no-peers[drop-bg] * {
x-peer { x-peer {
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
} }
x-peer label { x-peer label {
width: var(--peer-width); width: var(--peer-width);
padding: 8px;
cursor: pointer; cursor: pointer;
touch-action: manipulation; touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative; position: relative;
} }
x-peer .name {
width: var(--peer-width);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
input[type="file"] { input[type="file"] {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
@ -281,27 +409,43 @@ input[type="file"] {
x-peer x-icon { x-peer x-icon {
--icon-size: 40px; --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); width: var(--icon-size);
padding: 12px; padding: 12px;
border-radius: 50%; border-radius: 50%;
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
display: flex; 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); background: var(--paired-device-color);
} }
x-peer.ws-peer x-icon { x-peer x-icon > .highlight-wrapper {
border: solid 4px var(--ws-peer-color); align-self: center;
align-items: center;
margin: 7px auto 0;
height: 6px;
} }
x-peer.ws-peer .progress { x-peer x-icon > .highlight-wrapper > .highlight {
margin-top: 4px; 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]):hover x-icon,
@ -315,6 +459,35 @@ x-peer[status] x-icon {
transform: scale(1); 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, .status,
.device-name, .device-name,
.connection-hash { .connection-hash {
@ -380,12 +553,10 @@ x-peer[drop] x-icon {
/* Footer */ /* Footer */
footer { footer {
position: absolute; position: relative;
bottom: 0; margin-top: auto;
left: 0; z-index: 2;
right: 0;
align-items: center; align-items: center;
padding: 0 0 16px 0;
text-align: center; text-align: center;
transition: color 300ms; transition: color 300ms;
} }
@ -394,6 +565,7 @@ footer .logo {
--icon-size: 80px; --icon-size: 80px;
margin-bottom: 8px; margin-bottom: 8px;
color: var(--primary-color); color: var(--primary-color);
margin-top: -10px;
} }
footer .font-body2 { footer .font-body2 {
@ -434,11 +606,14 @@ x-dialog x-paper {
will-change: transform; will-change: transform;
} }
#pairDeviceDialog x-paper { #pair-device-dialog x-paper {
position: absolute; position: absolute;
top: max(50%, 350px); top: max(50%, 350px);
height: 650px; height: 650px;
margin-top: -325px; margin-top: -325px;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
x-dialog:not([show]) { x-dialog:not([show]) {
@ -470,13 +645,13 @@ x-dialog .font-subheading {
/* PairDevicesDialog */ /* PairDevicesDialog */
#keyInputContainer { #key-input-container {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
#keyInputContainer>input { #key-input-container>input {
width: 45px; width: 45px;
height: 45px; height: 45px;
font-size: 30px; font-size: 30px;
@ -492,15 +667,15 @@ x-dialog .font-subheading {
justify-content: center; justify-content: center;
} }
#keyInputContainer>input + * { #key-input-container>input + * {
margin-left: 6px; margin-left: 6px;
} }
#keyInputContainer>input:nth-of-type(4) { #key-input-container>input:nth-of-type(4) {
margin-left: 18px; margin-left: 18px;
} }
#roomKey { #room-key {
font-size: 50px; font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block; display: inline-block;
@ -508,19 +683,20 @@ x-dialog .font-subheading {
margin: 15px -15px; margin: 15px -15px;
} }
#roomKeyQrCode { #room-key-qr-code {
padding: inherit; padding: inherit;
margin: auto; margin: auto;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
#pairDeviceDialog hr { #pair-device-dialog hr {
margin-top: 40px; margin-top: 40px;
margin-bottom: 40px; margin-bottom: 40px;
width: 100%;
} }
#pairDeviceDialog x-background { #pair-device-dialog x-background {
padding: 16px!important; padding: 16px!important;
} }
@ -535,13 +711,13 @@ x-dialog h2 {
margin-top: 1rem; margin-top: 1rem;
} }
#receiveRequestDialog h2, #receive-request-dialog h2,
#receiveFileDialog h2 { #receive-file-dialog h2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
x-dialog .row-reverse { x-dialog .row-reverse {
margin: 40px -24px auto; margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
} }
@ -565,11 +741,11 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#fileName { #file-name {
font-style: italic; font-style: italic;
} }
#fileStem { #file-stem {
max-width: 80%; max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -583,13 +759,13 @@ x-dialog .row-reverse {
/* Send Text Dialog */ /* Send Text Dialog */
#textInput { #text-input {
min-height: 120px; min-height: 120px;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
#receiveTextDialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: 300px;
@ -602,15 +778,15 @@ x-dialog .row-reverse {
margin-top:36px; margin-top:36px;
} }
#receiveTextDialog #text a { #receive-text-dialog #text a {
cursor: pointer; cursor: pointer;
} }
#receiveTextDialog #text a:hover { #receive-text-dialog #text a:hover {
text-decoration: underline; text-decoration: underline;
} }
#receiveTextDialog h3 { #receive-text-dialog h3 {
/* Select the received text when double-clicking the dialog */ /* Select the received text when double-clicking the dialog */
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
@ -621,26 +797,26 @@ x-dialog .row-reverse {
margin: auto -25px; margin: auto -25px;
} }
#receiveTextDescriptionContainer { #receive-text-description-container {
margin-bottom: 25px; margin-bottom: 25px;
} }
#base64PasteBtn { #base64-paste-btn {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; border: solid 12px #438cff;
} }
#base64PasteDialog button { #base64-paste-dialog button {
margin: auto; margin: auto;
border-radius: 8px; border-radius: 8px;
} }
#base64PasteDialog button[close] { #base64-paste-dialog button[close] {
margin-top: 20px; margin-top: 20px;
} }
#base64PasteDialog button[close]:before { #base64-paste-dialog button[close]:before {
border-radius: 8px; border-radius: 8px;
} }
@ -698,16 +874,18 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancelPasteModeBtn { #cancel-paste-mode-btn {
z-index: 2; z-index: 2;
margin-top: 0; margin: 0;
padding: 0;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
left: 0; left: 0;
width: 100%; width: 100vw;
height: 56px; height: 56px;
border-bottom: solid 2.5px var(--border-color); background-color: var(--primary-color);
color: rgb(238, 238, 238);
} }
.button:focus:before, .button:focus:before,
@ -818,7 +996,7 @@ button::-moz-focus-inner {
width: 80px; width: 80px;
height: 80px; height: 80px;
position: absolute; position: absolute;
top: 0; top: -8px;
clip: rect(0px, 80px, 80px, 40px); clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg); --progress: rotate(0deg);
transition: transform 200ms; transition: transform 200ms;
@ -885,13 +1063,16 @@ x-toast:not([show]):not(:hover) {
/* Instructions */ /* Instructions */
x-instructions { x-instructions {
position: absolute; position: relative;
top: 120px;
opacity: 0.5; opacity: 0.5;
transition: opacity 300ms; transition: opacity 300ms;
z-index: -1;
text-align: center; 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 { x-instructions:not([drop-peer]):not([drop-bg]):before {
@ -908,92 +1089,84 @@ x-instructions[drop-bg]:not([drop-peer]):before {
x-instructions p { x-instructions p {
display: none; display: none;
margin: 0 auto auto;
max-width: 80%;
} }
x-peers:empty~x-instructions { x-peers:empty~x-instructions {
opacity: 0; 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); border-bottom: solid 4px var(--ws-peer-color);
padding-bottom: 1px;
} }
/* Responsive Styles */ /* Responsive Styles */
@media (min-height: 800px) { @media screen and (min-height: 800px) {
footer { #websocket-fallback {
margin-bottom: 16px; padding-bottom: 15px;
} }
} }
@media screen and (min-height: 800px), @media (hover: hover) and (pointer: fine) {
screen and (min-width: 1100px) {
x-instructions:not([drop-peer]):not([drop-bg]):before { x-instructions:not([drop-peer]):not([drop-bg]):before {
content: attr(desktop); 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 Color Themes
*/ */
/* Default colors */ /* Default colors */
body { body {
--text-color: #333; --text-color: 51,51,51;
--bg-color: #fff; --bg-color: 250,250,250; /*rgb code*/
--bg-color-test: 18,18,18;
--bg-color-secondary: #f1f3f4; --bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8; --border-color: #e7e8e8;
} }
/* Dark theme colors */ /* Dark theme colors */
body.dark-theme { body.dark-theme {
--text-color: #eee; --text-color: 238,238,238;
--bg-color: #121212; --bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333; --bg-color-secondary: #333;
--border-color: #252525; --border-color: #252525;
} }
/* Colored Elements */ /* Colored Elements */
body { body {
color: var(--text-color); color: rgb(var(--text-color));
background-color: var(--bg-color); background-color: rgb(var(--bg-color));
transition: background-color 0.5s ease; transition: background-color 0.5s ease;
} }
x-dialog x-paper { x-dialog x-paper {
background-color: var(--bg-color); background-color: rgb(var(--bg-color));
} }
.textarea { .textarea {
color: var(--text-color) !important; color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important; background-color: var(--bg-color-secondary) !important;
} }
@ -1031,16 +1204,16 @@ x-dialog x-paper {
/* defaults to dark theme */ /* defaults to dark theme */
body { body {
--text-color: #eee; --text-color: 238,238,238;
--bg-color: #121212; --bg-color: 18,18,18; /*rgb code*/
--bg-color-secondary: #333; --bg-color-secondary: #333;
--border-color: #252525; --border-color: #252525;
} }
/* Override dark mode with light mode styles if the user decides to swap */ /* Override dark mode with light mode styles if the user decides to swap */
body.light-theme { body.light-theme {
--text-color: #333; --text-color: 51,51,51;
--bg-color: #fafafa; --bg-color: 250,250,250; /*rgb code*/
--bg-color-secondary: #f1f3f4; --bg-color-secondary: #f1f3f4;
--border-color: #e7e8e8; --border-color: #e7e8e8;
} }
@ -1058,6 +1231,15 @@ x-dialog x-paper {
} }
} }
/*
iOS specific styles
*/
@supports (-webkit-overflow-scrolling: touch) {
html {
min-height: -webkit-fill-available;
}
}
/* webkit scrollbar style*/ /* webkit scrollbar style*/
::-webkit-scrollbar{ ::-webkit-scrollbar{