mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-23 07:30:54 +08:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67a1b04da2 | |||
| 8b2eb67266 | |||
| 827b10219d | |||
| e7ab5e26cc | |||
| 8bcaa3f60f | |||
| c0a4224a59 | |||
| 1e35bab327 | |||
| bb0493d071 | |||
| bfb5aa8546 | |||
| a9d7960a59 | |||
| 39ca5b2d21 | |||
| cf715b2872 | |||
| bbb8c1b10f | |||
| d6ef5887dd | |||
| f9f1abef7a | |||
| d244f5fa47 | |||
| 3a2d8c75f7 | |||
| 545cdc2459 | |||
| de76da52fe | |||
| 66359da2ca | |||
| 74b88c2e7d |
@@ -63,6 +63,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
|||||||
* Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
|
* Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
|
||||||
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
|
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
|
||||||
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
||||||
|
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|||||||
+73
-11
@@ -17,7 +17,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ls
|
|||||||
Set options by using the following flags in the `docker run` command:
|
Set options by using the following flags in the `docker run` command:
|
||||||
|
|
||||||
##### Port
|
##### Port
|
||||||
```
|
```bash
|
||||||
-p 127.0.0.1:8080:3000
|
-p 127.0.0.1:8080:3000
|
||||||
```
|
```
|
||||||
> Specify the port used by the docker image
|
> Specify the port used by the docker image
|
||||||
@@ -30,7 +30,7 @@ Set options by using the following flags in the `docker run` command:
|
|||||||
> Limits clients to 1000 requests per 5 min
|
> Limits clients to 1000 requests per 5 min
|
||||||
|
|
||||||
##### Websocket Fallback (for VPN)
|
##### Websocket Fallback (for VPN)
|
||||||
```
|
```bash
|
||||||
-e WS_FALLBACK=true
|
-e WS_FALLBACK=true
|
||||||
```
|
```
|
||||||
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||||
@@ -42,6 +42,36 @@ 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.
|
> 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.
|
> Additionally, beware that all traffic using this fallback debits the servers data plan.
|
||||||
|
|
||||||
|
##### Specify STUN/TURN Servers
|
||||||
|
```bash
|
||||||
|
-e RTC_CONFIG="rtc_config.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
|
||||||
|
> You can use `pairdrop/rtc_config_example.json` as a starting point.
|
||||||
|
>
|
||||||
|
> Default configuration:
|
||||||
|
> ```json
|
||||||
|
> {
|
||||||
|
> "sdpSemantics": "unified-plan",
|
||||||
|
> "iceServers": [
|
||||||
|
> {
|
||||||
|
> "urls": "stun:stun.l.google.com:19302"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "stun:openrelay.metered.ca:80"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "turn:openrelay.metered.ca:443",
|
||||||
|
> "username": "openrelayproject",
|
||||||
|
> "credential": "openrelayproject"
|
||||||
|
> }
|
||||||
|
> ]
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
### Docker Image from GHCR
|
### Docker Image from GHCR
|
||||||
```bash
|
```bash
|
||||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
|
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
|
||||||
@@ -141,6 +171,38 @@ $env:PORT=3010; npm start
|
|||||||
```
|
```
|
||||||
> Specify the port PairDrop is running on. (Default: 3000)
|
> Specify the port PairDrop is running on. (Default: 3000)
|
||||||
|
|
||||||
|
#### Specify STUN/TURN Server
|
||||||
|
On Unix based systems
|
||||||
|
```bash
|
||||||
|
RTC_CONFIG="rtc_config.json" npm start
|
||||||
|
```
|
||||||
|
On Windows
|
||||||
|
```bash
|
||||||
|
$env:RTC_CONFIG="rtc_config.json"; npm start
|
||||||
|
```
|
||||||
|
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
|
||||||
|
> You can use `pairdrop/rtc_config_example.json` as a starting point.
|
||||||
|
>
|
||||||
|
> Default configuration:
|
||||||
|
> ```json
|
||||||
|
> {
|
||||||
|
> "sdpSemantics": "unified-plan",
|
||||||
|
> "iceServers": [
|
||||||
|
> {
|
||||||
|
> "urls": "stun:stun.l.google.com:19302"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "stun:openrelay.metered.ca:80"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "turn:openrelay.metered.ca:443",
|
||||||
|
> "username": "openrelayproject",
|
||||||
|
> "credential": "openrelayproject"
|
||||||
|
> }
|
||||||
|
> ]
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
### Options / Flags
|
### Options / Flags
|
||||||
#### Local Run
|
#### Local Run
|
||||||
```bash
|
```bash
|
||||||
@@ -262,13 +324,13 @@ server {
|
|||||||
|
|
||||||
### Using Apache
|
### Using Apache
|
||||||
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
|
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
|
||||||
```shell
|
```bash
|
||||||
a2enmod proxy
|
a2enmod proxy
|
||||||
```
|
```
|
||||||
```shell
|
```bash
|
||||||
a2enmod proxy_http
|
a2enmod proxy_http
|
||||||
```
|
```
|
||||||
```shell
|
```bash
|
||||||
a2enmod proxy_wstunnel
|
a2enmod proxy_wstunnel
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -278,7 +340,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
|||||||
|
|
||||||
**pairdrop.conf**
|
**pairdrop.conf**
|
||||||
#### Allow http and https requests
|
#### Allow http and https requests
|
||||||
```
|
```apacheconf
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ProxyPass / http://127.0.0.1:3000/
|
ProxyPass / http://127.0.0.1:3000/
|
||||||
RewriteEngine on
|
RewriteEngine on
|
||||||
@@ -295,7 +357,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
|||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
#### Automatic http to https redirect:
|
#### Automatic http to https redirect:
|
||||||
```
|
```apacheconf
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
Redirect permanent / https://127.0.0.1:3000/
|
Redirect permanent / https://127.0.0.1:3000/
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
@@ -308,10 +370,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
|||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
Activate the new virtual host and reload apache:
|
Activate the new virtual host and reload apache:
|
||||||
```shell
|
```bash
|
||||||
a2ensite pairdrop
|
a2ensite pairdrop
|
||||||
```
|
```
|
||||||
```shell
|
```bash
|
||||||
service apache2 reload
|
service apache2 reload
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -322,7 +384,7 @@ All files needed for developing are available on the branch `dev`.
|
|||||||
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
|
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
Then, clone the repository and run docker-compose:
|
Then, clone the repository and run docker-compose:
|
||||||
```shell
|
```bash
|
||||||
git clone https://github.com/schlagmichdoch/PairDrop.git
|
git clone https://github.com/schlagmichdoch/PairDrop.git
|
||||||
|
|
||||||
cd PairDrop
|
cd PairDrop
|
||||||
@@ -347,7 +409,7 @@ The nginx container creates a CA certificate and a website certificate for you.
|
|||||||
|
|
||||||
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
|
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
|
||||||
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
|
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
|
||||||
- On MacOS, double click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
|
- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
|
||||||
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
|
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
|
||||||
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
|
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const process = require('process')
|
const process = require('process')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const {spawn} = require('child_process')
|
const {spawn} = require('child_process')
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
// Handle SIGINT
|
// Handle SIGINT
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
@@ -49,6 +51,25 @@ if (process.argv.includes('--auto-restart')) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rtcConfig = process.env.RTC_CONFIG
|
||||||
|
? fs.readFileSync(process.env.RTC_CONFIG, 'utf8')
|
||||||
|
: {
|
||||||
|
"sdpSemantics": "unified-plan",
|
||||||
|
"iceServers": [
|
||||||
|
{
|
||||||
|
"urls": "stun:stun.l.google.com:19302"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"urls": "stun:openrelay.metered.ca:80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"urls": "turn:openrelay.metered.ca:443",
|
||||||
|
"username": "openrelayproject",
|
||||||
|
"credential": "openrelayproject"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const RateLimit = require('express-rate-limit');
|
const RateLimit = require('express-rate-limit');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
@@ -99,7 +120,6 @@ const { uniqueNamesGenerator, animals, colors } = require('unique-names-generato
|
|||||||
class PairDropServer {
|
class PairDropServer {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const WebSocket = require('ws');
|
|
||||||
this._wss = new WebSocket.Server({ server });
|
this._wss = new WebSocket.Server({ server });
|
||||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||||
|
|
||||||
@@ -110,10 +130,14 @@ class PairDropServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onConnection(peer) {
|
_onConnection(peer) {
|
||||||
this._joinRoom(peer);
|
|
||||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||||
peer.socket.onerror = e => console.error(e);
|
peer.socket.onerror = e => console.error(e);
|
||||||
this._keepAlive(peer);
|
this._keepAlive(peer);
|
||||||
|
this._send(peer, {
|
||||||
|
type: 'rtc-config',
|
||||||
|
config: rtcConfig
|
||||||
|
});
|
||||||
|
this._joinRoom(peer);
|
||||||
|
|
||||||
// send displayName
|
// send displayName
|
||||||
this._send(peer, {
|
this._send(peer, {
|
||||||
@@ -317,6 +341,10 @@ class PairDropServer {
|
|||||||
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||||
const room = roomType === 'ip' ? peer.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 room doesn't exist, create it
|
||||||
if (!this._rooms[room]) {
|
if (!this._rooms[room]) {
|
||||||
this._rooms[room] = {};
|
this._rooms[room] = {};
|
||||||
@@ -341,10 +369,6 @@ class PairDropServer {
|
|||||||
// delete the peer
|
// delete the peer
|
||||||
delete this._rooms[room][peer.id];
|
delete this._rooms[room][peer.id];
|
||||||
|
|
||||||
if (roomType === 'ip') {
|
|
||||||
peer.socket.terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
//if room is empty, delete the room
|
//if room is empty, delete the room
|
||||||
if (!Object.keys(this._rooms[room]).length) {
|
if (!Object.keys(this._rooms[room]).length) {
|
||||||
delete this._rooms[room];
|
delete this._rooms[room];
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+49
-43
@@ -69,7 +69,7 @@
|
|||||||
<use xlink:href="#clear-pair-devices-icon" />
|
<use xlink:href="#clear-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a>
|
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||||
</header>
|
</header>
|
||||||
<!-- Center -->
|
<!-- Center -->
|
||||||
<div id="center">
|
<div id="center">
|
||||||
@@ -106,18 +106,17 @@
|
|||||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div id="key-input-container">
|
<div id="key-input-container">
|
||||||
<input id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<button class="button" type="submit" disabled>Pair</button>
|
||||||
<div class="separator"></div>
|
<button class="button" close>Cancel</button>
|
||||||
<a class="button" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -130,9 +129,9 @@
|
|||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Unpair Devices</h2>
|
<h2 class="center">Unpair Devices</h2>
|
||||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
<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>
|
<button class="button" type="submit">Unpair Devices</button>
|
||||||
<a class="button" close>Cancel</a>
|
<button class="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -142,25 +141,23 @@
|
|||||||
<x-dialog id="receive-request-dialog">
|
<x-dialog id="receive-request-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">PairDrop</h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description">
|
<div class="center column file-description">
|
||||||
<div>
|
<div>
|
||||||
<span id="requesting-peer-display-name"></span>
|
<span class="display-name"></span>
|
||||||
<span>would like to share</span>
|
<span>would like to share</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="file-name" class="row" >
|
<div class="row file-name" >
|
||||||
<span id="file-stem"></span>
|
<span class="file-stem"></span>
|
||||||
<span id="file-extension"></span>
|
<span class="file-extension"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row file-other">
|
||||||
<span id="file-other"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-body2 text-center file-size"></div>
|
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||||
<div class="separator"></div>
|
|
||||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
@@ -170,13 +167,23 @@
|
|||||||
<x-dialog id="receive-file-dialog">
|
<x-dialog id="receive-file-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 id="receive-title" class="center"></h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description"></div>
|
<div class="center column file-description">
|
||||||
<div class="font-body2 text-center file-size"></div>
|
<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="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<a id="share-or-download" class="button" autofocus></a>
|
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||||
<div class="separator"></div>
|
<button id="download-btn" class="button" autofocus>Download</button>
|
||||||
<button class="button" close>Close</button>
|
<button class="button" close>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
@@ -187,16 +194,16 @@
|
|||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="text-center">PairDrop</h2>
|
<h2 class="text-center">Send Message</h2>
|
||||||
<div class="text-center">
|
<div class="dialog-subheader text-center">
|
||||||
<span>Send a Message to</span>
|
<span>Send a Message to</span>
|
||||||
<span id="text-send-peer-display-name"></span>
|
<span class="display-name"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row-separator"></div>
|
||||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
<div class="row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<div class="separator"></div>
|
<button class="button" title="ESCAPE" close>Cancel</button>
|
||||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -206,16 +213,15 @@
|
|||||||
<x-dialog id="receive-text-dialog">
|
<x-dialog id="receive-text-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2>PairDrop - Message Received</h2>
|
<h2 class="text-center">Message Received</h2>
|
||||||
<div id="receive-text-description-container">
|
<div class="text-center dialog-subheader">
|
||||||
<span id="receive-text-peer-display-name"></span>
|
<span class="display-name"></span>
|
||||||
<span>sent the following message:</span>
|
<span>has sent:</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row-separator"></div>
|
||||||
<div id="text"></div>
|
<div id="text"></div>
|
||||||
<div class="row-reverse">
|
<div class="center row-reverse">
|
||||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||||
<div class="separator"></div>
|
|
||||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
@@ -236,14 +242,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- About Page -->
|
<!-- About Page -->
|
||||||
<x-about id="about" class="full center column">
|
<x-about id="about" class="full center column">
|
||||||
<section class="center column fade-in">
|
<header class="row-reverse fade-in">
|
||||||
<header class="row-reverse">
|
|
||||||
<a href="#" class="close icon-button">
|
<a href="#" class="close icon-button">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#close-icon" />
|
<use xlink:href="#close-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
<section class="center column fade-in">
|
||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
+29
-29
@@ -36,6 +36,7 @@ class ServerConnection {
|
|||||||
_onOpen() {
|
_onOpen() {
|
||||||
console.log('WS: server connected');
|
console.log('WS: server connected');
|
||||||
Events.fire('ws-connected');
|
Events.fire('ws-connected');
|
||||||
|
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendRoomSecrets(roomSecrets) {
|
_sendRoomSecrets(roomSecrets) {
|
||||||
@@ -58,10 +59,17 @@ class ServerConnection {
|
|||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setRtcConfig(config) {
|
||||||
|
window.rtcConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
_onMessage(msg) {
|
_onMessage(msg) {
|
||||||
msg = JSON.parse(msg);
|
msg = JSON.parse(msg);
|
||||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
|
case 'rtc-config':
|
||||||
|
this._setRtcConfig(msg.config);
|
||||||
|
break;
|
||||||
case 'peers':
|
case 'peers':
|
||||||
Events.fire('peers', msg);
|
Events.fire('peers', msg);
|
||||||
break;
|
break;
|
||||||
@@ -145,15 +153,17 @@ class ServerConnection {
|
|||||||
this._socket.close();
|
this._socket.close();
|
||||||
this._socket = null;
|
this._socket = null;
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisconnect() {
|
_onDisconnect() {
|
||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
Events.fire('notify-user', 'Connecting..');
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onVisibilityChange() {
|
_onVisibilityChange() {
|
||||||
@@ -319,26 +329,25 @@ class Peer {
|
|||||||
this._onChunkReceived(message);
|
this._onChunkReceived(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message = JSON.parse(message);
|
const messageJSON = JSON.parse(message);
|
||||||
console.log('RTC:', message);
|
switch (messageJSON.type) {
|
||||||
switch (message.type) {
|
|
||||||
case 'request':
|
case 'request':
|
||||||
this._onFilesTransferRequest(message);
|
this._onFilesTransferRequest(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'header':
|
case 'header':
|
||||||
this._onFilesHeader(message);
|
this._onFilesHeader(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition':
|
case 'partition':
|
||||||
this._onReceivedPartitionEnd(message);
|
this._onReceivedPartitionEnd(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition-received':
|
case 'partition-received':
|
||||||
this._sendNextPartition();
|
this._sendNextPartition();
|
||||||
break;
|
break;
|
||||||
case 'progress':
|
case 'progress':
|
||||||
this._onDownloadProgress(message.progress);
|
this._onDownloadProgress(messageJSON.progress);
|
||||||
break;
|
break;
|
||||||
case 'files-transfer-response':
|
case 'files-transfer-response':
|
||||||
this._onFileTransferRequestResponded(message);
|
this._onFileTransferRequestResponded(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'file-transfer-complete':
|
case 'file-transfer-complete':
|
||||||
this._onFileTransferCompleted();
|
this._onFileTransferCompleted();
|
||||||
@@ -347,7 +356,7 @@ class Peer {
|
|||||||
this._onMessageTransferCompleted();
|
this._onMessageTransferCompleted();
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
this._onTextReceived(message);
|
this._onTextReceived(messageJSON);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,7 +450,7 @@ class Peer {
|
|||||||
if (!this._requestAccepted.header.length) {
|
if (!this._requestAccepted.header.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
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._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
}
|
}
|
||||||
@@ -492,6 +501,7 @@ class RTCPeer extends Peer {
|
|||||||
|
|
||||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||||
super(serverConnection, peerId, roomType, roomSecret);
|
super(serverConnection, peerId, roomType, roomSecret);
|
||||||
|
this.rtcSupported = true;
|
||||||
if (!peerId) return; // we will listen for a caller
|
if (!peerId) return; // we will listen for a caller
|
||||||
this._connect(peerId, true);
|
this._connect(peerId, true);
|
||||||
}
|
}
|
||||||
@@ -509,7 +519,7 @@ class RTCPeer extends Peer {
|
|||||||
_openConnection(peerId, isCaller) {
|
_openConnection(peerId, isCaller) {
|
||||||
this._isCaller = isCaller;
|
this._isCaller = isCaller;
|
||||||
this._peerId = peerId;
|
this._peerId = peerId;
|
||||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||||
@@ -568,6 +578,13 @@ class RTCPeer extends Peer {
|
|||||||
this._channel = channel;
|
this._channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onMessage(message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
console.log('RTC:', JSON.parse(message));
|
||||||
|
}
|
||||||
|
super._onMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
getConnectionHash() {
|
getConnectionHash() {
|
||||||
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
|
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
|
||||||
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
|
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
|
||||||
@@ -852,20 +869,3 @@ class Events {
|
|||||||
return window.removeEventListener(type, callback, false);
|
return window.removeEventListener(type, callback, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RTCPeer.config = {
|
|
||||||
'sdpSemantics': 'unified-plan',
|
|
||||||
'iceServers': [
|
|
||||||
{
|
|
||||||
urls: 'stun:stun.l.google.com:19302'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urls: 'stun:openrelay.metered.ca:80'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urls: 'turn:openrelay.metered.ca:443',
|
|
||||||
username: 'openrelayproject',
|
|
||||||
credential: 'openrelayproject',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
+149
-119
@@ -1,6 +1,5 @@
|
|||||||
const $ = query => document.getElementById(query);
|
const $ = query => document.getElementById(query);
|
||||||
const $$ = query => document.body.querySelector(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.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||||
window.android = /android/i.test(navigator.userAgent);
|
window.android = /android/i.test(navigator.userAgent);
|
||||||
@@ -28,7 +27,7 @@ class PeersUI {
|
|||||||
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
||||||
this.peers = {};
|
this.peers = {};
|
||||||
|
|
||||||
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn');
|
this.$cancelPasteModeBtn = $('cancel-paste-mode');
|
||||||
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
||||||
|
|
||||||
Events.on('dragover', e => this._onDragOver(e));
|
Events.on('dragover', e => this._onDragOver(e));
|
||||||
@@ -473,10 +472,14 @@ class Dialog {
|
|||||||
class ReceiveDialog extends Dialog {
|
class ReceiveDialog extends Dialog {
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
super(id);
|
super(id);
|
||||||
|
this.$fileDescription = this.$el.querySelector('.file-description');
|
||||||
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
|
this.$displayName = this.$el.querySelector('.display-name');
|
||||||
this.$fileSizeNode = this.$el.querySelector('.file-size');
|
this.$fileStem = this.$el.querySelector('.file-stem');
|
||||||
this.$previewBox = this.$el.querySelector('.file-preview')
|
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) {
|
_formatFileSize(bytes) {
|
||||||
@@ -492,6 +495,26 @@ class ReceiveDialog extends Dialog {
|
|||||||
return bytes + ' Bytes';
|
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 {
|
class ReceiveFileDialog extends ReceiveDialog {
|
||||||
@@ -499,24 +522,25 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('receive-file-dialog');
|
super('receive-file-dialog');
|
||||||
|
|
||||||
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download');
|
this.$downloadBtn = this.$el.querySelector('#download-btn');
|
||||||
this.$receiveTitleNode = this.$el.querySelector('#receive-title')
|
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 = [];
|
this._filesQueue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFilesReceived(sender, files, request) {
|
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||||
this._nextFiles(sender, files, request);
|
const displayName = $(sender).ui._displayName()
|
||||||
|
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||||
|
this._nextFiles();
|
||||||
window.blop.play();
|
window.blop.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextFiles(sender, nextFiles, nextRequest) {
|
_nextFiles() {
|
||||||
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
|
|
||||||
if (this._busy) return;
|
if (this._busy) return;
|
||||||
this._busy = true;
|
this._busy = true;
|
||||||
const {peerId, files, request} = this._filesQueue.shift();
|
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||||
this._displayFiles(peerId, files, request);
|
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
_dequeueFile() {
|
_dequeueFile() {
|
||||||
@@ -547,7 +571,6 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
let element = document.createElement(previewElement[mime]);
|
let element = document.createElement(previewElement[mime]);
|
||||||
element.src = URL.createObjectURL(file);
|
element.src = URL.createObjectURL(file);
|
||||||
element.controls = true;
|
element.controls = true;
|
||||||
element.classList.add('element-preview');
|
|
||||||
element.onload = _ => {
|
element.onload = _ => {
|
||||||
this.$previewBox.appendChild(element);
|
this.$previewBox.appendChild(element);
|
||||||
resolve(true)
|
resolve(true)
|
||||||
@@ -558,30 +581,32 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _displayFiles(peerId, files, request) {
|
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
||||||
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
|
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
||||||
|
|
||||||
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});
|
|
||||||
|
|
||||||
|
let descriptor, url, filenameDownload;
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
url = URL.createObjectURL(files[0])
|
descriptor = imagesOnly ? 'Image' : 'File';
|
||||||
title = `PairDrop - ${descriptor} Received`
|
|
||||||
filenameDownload = files[0].name;
|
|
||||||
} else {
|
} else {
|
||||||
title = `PairDrop - ${files.length} ${descriptor}s Received`
|
descriptor = imagesOnly ? 'Images' : 'Files';
|
||||||
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
|
}
|
||||||
if(files.length>2) description += "s";
|
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;
|
let bytesCompleted = 0;
|
||||||
zipper.createNewZipWriter();
|
zipper.createNewZipWriter();
|
||||||
for (let i=0; i<files.length; i++) {
|
for (let i=0; i<files.length; i++) {
|
||||||
@@ -589,7 +614,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
onprogress: (progress) => {
|
onprogress: (progress) => {
|
||||||
Events.fire('set-progress', {
|
Events.fire('set-progress', {
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
progress: (bytesCompleted + progress) / request.totalSize,
|
progress: (bytesCompleted + progress) / totalSize,
|
||||||
status: 'process'
|
status: 'process'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -609,49 +634,58 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
let minutes = now.getMinutes().toString();
|
let minutes = now.getMinutes().toString();
|
||||||
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
||||||
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
downloadZipped = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$receiveTitleNode.textContent = title;
|
this.$downloadBtn.innerText = "Download";
|
||||||
this.$fileDescriptionNode.textContent = description;
|
this.$downloadBtn.onclick = _ => {
|
||||||
this.$fileSizeNode.textContent = size;
|
if (downloadZipped) {
|
||||||
|
let tmpZipBtn = document.createElement("a");
|
||||||
if (shareInsteadOfDownload) {
|
tmpZipBtn.download = filenameDownload;
|
||||||
this.$shareOrDownloadBtn.innerText = "Share";
|
tmpZipBtn.href = url;
|
||||||
this.continue = _ => {
|
tmpZipBtn.click();
|
||||||
navigator.share({files: files})
|
|
||||||
.catch(err => console.error(err));
|
|
||||||
}
|
|
||||||
this.continueCallback = _ => this.continue();
|
|
||||||
} else {
|
} else {
|
||||||
this.$shareOrDownloadBtn.innerText = "Download again";
|
this._downloadFilesIndividually(files);
|
||||||
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(_ => {
|
this.createPreviewElement(files[0]).finally(_ => {
|
||||||
document.title = files.length === 1
|
document.title = files.length === 1
|
||||||
? 'File received - PairDrop'
|
? 'File received - PairDrop'
|
||||||
: `(${files.length}) Files received - PairDrop`;
|
: `${files.length} Files received - PairDrop`;
|
||||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||||
this.show();
|
|
||||||
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
|
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));
|
}).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() {
|
hide() {
|
||||||
this.$shareOrDownloadBtn.removeAttribute('href');
|
this.$shareBtn.setAttribute('hidden', '');
|
||||||
this.$shareOrDownloadBtn.removeAttribute('download');
|
|
||||||
this.$previewBox.innerHTML = '';
|
this.$previewBox.innerHTML = '';
|
||||||
super.hide();
|
super.hide();
|
||||||
this._dequeueFile();
|
this._dequeueFile();
|
||||||
@@ -663,11 +697,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('receive-request-dialog');
|
super('receive-request-dialog');
|
||||||
|
|
||||||
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name');
|
|
||||||
this.$fileStemNode = this.$el.querySelector('#file-stem');
|
|
||||||
this.$fileExtensionNode = this.$el.querySelector('#file-extension');
|
|
||||||
this.$fileOtherNode = this.$el.querySelector('#file-other');
|
|
||||||
|
|
||||||
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
||||||
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
||||||
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
||||||
@@ -699,32 +728,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
_showRequestDialog(request, peerId) {
|
_showRequestDialog(request, peerId) {
|
||||||
this.correspondingPeerId = peerId;
|
this.correspondingPeerId = peerId;
|
||||||
|
|
||||||
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
const displayName = $(peerId).ui._displayName();
|
||||||
|
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
||||||
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);
|
|
||||||
|
|
||||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||||
let element = document.createElement('img');
|
let element = document.createElement('img');
|
||||||
element.src = request.thumbnailDataUrl;
|
element.src = request.thumbnailDataUrl;
|
||||||
element.classList.add('element-preview');
|
|
||||||
|
|
||||||
this.$previewBox.appendChild(element)
|
this.$previewBox.appendChild(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = 'File Transfer Requested - PairDrop';
|
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");
|
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||||
this.show();
|
this.show();
|
||||||
}
|
}
|
||||||
@@ -759,7 +774,7 @@ class PairDeviceDialog extends Dialog {
|
|||||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||||
let createJoinForm = this.$el.querySelector('form');
|
let createJoinForm = this.$el.querySelector('form');
|
||||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||||
|
|
||||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||||
@@ -838,7 +853,7 @@ class PairDeviceDialog extends Dialog {
|
|||||||
})
|
})
|
||||||
this.$submitBtn.removeAttribute("disabled");
|
this.$submitBtn.removeAttribute("disabled");
|
||||||
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
||||||
this._onSubmit();
|
this._pairDeviceJoin(this.inputRoomKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -888,7 +903,8 @@ class PairDeviceDialog extends Dialog {
|
|||||||
return url.href;
|
return url.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSubmit() {
|
_onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
this._pairDeviceJoin(this.inputRoomKey);
|
this._pairDeviceJoin(this.inputRoomKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,14 +991,19 @@ class ClearDevicesDialog extends Dialog {
|
|||||||
super('clear-devices-dialog');
|
super('clear-devices-dialog');
|
||||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||||
let clearDevicesForm = this.$el.querySelector('form');
|
let clearDevicesForm = this.$el.querySelector('form');
|
||||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClearPairDevices() {
|
_onClearPairDevices() {
|
||||||
this.show();
|
this.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSubmit() {
|
_onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._clearRoomSecrets();
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearRoomSecrets() {
|
||||||
Events.fire('clear-room-secrets');
|
Events.fire('clear-room-secrets');
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
@@ -993,10 +1014,10 @@ class SendTextDialog extends Dialog {
|
|||||||
super('send-text-dialog');
|
super('send-text-dialog');
|
||||||
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
||||||
this.$text = this.$el.querySelector('#text-input');
|
this.$text = this.$el.querySelector('#text-input');
|
||||||
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name');
|
this.$peerDisplayName = this.$el.querySelector('.display-name');
|
||||||
this.$form = this.$el.querySelector('form');
|
this.$form = this.$el.querySelector('form');
|
||||||
this.$submit = this.$el.querySelector('button[type="submit"]');
|
this.$submit = this.$el.querySelector('button[type="submit"]');
|
||||||
this.$form.addEventListener('submit', _ => this._send());
|
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
||||||
this.$text.addEventListener('input', e => this._onChange(e));
|
this.$text.addEventListener('input', e => this._onChange(e));
|
||||||
Events.on("keydown", e => this._onKeyDown(e));
|
Events.on("keydown", e => this._onKeyDown(e));
|
||||||
}
|
}
|
||||||
@@ -1038,6 +1059,11 @@ class SendTextDialog extends Dialog {
|
|||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._send();
|
||||||
|
}
|
||||||
|
|
||||||
_send() {
|
_send() {
|
||||||
Events.fire('send-text', {
|
Events.fire('send-text', {
|
||||||
to: this.correspondingPeerId,
|
to: this.correspondingPeerId,
|
||||||
@@ -1061,7 +1087,7 @@ class ReceiveTextDialog extends Dialog {
|
|||||||
|
|
||||||
Events.on("keydown", e => this._onKeyDown(e));
|
Events.on("keydown", e => this._onKeyDown(e));
|
||||||
|
|
||||||
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name');
|
this.$displayNameNode = this.$el.querySelector('.display-name');
|
||||||
this._receiveTextQueue = [];
|
this._receiveTextQueue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,18 +1117,23 @@ class ReceiveTextDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_showReceiveTextDialog(text, peerId) {
|
_showReceiveTextDialog(text, peerId) {
|
||||||
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
this.$displayNameNode.innerText = $(peerId).ui._displayName();
|
||||||
|
|
||||||
if (isURL(text)) {
|
this.$text.innerText = text;
|
||||||
const $a = document.createElement('a');
|
this.$text.classList.remove('text-center');
|
||||||
$a.href = text;
|
|
||||||
$a.target = '_blank';
|
// Beautify text if text is short
|
||||||
$a.textContent = text;
|
if (text.length < 2000) {
|
||||||
this.$text.innerHTML = '';
|
// replace urls with actual links
|
||||||
this.$text.appendChild($a);
|
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
|
||||||
} else {
|
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||||
this.$text.textContent = text;
|
});
|
||||||
|
|
||||||
|
if (!/\s/.test(text)) {
|
||||||
|
this.$text.classList.add('text-center');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._setDocumentTitleMessages();
|
this._setDocumentTitleMessages();
|
||||||
|
|
||||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||||
@@ -1112,7 +1143,7 @@ class ReceiveTextDialog extends Dialog {
|
|||||||
_setDocumentTitleMessages() {
|
_setDocumentTitleMessages() {
|
||||||
document.title = !this._receiveTextQueue.length
|
document.title = !this._receiveTextQueue.length
|
||||||
? 'Message Received - PairDrop'
|
? 'Message Received - PairDrop'
|
||||||
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`;
|
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onCopy() {
|
async _onCopy() {
|
||||||
@@ -1187,7 +1218,7 @@ class Base64ZipDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setPasteBtnToProcessing() {
|
_setPasteBtnToProcessing() {
|
||||||
this.$pasteBtn.pointerEvents = "none";
|
this.$pasteBtn.style.pointerEvents = "none";
|
||||||
this.$pasteBtn.innerText = "Processing...";
|
this.$pasteBtn.innerText = "Processing...";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,7 +1363,7 @@ class Notifications {
|
|||||||
_messageNotification(message, peerId) {
|
_messageNotification(message, peerId) {
|
||||||
if (document.visibilityState !== 'visible') {
|
if (document.visibilityState !== 'visible') {
|
||||||
const peerDisplayName = $(peerId).ui._displayName();
|
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);
|
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
|
||||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||||
} else {
|
} else {
|
||||||
@@ -1393,7 +1424,7 @@ class NetworkStatusUI {
|
|||||||
constructor() {
|
constructor() {
|
||||||
Events.on('offline', _ => this._showOfflineMessage());
|
Events.on('offline', _ => this._showOfflineMessage());
|
||||||
Events.on('online', _ => this._showOnlineMessage());
|
Events.on('online', _ => this._showOnlineMessage());
|
||||||
Events.on('ws-connected', _ => this._showOnlineMessage());
|
Events.on('ws-connected', _ => this._onWsConnected());
|
||||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||||
if (!navigator.onLine) this._showOfflineMessage();
|
if (!navigator.onLine) this._showOfflineMessage();
|
||||||
}
|
}
|
||||||
@@ -1404,17 +1435,16 @@ class NetworkStatusUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_showOnlineMessage() {
|
_showOnlineMessage() {
|
||||||
window.animateBackground(true);
|
|
||||||
if (!this.firstConnect) {
|
|
||||||
this.firstConnect = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Events.fire('notify-user', 'You are back online');
|
Events.fire('notify-user', 'You are back online');
|
||||||
|
window.animateBackground(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWsConnected() {
|
||||||
|
window.animateBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWsDisconnected() {
|
_onWsDisconnected() {
|
||||||
window.animateBackground(false);
|
window.animateBackground(false);
|
||||||
if (!this.firstConnect) this.firstConnect = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1752,8 +1782,8 @@ Events.on('load', () => {
|
|||||||
let x0, y0, w, h, dw, offset;
|
let x0, y0, w, h, dw, offset;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
w = window.innerWidth;
|
w = document.documentElement.clientWidth;
|
||||||
h = window.innerHeight;
|
h = document.documentElement.clientHeight;
|
||||||
c.width = w;
|
c.width = w;
|
||||||
c.height = h;
|
c.height = h;
|
||||||
offset = $$('footer').offsetHeight - 32;
|
offset = $$('footer').offsetHeight - 32;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.2.0';
|
const cacheVersion = 'v1.3.0';
|
||||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+56
-63
@@ -564,7 +564,7 @@ x-dialog x-background {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
padding: 35px;
|
padding: 15px;
|
||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,19 +575,20 @@ x-dialog x-paper {
|
|||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: transform 300ms;
|
transition: transform 300ms;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-paper {
|
#pair-device-dialog x-paper {
|
||||||
position: absolute;
|
|
||||||
top: max(50%, 350px);
|
|
||||||
height: 650px;
|
|
||||||
margin-top: -325px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
position: absolute;
|
||||||
|
top: max(50%, 350px);
|
||||||
|
margin-top: -328.5px;
|
||||||
|
width: calc(100vw - 20px);
|
||||||
|
height: 625px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog:not([show]) {
|
x-dialog:not([show]) {
|
||||||
@@ -602,12 +603,6 @@ x-dialog:not([show]) x-background {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog .row-reverse>.button {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: -16px;
|
|
||||||
width: 50%;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog a {
|
x-dialog a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
@@ -646,7 +641,7 @@ x-dialog .font-subheading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container>input:nth-of-type(4) {
|
#key-input-container>input:nth-of-type(4) {
|
||||||
margin-left: 18px;
|
margin-left: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#room-key {
|
#room-key {
|
||||||
@@ -658,16 +653,11 @@ x-dialog .font-subheading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#room-key-qr-code {
|
#room-key-qr-code {
|
||||||
padding: inherit;
|
margin: 16px;
|
||||||
margin: auto;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog hr {
|
#pair-device-dialog hr {
|
||||||
margin-top: 40px;
|
margin: 40px -24px;
|
||||||
margin-bottom: 40px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-background {
|
#pair-device-dialog x-background {
|
||||||
@@ -681,29 +671,24 @@ x-dialog .row {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog h2 {
|
/* button row*/
|
||||||
margin-top: 1rem;
|
x-paper > div:last-child {
|
||||||
}
|
margin: auto -24px -15px;
|
||||||
|
|
||||||
#receive-request-dialog h2,
|
|
||||||
#receive-file-dialog h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog .row-reverse {
|
|
||||||
margin: 40px -24px 0;
|
|
||||||
border-top: solid 2.5px var(--border-color);
|
border-top: solid 2.5px var(--border-color);
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
x-paper > div:last-child > .button {
|
||||||
border: solid 1.25px var(--border-color);
|
height: 100%;
|
||||||
margin-bottom: -16px;
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > div:last-child > .button:not(:last-child) {
|
||||||
|
border-left: solid 2.5px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description {
|
.file-description {
|
||||||
word-break: break-word;
|
margin-bottom: 25px;
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description .row {
|
.file-description .row {
|
||||||
@@ -715,26 +700,26 @@ x-dialog .row-reverse {
|
|||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-name {
|
.file-name {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-stem {
|
.file-stem {
|
||||||
max-width: 80%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
word-break: break-all;
|
white-space: nowrap;
|
||||||
max-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-size{
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send Text Dialog */
|
/* Send Text Dialog */
|
||||||
|
|
||||||
|
x-dialog .dialog-subheader {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
#text-input {
|
#text-input {
|
||||||
min-height: 120px;
|
min-height: 200px;
|
||||||
|
margin: 14px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receive Text Dialog */
|
/* Receive Text Dialog */
|
||||||
@@ -742,14 +727,14 @@ x-dialog .row-reverse {
|
|||||||
#receive-text-dialog #text {
|
#receive-text-dialog #text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 393px);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-user-select: all;
|
-webkit-user-select: all;
|
||||||
-moz-user-select: all;
|
-moz-user-select: all;
|
||||||
user-select: all;
|
user-select: all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-top:36px;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receive-text-dialog #text a {
|
#receive-text-dialog #text a {
|
||||||
@@ -768,11 +753,7 @@ x-dialog .row-reverse {
|
|||||||
|
|
||||||
.row-separator {
|
.row-separator {
|
||||||
border-bottom: solid 2.5px var(--border-color);
|
border-bottom: solid 2.5px var(--border-color);
|
||||||
margin: auto -25px;
|
margin: auto -24px;
|
||||||
}
|
|
||||||
|
|
||||||
#receive-text-description-container {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-btn {
|
#base64-paste-btn {
|
||||||
@@ -800,7 +781,6 @@ x-dialog .row-reverse {
|
|||||||
padding: 2px 16px 0;
|
padding: 2px 16px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
min-width: 100px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -811,6 +791,7 @@ x-dialog .row-reverse {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
@@ -848,7 +829,7 @@ x-dialog .row-reverse {
|
|||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cancel-paste-mode-btn {
|
#cancel-paste-mode {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -875,7 +856,6 @@ button::-moz-focus-inner {
|
|||||||
|
|
||||||
|
|
||||||
/* Icon Button */
|
/* Icon Button */
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -885,10 +865,7 @@ button::-moz-focus-inner {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Text Input */
|
/* Text Input */
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -902,9 +879,8 @@ button::-moz-focus-inner {
|
|||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
resize: none;
|
resize: none;
|
||||||
min-height: 40px;
|
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 254px);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,6 +939,13 @@ button::-moz-focus-inner {
|
|||||||
margin: 8px 8px -16px;
|
margin: 8px 8px -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#about section {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#about header {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading Indicator */
|
/* Loading Indicator */
|
||||||
|
|
||||||
@@ -1094,6 +1077,14 @@ x-peers:empty~x-instructions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Styles */
|
/* Responsive Styles */
|
||||||
|
@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) {
|
@media screen and (min-height: 800px) {
|
||||||
footer {
|
footer {
|
||||||
@@ -1166,7 +1157,9 @@ x-dialog x-paper {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-preview {
|
.file-preview > img,
|
||||||
|
.file-preview > audio,
|
||||||
|
.file-preview > video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<use xlink:href="#clear-pair-devices-icon" />
|
<use xlink:href="#clear-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a>
|
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||||
</header>
|
</header>
|
||||||
<!-- Center -->
|
<!-- Center -->
|
||||||
<div id="center">
|
<div id="center">
|
||||||
@@ -109,18 +109,17 @@
|
|||||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div id="key-input-container">
|
<div id="key-input-container">
|
||||||
<input id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<button class="button" type="submit" disabled>Pair</button>
|
||||||
<div class="separator"></div>
|
<button class="button" close>Cancel</button>
|
||||||
<a class="button" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -133,9 +132,9 @@
|
|||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Unpair Devices</h2>
|
<h2 class="center">Unpair Devices</h2>
|
||||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
<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>
|
<button class="button" type="submit">Unpair Devices</button>
|
||||||
<a class="button" close>Cancel</a>
|
<button class="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -145,25 +144,23 @@
|
|||||||
<x-dialog id="receive-request-dialog">
|
<x-dialog id="receive-request-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">PairDrop</h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description">
|
<div class="center column file-description">
|
||||||
<div>
|
<div>
|
||||||
<span id="requesting-peer-display-name"></span>
|
<span class="display-name"></span>
|
||||||
<span>would like to share</span>
|
<span>would like to share</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="file-name" class="row" >
|
<div class="row file-name" >
|
||||||
<span id="file-stem"></span>
|
<span class="file-stem"></span>
|
||||||
<span id="file-extension"></span>
|
<span class="file-extension"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row file-other">
|
||||||
<span id="file-other"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-body2 text-center file-size"></div>
|
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||||
<div class="separator"></div>
|
|
||||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
@@ -173,13 +170,23 @@
|
|||||||
<x-dialog id="receive-file-dialog">
|
<x-dialog id="receive-file-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 id="receive-title" class="center"></h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description"></div>
|
<div class="center column file-description">
|
||||||
<div class="font-body2 text-center file-size"></div>
|
<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="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<a id="share-or-download" class="button" autofocus></a>
|
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||||
<div class="separator"></div>
|
<button id="download-btn" class="button" autofocus>Download</button>
|
||||||
<button class="button" close>Close</button>
|
<button class="button" close>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
@@ -190,16 +197,16 @@
|
|||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="text-center">PairDrop</h2>
|
<h2 class="text-center">Send Message</h2>
|
||||||
<div class="text-center">
|
<div class="dialog-subheader text-center">
|
||||||
<span>Send a Message to</span>
|
<span>Send a Message to</span>
|
||||||
<span id="text-send-peer-display-name"></span>
|
<span class="display-name"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row-separator"></div>
|
||||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
<div class="row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<div class="separator"></div>
|
<button class="button" title="ESCAPE" close>Cancel</button>
|
||||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
@@ -209,16 +216,15 @@
|
|||||||
<x-dialog id="receive-text-dialog">
|
<x-dialog id="receive-text-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2>PairDrop - Message Received</h2>
|
<h2 class="text-center">Message Received</h2>
|
||||||
<div id="receive-text-description-container">
|
<div class="text-center dialog-subheader">
|
||||||
<span id="receive-text-peer-display-name"></span>
|
<span class="display-name"></span>
|
||||||
<span>sent the following message:</span>
|
<span>has sent:</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row-separator"></div>
|
||||||
<div id="text"></div>
|
<div id="text"></div>
|
||||||
<div class="row-reverse">
|
<div class="center row-reverse">
|
||||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||||
<div class="separator"></div>
|
|
||||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class ServerConnection {
|
|||||||
_onOpen() {
|
_onOpen() {
|
||||||
console.log('WS: server connected');
|
console.log('WS: server connected');
|
||||||
Events.fire('ws-connected');
|
Events.fire('ws-connected');
|
||||||
|
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendRoomSecrets(roomSecrets) {
|
_sendRoomSecrets(roomSecrets) {
|
||||||
@@ -56,10 +57,17 @@ class ServerConnection {
|
|||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setRtcConfig(config) {
|
||||||
|
window.rtcConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
_onMessage(msg) {
|
_onMessage(msg) {
|
||||||
msg = JSON.parse(msg);
|
msg = JSON.parse(msg);
|
||||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
|
case 'rtc-config':
|
||||||
|
this._setRtcConfig(msg.config);
|
||||||
|
break;
|
||||||
case 'peers':
|
case 'peers':
|
||||||
Events.fire('peers', msg);
|
Events.fire('peers', msg);
|
||||||
break;
|
break;
|
||||||
@@ -155,15 +163,17 @@ class ServerConnection {
|
|||||||
this._socket.close();
|
this._socket.close();
|
||||||
this._socket = null;
|
this._socket = null;
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisconnect() {
|
_onDisconnect() {
|
||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
Events.fire('notify-user', 'Connecting..');
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onVisibilityChange() {
|
_onVisibilityChange() {
|
||||||
@@ -324,31 +334,30 @@ class Peer {
|
|||||||
this.sendJSON({ type: 'progress', progress: progress });
|
this.sendJSON({ type: 'progress', progress: progress });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessage(message, logMessage = true) {
|
_onMessage(message) {
|
||||||
if (typeof message !== 'string') {
|
if (typeof message !== 'string') {
|
||||||
this._onChunkReceived(message);
|
this._onChunkReceived(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message = JSON.parse(message);
|
const messageJSON = JSON.parse(message);
|
||||||
if (logMessage) console.log('RTC:', message);
|
switch (messageJSON.type) {
|
||||||
switch (message.type) {
|
|
||||||
case 'request':
|
case 'request':
|
||||||
this._onFilesTransferRequest(message);
|
this._onFilesTransferRequest(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'header':
|
case 'header':
|
||||||
this._onFilesHeader(message);
|
this._onFilesHeader(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition':
|
case 'partition':
|
||||||
this._onReceivedPartitionEnd(message);
|
this._onReceivedPartitionEnd(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition-received':
|
case 'partition-received':
|
||||||
this._sendNextPartition();
|
this._sendNextPartition();
|
||||||
break;
|
break;
|
||||||
case 'progress':
|
case 'progress':
|
||||||
this._onDownloadProgress(message.progress);
|
this._onDownloadProgress(messageJSON.progress);
|
||||||
break;
|
break;
|
||||||
case 'files-transfer-response':
|
case 'files-transfer-response':
|
||||||
this._onFileTransferRequestResponded(message);
|
this._onFileTransferRequestResponded(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'file-transfer-complete':
|
case 'file-transfer-complete':
|
||||||
this._onFileTransferCompleted();
|
this._onFileTransferCompleted();
|
||||||
@@ -357,7 +366,7 @@ class Peer {
|
|||||||
this._onMessageTransferCompleted();
|
this._onMessageTransferCompleted();
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
this._onTextReceived(message);
|
this._onTextReceived(messageJSON);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,7 +460,7 @@ class Peer {
|
|||||||
if (!this._requestAccepted.header.length) {
|
if (!this._requestAccepted.header.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
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._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
}
|
}
|
||||||
@@ -502,6 +511,7 @@ class RTCPeer extends Peer {
|
|||||||
|
|
||||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||||
super(serverConnection, peerId, roomType, roomSecret);
|
super(serverConnection, peerId, roomType, roomSecret);
|
||||||
|
this.rtcSupported = true;
|
||||||
if (!peerId) return; // we will listen for a caller
|
if (!peerId) return; // we will listen for a caller
|
||||||
this._connect(peerId, true);
|
this._connect(peerId, true);
|
||||||
}
|
}
|
||||||
@@ -519,7 +529,7 @@ class RTCPeer extends Peer {
|
|||||||
_openConnection(peerId, isCaller) {
|
_openConnection(peerId, isCaller) {
|
||||||
this._isCaller = isCaller;
|
this._isCaller = isCaller;
|
||||||
this._peerId = peerId;
|
this._peerId = peerId;
|
||||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||||
@@ -578,6 +588,13 @@ class RTCPeer extends Peer {
|
|||||||
this._channel = channel;
|
this._channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onMessage(message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
console.log('RTC:', JSON.parse(message));
|
||||||
|
}
|
||||||
|
super._onMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
getConnectionHash() {
|
getConnectionHash() {
|
||||||
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
|
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
|
||||||
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
|
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
|
||||||
@@ -682,6 +699,7 @@ class WSPeer extends Peer {
|
|||||||
|
|
||||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||||
super(serverConnection, peerId, roomType, roomSecret);
|
super(serverConnection, peerId, roomType, roomSecret);
|
||||||
|
this.rtcSupported = false;
|
||||||
if (!peerId) return; // we will listen for a caller
|
if (!peerId) return; // we will listen for a caller
|
||||||
this._sendSignal();
|
this._sendSignal();
|
||||||
}
|
}
|
||||||
@@ -700,15 +718,15 @@ class WSPeer extends Peer {
|
|||||||
this._server.send(message);
|
this._server.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendSignal() {
|
_sendSignal(connected = false) {
|
||||||
this.sendJSON({type: 'signal'});
|
this.sendJSON({type: 'signal', connected: connected});
|
||||||
}
|
}
|
||||||
|
|
||||||
onServerMessage(message) {
|
onServerMessage(message) {
|
||||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
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._peerId = message.sender.id;
|
||||||
this._sendSignal();
|
this._sendSignal(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConnectionHash() {
|
getConnectionHash() {
|
||||||
@@ -730,6 +748,7 @@ class PeersManager {
|
|||||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(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));
|
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,7 +768,7 @@ class PeersManager {
|
|||||||
_onWsRelay(message) {
|
_onWsRelay(message) {
|
||||||
const messageJSON = JSON.parse(message)
|
const messageJSON = JSON.parse(message)
|
||||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
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) {
|
_onPeers(msg) {
|
||||||
@@ -797,15 +816,24 @@ class PeersManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onPeerLeft(msg) {
|
_onPeerLeft(msg) {
|
||||||
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) {
|
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
|
||||||
console.log('WSPeer left:', msg.peerId)
|
console.log('WSPeer left:', msg.peerId);
|
||||||
Events.fire('peer-disconnected', msg.peerId)
|
Events.fire('peer-disconnected', msg.peerId);
|
||||||
} else if (msg.disconnect === true) {
|
} else if (msg.disconnect === true) {
|
||||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||||
Events.fire('peer-disconnected', msg.peerId);
|
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) {
|
_onPeerDisconnected(peerId) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
delete this.peers[peerId];
|
delete this.peers[peerId];
|
||||||
@@ -919,20 +947,3 @@ class Events {
|
|||||||
return window.removeEventListener(type, callback, false);
|
return window.removeEventListener(type, callback, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RTCPeer.config = {
|
|
||||||
'sdpSemantics': 'unified-plan',
|
|
||||||
'iceServers': [
|
|
||||||
{
|
|
||||||
urls: 'stun:stun.l.google.com:19302'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urls: 'stun:openrelay.metered.ca:80'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urls: 'turn:openrelay.metered.ca:443',
|
|
||||||
username: 'openrelayproject',
|
|
||||||
credential: 'openrelayproject',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const $ = query => document.getElementById(query);
|
const $ = query => document.getElementById(query);
|
||||||
const $$ = query => document.body.querySelector(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.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||||
window.android = /android/i.test(navigator.userAgent);
|
window.android = /android/i.test(navigator.userAgent);
|
||||||
@@ -28,7 +27,7 @@ class PeersUI {
|
|||||||
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
||||||
this.peers = {};
|
this.peers = {};
|
||||||
|
|
||||||
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn');
|
this.$cancelPasteModeBtn = $('cancel-paste-mode');
|
||||||
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
||||||
|
|
||||||
Events.on('dragover', e => this._onDragOver(e));
|
Events.on('dragover', e => this._onDragOver(e));
|
||||||
@@ -474,10 +473,14 @@ class Dialog {
|
|||||||
class ReceiveDialog extends Dialog {
|
class ReceiveDialog extends Dialog {
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
super(id);
|
super(id);
|
||||||
|
this.$fileDescription = this.$el.querySelector('.file-description');
|
||||||
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
|
this.$displayName = this.$el.querySelector('.display-name');
|
||||||
this.$fileSizeNode = this.$el.querySelector('.file-size');
|
this.$fileStem = this.$el.querySelector('.file-stem');
|
||||||
this.$previewBox = this.$el.querySelector('.file-preview')
|
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) {
|
_formatFileSize(bytes) {
|
||||||
@@ -493,6 +496,26 @@ class ReceiveDialog extends Dialog {
|
|||||||
return bytes + ' Bytes';
|
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 {
|
class ReceiveFileDialog extends ReceiveDialog {
|
||||||
@@ -500,24 +523,25 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('receive-file-dialog');
|
super('receive-file-dialog');
|
||||||
|
|
||||||
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download');
|
this.$downloadBtn = this.$el.querySelector('#download-btn');
|
||||||
this.$receiveTitleNode = this.$el.querySelector('#receive-title')
|
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 = [];
|
this._filesQueue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFilesReceived(sender, files, request) {
|
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||||
this._nextFiles(sender, files, request);
|
const displayName = $(sender).ui._displayName()
|
||||||
|
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||||
|
this._nextFiles();
|
||||||
window.blop.play();
|
window.blop.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextFiles(sender, nextFiles, nextRequest) {
|
_nextFiles() {
|
||||||
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
|
|
||||||
if (this._busy) return;
|
if (this._busy) return;
|
||||||
this._busy = true;
|
this._busy = true;
|
||||||
const {peerId, files, request} = this._filesQueue.shift();
|
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||||
this._displayFiles(peerId, files, request);
|
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
_dequeueFile() {
|
_dequeueFile() {
|
||||||
@@ -548,7 +572,6 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
let element = document.createElement(previewElement[mime]);
|
let element = document.createElement(previewElement[mime]);
|
||||||
element.src = URL.createObjectURL(file);
|
element.src = URL.createObjectURL(file);
|
||||||
element.controls = true;
|
element.controls = true;
|
||||||
element.classList.add('element-preview');
|
|
||||||
element.onload = _ => {
|
element.onload = _ => {
|
||||||
this.$previewBox.appendChild(element);
|
this.$previewBox.appendChild(element);
|
||||||
resolve(true)
|
resolve(true)
|
||||||
@@ -559,30 +582,32 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _displayFiles(peerId, files, request) {
|
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
||||||
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
|
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
||||||
|
|
||||||
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});
|
|
||||||
|
|
||||||
|
let descriptor, url, filenameDownload;
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
url = URL.createObjectURL(files[0])
|
descriptor = imagesOnly ? 'Image' : 'File';
|
||||||
title = `PairDrop - ${descriptor} Received`
|
|
||||||
filenameDownload = files[0].name;
|
|
||||||
} else {
|
} else {
|
||||||
title = `PairDrop - ${files.length} ${descriptor}s Received`
|
descriptor = imagesOnly ? 'Images' : 'Files';
|
||||||
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
|
}
|
||||||
if(files.length>2) description += "s";
|
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;
|
let bytesCompleted = 0;
|
||||||
zipper.createNewZipWriter();
|
zipper.createNewZipWriter();
|
||||||
for (let i=0; i<files.length; i++) {
|
for (let i=0; i<files.length; i++) {
|
||||||
@@ -590,7 +615,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
onprogress: (progress) => {
|
onprogress: (progress) => {
|
||||||
Events.fire('set-progress', {
|
Events.fire('set-progress', {
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
progress: (bytesCompleted + progress) / request.totalSize,
|
progress: (bytesCompleted + progress) / totalSize,
|
||||||
status: 'process'
|
status: 'process'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -610,49 +635,58 @@ class ReceiveFileDialog extends ReceiveDialog {
|
|||||||
let minutes = now.getMinutes().toString();
|
let minutes = now.getMinutes().toString();
|
||||||
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
||||||
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
downloadZipped = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$receiveTitleNode.textContent = title;
|
this.$downloadBtn.innerText = "Download";
|
||||||
this.$fileDescriptionNode.textContent = description;
|
this.$downloadBtn.onclick = _ => {
|
||||||
this.$fileSizeNode.textContent = size;
|
if (downloadZipped) {
|
||||||
|
let tmpZipBtn = document.createElement("a");
|
||||||
if (shareInsteadOfDownload) {
|
tmpZipBtn.download = filenameDownload;
|
||||||
this.$shareOrDownloadBtn.innerText = "Share";
|
tmpZipBtn.href = url;
|
||||||
this.continue = _ => {
|
tmpZipBtn.click();
|
||||||
navigator.share({files: files})
|
|
||||||
.catch(err => console.error(err));
|
|
||||||
}
|
|
||||||
this.continueCallback = _ => this.continue();
|
|
||||||
} else {
|
} else {
|
||||||
this.$shareOrDownloadBtn.innerText = "Download again";
|
this._downloadFilesIndividually(files);
|
||||||
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(_ => {
|
this.createPreviewElement(files[0]).finally(_ => {
|
||||||
document.title = files.length === 1
|
document.title = files.length === 1
|
||||||
? 'File received - PairDrop'
|
? 'File received - PairDrop'
|
||||||
: `(${files.length}) Files received - PairDrop`;
|
: `${files.length} Files received - PairDrop`;
|
||||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||||
this.show();
|
|
||||||
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
|
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));
|
}).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() {
|
hide() {
|
||||||
this.$shareOrDownloadBtn.removeAttribute('href');
|
this.$shareBtn.setAttribute('hidden', '');
|
||||||
this.$shareOrDownloadBtn.removeAttribute('download');
|
|
||||||
this.$previewBox.innerHTML = '';
|
this.$previewBox.innerHTML = '';
|
||||||
super.hide();
|
super.hide();
|
||||||
this._dequeueFile();
|
this._dequeueFile();
|
||||||
@@ -664,11 +698,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('receive-request-dialog');
|
super('receive-request-dialog');
|
||||||
|
|
||||||
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name');
|
|
||||||
this.$fileStemNode = this.$el.querySelector('#file-stem');
|
|
||||||
this.$fileExtensionNode = this.$el.querySelector('#file-extension');
|
|
||||||
this.$fileOtherNode = this.$el.querySelector('#file-other');
|
|
||||||
|
|
||||||
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
||||||
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
||||||
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
||||||
@@ -700,32 +729,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
|||||||
_showRequestDialog(request, peerId) {
|
_showRequestDialog(request, peerId) {
|
||||||
this.correspondingPeerId = peerId;
|
this.correspondingPeerId = peerId;
|
||||||
|
|
||||||
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
const displayName = $(peerId).ui._displayName();
|
||||||
|
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
||||||
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);
|
|
||||||
|
|
||||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||||
let element = document.createElement('img');
|
let element = document.createElement('img');
|
||||||
element.src = request.thumbnailDataUrl;
|
element.src = request.thumbnailDataUrl;
|
||||||
element.classList.add('element-preview');
|
|
||||||
|
|
||||||
this.$previewBox.appendChild(element)
|
this.$previewBox.appendChild(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = 'File Transfer Requested - PairDrop';
|
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");
|
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||||
this.show();
|
this.show();
|
||||||
}
|
}
|
||||||
@@ -760,7 +775,7 @@ class PairDeviceDialog extends Dialog {
|
|||||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||||
let createJoinForm = this.$el.querySelector('form');
|
let createJoinForm = this.$el.querySelector('form');
|
||||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||||
|
|
||||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||||
@@ -839,7 +854,7 @@ class PairDeviceDialog extends Dialog {
|
|||||||
})
|
})
|
||||||
this.$submitBtn.removeAttribute("disabled");
|
this.$submitBtn.removeAttribute("disabled");
|
||||||
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
||||||
this._onSubmit();
|
this._pairDeviceJoin(this.inputRoomKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -889,7 +904,8 @@ class PairDeviceDialog extends Dialog {
|
|||||||
return url.href;
|
return url.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSubmit() {
|
_onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
this._pairDeviceJoin(this.inputRoomKey);
|
this._pairDeviceJoin(this.inputRoomKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -976,14 +992,19 @@ class ClearDevicesDialog extends Dialog {
|
|||||||
super('clear-devices-dialog');
|
super('clear-devices-dialog');
|
||||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||||
let clearDevicesForm = this.$el.querySelector('form');
|
let clearDevicesForm = this.$el.querySelector('form');
|
||||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClearPairDevices() {
|
_onClearPairDevices() {
|
||||||
this.show();
|
this.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSubmit() {
|
_onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._clearRoomSecrets();
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearRoomSecrets() {
|
||||||
Events.fire('clear-room-secrets');
|
Events.fire('clear-room-secrets');
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
@@ -994,10 +1015,10 @@ class SendTextDialog extends Dialog {
|
|||||||
super('send-text-dialog');
|
super('send-text-dialog');
|
||||||
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
||||||
this.$text = this.$el.querySelector('#text-input');
|
this.$text = this.$el.querySelector('#text-input');
|
||||||
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name');
|
this.$peerDisplayName = this.$el.querySelector('.display-name');
|
||||||
this.$form = this.$el.querySelector('form');
|
this.$form = this.$el.querySelector('form');
|
||||||
this.$submit = this.$el.querySelector('button[type="submit"]');
|
this.$submit = this.$el.querySelector('button[type="submit"]');
|
||||||
this.$form.addEventListener('submit', _ => this._send());
|
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
||||||
this.$text.addEventListener('input', e => this._onChange(e));
|
this.$text.addEventListener('input', e => this._onChange(e));
|
||||||
Events.on("keydown", e => this._onKeyDown(e));
|
Events.on("keydown", e => this._onKeyDown(e));
|
||||||
}
|
}
|
||||||
@@ -1039,6 +1060,11 @@ class SendTextDialog extends Dialog {
|
|||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._send();
|
||||||
|
}
|
||||||
|
|
||||||
_send() {
|
_send() {
|
||||||
Events.fire('send-text', {
|
Events.fire('send-text', {
|
||||||
to: this.correspondingPeerId,
|
to: this.correspondingPeerId,
|
||||||
@@ -1062,7 +1088,7 @@ class ReceiveTextDialog extends Dialog {
|
|||||||
|
|
||||||
Events.on("keydown", e => this._onKeyDown(e));
|
Events.on("keydown", e => this._onKeyDown(e));
|
||||||
|
|
||||||
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name');
|
this.$displayNameNode = this.$el.querySelector('.display-name');
|
||||||
this._receiveTextQueue = [];
|
this._receiveTextQueue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,18 +1118,23 @@ class ReceiveTextDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_showReceiveTextDialog(text, peerId) {
|
_showReceiveTextDialog(text, peerId) {
|
||||||
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
this.$displayNameNode.innerText = $(peerId).ui._displayName();
|
||||||
|
|
||||||
if (isURL(text)) {
|
this.$text.innerText = text;
|
||||||
const $a = document.createElement('a');
|
this.$text.classList.remove('text-center');
|
||||||
$a.href = text;
|
|
||||||
$a.target = '_blank';
|
// Beautify text if text is short
|
||||||
$a.textContent = text;
|
if (text.length < 2000) {
|
||||||
this.$text.innerHTML = '';
|
// replace urls with actual links
|
||||||
this.$text.appendChild($a);
|
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
|
||||||
} else {
|
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||||
this.$text.textContent = text;
|
});
|
||||||
|
|
||||||
|
if (!/\s/.test(text)) {
|
||||||
|
this.$text.classList.add('text-center');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._setDocumentTitleMessages();
|
this._setDocumentTitleMessages();
|
||||||
|
|
||||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||||
@@ -1113,7 +1144,7 @@ class ReceiveTextDialog extends Dialog {
|
|||||||
_setDocumentTitleMessages() {
|
_setDocumentTitleMessages() {
|
||||||
document.title = !this._receiveTextQueue.length
|
document.title = !this._receiveTextQueue.length
|
||||||
? 'Message Received - PairDrop'
|
? 'Message Received - PairDrop'
|
||||||
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`;
|
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onCopy() {
|
async _onCopy() {
|
||||||
@@ -1188,7 +1219,7 @@ class Base64ZipDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setPasteBtnToProcessing() {
|
_setPasteBtnToProcessing() {
|
||||||
this.$pasteBtn.pointerEvents = "none";
|
this.$pasteBtn.style.pointerEvents = "none";
|
||||||
this.$pasteBtn.innerText = "Processing...";
|
this.$pasteBtn.innerText = "Processing...";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1333,7 +1364,7 @@ class Notifications {
|
|||||||
_messageNotification(message, peerId) {
|
_messageNotification(message, peerId) {
|
||||||
if (document.visibilityState !== 'visible') {
|
if (document.visibilityState !== 'visible') {
|
||||||
const peerDisplayName = $(peerId).ui._displayName();
|
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);
|
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
|
||||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||||
} else {
|
} else {
|
||||||
@@ -1394,7 +1425,7 @@ class NetworkStatusUI {
|
|||||||
constructor() {
|
constructor() {
|
||||||
Events.on('offline', _ => this._showOfflineMessage());
|
Events.on('offline', _ => this._showOfflineMessage());
|
||||||
Events.on('online', _ => this._showOnlineMessage());
|
Events.on('online', _ => this._showOnlineMessage());
|
||||||
Events.on('ws-connected', _ => this._showOnlineMessage());
|
Events.on('ws-connected', _ => this._onWsConnected());
|
||||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||||
if (!navigator.onLine) this._showOfflineMessage();
|
if (!navigator.onLine) this._showOfflineMessage();
|
||||||
}
|
}
|
||||||
@@ -1405,17 +1436,16 @@ class NetworkStatusUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_showOnlineMessage() {
|
_showOnlineMessage() {
|
||||||
window.animateBackground(true);
|
|
||||||
if (!this.firstConnect) {
|
|
||||||
this.firstConnect = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Events.fire('notify-user', 'You are back online');
|
Events.fire('notify-user', 'You are back online');
|
||||||
|
window.animateBackground(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onWsConnected() {
|
||||||
|
window.animateBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWsDisconnected() {
|
_onWsDisconnected() {
|
||||||
window.animateBackground(false);
|
window.animateBackground(false);
|
||||||
if (!this.firstConnect) this.firstConnect = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1753,8 +1783,8 @@ Events.on('load', () => {
|
|||||||
let x0, y0, w, h, dw, offset;
|
let x0, y0, w, h, dw, offset;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
w = window.innerWidth;
|
w = document.documentElement.clientWidth;
|
||||||
h = window.innerHeight;
|
h = document.documentElement.clientHeight;
|
||||||
c.width = w;
|
c.width = w;
|
||||||
c.height = h;
|
c.height = h;
|
||||||
offset = $$('footer').offsetHeight - 32;
|
offset = $$('footer').offsetHeight - 32;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.2.0';
|
const cacheVersion = 'v1.3.0';
|
||||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ x-dialog x-background {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
padding: 35px;
|
padding: 15px;
|
||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,19 +601,20 @@ x-dialog x-paper {
|
|||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: transform 300ms;
|
transition: transform 300ms;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-paper {
|
#pair-device-dialog x-paper {
|
||||||
position: absolute;
|
|
||||||
top: max(50%, 350px);
|
|
||||||
height: 650px;
|
|
||||||
margin-top: -325px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
position: absolute;
|
||||||
|
top: max(50%, 350px);
|
||||||
|
margin-top: -328.5px;
|
||||||
|
width: calc(100vw - 20px);
|
||||||
|
height: 625px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog:not([show]) {
|
x-dialog:not([show]) {
|
||||||
@@ -628,12 +629,6 @@ x-dialog:not([show]) x-background {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog .row-reverse>.button {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: -16px;
|
|
||||||
width: 50%;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog a {
|
x-dialog a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
@@ -672,7 +667,7 @@ x-dialog .font-subheading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container>input:nth-of-type(4) {
|
#key-input-container>input:nth-of-type(4) {
|
||||||
margin-left: 18px;
|
margin-left: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#room-key {
|
#room-key {
|
||||||
@@ -684,16 +679,11 @@ x-dialog .font-subheading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#room-key-qr-code {
|
#room-key-qr-code {
|
||||||
padding: inherit;
|
margin: 16px;
|
||||||
margin: auto;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog hr {
|
#pair-device-dialog hr {
|
||||||
margin-top: 40px;
|
margin: 40px -24px;
|
||||||
margin-bottom: 40px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-background {
|
#pair-device-dialog x-background {
|
||||||
@@ -707,29 +697,24 @@ x-dialog .row {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog h2 {
|
/* button row*/
|
||||||
margin-top: 1rem;
|
x-paper > div:last-child {
|
||||||
}
|
margin: auto -24px -15px;
|
||||||
|
|
||||||
#receive-request-dialog h2,
|
|
||||||
#receive-file-dialog h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog .row-reverse {
|
|
||||||
margin: 40px -24px 0;
|
|
||||||
border-top: solid 2.5px var(--border-color);
|
border-top: solid 2.5px var(--border-color);
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
x-paper > div:last-child > .button {
|
||||||
border: solid 1.25px var(--border-color);
|
height: 100%;
|
||||||
margin-bottom: -16px;
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > div:last-child > .button:not(:last-child) {
|
||||||
|
border-left: solid 2.5px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description {
|
.file-description {
|
||||||
word-break: break-word;
|
margin-bottom: 25px;
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description .row {
|
.file-description .row {
|
||||||
@@ -741,26 +726,26 @@ x-dialog .row-reverse {
|
|||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-name {
|
.file-name {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-stem {
|
.file-stem {
|
||||||
max-width: 80%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
word-break: break-all;
|
white-space: nowrap;
|
||||||
max-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-size{
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send Text Dialog */
|
/* Send Text Dialog */
|
||||||
|
|
||||||
|
x-dialog .dialog-subheader {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
#text-input {
|
#text-input {
|
||||||
min-height: 120px;
|
min-height: 200px;
|
||||||
|
margin: 14px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receive Text Dialog */
|
/* Receive Text Dialog */
|
||||||
@@ -768,14 +753,14 @@ x-dialog .row-reverse {
|
|||||||
#receive-text-dialog #text {
|
#receive-text-dialog #text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 393px);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-user-select: all;
|
-webkit-user-select: all;
|
||||||
-moz-user-select: all;
|
-moz-user-select: all;
|
||||||
user-select: all;
|
user-select: all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-top:36px;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receive-text-dialog #text a {
|
#receive-text-dialog #text a {
|
||||||
@@ -794,11 +779,7 @@ x-dialog .row-reverse {
|
|||||||
|
|
||||||
.row-separator {
|
.row-separator {
|
||||||
border-bottom: solid 2.5px var(--border-color);
|
border-bottom: solid 2.5px var(--border-color);
|
||||||
margin: auto -25px;
|
margin: auto -24px;
|
||||||
}
|
|
||||||
|
|
||||||
#receive-text-description-container {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-btn {
|
#base64-paste-btn {
|
||||||
@@ -826,7 +807,6 @@ x-dialog .row-reverse {
|
|||||||
padding: 2px 16px 0;
|
padding: 2px 16px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
min-width: 100px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -837,6 +817,7 @@ x-dialog .row-reverse {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
@@ -874,7 +855,7 @@ x-dialog .row-reverse {
|
|||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cancel-paste-mode-btn {
|
#cancel-paste-mode {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -901,7 +882,6 @@ button::-moz-focus-inner {
|
|||||||
|
|
||||||
|
|
||||||
/* Icon Button */
|
/* Icon Button */
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -911,10 +891,7 @@ button::-moz-focus-inner {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Text Input */
|
/* Text Input */
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -928,9 +905,8 @@ button::-moz-focus-inner {
|
|||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
resize: none;
|
resize: none;
|
||||||
min-height: 40px;
|
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 254px);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -989,6 +965,13 @@ button::-moz-focus-inner {
|
|||||||
margin: 8px 8px -16px;
|
margin: 8px 8px -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#about section {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#about header {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading Indicator */
|
/* Loading Indicator */
|
||||||
|
|
||||||
@@ -1120,6 +1103,14 @@ x-peers:empty~x-instructions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Styles */
|
/* Responsive Styles */
|
||||||
|
@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) {
|
@media screen and (min-height: 800px) {
|
||||||
#websocket-fallback {
|
#websocket-fallback {
|
||||||
@@ -1192,7 +1183,9 @@ x-dialog x-paper {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-preview {
|
.file-preview > img,
|
||||||
|
.file-preview > audio,
|
||||||
|
.file-preview > video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"sdpSemantics": "unified-plan",
|
||||||
|
"iceServers": [
|
||||||
|
{
|
||||||
|
"urls": "stun:stun.l.google.com:19302"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"urls": "stun:openrelay.metered.ca:80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"urls": "turn:openrelay.metered.ca:443",
|
||||||
|
"username": "openrelayproject",
|
||||||
|
"credential": "openrelayproject"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user