mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-06 18:03:48 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d7960a59 | ||
|
|
39ca5b2d21 | ||
|
|
cf715b2872 | ||
|
|
bbb8c1b10f | ||
|
|
d6ef5887dd | ||
|
|
f9f1abef7a | ||
|
|
d244f5fa47 | ||
|
|
3a2d8c75f7 | ||
|
|
545cdc2459 | ||
|
|
a1fdd81629 | ||
|
|
7220e85422 | ||
|
|
a3b348d9b6 | ||
|
|
4566528179 | ||
|
|
7b08973cef | ||
|
|
eda60a3d78 | ||
|
|
e96ca53aa4 | ||
|
|
11d6a8a372 | ||
|
|
75726ae5f4 | ||
|
|
80dc36c00a | ||
|
|
765b4e65b1 | ||
|
|
b6238b05ae |
@@ -2,39 +2,34 @@
|
||||
The easiest way to get PairDrop up and running is by using Docker.
|
||||
|
||||
## Deployment with Docker
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
|
||||
### Image from Docker Hub
|
||||
### Docker Image from Docker Hub
|
||||
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
|
||||
```
|
||||
|
||||
### Image from GHCR
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop
|
||||
```
|
||||
|
||||
### Options / Flags
|
||||
#### Options / Flags
|
||||
Set options by using the following flags in the `docker run` command:
|
||||
|
||||
#### Port
|
||||
##### Port
|
||||
```
|
||||
-p 127.0.0.1:8080:3000
|
||||
```
|
||||
> Specify the port used by the docker image
|
||||
> - 3000 -> `-p 127.0.0.1:3000:3000`
|
||||
> - 8080 -> `-p 127.0.0.1:8080:3000`
|
||||
#### Rate limiting requests
|
||||
##### Rate limiting requests
|
||||
```
|
||||
-e RATE_LIMIT=true
|
||||
```
|
||||
> Limits clients to 100 requests per 5 min
|
||||
> Limits clients to 1000 requests per 5 min
|
||||
|
||||
#### Websocket Fallback (for VPN)
|
||||
##### Websocket Fallback (for VPN)
|
||||
```
|
||||
-e WS_FALLBACK=true
|
||||
```
|
||||
@@ -47,10 +42,18 @@ Set options by using the following flags in the `docker run` command:
|
||||
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
|
||||
> Additionally, beware that all traffic using this fallback debits the servers data plan.
|
||||
|
||||
<br>
|
||||
### Docker Image from GHCR
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
|
||||
```
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
>
|
||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||
|
||||
## Deployment with Docker with self-built image
|
||||
### Build the image
|
||||
### Docker Image self-built
|
||||
#### Build the image
|
||||
```bash
|
||||
docker build --pull . -f Dockerfile -t pairdrop
|
||||
```
|
||||
@@ -58,15 +61,45 @@ docker build --pull . -f Dockerfile -t pairdrop
|
||||
>
|
||||
> `--pull` ensures always the latest node image is used.
|
||||
|
||||
### Run the image
|
||||
#### Run the image
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
|
||||
```
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
>
|
||||
> To specify options replace `npm run start:prod` according to [the documentation above.](#options--flags)
|
||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||
|
||||
<br>
|
||||
|
||||
## Deployment with Docker Compose
|
||||
Here's an example docker-compose file:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
pairdrop:
|
||||
image: lscr.io/linuxserver/pairdrop:latest
|
||||
container_name: pairdrop
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000 # UID to run the application as
|
||||
- PGID=1000 # GID to run the application as
|
||||
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
|
||||
- TZ=Etc/UTC # Time Zone
|
||||
ports:
|
||||
- 127.0.0.1:3000:3000 # Web UI
|
||||
```
|
||||
|
||||
Run the compose file with `docker compose up -d`.
|
||||
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
|
||||
<br>
|
||||
|
||||
## Deployment with node
|
||||
|
||||
@@ -117,7 +150,7 @@ npm start -- --localhost-only
|
||||
>
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> Use this when deploying PairDrop with node to prevent bypassing the proxy and reach the docker container directly.
|
||||
> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly.
|
||||
|
||||
#### Automatic restart on error
|
||||
```bash
|
||||
@@ -131,7 +164,7 @@ npm start -- --auto-restart
|
||||
```bash
|
||||
npm start -- --rate-limit
|
||||
```
|
||||
> Limits clients to 100 requests per 5 min
|
||||
> Limits clients to 1000 requests per 5 min
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
12
index.js
12
index.js
@@ -1,6 +1,7 @@
|
||||
const process = require('process')
|
||||
const crypto = require('crypto')
|
||||
const {spawn} = require('child_process')
|
||||
const WebSocket = require('ws');
|
||||
|
||||
// Handle SIGINT
|
||||
process.on('SIGINT', () => {
|
||||
@@ -99,7 +100,6 @@ const { uniqueNamesGenerator, animals, colors } = require('unique-names-generato
|
||||
class PairDropServer {
|
||||
|
||||
constructor() {
|
||||
const WebSocket = require('ws');
|
||||
this._wss = new WebSocket.Server({ server });
|
||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||
|
||||
@@ -110,10 +110,10 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_onConnection(peer) {
|
||||
this._joinRoom(peer);
|
||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||
peer.socket.onerror = e => console.error(e);
|
||||
this._keepAlive(peer);
|
||||
this._joinRoom(peer);
|
||||
|
||||
// send displayName
|
||||
this._send(peer, {
|
||||
@@ -317,6 +317,10 @@ class PairDropServer {
|
||||
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
|
||||
if (this._rooms[room] && this._rooms[room][peer.id]) {
|
||||
this._leaveRoom(peer, roomType, roomSecret);
|
||||
}
|
||||
|
||||
// if room doesn't exist, create it
|
||||
if (!this._rooms[room]) {
|
||||
this._rooms[room] = {};
|
||||
@@ -341,10 +345,6 @@ class PairDropServer {
|
||||
// delete the peer
|
||||
delete this._rooms[room][peer.id];
|
||||
|
||||
if (roomType === 'ip') {
|
||||
peer.socket.terminate();
|
||||
}
|
||||
|
||||
//if room is empty, delete the room
|
||||
if (!Object.keys(this._rooms[room]).length) {
|
||||
delete this._rooms[room];
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.1.3",
|
||||
"version": "1.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pairdrop",
|
||||
"version": "1.1.3",
|
||||
"version": "1.2.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.1.3",
|
||||
"version": "1.2.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -69,162 +69,176 @@
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||
</header>
|
||||
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<p id="pasteFilename"></p>
|
||||
</x-instructions>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
<!-- Peers -->
|
||||
<div class="x-peers-filler"></div>
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<p id="paste-filename"></p>
|
||||
</x-instructions>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="column">
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="displayName" placeholder=" "></div>
|
||||
<div id="display-name" placeholder=" "></div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Pair Device Dialog -->
|
||||
<x-dialog id="pairDeviceDialog">
|
||||
<x-dialog id="pair-device-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Pair Devices</h2>
|
||||
<div class="center" id="roomKeyQrCode"></div>
|
||||
<h1 class="center" id="roomKey">000 000</h1>
|
||||
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<div id="room-key-qr-code" class="center"></div>
|
||||
<h1 id="room-key" class="center">000 000</h1>
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="keyInputContainer">
|
||||
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Clear Devices Dialog -->
|
||||
<x-dialog id="clearDevicesDialog">
|
||||
<x-dialog id="clear-devices-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Request Dialog -->
|
||||
<x-dialog id="receiveRequestDialog">
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">PairDrop</h2>
|
||||
<div class="text-center file-description">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span id="requestingPeerDisplayName"></span>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
</div>
|
||||
<div class="row" id="fileName">
|
||||
<span id="fileStem"></span>
|
||||
<span id="fileExtension"></span>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="fileOther"></span>
|
||||
<div class="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Receive File Dialog -->
|
||||
<x-dialog id="receiveFileDialog">
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center" id="receiveTitle"></h2>
|
||||
<div class="text-center file-description"></div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<a class="button" id="shareOrDownload" autofocus></a>
|
||||
<div class="separator"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Send Text Dialog -->
|
||||
<x-dialog id="sendTextDialog">
|
||||
<x-dialog id="send-text-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Send a Message</h2>
|
||||
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="row-reverse">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span class="display-name"></span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
||||
<button class="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Text Dialog -->
|
||||
<x-dialog id="receiveTextDialog">
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Message Received</h2>
|
||||
<div id="receiveTextDescriptionContainer">
|
||||
<span id="receiveTextPeerDisplayName"></span>
|
||||
<span>sent the following message:</span>
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="close" title="ESCAPE">Close</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- base64PasteDialog Dialog -->
|
||||
<x-dialog id="base64PasteDialog">
|
||||
<!-- base64 Paste Dialog -->
|
||||
<x-dialog id="base64-paste-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" close>Close</button>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast class="row" shadow="1" id="toast"></x-toast>
|
||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
|
||||
@@ -320,7 +320,6 @@ class Peer {
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
case 'request':
|
||||
this._onFilesTransferRequest(message);
|
||||
@@ -441,7 +440,7 @@ class Peer {
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -492,6 +491,7 @@ class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
@@ -568,6 +568,14 @@ class RTCPeer extends Peer {
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message === 'string') {
|
||||
message = JSON.parse(message);
|
||||
console.log('RTC:', message);
|
||||
}
|
||||
super._onMessage(message);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
|
||||
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const $ = query => document.getElementById(query);
|
||||
const $$ = query => document.body.querySelector(query);
|
||||
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
|
||||
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
window.android = /android/i.test(navigator.userAgent);
|
||||
@@ -10,7 +9,7 @@ window.pasteMode.activated = false;
|
||||
// set display name
|
||||
Events.on('display-name', e => {
|
||||
const me = e.detail.message;
|
||||
const $displayName = $('displayName')
|
||||
const $displayName = $('display-name')
|
||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
||||
$displayName.title = me.deviceName;
|
||||
});
|
||||
@@ -28,7 +27,7 @@ class PeersUI {
|
||||
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
||||
this.peers = {};
|
||||
|
||||
this.$cancelPasteModeBtn = $('cancelPasteModeBtn');
|
||||
this.$cancelPasteModeBtn = $('cancel-paste-mode');
|
||||
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
||||
|
||||
Events.on('dragover', e => this._onDragOver(e));
|
||||
@@ -38,8 +37,12 @@ class PeersUI {
|
||||
Events.on('drop', e => this._onDrop(e));
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
|
||||
this.$xPeers = $$('x-peers');
|
||||
this.$xNoPeers = $$('x-no-peers');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
|
||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -53,11 +56,11 @@ class PeersUI {
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomType = roomType;
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].roomType = peer.roomType;
|
||||
this._redrawPeer(peer);
|
||||
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
|
||||
this._redrawPeer(this.peers[peer.id]);
|
||||
return; // peer already exists
|
||||
}
|
||||
this.peers[peer.id] = peer;
|
||||
@@ -72,7 +75,15 @@ class PeersUI {
|
||||
const peerNode = $(peer.id);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peerNode.classList.add(`type-${peer.roomType}`)
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
}
|
||||
|
||||
evaluateOverflowing() {
|
||||
if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {
|
||||
this.$xPeers.classList.add('overflowing');
|
||||
} else {
|
||||
this.$xPeers.classList.remove('overflowing');
|
||||
}
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
@@ -83,6 +94,7 @@ class PeersUI {
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
this.evaluateOverflowing();
|
||||
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
@@ -213,6 +225,18 @@ class PeersUI {
|
||||
|
||||
class PeerUI {
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
|
||||
$$('x-peers').appendChild(this.$el)
|
||||
Events.fire('peer-added');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
html() {
|
||||
let title;
|
||||
let input = '';
|
||||
@@ -225,17 +249,24 @@ class PeerUI {
|
||||
this.$el.innerHTML = `
|
||||
<label class="column center" title="${title}">
|
||||
${input}
|
||||
<x-icon shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
<x-icon>
|
||||
<div class="icon-wrapper" shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
</div>
|
||||
<div class="highlight-wrapper center">
|
||||
<div class="highlight" shadow="1"></div>
|
||||
</div>
|
||||
</x-icon>
|
||||
<div class="progress">
|
||||
<div class="circle"></div>
|
||||
<div class="circle right"></div>
|
||||
</div>
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
<div class="device-descriptor">
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
</div>
|
||||
</label>`;
|
||||
|
||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
@@ -245,23 +276,12 @@ class PeerUI {
|
||||
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
|
||||
}
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
this.$el = document.createElement('x-peer');
|
||||
this.$el.id = this._peer.id;
|
||||
this.$el.ui = this;
|
||||
this.$el.classList.add(`type-${this._roomType}`);
|
||||
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
this.$el.classList.add('center');
|
||||
this.html();
|
||||
|
||||
this._callbackInput = e => this._onFilesSelected(e)
|
||||
@@ -272,7 +292,7 @@ class PeerUI {
|
||||
this._callbackDragLeave = e => this._onDragEnd(e)
|
||||
this._callbackDragOver = e => this._onDragOver(e)
|
||||
this._callbackContextMenu = e => this._onRightClick(e)
|
||||
this._callbackTouchStart = _ => this._onTouchStart()
|
||||
this._callbackTouchStart = e => this._onTouchStart(e)
|
||||
this._callbackTouchEnd = e => this._onTouchEnd(e)
|
||||
this._callbackPointerDown = e => this._onPointerDown(e)
|
||||
// PasteMode
|
||||
@@ -393,21 +413,28 @@ class PeerUI {
|
||||
|
||||
_onRightClick(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
|
||||
_onTouchStart() {
|
||||
_onTouchStart(e) {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610);
|
||||
}
|
||||
|
||||
_onTouchEnd(e) {
|
||||
if (Date.now() - this._touchStart < 500) {
|
||||
clearTimeout(this._touchTimer);
|
||||
} else { // this was a long tap
|
||||
if (e) e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
} else if (this._touchTimer) { // this was a long tap
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
this._touchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,10 +472,14 @@ class Dialog {
|
||||
class ReceiveDialog extends Dialog {
|
||||
constructor(id) {
|
||||
super(id);
|
||||
|
||||
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
|
||||
this.$fileSizeNode = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview')
|
||||
this.$fileDescription = this.$el.querySelector('.file-description');
|
||||
this.$displayName = this.$el.querySelector('.display-name');
|
||||
this.$fileStem = this.$el.querySelector('.file-stem');
|
||||
this.$fileExtension = this.$el.querySelector('.file-extension');
|
||||
this.$fileOther = this.$el.querySelector('.file-other');
|
||||
this.$fileSize = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview');
|
||||
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
|
||||
}
|
||||
|
||||
_formatFileSize(bytes) {
|
||||
@@ -464,31 +495,52 @@ class ReceiveDialog extends Dialog {
|
||||
return bytes + ' Bytes';
|
||||
}
|
||||
}
|
||||
|
||||
_parseFileData(displayName, files, imagesOnly, totalSize) {
|
||||
if (files.length > 1) {
|
||||
let fileOtherText = ` and ${files.length - 1} other `;
|
||||
if (files.length === 2) {
|
||||
fileOtherText += imagesOnly ? 'image' : 'file';
|
||||
} else {
|
||||
fileOtherText += imagesOnly ? 'images' : 'files';
|
||||
}
|
||||
this.$fileOther.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
const fileName = files[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtension.innerText = fileExtension;
|
||||
this.$displayName.innerText = displayName;
|
||||
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveFileDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveFileDialog');
|
||||
super('receive-file-dialog');
|
||||
|
||||
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload');
|
||||
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle')
|
||||
this.$downloadBtn = this.$el.querySelector('#download-btn');
|
||||
this.$shareBtn = this.$el.querySelector('#share-btn');
|
||||
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
|
||||
this._filesQueue = [];
|
||||
}
|
||||
|
||||
_onFilesReceived(sender, files, request) {
|
||||
this._nextFiles(sender, files, request);
|
||||
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||
const displayName = $(sender).ui._displayName()
|
||||
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||
this._nextFiles();
|
||||
window.blop.play();
|
||||
}
|
||||
|
||||
_nextFiles(sender, nextFiles, nextRequest) {
|
||||
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
|
||||
_nextFiles() {
|
||||
if (this._busy) return;
|
||||
this._busy = true;
|
||||
const {peerId, files, request} = this._filesQueue.shift();
|
||||
this._displayFiles(peerId, files, request);
|
||||
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
@@ -519,7 +571,6 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let element = document.createElement(previewElement[mime]);
|
||||
element.src = URL.createObjectURL(file);
|
||||
element.controls = true;
|
||||
element.classList.add('element-preview');
|
||||
element.onload = _ => {
|
||||
this.$previewBox.appendChild(element);
|
||||
resolve(true)
|
||||
@@ -530,30 +581,32 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
});
|
||||
}
|
||||
|
||||
async _displayFiles(peerId, files, request) {
|
||||
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
|
||||
|
||||
let url;
|
||||
let title;
|
||||
let filenameDownload;
|
||||
|
||||
let descriptor = request.imagesOnly ? "Image" : "File";
|
||||
|
||||
let size = this._formatFileSize(request.totalSize);
|
||||
let description = files[0].name;
|
||||
|
||||
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
||||
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
||||
|
||||
let descriptor, url, filenameDownload;
|
||||
if (files.length === 1) {
|
||||
url = URL.createObjectURL(files[0])
|
||||
title = `PairDrop - ${descriptor} Received`
|
||||
filenameDownload = files[0].name;
|
||||
descriptor = imagesOnly ? 'Image' : 'File';
|
||||
} else {
|
||||
title = `PairDrop - ${files.length} ${descriptor}s Received`
|
||||
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
|
||||
if(files.length>2) description += "s";
|
||||
descriptor = imagesOnly ? 'Images' : 'Files';
|
||||
}
|
||||
this.$receiveTitle.innerText = `${descriptor} Received`;
|
||||
|
||||
if(!shareInsteadOfDownload) {
|
||||
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
if (canShare) {
|
||||
this.$shareBtn.removeAttribute('hidden');
|
||||
this.$shareBtn.onclick = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let downloadZipped = false;
|
||||
if (files.length > 1) {
|
||||
downloadZipped = true;
|
||||
try {
|
||||
let bytesCompleted = 0;
|
||||
zipper.createNewZipWriter();
|
||||
for (let i=0; i<files.length; i++) {
|
||||
@@ -561,7 +614,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
onprogress: (progress) => {
|
||||
Events.fire('set-progress', {
|
||||
peerId: peerId,
|
||||
progress: (bytesCompleted + progress) / request.totalSize,
|
||||
progress: (bytesCompleted + progress) / totalSize,
|
||||
status: 'process'
|
||||
})
|
||||
}
|
||||
@@ -581,47 +634,58 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let minutes = now.getMinutes().toString();
|
||||
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
||||
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
downloadZipped = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.$receiveTitleNode.textContent = title;
|
||||
this.$fileDescriptionNode.textContent = description;
|
||||
this.$fileSizeNode.textContent = size;
|
||||
|
||||
if (shareInsteadOfDownload) {
|
||||
this.$shareOrDownloadBtn.innerText = "Share";
|
||||
this.continue = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => console.error(err));
|
||||
this.$downloadBtn.innerText = "Download";
|
||||
this.$downloadBtn.onclick = _ => {
|
||||
if (downloadZipped) {
|
||||
let tmpZipBtn = document.createElement("a");
|
||||
tmpZipBtn.download = filenameDownload;
|
||||
tmpZipBtn.href = url;
|
||||
tmpZipBtn.click();
|
||||
} else {
|
||||
this._downloadFilesIndividually(files);
|
||||
}
|
||||
this.continueCallback = _ => this.continue();
|
||||
} else {
|
||||
this.$shareOrDownloadBtn.innerText = "Download again";
|
||||
this.continue = _ => {
|
||||
let tmpBtn = document.createElement("a");
|
||||
tmpBtn.download = filenameDownload;
|
||||
tmpBtn.href = url;
|
||||
tmpBtn.click();
|
||||
};
|
||||
this.continueCallback = _ => {
|
||||
this.continue();
|
||||
this.hide();
|
||||
};
|
||||
}
|
||||
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
|
||||
|
||||
if (!canShare) {
|
||||
this.$downloadBtn.innerText = "Download again";
|
||||
}
|
||||
Events.fire('notify-user', `${descriptor} downloaded successfully`);
|
||||
this.$downloadBtn.style.pointerEvents = "none";
|
||||
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
|
||||
};
|
||||
|
||||
this.createPreviewElement(files[0]).finally(_ => {
|
||||
document.title = `PairDrop - ${files.length} Files received`;
|
||||
document.title = files.length === 1
|
||||
? 'File received - PairDrop'
|
||||
: `(${files.length}) Files received - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
|
||||
this.continue();
|
||||
this.show();
|
||||
|
||||
if (canShare) {
|
||||
this.$shareBtn.click();
|
||||
} else {
|
||||
this.$downloadBtn.click();
|
||||
}
|
||||
}).catch(r => console.error(r));
|
||||
}
|
||||
|
||||
_downloadFilesIndividually(files) {
|
||||
let tmpBtn = document.createElement("a");
|
||||
for (let i=0; i<files.length; i++) {
|
||||
tmpBtn.download = files[i].name;
|
||||
tmpBtn.href = URL.createObjectURL(files[i]);
|
||||
tmpBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$shareOrDownloadBtn.removeAttribute('href');
|
||||
this.$shareOrDownloadBtn.removeAttribute('download');
|
||||
this.$shareBtn.setAttribute('hidden', '');
|
||||
this.$previewBox.innerHTML = '';
|
||||
super.hide();
|
||||
this._dequeueFile();
|
||||
@@ -631,15 +695,10 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveRequestDialog');
|
||||
super('receive-request-dialog');
|
||||
|
||||
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName');
|
||||
this.$fileStemNode = this.$el.querySelector('#fileStem');
|
||||
this.$fileExtensionNode = this.$el.querySelector('#fileExtension');
|
||||
this.$fileOtherNode = this.$el.querySelector('#fileOther');
|
||||
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#declineRequest');
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
||||
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
||||
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
|
||||
|
||||
@@ -669,32 +728,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
_showRequestDialog(request, peerId) {
|
||||
this.correspondingPeerId = peerId;
|
||||
|
||||
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
const fileName = request.header[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtensionNode.innerText = fileExtension
|
||||
|
||||
if (request.header.length >= 2) {
|
||||
let fileOtherText = ` and ${request.header.length - 1} other `;
|
||||
fileOtherText += request.imagesOnly ? 'image' : 'file';
|
||||
if (request.header.length > 2) fileOtherText += "s";
|
||||
this.$fileOtherNode.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
|
||||
const displayName = $(peerId).ui._displayName();
|
||||
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
||||
|
||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||
let element = document.createElement('img');
|
||||
element.src = request.thumbnailDataUrl;
|
||||
element.classList.add('element-preview');
|
||||
|
||||
this.$previewBox.appendChild(element)
|
||||
}
|
||||
|
||||
document.title = 'PairDrop - File Transfer Requested';
|
||||
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
|
||||
|
||||
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
@@ -720,16 +765,16 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pairDeviceDialog');
|
||||
super('pair-device-dialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
this.$roomKey = this.$el.querySelector('#roomKey');
|
||||
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
|
||||
this.$roomKey = this.$el.querySelector('#room-key');
|
||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
||||
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
|
||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||
@@ -799,7 +844,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
evaluateRoomKeyChars() {
|
||||
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
|
||||
if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) {
|
||||
this.$submitBtn.setAttribute("disabled", "");
|
||||
} else {
|
||||
this.inputRoomKey = "";
|
||||
@@ -808,7 +853,7 @@ class PairDeviceDialog extends Dialog {
|
||||
})
|
||||
this.$submitBtn.removeAttribute("disabled");
|
||||
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
||||
this._onSubmit();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -843,7 +888,7 @@ class PairDeviceDialog extends Dialog {
|
||||
height: 150,
|
||||
padding: 0,
|
||||
background: "transparent",
|
||||
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
|
||||
color: `rgb(var(--text-color))`,
|
||||
ecl: "L",
|
||||
join: true
|
||||
});
|
||||
@@ -858,7 +903,8 @@ class PairDeviceDialog extends Dialog {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
|
||||
@@ -935,23 +981,29 @@ class PairDeviceDialog extends Dialog {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
|
||||
}
|
||||
Events.fire('bg-resize');
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clearDevicesDialog');
|
||||
super('clear-devices-dialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
||||
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._clearRoomSecrets();
|
||||
}
|
||||
|
||||
_clearRoomSecrets() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
}
|
||||
@@ -959,12 +1011,13 @@ class ClearDevicesDialog extends Dialog {
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail));
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
super('send-text-dialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
||||
this.$text = this.$el.querySelector('#text-input');
|
||||
this.$peerDisplayName = this.$el.querySelector('.display-name');
|
||||
this.$form = this.$el.querySelector('form');
|
||||
this.$submit = this.$el.querySelector('button[type="submit"]');
|
||||
this.$form.addEventListener('submit', _ => this._send());
|
||||
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$text.addEventListener('input', e => this._onChange(e));
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
}
|
||||
@@ -992,8 +1045,9 @@ class SendTextDialog extends Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
_onRecipient(peerId) {
|
||||
_onRecipient(peerId, deviceName) {
|
||||
this.correspondingPeerId = peerId;
|
||||
this.$peerDisplayName.innerText = deviceName;
|
||||
this.show();
|
||||
|
||||
const range = document.createRange();
|
||||
@@ -1005,6 +1059,11 @@ class SendTextDialog extends Dialog {
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._send();
|
||||
}
|
||||
|
||||
_send() {
|
||||
Events.fire('send-text', {
|
||||
to: this.correspondingPeerId,
|
||||
@@ -1017,7 +1076,7 @@ class SendTextDialog extends Dialog {
|
||||
|
||||
class ReceiveTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('receiveTextDialog');
|
||||
super('receive-text-dialog');
|
||||
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
|
||||
this.$text = this.$el.querySelector('#text');
|
||||
this.$copy = this.$el.querySelector('#copy');
|
||||
@@ -1028,7 +1087,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
|
||||
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName');
|
||||
this.$displayNameNode = this.$el.querySelector('.display-name');
|
||||
this._receiveTextQueue = [];
|
||||
}
|
||||
|
||||
@@ -1046,6 +1105,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
_onText(text, peerId) {
|
||||
window.blop.play();
|
||||
this._receiveTextQueue.push({text: text, peerId: peerId});
|
||||
this._setDocumentTitleMessages();
|
||||
if (this.$el.attributes["show"]) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
@@ -1057,23 +1117,35 @@ class ReceiveTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
_showReceiveTextDialog(text, peerId) {
|
||||
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
this.$displayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
if (isURL(text)) {
|
||||
const $a = document.createElement('a');
|
||||
$a.href = text;
|
||||
$a.target = '_blank';
|
||||
$a.textContent = text;
|
||||
this.$text.innerHTML = '';
|
||||
this.$text.appendChild($a);
|
||||
} else {
|
||||
this.$text.textContent = text;
|
||||
this.$text.innerText = text;
|
||||
this.$text.classList.remove('text-center');
|
||||
|
||||
// Beautify text if text is short
|
||||
if (text.length < 2000) {
|
||||
// replace urls with actual links
|
||||
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
});
|
||||
|
||||
if (!/\s/.test(text)) {
|
||||
this.$text.classList.add('text-center');
|
||||
}
|
||||
}
|
||||
document.title = 'PairDrop - Message Received';
|
||||
|
||||
this._setDocumentTitleMessages();
|
||||
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
|
||||
_setDocumentTitleMessages() {
|
||||
document.title = !this._receiveTextQueue.length
|
||||
? 'Message Received - PairDrop'
|
||||
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`;
|
||||
}
|
||||
|
||||
async _onCopy() {
|
||||
await navigator.clipboard.writeText(this.$text.textContent);
|
||||
Events.fire('notify-user', 'Copied to clipboard');
|
||||
@@ -1089,13 +1161,13 @@ class ReceiveTextDialog extends Dialog {
|
||||
class Base64ZipDialog extends Dialog {
|
||||
|
||||
constructor() {
|
||||
super('base64PasteDialog');
|
||||
super('base64-paste-dialog');
|
||||
const urlParams = new URL(window.location).searchParams;
|
||||
const base64Text = urlParams.get('base64text');
|
||||
const base64Zip = urlParams.get('base64zip');
|
||||
const base64Hash = window.location.hash.substring(1);
|
||||
|
||||
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
|
||||
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
||||
|
||||
if (base64Text) {
|
||||
this.show();
|
||||
@@ -1146,7 +1218,7 @@ class Base64ZipDialog extends Dialog {
|
||||
}
|
||||
|
||||
_setPasteBtnToProcessing() {
|
||||
this.$pasteBtn.pointerEvents = "none";
|
||||
this.$pasteBtn.style.pointerEvents = "none";
|
||||
this.$pasteBtn.innerText = "Processing...";
|
||||
}
|
||||
|
||||
@@ -1246,6 +1318,7 @@ class Notifications {
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||
}
|
||||
// Todo: fix Notifications
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
||||
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
||||
}
|
||||
@@ -1290,7 +1363,7 @@ class Notifications {
|
||||
_messageNotification(message, peerId) {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
const peerDisplayName = $(peerId).ui._displayName();
|
||||
if (isURL(message)) {
|
||||
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
|
||||
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
|
||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||
} else {
|
||||
@@ -1321,7 +1394,7 @@ class Notifications {
|
||||
}
|
||||
|
||||
_download(notification) {
|
||||
$('shareOrDownload').click();
|
||||
$('share-or-download').click();
|
||||
notification.close();
|
||||
}
|
||||
|
||||
@@ -1710,23 +1783,19 @@ Events.on('load', () => {
|
||||
let x0, y0, w, h, dw, offset;
|
||||
|
||||
function init() {
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
w = document.documentElement.clientWidth;
|
||||
h = document.documentElement.clientHeight;
|
||||
c.width = w;
|
||||
c.height = h;
|
||||
offset = h > 800
|
||||
? 116
|
||||
: h > 380
|
||||
? 100
|
||||
: 65;
|
||||
|
||||
if (w < 420) offset += 20;
|
||||
offset = $$('footer').offsetHeight - 32;
|
||||
if (h > 800) offset += 16;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.max(w, h, 1000) / 13;
|
||||
drawCircles();
|
||||
}
|
||||
window.onresize = init;
|
||||
Events.on('bg-resize', _ => init());
|
||||
window.onresize = _ => Events.fire('bg-resize');
|
||||
|
||||
function drawCircle(radius) {
|
||||
ctx.beginPath();
|
||||
@@ -1791,9 +1860,3 @@ Notifications permission has been blocked
|
||||
as the user has dismissed the permission prompt several times.
|
||||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = _ => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.1.3';
|
||||
const cacheVersion = 'v1.2.1';
|
||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -10,28 +10,25 @@
|
||||
|
||||
/* Layout */
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
overscroll-behavior: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
@@ -73,10 +70,7 @@ body {
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
@@ -119,9 +113,9 @@ h3 {
|
||||
}
|
||||
|
||||
.font-subheading {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
line-height: 18px;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
@@ -199,20 +193,151 @@ body>header a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#center {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 132px;
|
||||
max-height: calc(100vh - 56px - var(--footer-height));
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 425px) {
|
||||
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Peers List */
|
||||
|
||||
#x-peers-filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
x-peers {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-grow: 1;
|
||||
align-items: start !important;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 2;
|
||||
transition: color 300ms;
|
||||
transition: --bg-color 0.5s ease;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-x: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
--peers-per-row: 6; /* default if browser does not support :has selector */
|
||||
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
|
||||
width: var(--x-peers-width);
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
x-peers.overflowing {
|
||||
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
|
||||
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
|
||||
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||
|
||||
/* Opera doesn't support this in the shorthand */
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
|
||||
@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px),
|
||||
screen and (min-height: 486px) and (max-height: 631px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(7)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 649px) and (max-width: 425px),
|
||||
screen and (min-height: 631px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(28)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Empty Peers List */
|
||||
|
||||
x-no-peers {
|
||||
height: 114px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
@@ -254,25 +379,19 @@ x-no-peers[drop-bg] * {
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 8px;
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
x-peer .name {
|
||||
width: var(--peer-width);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
@@ -280,21 +399,45 @@ input[type="file"] {
|
||||
|
||||
x-peer x-icon {
|
||||
--icon-size: 40px;
|
||||
margin-bottom: 4px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
x-peer .icon-wrapper {
|
||||
width: var(--icon-size);
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip) x-icon {
|
||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
margin: 7px auto 0;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||
background-color: var(--paired-device-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer:not([status]):hover x-icon,
|
||||
x-peer:not([status]):focus x-icon {
|
||||
transform: scale(1.05);
|
||||
@@ -306,6 +449,18 @@ x-peer[status] x-icon {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status,
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
@@ -371,10 +526,9 @@ x-peer[drop] x-icon {
|
||||
/* Footer */
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
z-index: 2;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
@@ -385,6 +539,7 @@ footer .logo {
|
||||
--icon-size: 80px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
@@ -409,7 +564,7 @@ x-dialog x-background {
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
padding: 35px;
|
||||
padding: 15px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
@@ -420,16 +575,20 @@ x-dialog x-paper {
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pairDeviceDialog x-paper {
|
||||
#pair-device-dialog x-paper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
height: 650px;
|
||||
margin-top: -325px;
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
@@ -444,12 +603,6 @@ x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 0;
|
||||
margin-bottom: -16px;
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
x-dialog a {
|
||||
color: var(--primary-color);
|
||||
@@ -461,13 +614,13 @@ x-dialog .font-subheading {
|
||||
|
||||
/* PairDevicesDialog */
|
||||
|
||||
#keyInputContainer {
|
||||
#key-input-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input {
|
||||
#key-input-container>input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
@@ -483,15 +636,15 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input + * {
|
||||
#key-input-container>input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#keyInputContainer>input:nth-of-type(4) {
|
||||
margin-left: 18px;
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#roomKey {
|
||||
#room-key {
|
||||
font-size: 50px;
|
||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||
display: inline-block;
|
||||
@@ -499,19 +652,15 @@ x-dialog .font-subheading {
|
||||
margin: 15px -15px;
|
||||
}
|
||||
|
||||
#roomKeyQrCode {
|
||||
padding: inherit;
|
||||
margin: auto;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
#room-key-qr-code {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
#pair-device-dialog hr {
|
||||
margin: 40px -24px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog x-background {
|
||||
#pair-device-dialog x-background {
|
||||
padding: 16px!important;
|
||||
}
|
||||
|
||||
@@ -522,29 +671,24 @@ x-dialog .row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#receiveRequestDialog h2,
|
||||
#receiveFileDialog h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse {
|
||||
margin: 40px -24px auto;
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: solid 1.25px var(--border-color);
|
||||
margin-bottom: -16px;
|
||||
x-paper > div:last-child > .button {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
word-break: break-word;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
@@ -556,52 +700,52 @@ x-dialog .row-reverse {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
#fileName {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#fileStem {
|
||||
max-width: 80%;
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.file-size{
|
||||
margin-bottom: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
#textInput {
|
||||
min-height: 120px;
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
|
||||
#receiveTextDialog #text {
|
||||
#receive-text-dialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
white-space: pre-wrap;
|
||||
margin-top:36px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a {
|
||||
#receive-text-dialog #text a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a:hover {
|
||||
#receive-text-dialog #text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#receiveTextDialog h3 {
|
||||
#receive-text-dialog h3 {
|
||||
/* Select the received text when double-clicking the dialog */
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
@@ -609,29 +753,25 @@ x-dialog .row-reverse {
|
||||
|
||||
.row-separator {
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
margin: auto -25px;
|
||||
margin: auto -24px;
|
||||
}
|
||||
|
||||
#receiveTextDescriptionContainer {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#base64PasteBtn {
|
||||
#base64-paste-btn {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
}
|
||||
|
||||
#base64PasteDialog button {
|
||||
#base64-paste-dialog button {
|
||||
margin: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64PasteDialog button[close] {
|
||||
#base64-paste-dialog button[close] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#base64PasteDialog button[close]:before {
|
||||
#base64-paste-dialog button[close]:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -641,7 +781,6 @@ x-dialog .row-reverse {
|
||||
padding: 2px 16px 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
@@ -652,6 +791,7 @@ x-dialog .row-reverse {
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@@ -689,16 +829,18 @@ x-dialog .row-reverse {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancelPasteModeBtn {
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: 56px;
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
background-color: var(--primary-color);
|
||||
color: rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.button:focus:before,
|
||||
@@ -714,7 +856,6 @@ button::-moz-focus-inner {
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -724,10 +865,7 @@ button::-moz-focus-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -741,9 +879,8 @@ button::-moz-focus-inner {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 254px);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -809,7 +946,7 @@ button::-moz-focus-inner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -8px;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
@@ -876,13 +1013,16 @@ x-toast:not([show]):not(:hover) {
|
||||
/* Instructions */
|
||||
|
||||
x-instructions {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
z-index: -1;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
@@ -899,88 +1039,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
|
||||
|
||||
x-instructions p {
|
||||
display: none;
|
||||
margin: 0 auto auto;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
x-peer {
|
||||
transform: scale(0.95);
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#websocket-fallback {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
#websocket-fallback>span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#websocket-fallback > span > span {
|
||||
border-bottom: solid 4px var(--ws-peer-color);
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 800px) {
|
||||
@media screen and (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px),
|
||||
screen and (min-width: 1100px) {
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
content: attr(desktop);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 420px) {
|
||||
x-instructions {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
|
||||
|
||||
html {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Color Themes
|
||||
*/
|
||||
|
||||
/* Default colors */
|
||||
body {
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-test: 18,18,18;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
body.dark-theme {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Colored Elements */
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
color: rgb(var(--text-color));
|
||||
background-color: rgb(var(--bg-color));
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
background-color: var(--bg-color);
|
||||
background-color: rgb(var(--bg-color));
|
||||
}
|
||||
|
||||
.textarea {
|
||||
color: var(--text-color) !important;
|
||||
color: rgb(var(--text-color)) !important;
|
||||
background-color: var(--bg-color-secondary) !important;
|
||||
}
|
||||
|
||||
@@ -1006,7 +1150,9 @@ x-dialog x-paper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.element-preview {
|
||||
.file-preview > img,
|
||||
.file-preview > audio,
|
||||
.file-preview > video {
|
||||
max-width: 100%;
|
||||
max-height: 40vh;
|
||||
margin: auto;
|
||||
@@ -1018,16 +1164,16 @@ x-dialog x-paper {
|
||||
|
||||
/* defaults to dark theme */
|
||||
body {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Override dark mode with light mode styles if the user decides to swap */
|
||||
body.light-theme {
|
||||
--text-color: #333;
|
||||
--bg-color: #fafafa;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
@@ -1045,6 +1191,15 @@ x-dialog x-paper {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
html {
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
}
|
||||
|
||||
/* webkit scrollbar style*/
|
||||
|
||||
::-webkit-scrollbar{
|
||||
|
||||
@@ -69,165 +69,179 @@
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||
</header>
|
||||
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
<br>
|
||||
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
|
||||
<p id="pasteFilename"></p>
|
||||
</x-instructions>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
<!-- Peers -->
|
||||
<div class="x-peers-filler"></div>
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<p id="paste-filename"></p>
|
||||
</x-instructions>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="column">
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="displayName" placeholder=" "></div>
|
||||
<div id="display-name" placeholder=" "></div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
</div>
|
||||
<div id="websocket-fallback">
|
||||
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Pair Device Dialog -->
|
||||
<x-dialog id="pairDeviceDialog">
|
||||
<x-dialog id="pair-device-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Pair Devices</h2>
|
||||
<div class="center" id="roomKeyQrCode"></div>
|
||||
<h1 class="center" id="roomKey">000 000</h1>
|
||||
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<div id="room-key-qr-code" class="center"></div>
|
||||
<h1 id="room-key" class="center">000 000</h1>
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="keyInputContainer">
|
||||
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Clear Devices Dialog -->
|
||||
<x-dialog id="clearDevicesDialog">
|
||||
<x-dialog id="clear-devices-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Request Dialog -->
|
||||
<x-dialog id="receiveRequestDialog">
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">PairDrop</h2>
|
||||
<div class="text-center file-description">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span id="requestingPeerDisplayName"></span>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
</div>
|
||||
<div class="row" id="fileName">
|
||||
<span id="fileStem"></span>
|
||||
<span id="fileExtension"></span>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="fileOther"></span>
|
||||
<div class="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Receive File Dialog -->
|
||||
<x-dialog id="receiveFileDialog">
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center" id="receiveTitle"></h2>
|
||||
<div class="text-center file-description"></div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<a class="button" id="shareOrDownload" autofocus></a>
|
||||
<div class="separator"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Send Text Dialog -->
|
||||
<x-dialog id="sendTextDialog">
|
||||
<x-dialog id="send-text-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Send a Message</h2>
|
||||
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="row-reverse">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span class="display-name"></span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
||||
<button class="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Text Dialog -->
|
||||
<x-dialog id="receiveTextDialog">
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Message Received</h2>
|
||||
<div id="receiveTextDescriptionContainer">
|
||||
<span id="receiveTextPeerDisplayName"></span>
|
||||
<span>sent the following message:</span>
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="close" title="ESCAPE">Close</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- base64PasteDialog Dialog -->
|
||||
<x-dialog id="base64PasteDialog">
|
||||
<!-- base64 Paste Dialog -->
|
||||
<x-dialog id="base64-paste-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" close>Close</button>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast class="row" shadow="1" id="toast"></x-toast>
|
||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
|
||||
@@ -324,13 +324,12 @@ class Peer {
|
||||
this.sendJSON({ type: 'progress', progress: progress });
|
||||
}
|
||||
|
||||
_onMessage(message, logMessage = true) {
|
||||
_onMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
if (logMessage) console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
case 'request':
|
||||
this._onFilesTransferRequest(message);
|
||||
@@ -451,7 +450,7 @@ class Peer {
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -502,6 +501,7 @@ class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
@@ -578,6 +578,14 @@ class RTCPeer extends Peer {
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message === 'string') {
|
||||
message = JSON.parse(message);
|
||||
console.log('RTC:', message);
|
||||
}
|
||||
super._onMessage(message);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
|
||||
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
|
||||
@@ -682,6 +690,7 @@ class WSPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = false;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._sendSignal();
|
||||
}
|
||||
@@ -700,15 +709,15 @@ class WSPeer extends Peer {
|
||||
this._server.send(message);
|
||||
}
|
||||
|
||||
_sendSignal() {
|
||||
this.sendJSON({type: 'signal'});
|
||||
_sendSignal(connected = false) {
|
||||
this.sendJSON({type: 'signal', connected: connected});
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (this._peerId) return;
|
||||
if (message.connected) return;
|
||||
this._peerId = message.sender.id;
|
||||
this._sendSignal();
|
||||
this._sendSignal(true);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
@@ -730,6 +739,7 @@ class PeersManager {
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||
}
|
||||
|
||||
@@ -749,7 +759,7 @@ class PeersManager {
|
||||
_onWsRelay(message) {
|
||||
const messageJSON = JSON.parse(message)
|
||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
||||
this.peers[messageJSON.sender.id]._onMessage(message, false)
|
||||
this.peers[messageJSON.sender.id]._onMessage(message)
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
@@ -797,15 +807,24 @@ class PeersManager {
|
||||
}
|
||||
|
||||
_onPeerLeft(msg) {
|
||||
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) {
|
||||
console.log('WSPeer left:', msg.peerId)
|
||||
Events.fire('peer-disconnected', msg.peerId)
|
||||
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
console.log('WSPeer left:', msg.peerId);
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
} else if (msg.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
for (const peerId in this.peers) {
|
||||
console.debug(this.peers[peerId].rtcSupported);
|
||||
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
Events.fire('peer-disconnected', peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const $ = query => document.getElementById(query);
|
||||
const $$ = query => document.body.querySelector(query);
|
||||
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
|
||||
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
window.android = /android/i.test(navigator.userAgent);
|
||||
@@ -10,7 +9,7 @@ window.pasteMode.activated = false;
|
||||
// set display name
|
||||
Events.on('display-name', e => {
|
||||
const me = e.detail.message;
|
||||
const $displayName = $('displayName')
|
||||
const $displayName = $('display-name')
|
||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
||||
$displayName.title = me.deviceName;
|
||||
});
|
||||
@@ -28,7 +27,7 @@ class PeersUI {
|
||||
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
||||
this.peers = {};
|
||||
|
||||
this.$cancelPasteModeBtn = $('cancelPasteModeBtn');
|
||||
this.$cancelPasteModeBtn = $('cancel-paste-mode');
|
||||
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
||||
|
||||
Events.on('dragover', e => this._onDragOver(e));
|
||||
@@ -38,8 +37,12 @@ class PeersUI {
|
||||
Events.on('drop', e => this._onDrop(e));
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
|
||||
this.$xPeers = $$('x-peers');
|
||||
this.$xNoPeers = $$('x-no-peers');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
|
||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -53,11 +56,11 @@ class PeersUI {
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomType = roomType;
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].roomType = peer.roomType;
|
||||
this._redrawPeer(peer);
|
||||
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
|
||||
this._redrawPeer(this.peers[peer.id]);
|
||||
return; // peer already exists
|
||||
}
|
||||
this.peers[peer.id] = peer;
|
||||
@@ -72,7 +75,15 @@ class PeersUI {
|
||||
const peerNode = $(peer.id);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peerNode.classList.add(`type-${peer.roomType}`)
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
}
|
||||
|
||||
evaluateOverflowing() {
|
||||
if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {
|
||||
this.$xPeers.classList.add('overflowing');
|
||||
} else {
|
||||
this.$xPeers.classList.remove('overflowing');
|
||||
}
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
@@ -83,6 +94,7 @@ class PeersUI {
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
this.evaluateOverflowing();
|
||||
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
@@ -213,6 +225,18 @@ class PeersUI {
|
||||
|
||||
class PeerUI {
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
|
||||
$$('x-peers').appendChild(this.$el)
|
||||
Events.fire('peer-added');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
html() {
|
||||
let title;
|
||||
let input = '';
|
||||
@@ -225,17 +249,24 @@ class PeerUI {
|
||||
this.$el.innerHTML = `
|
||||
<label class="column center" title="${title}">
|
||||
${input}
|
||||
<x-icon shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
<x-icon>
|
||||
<div class="icon-wrapper" shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
</div>
|
||||
<div class="highlight-wrapper center">
|
||||
<div class="highlight" shadow="1"></div>
|
||||
</div>
|
||||
</x-icon>
|
||||
<div class="progress">
|
||||
<div class="circle"></div>
|
||||
<div class="circle right"></div>
|
||||
</div>
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
<div class="device-descriptor">
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
</div>
|
||||
</label>`;
|
||||
|
||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
@@ -245,23 +276,12 @@ class PeerUI {
|
||||
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
|
||||
}
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
this.$el = document.createElement('x-peer');
|
||||
this.$el.id = this._peer.id;
|
||||
this.$el.ui = this;
|
||||
this.$el.classList.add(`type-${this._roomType}`);
|
||||
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
this.$el.classList.add('center');
|
||||
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer')
|
||||
this.html();
|
||||
|
||||
@@ -273,7 +293,7 @@ class PeerUI {
|
||||
this._callbackDragLeave = e => this._onDragEnd(e)
|
||||
this._callbackDragOver = e => this._onDragOver(e)
|
||||
this._callbackContextMenu = e => this._onRightClick(e)
|
||||
this._callbackTouchStart = _ => this._onTouchStart()
|
||||
this._callbackTouchStart = e => this._onTouchStart(e)
|
||||
this._callbackTouchEnd = e => this._onTouchEnd(e)
|
||||
this._callbackPointerDown = e => this._onPointerDown(e)
|
||||
// PasteMode
|
||||
@@ -394,21 +414,28 @@ class PeerUI {
|
||||
|
||||
_onRightClick(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
|
||||
_onTouchStart() {
|
||||
_onTouchStart(e) {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610);
|
||||
}
|
||||
|
||||
_onTouchEnd(e) {
|
||||
if (Date.now() - this._touchStart < 500) {
|
||||
clearTimeout(this._touchTimer);
|
||||
} else { // this was a long tap
|
||||
if (e) e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
} else if (this._touchTimer) { // this was a long tap
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
this._touchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,10 +473,14 @@ class Dialog {
|
||||
class ReceiveDialog extends Dialog {
|
||||
constructor(id) {
|
||||
super(id);
|
||||
|
||||
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
|
||||
this.$fileSizeNode = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview')
|
||||
this.$fileDescription = this.$el.querySelector('.file-description');
|
||||
this.$displayName = this.$el.querySelector('.display-name');
|
||||
this.$fileStem = this.$el.querySelector('.file-stem');
|
||||
this.$fileExtension = this.$el.querySelector('.file-extension');
|
||||
this.$fileOther = this.$el.querySelector('.file-other');
|
||||
this.$fileSize = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview');
|
||||
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
|
||||
}
|
||||
|
||||
_formatFileSize(bytes) {
|
||||
@@ -465,31 +496,52 @@ class ReceiveDialog extends Dialog {
|
||||
return bytes + ' Bytes';
|
||||
}
|
||||
}
|
||||
|
||||
_parseFileData(displayName, files, imagesOnly, totalSize) {
|
||||
if (files.length > 1) {
|
||||
let fileOtherText = ` and ${files.length - 1} other `;
|
||||
if (files.length === 2) {
|
||||
fileOtherText += imagesOnly ? 'image' : 'file';
|
||||
} else {
|
||||
fileOtherText += imagesOnly ? 'images' : 'files';
|
||||
}
|
||||
this.$fileOther.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
const fileName = files[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtension.innerText = fileExtension;
|
||||
this.$displayName.innerText = displayName;
|
||||
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveFileDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveFileDialog');
|
||||
super('receive-file-dialog');
|
||||
|
||||
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload');
|
||||
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle')
|
||||
this.$downloadBtn = this.$el.querySelector('#download-btn');
|
||||
this.$shareBtn = this.$el.querySelector('#share-btn');
|
||||
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
|
||||
this._filesQueue = [];
|
||||
}
|
||||
|
||||
_onFilesReceived(sender, files, request) {
|
||||
this._nextFiles(sender, files, request);
|
||||
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||
const displayName = $(sender).ui._displayName()
|
||||
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||
this._nextFiles();
|
||||
window.blop.play();
|
||||
}
|
||||
|
||||
_nextFiles(sender, nextFiles, nextRequest) {
|
||||
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
|
||||
_nextFiles() {
|
||||
if (this._busy) return;
|
||||
this._busy = true;
|
||||
const {peerId, files, request} = this._filesQueue.shift();
|
||||
this._displayFiles(peerId, files, request);
|
||||
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
@@ -520,7 +572,6 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let element = document.createElement(previewElement[mime]);
|
||||
element.src = URL.createObjectURL(file);
|
||||
element.controls = true;
|
||||
element.classList.add('element-preview');
|
||||
element.onload = _ => {
|
||||
this.$previewBox.appendChild(element);
|
||||
resolve(true)
|
||||
@@ -531,30 +582,32 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
});
|
||||
}
|
||||
|
||||
async _displayFiles(peerId, files, request) {
|
||||
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
|
||||
|
||||
let url;
|
||||
let title;
|
||||
let filenameDownload;
|
||||
|
||||
let descriptor = request.imagesOnly ? "Image" : "File";
|
||||
|
||||
let size = this._formatFileSize(request.totalSize);
|
||||
let description = files[0].name;
|
||||
|
||||
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
||||
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
||||
|
||||
let descriptor, url, filenameDownload;
|
||||
if (files.length === 1) {
|
||||
url = URL.createObjectURL(files[0])
|
||||
title = `PairDrop - ${descriptor} Received`
|
||||
filenameDownload = files[0].name;
|
||||
descriptor = imagesOnly ? 'Image' : 'File';
|
||||
} else {
|
||||
title = `PairDrop - ${files.length} ${descriptor}s Received`
|
||||
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
|
||||
if(files.length>2) description += "s";
|
||||
descriptor = imagesOnly ? 'Images' : 'Files';
|
||||
}
|
||||
this.$receiveTitle.innerText = `${descriptor} Received`;
|
||||
|
||||
if(!shareInsteadOfDownload) {
|
||||
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
if (canShare) {
|
||||
this.$shareBtn.removeAttribute('hidden');
|
||||
this.$shareBtn.onclick = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let downloadZipped = false;
|
||||
if (files.length > 1) {
|
||||
downloadZipped = true;
|
||||
try {
|
||||
let bytesCompleted = 0;
|
||||
zipper.createNewZipWriter();
|
||||
for (let i=0; i<files.length; i++) {
|
||||
@@ -562,7 +615,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
onprogress: (progress) => {
|
||||
Events.fire('set-progress', {
|
||||
peerId: peerId,
|
||||
progress: (bytesCompleted + progress) / request.totalSize,
|
||||
progress: (bytesCompleted + progress) / totalSize,
|
||||
status: 'process'
|
||||
})
|
||||
}
|
||||
@@ -582,47 +635,58 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let minutes = now.getMinutes().toString();
|
||||
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
||||
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
downloadZipped = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.$receiveTitleNode.textContent = title;
|
||||
this.$fileDescriptionNode.textContent = description;
|
||||
this.$fileSizeNode.textContent = size;
|
||||
|
||||
if (shareInsteadOfDownload) {
|
||||
this.$shareOrDownloadBtn.innerText = "Share";
|
||||
this.continue = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => console.error(err));
|
||||
this.$downloadBtn.innerText = "Download";
|
||||
this.$downloadBtn.onclick = _ => {
|
||||
if (downloadZipped) {
|
||||
let tmpZipBtn = document.createElement("a");
|
||||
tmpZipBtn.download = filenameDownload;
|
||||
tmpZipBtn.href = url;
|
||||
tmpZipBtn.click();
|
||||
} else {
|
||||
this._downloadFilesIndividually(files);
|
||||
}
|
||||
this.continueCallback = _ => this.continue();
|
||||
} else {
|
||||
this.$shareOrDownloadBtn.innerText = "Download again";
|
||||
this.continue = _ => {
|
||||
let tmpBtn = document.createElement("a");
|
||||
tmpBtn.download = filenameDownload;
|
||||
tmpBtn.href = url;
|
||||
tmpBtn.click();
|
||||
};
|
||||
this.continueCallback = _ => {
|
||||
this.continue();
|
||||
this.hide();
|
||||
};
|
||||
}
|
||||
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
|
||||
|
||||
if (!canShare) {
|
||||
this.$downloadBtn.innerText = "Download again";
|
||||
}
|
||||
Events.fire('notify-user', `${descriptor} downloaded successfully`);
|
||||
this.$downloadBtn.style.pointerEvents = "none";
|
||||
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
|
||||
};
|
||||
|
||||
this.createPreviewElement(files[0]).finally(_ => {
|
||||
document.title = `PairDrop - ${files.length} Files received`;
|
||||
document.title = files.length === 1
|
||||
? 'File received - PairDrop'
|
||||
: `(${files.length}) Files received - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
|
||||
this.continue();
|
||||
this.show();
|
||||
|
||||
if (canShare) {
|
||||
this.$shareBtn.click();
|
||||
} else {
|
||||
this.$downloadBtn.click();
|
||||
}
|
||||
}).catch(r => console.error(r));
|
||||
}
|
||||
|
||||
_downloadFilesIndividually(files) {
|
||||
let tmpBtn = document.createElement("a");
|
||||
for (let i=0; i<files.length; i++) {
|
||||
tmpBtn.download = files[i].name;
|
||||
tmpBtn.href = URL.createObjectURL(files[i]);
|
||||
tmpBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$shareOrDownloadBtn.removeAttribute('href');
|
||||
this.$shareOrDownloadBtn.removeAttribute('download');
|
||||
this.$shareBtn.setAttribute('hidden', '');
|
||||
this.$previewBox.innerHTML = '';
|
||||
super.hide();
|
||||
this._dequeueFile();
|
||||
@@ -632,15 +696,10 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveRequestDialog');
|
||||
super('receive-request-dialog');
|
||||
|
||||
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName');
|
||||
this.$fileStemNode = this.$el.querySelector('#fileStem');
|
||||
this.$fileExtensionNode = this.$el.querySelector('#fileExtension');
|
||||
this.$fileOtherNode = this.$el.querySelector('#fileOther');
|
||||
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#declineRequest');
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
||||
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
||||
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
|
||||
|
||||
@@ -670,32 +729,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
_showRequestDialog(request, peerId) {
|
||||
this.correspondingPeerId = peerId;
|
||||
|
||||
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
const fileName = request.header[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtensionNode.innerText = fileExtension
|
||||
|
||||
if (request.header.length >= 2) {
|
||||
let fileOtherText = ` and ${request.header.length - 1} other `;
|
||||
fileOtherText += request.imagesOnly ? 'image' : 'file';
|
||||
if (request.header.length > 2) fileOtherText += "s";
|
||||
this.$fileOtherNode.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
|
||||
const displayName = $(peerId).ui._displayName();
|
||||
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
||||
|
||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||
let element = document.createElement('img');
|
||||
element.src = request.thumbnailDataUrl;
|
||||
element.classList.add('element-preview');
|
||||
|
||||
this.$previewBox.appendChild(element)
|
||||
}
|
||||
|
||||
document.title = 'PairDrop - File Transfer Requested';
|
||||
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
|
||||
|
||||
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
@@ -721,16 +766,16 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pairDeviceDialog');
|
||||
super('pair-device-dialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
this.$roomKey = this.$el.querySelector('#roomKey');
|
||||
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
|
||||
this.$roomKey = this.$el.querySelector('#room-key');
|
||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
||||
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
|
||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||
@@ -800,7 +845,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
evaluateRoomKeyChars() {
|
||||
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
|
||||
if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) {
|
||||
this.$submitBtn.setAttribute("disabled", "");
|
||||
} else {
|
||||
this.inputRoomKey = "";
|
||||
@@ -809,7 +854,7 @@ class PairDeviceDialog extends Dialog {
|
||||
})
|
||||
this.$submitBtn.removeAttribute("disabled");
|
||||
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
||||
this._onSubmit();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -844,7 +889,7 @@ class PairDeviceDialog extends Dialog {
|
||||
height: 150,
|
||||
padding: 0,
|
||||
background: "transparent",
|
||||
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
|
||||
color: `rgb(var(--text-color))`,
|
||||
ecl: "L",
|
||||
join: true
|
||||
});
|
||||
@@ -859,7 +904,8 @@ class PairDeviceDialog extends Dialog {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
|
||||
@@ -936,23 +982,29 @@ class PairDeviceDialog extends Dialog {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
|
||||
}
|
||||
Events.fire('bg-resize');
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clearDevicesDialog');
|
||||
super('clear-devices-dialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
||||
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._clearRoomSecrets();
|
||||
}
|
||||
|
||||
_clearRoomSecrets() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
}
|
||||
@@ -960,12 +1012,13 @@ class ClearDevicesDialog extends Dialog {
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail));
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
super('send-text-dialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
||||
this.$text = this.$el.querySelector('#text-input');
|
||||
this.$peerDisplayName = this.$el.querySelector('.display-name');
|
||||
this.$form = this.$el.querySelector('form');
|
||||
this.$submit = this.$el.querySelector('button[type="submit"]');
|
||||
this.$form.addEventListener('submit', _ => this._send());
|
||||
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$text.addEventListener('input', e => this._onChange(e));
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
}
|
||||
@@ -993,8 +1046,9 @@ class SendTextDialog extends Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
_onRecipient(peerId) {
|
||||
_onRecipient(peerId, deviceName) {
|
||||
this.correspondingPeerId = peerId;
|
||||
this.$peerDisplayName.innerText = deviceName;
|
||||
this.show();
|
||||
|
||||
const range = document.createRange();
|
||||
@@ -1006,6 +1060,11 @@ class SendTextDialog extends Dialog {
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._send();
|
||||
}
|
||||
|
||||
_send() {
|
||||
Events.fire('send-text', {
|
||||
to: this.correspondingPeerId,
|
||||
@@ -1018,7 +1077,7 @@ class SendTextDialog extends Dialog {
|
||||
|
||||
class ReceiveTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('receiveTextDialog');
|
||||
super('receive-text-dialog');
|
||||
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
|
||||
this.$text = this.$el.querySelector('#text');
|
||||
this.$copy = this.$el.querySelector('#copy');
|
||||
@@ -1029,7 +1088,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
|
||||
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName');
|
||||
this.$displayNameNode = this.$el.querySelector('.display-name');
|
||||
this._receiveTextQueue = [];
|
||||
}
|
||||
|
||||
@@ -1047,6 +1106,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
_onText(text, peerId) {
|
||||
window.blop.play();
|
||||
this._receiveTextQueue.push({text: text, peerId: peerId});
|
||||
this._setDocumentTitleMessages();
|
||||
if (this.$el.attributes["show"]) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
@@ -1058,23 +1118,35 @@ class ReceiveTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
_showReceiveTextDialog(text, peerId) {
|
||||
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
this.$displayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
if (isURL(text)) {
|
||||
const $a = document.createElement('a');
|
||||
$a.href = text;
|
||||
$a.target = '_blank';
|
||||
$a.textContent = text;
|
||||
this.$text.innerHTML = '';
|
||||
this.$text.appendChild($a);
|
||||
} else {
|
||||
this.$text.textContent = text;
|
||||
this.$text.innerText = text;
|
||||
this.$text.classList.remove('text-center');
|
||||
|
||||
// Beautify text if text is short
|
||||
if (text.length < 2000) {
|
||||
// replace urls with actual links
|
||||
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
});
|
||||
|
||||
if (!/\s/.test(text)) {
|
||||
this.$text.classList.add('text-center');
|
||||
}
|
||||
}
|
||||
document.title = 'PairDrop - Message Received';
|
||||
|
||||
this._setDocumentTitleMessages();
|
||||
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
|
||||
_setDocumentTitleMessages() {
|
||||
document.title = !this._receiveTextQueue.length
|
||||
? 'Message Received - PairDrop'
|
||||
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`;
|
||||
}
|
||||
|
||||
async _onCopy() {
|
||||
await navigator.clipboard.writeText(this.$text.textContent);
|
||||
Events.fire('notify-user', 'Copied to clipboard');
|
||||
@@ -1090,13 +1162,13 @@ class ReceiveTextDialog extends Dialog {
|
||||
class Base64ZipDialog extends Dialog {
|
||||
|
||||
constructor() {
|
||||
super('base64PasteDialog');
|
||||
super('base64-paste-dialog');
|
||||
const urlParams = new URL(window.location).searchParams;
|
||||
const base64Text = urlParams.get('base64text');
|
||||
const base64Zip = urlParams.get('base64zip');
|
||||
const base64Hash = window.location.hash.substring(1);
|
||||
|
||||
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
|
||||
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
||||
|
||||
if (base64Text) {
|
||||
this.show();
|
||||
@@ -1147,7 +1219,7 @@ class Base64ZipDialog extends Dialog {
|
||||
}
|
||||
|
||||
_setPasteBtnToProcessing() {
|
||||
this.$pasteBtn.pointerEvents = "none";
|
||||
this.$pasteBtn.style.pointerEvents = "none";
|
||||
this.$pasteBtn.innerText = "Processing...";
|
||||
}
|
||||
|
||||
@@ -1247,6 +1319,7 @@ class Notifications {
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||
}
|
||||
// Todo: fix Notifications
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
||||
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
||||
}
|
||||
@@ -1291,7 +1364,7 @@ class Notifications {
|
||||
_messageNotification(message, peerId) {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
const peerDisplayName = $(peerId).ui._displayName();
|
||||
if (isURL(message)) {
|
||||
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
|
||||
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
|
||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||
} else {
|
||||
@@ -1322,7 +1395,7 @@ class Notifications {
|
||||
}
|
||||
|
||||
_download(notification) {
|
||||
$('shareOrDownload').click();
|
||||
$('share-or-download').click();
|
||||
notification.close();
|
||||
}
|
||||
|
||||
@@ -1711,23 +1784,18 @@ Events.on('load', () => {
|
||||
let x0, y0, w, h, dw, offset;
|
||||
|
||||
function init() {
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
w = document.documentElement.clientWidth;
|
||||
h = document.documentElement.clientHeight;
|
||||
c.width = w;
|
||||
c.height = h;
|
||||
offset = h > 800
|
||||
? 116
|
||||
: h > 380
|
||||
? 100
|
||||
: 65;
|
||||
|
||||
if (w < 420) offset += 20;
|
||||
offset = $$('footer').offsetHeight - 32;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.max(w, h, 1000) / 13;
|
||||
drawCircles();
|
||||
}
|
||||
window.onresize = init;
|
||||
Events.on('bg-resize', _ => init());
|
||||
window.onresize = _ => Events.fire('bg-resize');
|
||||
|
||||
function drawCircle(radius) {
|
||||
ctx.beginPath();
|
||||
@@ -1792,9 +1860,3 @@ Notifications permission has been blocked
|
||||
as the user has dismissed the permission prompt several times.
|
||||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = _ => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.1.3';
|
||||
const cacheVersion = 'v1.2.1';
|
||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -11,28 +11,25 @@
|
||||
|
||||
/* Layout */
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
overscroll-behavior: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
@@ -74,10 +71,7 @@ body {
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
@@ -120,9 +114,9 @@ h3 {
|
||||
}
|
||||
|
||||
.font-subheading {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
line-height: 18px;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
@@ -200,20 +194,160 @@ body>header a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#center {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 146px;
|
||||
max-height: calc(100vh - 56px - var(--footer-height));
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 402px) and (max-width: 425px) {
|
||||
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 164px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 402px) {
|
||||
#center {
|
||||
--footer-height: 184px;
|
||||
}
|
||||
}
|
||||
/* Peers List */
|
||||
|
||||
#x-peers-filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
x-peers {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-grow: 1;
|
||||
align-items: start !important;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 2;
|
||||
transition: color 300ms;
|
||||
transition: --bg-color 0.5s ease;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-x: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
--peers-per-row: 6; /* default if browser does not support :has selector */
|
||||
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
|
||||
width: var(--x-peers-width);
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
x-peers.overflowing {
|
||||
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
|
||||
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
|
||||
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||
|
||||
/* Opera doesn't support this in the shorthand */
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
|
||||
/* peers-per-row if height is too small for 2 rows */
|
||||
@media screen and (min-height: 538px) and (max-height: 683px) and (max-width: 402px),
|
||||
screen and (min-height: 517px) and (max-height: 664px) and (max-width: 426px),
|
||||
screen and (min-height: 501px) and (max-height: 647px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(7)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
/* peers-per-row if height is too small for 3 rows */
|
||||
@media screen and (min-height: 683px) and (max-width: 402px),
|
||||
screen and (min-height: 664px) and (max-width: 426px),
|
||||
screen and (min-height: 647px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(28)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Empty Peers List */
|
||||
|
||||
x-no-peers {
|
||||
height: 114px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
@@ -255,25 +389,19 @@ x-no-peers[drop-bg] * {
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 8px;
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
x-peer .name {
|
||||
width: var(--peer-width);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
@@ -281,27 +409,43 @@ input[type="file"] {
|
||||
|
||||
x-peer x-icon {
|
||||
--icon-size: 40px;
|
||||
margin-bottom: 4px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
x-peer .icon-wrapper {
|
||||
width: var(--icon-size);
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip) x-icon {
|
||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-peer.ws-peer x-icon {
|
||||
border: solid 4px var(--ws-peer-color);
|
||||
x-peer x-icon > .highlight-wrapper {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
margin: 7px auto 0;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
x-peer.ws-peer .progress {
|
||||
margin-top: 4px;
|
||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||
background-color: var(--paired-device-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer:not([status]):hover x-icon,
|
||||
@@ -315,6 +459,35 @@ x-peer[status] x-icon {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
|
||||
x-peer.ws-peer {
|
||||
margin-top: -1.5px;
|
||||
}
|
||||
|
||||
x-peer.ws-peer .progress {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
x-peer.ws-peer .icon-wrapper{
|
||||
border: solid 3px var(--ws-peer-color);
|
||||
}
|
||||
|
||||
x-peer.ws-peer .highlight-wrapper {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status,
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
@@ -380,12 +553,10 @@ x-peer[drop] x-icon {
|
||||
/* Footer */
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
z-index: 2;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
}
|
||||
@@ -394,6 +565,7 @@ footer .logo {
|
||||
--icon-size: 80px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
@@ -418,7 +590,7 @@ x-dialog x-background {
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
padding: 35px;
|
||||
padding: 15px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
@@ -429,16 +601,20 @@ x-dialog x-paper {
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pairDeviceDialog x-paper {
|
||||
#pair-device-dialog x-paper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
height: 650px;
|
||||
margin-top: -325px;
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
@@ -453,12 +629,6 @@ x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 0;
|
||||
margin-bottom: -16px;
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
x-dialog a {
|
||||
color: var(--primary-color);
|
||||
@@ -470,13 +640,13 @@ x-dialog .font-subheading {
|
||||
|
||||
/* PairDevicesDialog */
|
||||
|
||||
#keyInputContainer {
|
||||
#key-input-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input {
|
||||
#key-input-container>input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
@@ -492,15 +662,15 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input + * {
|
||||
#key-input-container>input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#keyInputContainer>input:nth-of-type(4) {
|
||||
margin-left: 18px;
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#roomKey {
|
||||
#room-key {
|
||||
font-size: 50px;
|
||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||
display: inline-block;
|
||||
@@ -508,19 +678,15 @@ x-dialog .font-subheading {
|
||||
margin: 15px -15px;
|
||||
}
|
||||
|
||||
#roomKeyQrCode {
|
||||
padding: inherit;
|
||||
margin: auto;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
#room-key-qr-code {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
#pair-device-dialog hr {
|
||||
margin: 40px -24px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog x-background {
|
||||
#pair-device-dialog x-background {
|
||||
padding: 16px!important;
|
||||
}
|
||||
|
||||
@@ -531,29 +697,24 @@ x-dialog .row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#receiveRequestDialog h2,
|
||||
#receiveFileDialog h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse {
|
||||
margin: 40px -24px auto;
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: solid 1.25px var(--border-color);
|
||||
margin-bottom: -16px;
|
||||
x-paper > div:last-child > .button {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
word-break: break-word;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
@@ -565,52 +726,52 @@ x-dialog .row-reverse {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
#fileName {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#fileStem {
|
||||
max-width: 80%;
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.file-size{
|
||||
margin-bottom: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
#textInput {
|
||||
min-height: 120px;
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
|
||||
#receiveTextDialog #text {
|
||||
#receive-text-dialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
white-space: pre-wrap;
|
||||
margin-top:36px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a {
|
||||
#receive-text-dialog #text a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a:hover {
|
||||
#receive-text-dialog #text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#receiveTextDialog h3 {
|
||||
#receive-text-dialog h3 {
|
||||
/* Select the received text when double-clicking the dialog */
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
@@ -618,29 +779,25 @@ x-dialog .row-reverse {
|
||||
|
||||
.row-separator {
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
margin: auto -25px;
|
||||
margin: auto -24px;
|
||||
}
|
||||
|
||||
#receiveTextDescriptionContainer {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#base64PasteBtn {
|
||||
#base64-paste-btn {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
}
|
||||
|
||||
#base64PasteDialog button {
|
||||
#base64-paste-dialog button {
|
||||
margin: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64PasteDialog button[close] {
|
||||
#base64-paste-dialog button[close] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#base64PasteDialog button[close]:before {
|
||||
#base64-paste-dialog button[close]:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -650,7 +807,6 @@ x-dialog .row-reverse {
|
||||
padding: 2px 16px 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
@@ -661,6 +817,7 @@ x-dialog .row-reverse {
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@@ -698,16 +855,18 @@ x-dialog .row-reverse {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancelPasteModeBtn {
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: 56px;
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
background-color: var(--primary-color);
|
||||
color: rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.button:focus:before,
|
||||
@@ -723,7 +882,6 @@ button::-moz-focus-inner {
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -733,10 +891,7 @@ button::-moz-focus-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -750,9 +905,8 @@ button::-moz-focus-inner {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 254px);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -818,7 +972,7 @@ button::-moz-focus-inner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -8px;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
@@ -885,13 +1039,16 @@ x-toast:not([show]):not(:hover) {
|
||||
/* Instructions */
|
||||
|
||||
x-instructions {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
z-index: -1;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
@@ -908,92 +1065,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
|
||||
|
||||
x-instructions p {
|
||||
display: none;
|
||||
margin: 0 auto auto;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.websocket-fallback {
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
x-peer {
|
||||
transform: scale(0.95);
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#websocket-fallback {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
#websocket-fallback>span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#websocket-fallback > span > span {
|
||||
border-bottom: solid 4px var(--ws-peer-color);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
|
||||
@media (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px),
|
||||
screen and (min-width: 1100px) {
|
||||
@media screen and (min-height: 800px) {
|
||||
#websocket-fallback {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
content: attr(desktop);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 420px) {
|
||||
x-instructions {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
|
||||
|
||||
html {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Color Themes
|
||||
*/
|
||||
|
||||
/* Default colors */
|
||||
body {
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-test: 18,18,18;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
body.dark-theme {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Colored Elements */
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
color: rgb(var(--text-color));
|
||||
background-color: rgb(var(--bg-color));
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
background-color: var(--bg-color);
|
||||
background-color: rgb(var(--bg-color));
|
||||
}
|
||||
|
||||
.textarea {
|
||||
color: var(--text-color) !important;
|
||||
color: rgb(var(--text-color)) !important;
|
||||
background-color: var(--bg-color-secondary) !important;
|
||||
}
|
||||
|
||||
@@ -1019,7 +1176,9 @@ x-dialog x-paper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.element-preview {
|
||||
.file-preview > img,
|
||||
.file-preview > audio,
|
||||
.file-preview > video {
|
||||
max-width: 100%;
|
||||
max-height: 40vh;
|
||||
margin: auto;
|
||||
@@ -1031,16 +1190,16 @@ x-dialog x-paper {
|
||||
|
||||
/* defaults to dark theme */
|
||||
body {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Override dark mode with light mode styles if the user decides to swap */
|
||||
body.light-theme {
|
||||
--text-color: #333;
|
||||
--bg-color: #fafafa;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
@@ -1058,6 +1217,15 @@ x-dialog x-paper {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
html {
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
}
|
||||
|
||||
/* webkit scrollbar style*/
|
||||
|
||||
::-webkit-scrollbar{
|
||||
|
||||
Reference in New Issue
Block a user