mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-06 18:03:48 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ac7786d0 | ||
|
|
edf2ab5eb3 | ||
|
|
c3863a9dd3 | ||
|
|
5934e94761 | ||
|
|
1bc23dc4b3 | ||
|
|
cc78b34d2e | ||
|
|
f34f5bd4b2 | ||
|
|
b2f6a75c99 | ||
|
|
82138c06f3 | ||
|
|
ee820ed6e0 | ||
|
|
b7e7fd1b68 | ||
|
|
96ed0e53b1 | ||
|
|
77b76a3b8d | ||
|
|
e37f9bd9fb | ||
|
|
67a1b04da2 | ||
|
|
8b2eb67266 | ||
|
|
827b10219d | ||
|
|
e7ab5e26cc | ||
|
|
451173caac | ||
|
|
8bcaa3f60f | ||
|
|
c0a4224a59 | ||
|
|
460e8ec79c | ||
|
|
002b31a113 | ||
|
|
1e35bab327 | ||
|
|
bb0493d071 | ||
|
|
bfb5aa8546 | ||
|
|
a9d7960a59 | ||
|
|
39ca5b2d21 | ||
|
|
cf715b2872 | ||
|
|
bbb8c1b10f | ||
|
|
d6ef5887dd | ||
|
|
f9f1abef7a | ||
|
|
d244f5fa47 | ||
|
|
3a2d8c75f7 | ||
|
|
545cdc2459 | ||
|
|
a1fdd81629 | ||
|
|
7220e85422 | ||
|
|
1eb53498b1 | ||
|
|
de76da52fe | ||
|
|
d56ee87437 | ||
|
|
a3b348d9b6 | ||
|
|
4566528179 | ||
|
|
7b08973cef | ||
|
|
eda60a3d78 | ||
|
|
e96ca53aa4 | ||
|
|
11d6a8a372 | ||
|
|
75726ae5f4 | ||
|
|
80dc36c00a | ||
|
|
765b4e65b1 | ||
|
|
e77f856515 | ||
|
|
0de92864eb | ||
|
|
8ecec5c1bf | ||
|
|
78cf0139b8 | ||
|
|
591c76c15a | ||
|
|
66359da2ca | ||
|
|
74b88c2e7d | ||
|
|
2a3d1d4105 | ||
|
|
5bff933b6e | ||
|
|
0ba1bd7113 | ||
|
|
0eb13d9d1b | ||
|
|
ad109d1724 | ||
|
|
f9e214a1e5 | ||
|
|
b6238b05ae | ||
|
|
0d8db3e309 | ||
|
|
c7647daff7 |
51
.github/workflows/github-image.yml
vendored
Normal file
51
.github/workflows/github-image.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# GitHub recommends pinning actions to a commit SHA.
|
||||
# To get a newer version, you will need to update the SHA.
|
||||
# You can also reference a tag or branch, but the action may change without warning.
|
||||
|
||||
name: GHCR Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
11
README.md
11
README.md
@@ -42,9 +42,9 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/)
|
||||
|
||||
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
|
||||
* Files are transferred only after a request is accepted first. On transfer completion they are downloaded automatically if possible.
|
||||
* Multiple files are downloaded as ZIP file
|
||||
* On iOS and Android the devices share menu is opened instead of downloading the files
|
||||
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
||||
* Multiple files are downloaded as a ZIP file
|
||||
* On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu.
|
||||
* Multiple files are transferred at once with an overall progress indicator
|
||||
|
||||
### Send Files or Text Directly From Share Menu, Context Menu or CLI
|
||||
@@ -54,7 +54,8 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||
|
||||
### Other changes
|
||||
* [Paste Mode](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||
* Change your display name permanently to easily differentiate your devices
|
||||
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
|
||||
* Warn user before PairDrop is closed on file transfer
|
||||
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
|
||||
@@ -63,6 +64,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* 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))
|
||||
* 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
|
||||
<div align="center">
|
||||
@@ -78,6 +80,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
|
||||
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
||||
* [cyrb53](https://github.com/bryc) super fast hash function
|
||||
|
||||
Have any questions? Read our [FAQ](/docs/faq.md).
|
||||
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
# Deployment Notes
|
||||
The easiest way to get PairDrop up and running is by using Docker.
|
||||
|
||||
## Deployment with Docker from Docker Hub
|
||||
## Deployment with Docker
|
||||
|
||||
### Docker Image from Docker Hub
|
||||
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
|
||||
```
|
||||
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
|
||||
### Options / Flags
|
||||
#### Options / Flags
|
||||
Set options by using the following flags in the `docker run` command:
|
||||
|
||||
#### Port
|
||||
```
|
||||
##### Port
|
||||
```bash
|
||||
-p 127.0.0.1:8080:3000
|
||||
```
|
||||
> Specify the port used by the docker image
|
||||
> - 3000 -> `-p 127.0.0.1:3000:3000`
|
||||
> - 8080 -> `-p 127.0.0.1:8080:3000`
|
||||
#### Rate limiting requests
|
||||
##### Rate limiting requests
|
||||
```
|
||||
-e RATE_LIMIT=true
|
||||
```
|
||||
> Limits clients to 100 requests per 5 min
|
||||
> Limits clients to 1000 requests per 5 min
|
||||
|
||||
#### Websocket Fallback (for VPN)
|
||||
```
|
||||
##### Websocket Fallback (for VPN)
|
||||
```bash
|
||||
-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.
|
||||
@@ -39,10 +42,48 @@ Set options by using the following flags in the `docker run` command:
|
||||
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
|
||||
> Additionally, beware that all traffic using this fallback debits the servers data plan.
|
||||
|
||||
##### 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>
|
||||
|
||||
## Deployment with Docker with self-built image
|
||||
### Build the image
|
||||
### Docker Image from GHCR
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
|
||||
```
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
>
|
||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||
|
||||
### Docker Image self-built
|
||||
#### Build the image
|
||||
```bash
|
||||
docker build --pull . -f Dockerfile -t pairdrop
|
||||
```
|
||||
@@ -50,15 +91,45 @@ docker build --pull . -f Dockerfile -t pairdrop
|
||||
>
|
||||
> `--pull` ensures always the latest node image is used.
|
||||
|
||||
### Run the image
|
||||
#### Run the image
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
|
||||
```
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
>
|
||||
> To specify options replace `npm run start:prod` according to [the documentation above.](#options--flags)
|
||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||
|
||||
<br>
|
||||
|
||||
## Deployment with Docker Compose
|
||||
Here's an example docker-compose file:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
pairdrop:
|
||||
image: lscr.io/linuxserver/pairdrop:latest
|
||||
container_name: pairdrop
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000 # UID to run the application as
|
||||
- PGID=1000 # GID to run the application as
|
||||
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
|
||||
- TZ=Etc/UTC # Time Zone
|
||||
ports:
|
||||
- 127.0.0.1:3000:3000 # Web UI
|
||||
```
|
||||
|
||||
Run the compose file with `docker compose up -d`.
|
||||
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
|
||||
<br>
|
||||
|
||||
## Deployment with node
|
||||
|
||||
@@ -100,6 +171,38 @@ $env:PORT=3010; npm start
|
||||
```
|
||||
> 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
|
||||
#### Local Run
|
||||
```bash
|
||||
@@ -109,7 +212,7 @@ npm start -- --localhost-only
|
||||
>
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> Use this when deploying PairDrop with node to prevent bypassing the proxy and reach the docker container directly.
|
||||
> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly.
|
||||
|
||||
#### Automatic restart on error
|
||||
```bash
|
||||
@@ -123,7 +226,7 @@ npm start -- --auto-restart
|
||||
```bash
|
||||
npm start -- --rate-limit
|
||||
```
|
||||
> Limits clients to 100 requests per 5 min
|
||||
> Limits clients to 1000 requests per 5 min
|
||||
|
||||
<br>
|
||||
|
||||
@@ -221,13 +324,13 @@ server {
|
||||
|
||||
### Using Apache
|
||||
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
|
||||
```shell
|
||||
```bash
|
||||
a2enmod proxy
|
||||
```
|
||||
```shell
|
||||
```bash
|
||||
a2enmod proxy_http
|
||||
```
|
||||
```shell
|
||||
```bash
|
||||
a2enmod proxy_wstunnel
|
||||
```
|
||||
|
||||
@@ -237,7 +340,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
|
||||
**pairdrop.conf**
|
||||
#### Allow http and https requests
|
||||
```
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
ProxyPass / http://127.0.0.1:3000/
|
||||
RewriteEngine on
|
||||
@@ -254,7 +357,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
</VirtualHost>
|
||||
```
|
||||
#### Automatic http to https redirect:
|
||||
```
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
Redirect permanent / https://127.0.0.1:3000/
|
||||
</VirtualHost>
|
||||
@@ -267,10 +370,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
</VirtualHost>
|
||||
```
|
||||
Activate the new virtual host and reload apache:
|
||||
```shell
|
||||
```bash
|
||||
a2ensite pairdrop
|
||||
```
|
||||
```shell
|
||||
```bash
|
||||
service apache2 reload
|
||||
```
|
||||
|
||||
@@ -281,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/)
|
||||
|
||||
Then, clone the repository and run docker-compose:
|
||||
```shell
|
||||
```bash
|
||||
git clone https://github.com/schlagmichdoch/PairDrop.git
|
||||
|
||||
cd PairDrop
|
||||
@@ -306,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.
|
||||
- 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.
|
||||
- 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).
|
||||
|
||||
|
||||
112
index.js
112
index.js
@@ -1,6 +1,13 @@
|
||||
const process = require('process')
|
||||
const crypto = require('crypto')
|
||||
const {spawn} = require('child_process')
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const parser = require('ua-parser-js');
|
||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
||||
const express = require('express');
|
||||
const RateLimit = require('express-rate-limit');
|
||||
const http = require('http');
|
||||
|
||||
// Handle SIGINT
|
||||
process.on('SIGINT', () => {
|
||||
@@ -49,9 +56,24 @@ if (process.argv.includes('--auto-restart')) {
|
||||
);
|
||||
}
|
||||
|
||||
const express = require('express');
|
||||
const RateLimit = require('express-rate-limit');
|
||||
const http = require('http');
|
||||
const rtcConfig = process.env.RTC_CONFIG
|
||||
? JSON.parse(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 app = express();
|
||||
|
||||
@@ -93,13 +115,9 @@ if (process.argv.includes('--localhost-only')) {
|
||||
server.listen(port);
|
||||
}
|
||||
|
||||
const parser = require('ua-parser-js');
|
||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
||||
|
||||
class PairDropServer {
|
||||
|
||||
constructor() {
|
||||
const WebSocket = require('ws');
|
||||
this._wss = new WebSocket.Server({ server });
|
||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||
|
||||
@@ -110,10 +128,14 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_onConnection(peer) {
|
||||
this._joinRoom(peer);
|
||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||
peer.socket.onerror = e => console.error(e);
|
||||
this._keepAlive(peer);
|
||||
this._send(peer, {
|
||||
type: 'rtc-config',
|
||||
config: rtcConfig
|
||||
});
|
||||
this._joinRoom(peer);
|
||||
|
||||
// send displayName
|
||||
this._send(peer, {
|
||||
@@ -121,7 +143,8 @@ class PairDropServer {
|
||||
message: {
|
||||
displayName: peer.name.displayName,
|
||||
deviceName: peer.name.deviceName,
|
||||
peerId: peer.id
|
||||
peerId: peer.id,
|
||||
peerIdHash: peer.id.hashCode128BitSalted()
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -317,6 +340,10 @@ class PairDropServer {
|
||||
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
|
||||
if (this._rooms[room] && this._rooms[room][peer.id]) {
|
||||
this._leaveRoom(peer, roomType, roomSecret);
|
||||
}
|
||||
|
||||
// if room doesn't exist, create it
|
||||
if (!this._rooms[room]) {
|
||||
this._rooms[room] = {};
|
||||
@@ -526,9 +553,11 @@ class Peer {
|
||||
}
|
||||
|
||||
_setPeerId(request) {
|
||||
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id");
|
||||
if (peer_id && Peer.isValidUuid(peer_id)) {
|
||||
this.id = peer_id;
|
||||
const searchParams = new URL(request.url, "http://server").searchParams;
|
||||
let peerId = searchParams.get("peer_id");
|
||||
let peerIdHash = searchParams.get("peer_id_hash");
|
||||
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
|
||||
this.id = peerId;
|
||||
} else {
|
||||
this.id = crypto.randomUUID();
|
||||
}
|
||||
@@ -587,6 +616,10 @@ class Peer {
|
||||
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
|
||||
}
|
||||
|
||||
isPeerIdHashValid(peerId, peerIdHash) {
|
||||
return peerIdHash === peerId.hashCode128BitSalted();
|
||||
}
|
||||
|
||||
addRoomSecret(roomSecret) {
|
||||
if (!(roomSecret in this.roomSecrets)) {
|
||||
this.roomSecrets.push(roomSecret);
|
||||
@@ -602,14 +635,55 @@ class Peer {
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode', {
|
||||
value: function() {
|
||||
var hash = 0, i, chr;
|
||||
for (i = 0; i < this.length; i++) {
|
||||
chr = this.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
return cyrb53(this);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
|
||||
value: function() {
|
||||
return hasher.hashCode128BitSalted(this);
|
||||
}
|
||||
});
|
||||
|
||||
const hasher = (() => {
|
||||
let seeds;
|
||||
return {
|
||||
hashCode128BitSalted(str) {
|
||||
if (!seeds) {
|
||||
// seeds are created on first call to salt hash.
|
||||
seeds = [4];
|
||||
for (let i=0; i<4; i++) {
|
||||
const randomBuffer = new Uint32Array(1);
|
||||
crypto.webcrypto.getRandomValues(randomBuffer);
|
||||
seeds[i] = randomBuffer[0];
|
||||
}
|
||||
}
|
||||
let hashCode = "";
|
||||
for (let i=0; i<4; i++) {
|
||||
hashCode += cyrb53(str, seeds[i]);
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
/*
|
||||
cyrb53 (c) 2018 bryc (github.com/bryc)
|
||||
A fast and simple hash function with decent collision resistance.
|
||||
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||
Public domain. Attribution appreciated.
|
||||
*/
|
||||
const cyrb53 = function(str, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1>>>0);
|
||||
};
|
||||
|
||||
new PairDropServer();
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.1.0",
|
||||
"version": "1.4.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pairdrop",
|
||||
"version": "1.1.0",
|
||||
"version": "1.4.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.1.0",
|
||||
"version": "1.4.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -106,7 +106,11 @@ sendFiles()
|
||||
zip -q -b /tmp/ -r "$zipPath" "$path"
|
||||
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
|
||||
|
||||
hash=$(base64 -w 0 "$zipPathTemp")
|
||||
if [[ $OS == "Mac" ]];then
|
||||
hash=$(base64 -i "$zipPathTemp")
|
||||
else
|
||||
hash=$(base64 -w 0 "$zipPathTemp")
|
||||
fi
|
||||
|
||||
# remove temporary temp file
|
||||
rm "$zipPathTemp"
|
||||
@@ -116,7 +120,11 @@ sendFiles()
|
||||
# Create zip file temporarily to send file
|
||||
zip -q -b /tmp/ "$zipPath" "$path"
|
||||
|
||||
hash=$(base64 -w 0 "$zipPath")
|
||||
if [[ $OS == "Mac" ]];then
|
||||
hash=$(base64 -i "$zipPath")
|
||||
else
|
||||
hash=$(base64 -w 0 "$zipPath")
|
||||
fi
|
||||
fi
|
||||
|
||||
# remove temporary temp file
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" >
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
@@ -69,173 +69,193 @@
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||
</header>
|
||||
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<p id="pasteFilename"></p>
|
||||
</x-instructions>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
<!-- Peers -->
|
||||
<div class="x-peers-filler"></div>
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<p id="paste-filename"></p>
|
||||
</x-instructions>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="column">
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="displayName" placeholder=" "></div>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Pair Device Dialog -->
|
||||
<x-dialog id="pairDeviceDialog">
|
||||
<x-dialog id="pair-device-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Pair Devices</h2>
|
||||
<div class="center" id="roomKeyQrCode"></div>
|
||||
<h1 class="center" id="roomKey">000 000</h1>
|
||||
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<div id="room-key-qr-code" class="center"></div>
|
||||
<h1 id="room-key" class="center">000 000</h1>
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="keyInputContainer">
|
||||
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Clear Devices Dialog -->
|
||||
<x-dialog id="clearDevicesDialog">
|
||||
<x-dialog id="clear-devices-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Request Dialog -->
|
||||
<x-dialog id="receiveRequestDialog">
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">PairDrop</h2>
|
||||
<div class="text-center file-description">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span id="requestingPeerDisplayName"></span>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
</div>
|
||||
<div class="row" id="fileName">
|
||||
<span id="fileStem"></span>
|
||||
<span id="fileExtension"></span>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="fileOther"></span>
|
||||
<div class="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Receive File Dialog -->
|
||||
<x-dialog id="receiveFileDialog">
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center" id="receiveTitle"></h2>
|
||||
<div class="text-center file-description"></div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<a class="button" id="shareOrDownload" autofocus></a>
|
||||
<div class="separator"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Send Text Dialog -->
|
||||
<x-dialog id="sendTextDialog">
|
||||
<x-dialog id="send-text-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Send a Message</h2>
|
||||
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="row-reverse">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span class="display-name"></span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
||||
<button class="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Text Dialog -->
|
||||
<x-dialog id="receiveTextDialog">
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Message Received</h2>
|
||||
<div id="receiveTextDescriptionContainer">
|
||||
<span id="receiveTextPeerDisplayName"></span>
|
||||
<span>sent the following message:</span>
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="close" title="ESCAPE">Close</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- base64PasteDialog Dialog -->
|
||||
<x-dialog id="base64PasteDialog">
|
||||
<!-- base64 Paste Dialog -->
|
||||
<x-dialog id="base64-paste-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" close>Close</button>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast class="row" shadow="1" id="toast"></x-toast>
|
||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<section class="center column fade-in">
|
||||
<header class="row-reverse">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
@@ -318,6 +338,10 @@
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||
</symbol>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
|
||||
@@ -21,10 +21,10 @@ class ServerConnection {
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
_connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(await this._endpoint());
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => this._onOpen();
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
@@ -36,6 +36,7 @@ class ServerConnection {
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
Events.fire('ws-connected');
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
@@ -52,16 +53,23 @@ class ServerConnection {
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
}
|
||||
|
||||
_setRtcConfig(config) {
|
||||
window.rtcConfig = config;
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'rtc-config':
|
||||
this._setRtcConfig(msg.config);
|
||||
break;
|
||||
case 'peers':
|
||||
Events.fire('peers', msg);
|
||||
break;
|
||||
@@ -110,34 +118,24 @@ class ServerConnection {
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
if (!peerId) {
|
||||
// save peerId to indexedDB to retrieve after PWA is installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
});
|
||||
}
|
||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
async _endpoint() {
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = await this._peerId();
|
||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
}
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
async _peerId() {
|
||||
// make peerId persistent when pwa is installed
|
||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
? await PersistentStorage.get('peerId')
|
||||
: sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
@@ -145,15 +143,17 @@ class ServerConnection {
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
@@ -319,26 +319,25 @@ class Peer {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
const messageJSON = JSON.parse(message);
|
||||
switch (messageJSON.type) {
|
||||
case 'request':
|
||||
this._onFilesTransferRequest(message);
|
||||
this._onFilesTransferRequest(messageJSON);
|
||||
break;
|
||||
case 'header':
|
||||
this._onFilesHeader(message);
|
||||
this._onFilesHeader(messageJSON);
|
||||
break;
|
||||
case 'partition':
|
||||
this._onReceivedPartitionEnd(message);
|
||||
this._onReceivedPartitionEnd(messageJSON);
|
||||
break;
|
||||
case 'partition-received':
|
||||
this._sendNextPartition();
|
||||
break;
|
||||
case 'progress':
|
||||
this._onDownloadProgress(message.progress);
|
||||
this._onDownloadProgress(messageJSON.progress);
|
||||
break;
|
||||
case 'files-transfer-response':
|
||||
this._onFileTransferRequestResponded(message);
|
||||
this._onFileTransferRequestResponded(messageJSON);
|
||||
break;
|
||||
case 'file-transfer-complete':
|
||||
this._onFileTransferCompleted();
|
||||
@@ -347,7 +346,10 @@ class Peer {
|
||||
this._onMessageTransferCompleted();
|
||||
break;
|
||||
case 'text':
|
||||
this._onTextReceived(message);
|
||||
this._onTextReceived(messageJSON);
|
||||
break;
|
||||
case 'display-name-changed':
|
||||
this._onDisplayNameChanged(messageJSON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -441,7 +443,7 @@ class Peer {
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -486,12 +488,18 @@ class Peer {
|
||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName) return;
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
@@ -509,7 +517,7 @@ class RTCPeer extends Peer {
|
||||
_openConnection(peerId, isCaller) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
||||
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||
@@ -558,14 +566,21 @@ class RTCPeer extends Peer {
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = _ => this._onChannelClosed();
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._closeChannel());
|
||||
this._channel = channel;
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._onPageHide());
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message === 'string') {
|
||||
console.log('RTC:', JSON.parse(message));
|
||||
}
|
||||
super._onMessage(message);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
@@ -601,10 +616,16 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
}
|
||||
|
||||
_closeChannel() {
|
||||
if (this._channel) this._channel.onclose = null;
|
||||
if (this._conn) this._conn.close();
|
||||
this._conn = null;
|
||||
_onPageHide() {
|
||||
this._disconnect();
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
if (this._conn && this._channel) {
|
||||
this._channel.onclose = null;
|
||||
this._channel.close();
|
||||
}
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
@@ -618,9 +639,11 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection disconnected');
|
||||
break;
|
||||
case 'failed':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection failed');
|
||||
break;
|
||||
}
|
||||
@@ -679,8 +702,11 @@ class PeersManager {
|
||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
@@ -704,10 +730,6 @@ class PeersManager {
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
@@ -739,6 +761,10 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
@@ -756,6 +782,23 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeerDisplayNameChanged(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (!peer) return;
|
||||
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
|
||||
}
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
@@ -852,20 +895,3 @@ class Events {
|
||||
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.body.querySelector(query);
|
||||
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
|
||||
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
window.android = /android/i.test(navigator.userAgent);
|
||||
@@ -10,9 +9,8 @@ window.pasteMode.activated = false;
|
||||
// set display name
|
||||
Events.on('display-name', e => {
|
||||
const me = e.detail.message;
|
||||
const $displayName = $('displayName')
|
||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
||||
$displayName.title = me.deviceName;
|
||||
const $displayName = $('display-name');
|
||||
$displayName.setAttribute('placeholder', me.displayName);
|
||||
});
|
||||
|
||||
class PeersUI {
|
||||
@@ -28,7 +26,7 @@ class PeersUI {
|
||||
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
||||
this.peers = {};
|
||||
|
||||
this.$cancelPasteModeBtn = $('cancelPasteModeBtn');
|
||||
this.$cancelPasteModeBtn = $('cancel-paste-mode');
|
||||
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
||||
|
||||
Events.on('dragover', e => this._onDragOver(e));
|
||||
@@ -38,8 +36,86 @@ class PeersUI {
|
||||
Events.on('drop', e => this._onDrop(e));
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
|
||||
this.$xPeers = $$('x-peers');
|
||||
this.$xNoPeers = $$('x-no-peers');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
|
||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||
|
||||
this.$displayName = $('display-name');
|
||||
|
||||
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||
|
||||
// Load saved display name on page load
|
||||
this._getSavedDisplayName().then(displayName => {
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||
});
|
||||
}
|
||||
|
||||
_insertDisplayName(displayName) {
|
||||
this.$displayName.textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDownDisplayName(e) {
|
||||
if (e.key === "Enter" || e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUpDisplayName(e) {
|
||||
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||
}
|
||||
|
||||
async _saveDisplayName(newDisplayName) {
|
||||
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
|
||||
const savedDisplayName = await this._getSavedDisplayName();
|
||||
if (newDisplayName === savedDisplayName) return;
|
||||
|
||||
if (newDisplayName) {
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
}).catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
}).finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
} else {
|
||||
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
}).finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getSavedDisplayName() {
|
||||
return new Promise((resolve) => {
|
||||
PersistentStorage.get('editedDisplayName')
|
||||
.then(displayName => resolve(displayName ?? ""))
|
||||
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
|
||||
});
|
||||
}
|
||||
|
||||
_changePeerDisplayName(peerId, displayName) {
|
||||
this.peers[peerId].name.displayName = displayName;
|
||||
const peerIdNode = $(peerId);
|
||||
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -53,11 +129,11 @@ class PeersUI {
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomType = roomType;
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].roomType = peer.roomType;
|
||||
this._redrawPeer(peer);
|
||||
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
|
||||
this._redrawPeer(this.peers[peer.id]);
|
||||
return; // peer already exists
|
||||
}
|
||||
this.peers[peer.id] = peer;
|
||||
@@ -72,7 +148,15 @@ class PeersUI {
|
||||
const peerNode = $(peer.id);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peerNode.classList.add(`type-${peer.roomType}`)
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
}
|
||||
|
||||
evaluateOverflowing() {
|
||||
if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {
|
||||
this.$xPeers.classList.add('overflowing');
|
||||
} else {
|
||||
this.$xPeers.classList.remove('overflowing');
|
||||
}
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
@@ -83,6 +167,7 @@ class PeersUI {
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
this.evaluateOverflowing();
|
||||
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
@@ -213,6 +298,18 @@ class PeersUI {
|
||||
|
||||
class PeerUI {
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
|
||||
$$('x-peers').appendChild(this.$el)
|
||||
Events.fire('peer-added');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
html() {
|
||||
let title;
|
||||
let input = '';
|
||||
@@ -225,17 +322,24 @@ class PeerUI {
|
||||
this.$el.innerHTML = `
|
||||
<label class="column center" title="${title}">
|
||||
${input}
|
||||
<x-icon shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
<x-icon>
|
||||
<div class="icon-wrapper" shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
</div>
|
||||
<div class="highlight-wrapper center">
|
||||
<div class="highlight" shadow="1"></div>
|
||||
</div>
|
||||
</x-icon>
|
||||
<div class="progress">
|
||||
<div class="circle"></div>
|
||||
<div class="circle right"></div>
|
||||
</div>
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
<div class="device-descriptor">
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
</div>
|
||||
</label>`;
|
||||
|
||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
@@ -245,23 +349,12 @@ class PeerUI {
|
||||
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
|
||||
}
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
this.$el = document.createElement('x-peer');
|
||||
this.$el.id = this._peer.id;
|
||||
this.$el.ui = this;
|
||||
this.$el.classList.add(`type-${this._roomType}`);
|
||||
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
this.$el.classList.add('center');
|
||||
this.html();
|
||||
|
||||
this._callbackInput = e => this._onFilesSelected(e)
|
||||
@@ -272,7 +365,7 @@ class PeerUI {
|
||||
this._callbackDragLeave = e => this._onDragEnd(e)
|
||||
this._callbackDragOver = e => this._onDragOver(e)
|
||||
this._callbackContextMenu = e => this._onRightClick(e)
|
||||
this._callbackTouchStart = _ => this._onTouchStart()
|
||||
this._callbackTouchStart = e => this._onTouchStart(e)
|
||||
this._callbackTouchEnd = e => this._onTouchEnd(e)
|
||||
this._callbackPointerDown = e => this._onPointerDown(e)
|
||||
// PasteMode
|
||||
@@ -393,21 +486,28 @@ class PeerUI {
|
||||
|
||||
_onRightClick(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
|
||||
_onTouchStart() {
|
||||
_onTouchStart(e) {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610);
|
||||
}
|
||||
|
||||
_onTouchEnd(e) {
|
||||
if (Date.now() - this._touchStart < 500) {
|
||||
clearTimeout(this._touchTimer);
|
||||
} else { // this was a long tap
|
||||
if (e) e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
} else if (this._touchTimer) { // this was a long tap
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
this._touchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,10 +545,14 @@ class Dialog {
|
||||
class ReceiveDialog extends Dialog {
|
||||
constructor(id) {
|
||||
super(id);
|
||||
|
||||
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
|
||||
this.$fileSizeNode = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview')
|
||||
this.$fileDescription = this.$el.querySelector('.file-description');
|
||||
this.$displayName = this.$el.querySelector('.display-name');
|
||||
this.$fileStem = this.$el.querySelector('.file-stem');
|
||||
this.$fileExtension = this.$el.querySelector('.file-extension');
|
||||
this.$fileOther = this.$el.querySelector('.file-other');
|
||||
this.$fileSize = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview');
|
||||
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
|
||||
}
|
||||
|
||||
_formatFileSize(bytes) {
|
||||
@@ -464,34 +568,56 @@ class ReceiveDialog extends Dialog {
|
||||
return bytes + ' Bytes';
|
||||
}
|
||||
}
|
||||
|
||||
_parseFileData(displayName, files, imagesOnly, totalSize) {
|
||||
if (files.length > 1) {
|
||||
let fileOtherText = ` and ${files.length - 1} other `;
|
||||
if (files.length === 2) {
|
||||
fileOtherText += imagesOnly ? 'image' : 'file';
|
||||
} else {
|
||||
fileOtherText += imagesOnly ? 'images' : 'files';
|
||||
}
|
||||
this.$fileOther.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
const fileName = files[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtension.innerText = fileExtension;
|
||||
this.$displayName.innerText = displayName;
|
||||
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveFileDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveFileDialog');
|
||||
super('receive-file-dialog');
|
||||
|
||||
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload');
|
||||
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle')
|
||||
this.$downloadBtn = this.$el.querySelector('#download-btn');
|
||||
this.$shareBtn = this.$el.querySelector('#share-btn');
|
||||
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
|
||||
this._filesQueue = [];
|
||||
}
|
||||
|
||||
_onFilesReceived(sender, files, request) {
|
||||
this._nextFiles(sender, files, request);
|
||||
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||
const displayName = $(sender).ui._displayName()
|
||||
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||
this._nextFiles();
|
||||
window.blop.play();
|
||||
}
|
||||
|
||||
_nextFiles(sender, nextFiles, nextRequest) {
|
||||
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
|
||||
_nextFiles() {
|
||||
if (this._busy) return;
|
||||
this._busy = true;
|
||||
const {peerId, files, request} = this._filesQueue.shift();
|
||||
this._displayFiles(peerId, files, request);
|
||||
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
// Todo: change count in document.title and move '- PairDrop' to back
|
||||
if (!this._filesQueue.length) { // nothing to do
|
||||
this._busy = false;
|
||||
return;
|
||||
@@ -519,7 +645,6 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let element = document.createElement(previewElement[mime]);
|
||||
element.src = URL.createObjectURL(file);
|
||||
element.controls = true;
|
||||
element.classList.add('element-preview');
|
||||
element.onload = _ => {
|
||||
this.$previewBox.appendChild(element);
|
||||
resolve(true)
|
||||
@@ -530,30 +655,32 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
});
|
||||
}
|
||||
|
||||
async _displayFiles(peerId, files, request) {
|
||||
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
|
||||
|
||||
let url;
|
||||
let title;
|
||||
let filenameDownload;
|
||||
|
||||
let descriptor = request.imagesOnly ? "Image" : "File";
|
||||
|
||||
let size = this._formatFileSize(request.totalSize);
|
||||
let description = files[0].name;
|
||||
|
||||
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
||||
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
||||
|
||||
let descriptor, url, filenameDownload;
|
||||
if (files.length === 1) {
|
||||
url = URL.createObjectURL(files[0])
|
||||
title = `PairDrop - ${descriptor} Received`
|
||||
filenameDownload = files[0].name;
|
||||
descriptor = imagesOnly ? 'Image' : 'File';
|
||||
} else {
|
||||
title = `PairDrop - ${files.length} ${descriptor}s Received`
|
||||
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
|
||||
if(files.length>2) description += "s";
|
||||
descriptor = imagesOnly ? 'Images' : 'Files';
|
||||
}
|
||||
this.$receiveTitle.innerText = `${descriptor} Received`;
|
||||
|
||||
if(!shareInsteadOfDownload) {
|
||||
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
if (canShare) {
|
||||
this.$shareBtn.removeAttribute('hidden');
|
||||
this.$shareBtn.onclick = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let downloadZipped = false;
|
||||
if (files.length > 1) {
|
||||
downloadZipped = true;
|
||||
try {
|
||||
let bytesCompleted = 0;
|
||||
zipper.createNewZipWriter();
|
||||
for (let i=0; i<files.length; i++) {
|
||||
@@ -561,7 +688,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
onprogress: (progress) => {
|
||||
Events.fire('set-progress', {
|
||||
peerId: peerId,
|
||||
progress: (bytesCompleted + progress) / request.totalSize,
|
||||
progress: (bytesCompleted + progress) / totalSize,
|
||||
status: 'process'
|
||||
})
|
||||
}
|
||||
@@ -581,47 +708,58 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let minutes = now.getMinutes().toString();
|
||||
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
||||
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
downloadZipped = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.$receiveTitleNode.textContent = title;
|
||||
this.$fileDescriptionNode.textContent = description;
|
||||
this.$fileSizeNode.textContent = size;
|
||||
|
||||
if (shareInsteadOfDownload) {
|
||||
this.$shareOrDownloadBtn.innerText = "Share";
|
||||
this.continue = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => console.error(err));
|
||||
this.$downloadBtn.innerText = "Download";
|
||||
this.$downloadBtn.onclick = _ => {
|
||||
if (downloadZipped) {
|
||||
let tmpZipBtn = document.createElement("a");
|
||||
tmpZipBtn.download = filenameDownload;
|
||||
tmpZipBtn.href = url;
|
||||
tmpZipBtn.click();
|
||||
} else {
|
||||
this._downloadFilesIndividually(files);
|
||||
}
|
||||
this.continueCallback = _ => this.continue();
|
||||
} else {
|
||||
this.$shareOrDownloadBtn.innerText = "Download again";
|
||||
this.continue = _ => {
|
||||
let tmpBtn = document.createElement("a");
|
||||
tmpBtn.download = filenameDownload;
|
||||
tmpBtn.href = url;
|
||||
tmpBtn.click();
|
||||
};
|
||||
this.continueCallback = _ => {
|
||||
this.continue();
|
||||
this.hide();
|
||||
};
|
||||
}
|
||||
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
|
||||
|
||||
if (!canShare) {
|
||||
this.$downloadBtn.innerText = "Download again";
|
||||
}
|
||||
Events.fire('notify-user', `${descriptor} downloaded successfully`);
|
||||
this.$downloadBtn.style.pointerEvents = "none";
|
||||
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
|
||||
};
|
||||
|
||||
this.createPreviewElement(files[0]).finally(_ => {
|
||||
document.title = `PairDrop - ${files.length} Files received`;
|
||||
document.title = files.length === 1
|
||||
? 'File received - PairDrop'
|
||||
: `${files.length} Files received - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
|
||||
this.continue();
|
||||
this.show();
|
||||
|
||||
if (canShare) {
|
||||
this.$shareBtn.click();
|
||||
} else {
|
||||
this.$downloadBtn.click();
|
||||
}
|
||||
}).catch(r => console.error(r));
|
||||
}
|
||||
|
||||
_downloadFilesIndividually(files) {
|
||||
let tmpBtn = document.createElement("a");
|
||||
for (let i=0; i<files.length; i++) {
|
||||
tmpBtn.download = files[i].name;
|
||||
tmpBtn.href = URL.createObjectURL(files[i]);
|
||||
tmpBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$shareOrDownloadBtn.removeAttribute('href');
|
||||
this.$shareOrDownloadBtn.removeAttribute('download');
|
||||
this.$shareBtn.setAttribute('hidden', '');
|
||||
this.$previewBox.innerHTML = '';
|
||||
super.hide();
|
||||
this._dequeueFile();
|
||||
@@ -631,15 +769,10 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveRequestDialog');
|
||||
super('receive-request-dialog');
|
||||
|
||||
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName');
|
||||
this.$fileStemNode = this.$el.querySelector('#fileStem');
|
||||
this.$fileExtensionNode = this.$el.querySelector('#fileExtension');
|
||||
this.$fileOtherNode = this.$el.querySelector('#fileOther');
|
||||
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#declineRequest');
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
||||
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
||||
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
|
||||
|
||||
@@ -669,32 +802,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
_showRequestDialog(request, peerId) {
|
||||
this.correspondingPeerId = peerId;
|
||||
|
||||
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
const fileName = request.header[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtensionNode.innerText = fileExtension
|
||||
|
||||
if (request.header.length >= 2) {
|
||||
let fileOtherText = ` and ${request.header.length - 1} other `;
|
||||
fileOtherText += request.imagesOnly ? 'image' : 'file';
|
||||
if (request.header.length > 2) fileOtherText += "s";
|
||||
this.$fileOtherNode.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
|
||||
const displayName = $(peerId).ui._displayName();
|
||||
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
||||
|
||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||
let element = document.createElement('img');
|
||||
element.src = request.thumbnailDataUrl;
|
||||
element.classList.add('element-preview');
|
||||
|
||||
this.$previewBox.appendChild(element)
|
||||
}
|
||||
|
||||
document.title = 'PairDrop - File Transfer Requested';
|
||||
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
|
||||
|
||||
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
@@ -720,16 +839,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pairDeviceDialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
|
||||
super('pair-device-dialog');
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
this.$roomKey = this.$el.querySelector('#roomKey');
|
||||
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
|
||||
this.$roomKey = this.$el.querySelector('#room-key');
|
||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||
this.$pairDeviceBtn = $('pair-device');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
||||
this.$createJoinForm = this.$el.querySelector('form');
|
||||
|
||||
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
|
||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||
@@ -799,7 +920,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
evaluateRoomKeyChars() {
|
||||
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
|
||||
if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) {
|
||||
this.$submitBtn.setAttribute("disabled", "");
|
||||
} else {
|
||||
this.inputRoomKey = "";
|
||||
@@ -808,7 +929,7 @@ class PairDeviceDialog extends Dialog {
|
||||
})
|
||||
this.$submitBtn.removeAttribute("disabled");
|
||||
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
||||
this._onSubmit();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,6 +943,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
@@ -839,11 +961,11 @@ class PairDeviceDialog extends Dialog {
|
||||
// Display the QR code for the url
|
||||
const qr = new QRCode({
|
||||
content: this._getShareRoomURL(),
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: 150,
|
||||
height: 150,
|
||||
padding: 0,
|
||||
background: "transparent",
|
||||
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
|
||||
color: `rgb(var(--text-color))`,
|
||||
ecl: "L",
|
||||
join: true
|
||||
});
|
||||
@@ -858,7 +980,8 @@ class PairDeviceDialog extends Dialog {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
|
||||
@@ -935,23 +1058,29 @@ class PairDeviceDialog extends Dialog {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
|
||||
}
|
||||
Events.fire('bg-resize');
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clearDevicesDialog');
|
||||
super('clear-devices-dialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
||||
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._clearRoomSecrets();
|
||||
}
|
||||
|
||||
_clearRoomSecrets() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
}
|
||||
@@ -959,12 +1088,13 @@ class ClearDevicesDialog extends Dialog {
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail));
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
super('send-text-dialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
||||
this.$text = this.$el.querySelector('#text-input');
|
||||
this.$peerDisplayName = this.$el.querySelector('.display-name');
|
||||
this.$form = this.$el.querySelector('form');
|
||||
this.$submit = this.$el.querySelector('button[type="submit"]');
|
||||
this.$form.addEventListener('submit', _ => this._send());
|
||||
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$text.addEventListener('input', e => this._onChange(e));
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
}
|
||||
@@ -992,8 +1122,9 @@ class SendTextDialog extends Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
_onRecipient(peerId) {
|
||||
_onRecipient(peerId, deviceName) {
|
||||
this.correspondingPeerId = peerId;
|
||||
this.$peerDisplayName.innerText = deviceName;
|
||||
this.show();
|
||||
|
||||
const range = document.createRange();
|
||||
@@ -1005,6 +1136,11 @@ class SendTextDialog extends Dialog {
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._send();
|
||||
}
|
||||
|
||||
_send() {
|
||||
Events.fire('send-text', {
|
||||
to: this.correspondingPeerId,
|
||||
@@ -1017,7 +1153,7 @@ class SendTextDialog extends Dialog {
|
||||
|
||||
class ReceiveTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('receiveTextDialog');
|
||||
super('receive-text-dialog');
|
||||
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
|
||||
this.$text = this.$el.querySelector('#text');
|
||||
this.$copy = this.$el.querySelector('#copy');
|
||||
@@ -1028,7 +1164,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
|
||||
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName');
|
||||
this.$displayNameNode = this.$el.querySelector('.display-name');
|
||||
this._receiveTextQueue = [];
|
||||
}
|
||||
|
||||
@@ -1046,6 +1182,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
_onText(text, peerId) {
|
||||
window.blop.play();
|
||||
this._receiveTextQueue.push({text: text, peerId: peerId});
|
||||
this._setDocumentTitleMessages();
|
||||
if (this.$el.attributes["show"]) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
@@ -1057,23 +1194,35 @@ class ReceiveTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
_showReceiveTextDialog(text, peerId) {
|
||||
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
this.$displayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
if (isURL(text)) {
|
||||
const $a = document.createElement('a');
|
||||
$a.href = text;
|
||||
$a.target = '_blank';
|
||||
$a.textContent = text;
|
||||
this.$text.innerHTML = '';
|
||||
this.$text.appendChild($a);
|
||||
} else {
|
||||
this.$text.textContent = text;
|
||||
this.$text.innerText = text;
|
||||
this.$text.classList.remove('text-center');
|
||||
|
||||
// Beautify text if text is short
|
||||
if (text.length < 2000) {
|
||||
// replace urls with actual links
|
||||
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
});
|
||||
|
||||
if (!/\s/.test(text)) {
|
||||
this.$text.classList.add('text-center');
|
||||
}
|
||||
}
|
||||
document.title = 'PairDrop - Message Received';
|
||||
|
||||
this._setDocumentTitleMessages();
|
||||
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
|
||||
_setDocumentTitleMessages() {
|
||||
document.title = !this._receiveTextQueue.length
|
||||
? 'Message Received - PairDrop'
|
||||
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
|
||||
}
|
||||
|
||||
async _onCopy() {
|
||||
await navigator.clipboard.writeText(this.$text.textContent);
|
||||
Events.fire('notify-user', 'Copied to clipboard');
|
||||
@@ -1089,13 +1238,13 @@ class ReceiveTextDialog extends Dialog {
|
||||
class Base64ZipDialog extends Dialog {
|
||||
|
||||
constructor() {
|
||||
super('base64PasteDialog');
|
||||
super('base64-paste-dialog');
|
||||
const urlParams = new URL(window.location).searchParams;
|
||||
const base64Text = urlParams.get('base64text');
|
||||
const base64Zip = urlParams.get('base64zip');
|
||||
const base64Hash = window.location.hash.substring(1);
|
||||
|
||||
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
|
||||
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
||||
|
||||
if (base64Text) {
|
||||
this.show();
|
||||
@@ -1146,7 +1295,7 @@ class Base64ZipDialog extends Dialog {
|
||||
}
|
||||
|
||||
_setPasteBtnToProcessing() {
|
||||
this.$pasteBtn.pointerEvents = "none";
|
||||
this.$pasteBtn.style.pointerEvents = "none";
|
||||
this.$pasteBtn.innerText = "Processing...";
|
||||
}
|
||||
|
||||
@@ -1246,6 +1395,7 @@ class Notifications {
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||
}
|
||||
// Todo: fix Notifications
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
||||
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
||||
}
|
||||
@@ -1290,7 +1440,7 @@ class Notifications {
|
||||
_messageNotification(message, peerId) {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
const peerDisplayName = $(peerId).ui._displayName();
|
||||
if (isURL(message)) {
|
||||
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
|
||||
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
|
||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||
} else {
|
||||
@@ -1321,7 +1471,7 @@ class Notifications {
|
||||
}
|
||||
|
||||
_download(notification) {
|
||||
$('shareOrDownload').click();
|
||||
$('share-or-download').click();
|
||||
notification.close();
|
||||
}
|
||||
|
||||
@@ -1351,7 +1501,7 @@ class NetworkStatusUI {
|
||||
constructor() {
|
||||
Events.on('offline', _ => this._showOfflineMessage());
|
||||
Events.on('online', _ => this._showOnlineMessage());
|
||||
Events.on('ws-connected', _ => this._showOnlineMessage());
|
||||
Events.on('ws-connected', _ => this._onWsConnected());
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
if (!navigator.onLine) this._showOfflineMessage();
|
||||
}
|
||||
@@ -1362,17 +1512,16 @@ class NetworkStatusUI {
|
||||
}
|
||||
|
||||
_showOnlineMessage() {
|
||||
window.animateBackground(true);
|
||||
if (!this.firstConnect) {
|
||||
this.firstConnect = true;
|
||||
return;
|
||||
}
|
||||
Events.fire('notify-user', 'You are back online');
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
window.animateBackground(false);
|
||||
if (!this.firstConnect) this.firstConnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1651,6 +1800,23 @@ class PersistentStorage {
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||
}
|
||||
|
||||
_broadcastMessage(message) {
|
||||
this.bc.postMessage(message);
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast message received:', e.data)
|
||||
Events.fire(e.data.type, e.data.detail);
|
||||
}
|
||||
}
|
||||
|
||||
class PairDrop {
|
||||
constructor() {
|
||||
Events.on('load', _ => {
|
||||
@@ -1670,6 +1836,7 @@ class PairDrop {
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
const webFileHandlersUI = new WebFileHandlersUI();
|
||||
const noSleepUI = new NoSleepUI();
|
||||
const broadCast = new Broadcast();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1710,23 +1877,19 @@ Events.on('load', () => {
|
||||
let x0, y0, w, h, dw, offset;
|
||||
|
||||
function init() {
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
w = document.documentElement.clientWidth;
|
||||
h = document.documentElement.clientHeight;
|
||||
c.width = w;
|
||||
c.height = h;
|
||||
offset = h > 800
|
||||
? 116
|
||||
: h > 380
|
||||
? 100
|
||||
: 65;
|
||||
|
||||
if (w < 420) offset += 20;
|
||||
offset = $$('footer').offsetHeight - 32;
|
||||
if (h > 800) offset += 16;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.max(w, h, 1000) / 13;
|
||||
drawCircles();
|
||||
}
|
||||
window.onresize = init;
|
||||
Events.on('bg-resize', _ => init());
|
||||
window.onresize = _ => Events.fire('bg-resize');
|
||||
|
||||
function drawCircle(radius) {
|
||||
ctx.beginPath();
|
||||
@@ -1791,9 +1954,3 @@ Notifications permission has been blocked
|
||||
as the user has dismissed the permission prompt several times.
|
||||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = _ => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.1.0';
|
||||
const cacheVersion = 'v1.4.1';
|
||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -10,25 +10,25 @@
|
||||
|
||||
/* Layout */
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
overscroll-behavior: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: hidden;
|
||||
min-height: 100vh;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
@@ -70,10 +70,7 @@ body {
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
@@ -116,9 +113,9 @@ h3 {
|
||||
}
|
||||
|
||||
.font-subheading {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
line-height: 18px;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
@@ -196,20 +193,151 @@ body>header a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#center {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 132px;
|
||||
max-height: calc(100vh - 56px - var(--footer-height));
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 425px) {
|
||||
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Peers List */
|
||||
|
||||
#x-peers-filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
x-peers {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-grow: 1;
|
||||
align-items: start !important;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 2;
|
||||
transition: color 300ms;
|
||||
transition: --bg-color 0.5s ease;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-x: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
--peers-per-row: 6; /* default if browser does not support :has selector */
|
||||
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
|
||||
width: var(--x-peers-width);
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
x-peers.overflowing {
|
||||
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
|
||||
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
|
||||
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||
|
||||
/* Opera doesn't support this in the shorthand */
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
|
||||
@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px),
|
||||
screen and (min-height: 486px) and (max-height: 631px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(7)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 649px) and (max-width: 425px),
|
||||
screen and (min-height: 631px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(28)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Empty Peers List */
|
||||
|
||||
x-no-peers {
|
||||
height: 114px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
@@ -251,25 +379,19 @@ x-no-peers[drop-bg] * {
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 8px;
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
x-peer .name {
|
||||
width: var(--peer-width);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
@@ -277,21 +399,45 @@ input[type="file"] {
|
||||
|
||||
x-peer x-icon {
|
||||
--icon-size: 40px;
|
||||
margin-bottom: 4px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
x-peer .icon-wrapper {
|
||||
width: var(--icon-size);
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip) x-icon {
|
||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
margin: 7px auto 0;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||
background-color: var(--paired-device-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer:not([status]):hover x-icon,
|
||||
x-peer:not([status]):focus x-icon {
|
||||
transform: scale(1.05);
|
||||
@@ -303,6 +449,19 @@ x-peer[status] x-icon {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status,
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
@@ -368,20 +527,21 @@ x-peer[drop] x-icon {
|
||||
/* Footer */
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
z-index: 2;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 80px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
@@ -399,6 +559,39 @@ footer .font-body2 {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#display-name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
max-width: 15em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -6px;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.1rem;
|
||||
border-radius: 1.3rem/30%;
|
||||
border-right: solid 1rem transparent;
|
||||
border-left: solid 1rem transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(var(--text-color), 43%);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
@@ -406,7 +599,8 @@ x-dialog x-background {
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
padding: 35px;
|
||||
padding: 15px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
@@ -416,11 +610,22 @@ x-dialog x-paper {
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -433,12 +638,6 @@ x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 0;
|
||||
margin-bottom: -16px;
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
x-dialog a {
|
||||
color: var(--primary-color);
|
||||
@@ -450,13 +649,13 @@ x-dialog .font-subheading {
|
||||
|
||||
/* PairDevicesDialog */
|
||||
|
||||
#keyInputContainer {
|
||||
#key-input-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input {
|
||||
#key-input-container>input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
@@ -472,15 +671,15 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input + * {
|
||||
#key-input-container>input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#keyInputContainer>input:nth-of-type(4) {
|
||||
margin-left: 18px;
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#roomKey {
|
||||
#room-key {
|
||||
font-size: 50px;
|
||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||
display: inline-block;
|
||||
@@ -488,19 +687,15 @@ x-dialog .font-subheading {
|
||||
margin: 15px -15px;
|
||||
}
|
||||
|
||||
#roomKeyQrCode {
|
||||
padding: inherit;
|
||||
margin: auto;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
#room-key-qr-code {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
#pair-device-dialog hr {
|
||||
margin: 40px -24px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog x-background {
|
||||
#pair-device-dialog x-background {
|
||||
padding: 16px!important;
|
||||
}
|
||||
|
||||
@@ -511,29 +706,24 @@ x-dialog .row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#receiveRequestDialog h2,
|
||||
#receiveFileDialog h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse {
|
||||
margin: 40px -24px auto;
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: solid 1.25px var(--border-color);
|
||||
margin-bottom: -16px;
|
||||
x-paper > div:last-child > .button {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
word-break: break-word;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
@@ -545,52 +735,52 @@ x-dialog .row-reverse {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
#fileName {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#fileStem {
|
||||
max-width: 80%;
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.file-size{
|
||||
margin-bottom: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
#textInput {
|
||||
min-height: 120px;
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
|
||||
#receiveTextDialog #text {
|
||||
#receive-text-dialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
white-space: pre-wrap;
|
||||
margin-top:36px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a {
|
||||
#receive-text-dialog #text a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a:hover {
|
||||
#receive-text-dialog #text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#receiveTextDialog h3 {
|
||||
#receive-text-dialog h3 {
|
||||
/* Select the received text when double-clicking the dialog */
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
@@ -598,28 +788,25 @@ x-dialog .row-reverse {
|
||||
|
||||
.row-separator {
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
margin: auto -25px;
|
||||
margin: auto -24px;
|
||||
}
|
||||
|
||||
#receiveTextDescriptionContainer {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#base64PasteBtn {
|
||||
#base64-paste-btn {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
}
|
||||
|
||||
#base64PasteDialog button {
|
||||
#base64-paste-dialog button {
|
||||
margin: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64PasteDialog button[close] {
|
||||
#base64-paste-dialog button[close] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#base64PasteDialog button[close]:before {
|
||||
|
||||
#base64-paste-dialog button[close]:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -629,7 +816,6 @@ x-dialog .row-reverse {
|
||||
padding: 2px 16px 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
@@ -640,6 +826,7 @@ x-dialog .row-reverse {
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@@ -677,16 +864,18 @@ x-dialog .row-reverse {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancelPasteModeBtn {
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: 56px;
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
background-color: var(--primary-color);
|
||||
color: rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.button:focus:before,
|
||||
@@ -702,7 +891,6 @@ button::-moz-focus-inner {
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -712,10 +900,7 @@ button::-moz-focus-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -729,9 +914,8 @@ button::-moz-focus-inner {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 254px);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -790,6 +974,13 @@ button::-moz-focus-inner {
|
||||
margin: 8px 8px -16px;
|
||||
}
|
||||
|
||||
#about section {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#about header {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* Loading Indicator */
|
||||
|
||||
@@ -797,7 +988,7 @@ button::-moz-focus-inner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -8px;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
@@ -839,11 +1030,11 @@ button::-moz-focus-inner {
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background-color: rgb(var(--text-color));
|
||||
color: rgb(var(--bg-color));
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
@@ -857,20 +1048,23 @@ x-toast {
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
|
||||
|
||||
/* Instructions */
|
||||
|
||||
x-instructions {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
z-index: -1;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
@@ -887,88 +1081,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
|
||||
|
||||
x-instructions p {
|
||||
display: none;
|
||||
margin: 0 auto auto;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
x-peer {
|
||||
transform: scale(0.95);
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#websocket-fallback {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
#websocket-fallback>span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#websocket-fallback > span > span {
|
||||
border-bottom: solid 4px var(--ws-peer-color);
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 800px) {
|
||||
@media screen and (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px),
|
||||
screen and (min-width: 1100px) {
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
content: attr(desktop);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 420px) {
|
||||
x-instructions {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
|
||||
|
||||
html {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Color Themes
|
||||
*/
|
||||
|
||||
/* Default colors */
|
||||
body {
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-test: 18,18,18;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
body.dark-theme {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Colored Elements */
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
color: rgb(var(--text-color));
|
||||
background-color: rgb(var(--bg-color));
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
background-color: var(--bg-color);
|
||||
background-color: rgb(var(--bg-color));
|
||||
}
|
||||
|
||||
.textarea {
|
||||
color: var(--text-color) !important;
|
||||
color: rgb(var(--text-color)) !important;
|
||||
background-color: var(--bg-color-secondary) !important;
|
||||
}
|
||||
|
||||
@@ -994,7 +1192,9 @@ x-dialog x-paper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.element-preview {
|
||||
.file-preview > img,
|
||||
.file-preview > audio,
|
||||
.file-preview > video {
|
||||
max-width: 100%;
|
||||
max-height: 40vh;
|
||||
margin: auto;
|
||||
@@ -1006,16 +1206,16 @@ x-dialog x-paper {
|
||||
|
||||
/* defaults to dark theme */
|
||||
body {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Override dark mode with light mode styles if the user decides to swap */
|
||||
body.light-theme {
|
||||
--text-color: #333;
|
||||
--bg-color: #fafafa;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
@@ -1033,6 +1233,15 @@ x-dialog x-paper {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
html {
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
}
|
||||
|
||||
/* webkit scrollbar style*/
|
||||
|
||||
::-webkit-scrollbar{
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" >
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
@@ -69,176 +69,196 @@
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||
</header>
|
||||
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
<br>
|
||||
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
|
||||
<p id="pasteFilename"></p>
|
||||
</x-instructions>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
<!-- Peers -->
|
||||
<div class="x-peers-filler"></div>
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<p id="paste-filename"></p>
|
||||
</x-instructions>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="column">
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="displayName" placeholder=" "></div>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
</div>
|
||||
<div id="websocket-fallback">
|
||||
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Pair Device Dialog -->
|
||||
<x-dialog id="pairDeviceDialog">
|
||||
<x-dialog id="pair-device-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Pair Devices</h2>
|
||||
<div class="center" id="roomKeyQrCode"></div>
|
||||
<h1 class="center" id="roomKey">000 000</h1>
|
||||
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<div id="room-key-qr-code" class="center"></div>
|
||||
<h1 id="room-key" class="center">000 000</h1>
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="keyInputContainer">
|
||||
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Clear Devices Dialog -->
|
||||
<x-dialog id="clearDevicesDialog">
|
||||
<x-dialog id="clear-devices-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Request Dialog -->
|
||||
<x-dialog id="receiveRequestDialog">
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">PairDrop</h2>
|
||||
<div class="text-center file-description">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span id="requestingPeerDisplayName"></span>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
</div>
|
||||
<div class="row" id="fileName">
|
||||
<span id="fileStem"></span>
|
||||
<span id="fileExtension"></span>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="fileOther"></span>
|
||||
<div class="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Receive File Dialog -->
|
||||
<x-dialog id="receiveFileDialog">
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center" id="receiveTitle"></h2>
|
||||
<div class="text-center file-description"></div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<a class="button" id="shareOrDownload" autofocus></a>
|
||||
<div class="separator"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Send Text Dialog -->
|
||||
<x-dialog id="sendTextDialog">
|
||||
<x-dialog id="send-text-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Send a Message</h2>
|
||||
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="row-reverse">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span class="display-name"></span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
||||
<button class="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Text Dialog -->
|
||||
<x-dialog id="receiveTextDialog">
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Message Received</h2>
|
||||
<div id="receiveTextDescriptionContainer">
|
||||
<span id="receiveTextPeerDisplayName"></span>
|
||||
<span>sent the following message:</span>
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
|
||||
<div class="separator"></div>
|
||||
<button class="button" id="close" title="ESCAPE">Close</button>
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- base64PasteDialog Dialog -->
|
||||
<x-dialog id="base64PasteDialog">
|
||||
<!-- base64 Paste Dialog -->
|
||||
<x-dialog id="base64-paste-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" close>Close</button>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast class="row" shadow="1" id="toast"></x-toast>
|
||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<section class="center column fade-in">
|
||||
<header class="row-reverse">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
@@ -321,6 +341,10 @@
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||
</symbol>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
|
||||
@@ -19,10 +19,10 @@ class ServerConnection {
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
_connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(await this._endpoint());
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => this._onOpen();
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
@@ -34,6 +34,7 @@ class ServerConnection {
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
Events.fire('ws-connected');
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
@@ -50,16 +51,23 @@ class ServerConnection {
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
}
|
||||
|
||||
_setRtcConfig(config) {
|
||||
window.rtcConfig = config;
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'rtc-config':
|
||||
this._setRtcConfig(msg.config);
|
||||
break;
|
||||
case 'peers':
|
||||
Events.fire('peers', msg);
|
||||
break;
|
||||
@@ -105,6 +113,7 @@ class ServerConnection {
|
||||
case 'file-transfer-complete':
|
||||
case 'message-transfer-complete':
|
||||
case 'text':
|
||||
case 'display-name-changed':
|
||||
case 'ws-chunk':
|
||||
Events.fire('ws-relay', JSON.stringify(msg));
|
||||
break;
|
||||
@@ -120,34 +129,24 @@ class ServerConnection {
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
if (!peerId) {
|
||||
// save peerId to indexedDB to retrieve after PWA is installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
});
|
||||
}
|
||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
async _endpoint() {
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = await this._peerId();
|
||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
}
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
async _peerId() {
|
||||
// make peerId persistent when pwa is installed
|
||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
? await PersistentStorage.get('peerId')
|
||||
: sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
@@ -155,15 +154,17 @@ class ServerConnection {
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
@@ -324,31 +325,30 @@ class Peer {
|
||||
this.sendJSON({ type: 'progress', progress: progress });
|
||||
}
|
||||
|
||||
_onMessage(message, logMessage = true) {
|
||||
_onMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
if (logMessage) console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
const messageJSON = JSON.parse(message);
|
||||
switch (messageJSON.type) {
|
||||
case 'request':
|
||||
this._onFilesTransferRequest(message);
|
||||
this._onFilesTransferRequest(messageJSON);
|
||||
break;
|
||||
case 'header':
|
||||
this._onFilesHeader(message);
|
||||
this._onFilesHeader(messageJSON);
|
||||
break;
|
||||
case 'partition':
|
||||
this._onReceivedPartitionEnd(message);
|
||||
this._onReceivedPartitionEnd(messageJSON);
|
||||
break;
|
||||
case 'partition-received':
|
||||
this._sendNextPartition();
|
||||
break;
|
||||
case 'progress':
|
||||
this._onDownloadProgress(message.progress);
|
||||
this._onDownloadProgress(messageJSON.progress);
|
||||
break;
|
||||
case 'files-transfer-response':
|
||||
this._onFileTransferRequestResponded(message);
|
||||
this._onFileTransferRequestResponded(messageJSON);
|
||||
break;
|
||||
case 'file-transfer-complete':
|
||||
this._onFileTransferCompleted();
|
||||
@@ -357,7 +357,10 @@ class Peer {
|
||||
this._onMessageTransferCompleted();
|
||||
break;
|
||||
case 'text':
|
||||
this._onTextReceived(message);
|
||||
this._onTextReceived(messageJSON);
|
||||
break;
|
||||
case 'display-name-changed':
|
||||
this._onDisplayNameChanged(messageJSON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -451,7 +454,7 @@ class Peer {
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -496,12 +499,18 @@ class Peer {
|
||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName) return;
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
@@ -519,7 +528,7 @@ class RTCPeer extends Peer {
|
||||
_openConnection(peerId, isCaller) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
||||
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||
@@ -568,14 +577,21 @@ class RTCPeer extends Peer {
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = _ => this._onChannelClosed();
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._closeChannel());
|
||||
this._channel = channel;
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._onPageHide());
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message === 'string') {
|
||||
console.log('RTC:', JSON.parse(message));
|
||||
}
|
||||
super._onMessage(message);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
@@ -611,10 +627,16 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
}
|
||||
|
||||
_closeChannel() {
|
||||
if (this._channel) this._channel.onclose = null;
|
||||
if (this._conn) this._conn.close();
|
||||
this._conn = null;
|
||||
_onPageHide() {
|
||||
this._disconnect();
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
if (this._conn && this._channel) {
|
||||
this._channel.onclose = null;
|
||||
this._channel.close();
|
||||
}
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
@@ -628,9 +650,11 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection disconnected');
|
||||
break;
|
||||
case 'failed':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection failed');
|
||||
break;
|
||||
}
|
||||
@@ -682,7 +706,9 @@ class WSPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = false;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._isCaller = true;
|
||||
this._sendSignal();
|
||||
}
|
||||
|
||||
@@ -694,21 +720,22 @@ class WSPeer extends Peer {
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
console.debug(message)
|
||||
message.to = this._peerId;
|
||||
message.roomType = this._roomType;
|
||||
message.roomSecret = this._roomSecret;
|
||||
this._server.send(message);
|
||||
}
|
||||
|
||||
_sendSignal() {
|
||||
this.sendJSON({type: 'signal'});
|
||||
_sendSignal(connected = false) {
|
||||
this.sendJSON({type: 'signal', connected: connected});
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (this._peerId) return;
|
||||
this._peerId = message.sender.id;
|
||||
this._sendSignal();
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (message.connected) return;
|
||||
this._sendSignal(true);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
@@ -728,8 +755,12 @@ class PeersManager {
|
||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||
}
|
||||
|
||||
@@ -749,7 +780,7 @@ class PeersManager {
|
||||
_onWsRelay(message) {
|
||||
const messageJSON = JSON.parse(message)
|
||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
||||
this.peers[messageJSON.sender.id]._onMessage(message, false)
|
||||
this.peers[messageJSON.sender.id]._onMessage(message)
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
@@ -768,10 +799,6 @@ class PeersManager {
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
@@ -797,15 +824,28 @@ class PeersManager {
|
||||
}
|
||||
|
||||
_onPeerLeft(msg) {
|
||||
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) {
|
||||
console.log('WSPeer left:', msg.peerId)
|
||||
Events.fire('peer-disconnected', msg.peerId)
|
||||
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
console.log('WSPeer left:', msg.peerId);
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
} else if (msg.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
for (const peerId in this.peers) {
|
||||
console.debug(this.peers[peerId].rtcSupported);
|
||||
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
Events.fire('peer-disconnected', peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
@@ -823,6 +863,23 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeerDisplayNameChanged(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (!peer) return;
|
||||
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
|
||||
}
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
@@ -919,20 +976,3 @@ class Events {
|
||||
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.body.querySelector(query);
|
||||
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
|
||||
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
window.android = /android/i.test(navigator.userAgent);
|
||||
@@ -10,9 +9,8 @@ window.pasteMode.activated = false;
|
||||
// set display name
|
||||
Events.on('display-name', e => {
|
||||
const me = e.detail.message;
|
||||
const $displayName = $('displayName')
|
||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
||||
$displayName.title = me.deviceName;
|
||||
const $displayName = $('display-name');
|
||||
$displayName.setAttribute('placeholder', me.displayName);
|
||||
});
|
||||
|
||||
class PeersUI {
|
||||
@@ -28,7 +26,7 @@ class PeersUI {
|
||||
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
|
||||
this.peers = {};
|
||||
|
||||
this.$cancelPasteModeBtn = $('cancelPasteModeBtn');
|
||||
this.$cancelPasteModeBtn = $('cancel-paste-mode');
|
||||
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
|
||||
|
||||
Events.on('dragover', e => this._onDragOver(e));
|
||||
@@ -38,8 +36,86 @@ class PeersUI {
|
||||
Events.on('drop', e => this._onDrop(e));
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
|
||||
this.$xPeers = $$('x-peers');
|
||||
this.$xNoPeers = $$('x-no-peers');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
|
||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||
|
||||
this.$displayName = $('display-name');
|
||||
|
||||
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||
|
||||
// Load saved display name on page load
|
||||
this._getSavedDisplayName().then(displayName => {
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||
});
|
||||
}
|
||||
|
||||
_insertDisplayName(displayName) {
|
||||
this.$displayName.textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDownDisplayName(e) {
|
||||
if (e.key === "Enter" || e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUpDisplayName(e) {
|
||||
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||
}
|
||||
|
||||
async _saveDisplayName(newDisplayName) {
|
||||
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
|
||||
const savedDisplayName = await this._getSavedDisplayName();
|
||||
if (newDisplayName === savedDisplayName) return;
|
||||
|
||||
if (newDisplayName) {
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
}).catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
}).finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
} else {
|
||||
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
}).finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getSavedDisplayName() {
|
||||
return new Promise((resolve) => {
|
||||
PersistentStorage.get('editedDisplayName')
|
||||
.then(displayName => resolve(displayName ?? ""))
|
||||
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
|
||||
});
|
||||
}
|
||||
|
||||
_changePeerDisplayName(peerId, displayName) {
|
||||
this.peers[peerId].name.displayName = displayName;
|
||||
const peerIdNode = $(peerId);
|
||||
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -53,11 +129,11 @@ class PeersUI {
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomType = roomType;
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].roomType = peer.roomType;
|
||||
this._redrawPeer(peer);
|
||||
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
|
||||
this._redrawPeer(this.peers[peer.id]);
|
||||
return; // peer already exists
|
||||
}
|
||||
this.peers[peer.id] = peer;
|
||||
@@ -72,7 +148,15 @@ class PeersUI {
|
||||
const peerNode = $(peer.id);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peerNode.classList.add(`type-${peer.roomType}`)
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
}
|
||||
|
||||
evaluateOverflowing() {
|
||||
if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {
|
||||
this.$xPeers.classList.add('overflowing');
|
||||
} else {
|
||||
this.$xPeers.classList.remove('overflowing');
|
||||
}
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
@@ -83,6 +167,7 @@ class PeersUI {
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
this.evaluateOverflowing();
|
||||
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
@@ -213,6 +298,18 @@ class PeersUI {
|
||||
|
||||
class PeerUI {
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
|
||||
$$('x-peers').appendChild(this.$el)
|
||||
Events.fire('peer-added');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
html() {
|
||||
let title;
|
||||
let input = '';
|
||||
@@ -225,17 +322,24 @@ class PeerUI {
|
||||
this.$el.innerHTML = `
|
||||
<label class="column center" title="${title}">
|
||||
${input}
|
||||
<x-icon shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
<x-icon>
|
||||
<div class="icon-wrapper" shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
</div>
|
||||
<div class="highlight-wrapper center">
|
||||
<div class="highlight" shadow="1"></div>
|
||||
</div>
|
||||
</x-icon>
|
||||
<div class="progress">
|
||||
<div class="circle"></div>
|
||||
<div class="circle right"></div>
|
||||
</div>
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
<div class="device-descriptor">
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span>
|
||||
</div>
|
||||
</label>`;
|
||||
|
||||
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
@@ -245,23 +349,12 @@ class PeerUI {
|
||||
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
|
||||
}
|
||||
|
||||
constructor(peer, connectionHash) {
|
||||
this._peer = peer;
|
||||
this._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._connectionHash = connectionHash;
|
||||
this._initDom();
|
||||
this._bindListeners();
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
this.$el = document.createElement('x-peer');
|
||||
this.$el.id = this._peer.id;
|
||||
this.$el.ui = this;
|
||||
this.$el.classList.add(`type-${this._roomType}`);
|
||||
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
this.$el.classList.add('center');
|
||||
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer')
|
||||
this.html();
|
||||
|
||||
@@ -273,7 +366,7 @@ class PeerUI {
|
||||
this._callbackDragLeave = e => this._onDragEnd(e)
|
||||
this._callbackDragOver = e => this._onDragOver(e)
|
||||
this._callbackContextMenu = e => this._onRightClick(e)
|
||||
this._callbackTouchStart = _ => this._onTouchStart()
|
||||
this._callbackTouchStart = e => this._onTouchStart(e)
|
||||
this._callbackTouchEnd = e => this._onTouchEnd(e)
|
||||
this._callbackPointerDown = e => this._onPointerDown(e)
|
||||
// PasteMode
|
||||
@@ -394,21 +487,28 @@ class PeerUI {
|
||||
|
||||
_onRightClick(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
|
||||
_onTouchStart() {
|
||||
_onTouchStart(e) {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610);
|
||||
}
|
||||
|
||||
_onTouchEnd(e) {
|
||||
if (Date.now() - this._touchStart < 500) {
|
||||
clearTimeout(this._touchTimer);
|
||||
} else { // this was a long tap
|
||||
if (e) e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
} else if (this._touchTimer) { // this was a long tap
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', {
|
||||
peerId: this._peer.id,
|
||||
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
||||
});
|
||||
}
|
||||
this._touchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,10 +546,14 @@ class Dialog {
|
||||
class ReceiveDialog extends Dialog {
|
||||
constructor(id) {
|
||||
super(id);
|
||||
|
||||
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
|
||||
this.$fileSizeNode = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview')
|
||||
this.$fileDescription = this.$el.querySelector('.file-description');
|
||||
this.$displayName = this.$el.querySelector('.display-name');
|
||||
this.$fileStem = this.$el.querySelector('.file-stem');
|
||||
this.$fileExtension = this.$el.querySelector('.file-extension');
|
||||
this.$fileOther = this.$el.querySelector('.file-other');
|
||||
this.$fileSize = this.$el.querySelector('.file-size');
|
||||
this.$previewBox = this.$el.querySelector('.file-preview');
|
||||
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
|
||||
}
|
||||
|
||||
_formatFileSize(bytes) {
|
||||
@@ -465,34 +569,56 @@ class ReceiveDialog extends Dialog {
|
||||
return bytes + ' Bytes';
|
||||
}
|
||||
}
|
||||
|
||||
_parseFileData(displayName, files, imagesOnly, totalSize) {
|
||||
if (files.length > 1) {
|
||||
let fileOtherText = ` and ${files.length - 1} other `;
|
||||
if (files.length === 2) {
|
||||
fileOtherText += imagesOnly ? 'image' : 'file';
|
||||
} else {
|
||||
fileOtherText += imagesOnly ? 'images' : 'files';
|
||||
}
|
||||
this.$fileOther.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
const fileName = files[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtension.innerText = fileExtension;
|
||||
this.$displayName.innerText = displayName;
|
||||
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveFileDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveFileDialog');
|
||||
super('receive-file-dialog');
|
||||
|
||||
this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload');
|
||||
this.$receiveTitleNode = this.$el.querySelector('#receiveTitle')
|
||||
this.$downloadBtn = this.$el.querySelector('#download-btn');
|
||||
this.$shareBtn = this.$el.querySelector('#share-btn');
|
||||
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
|
||||
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
|
||||
this._filesQueue = [];
|
||||
}
|
||||
|
||||
_onFilesReceived(sender, files, request) {
|
||||
this._nextFiles(sender, files, request);
|
||||
_onFilesReceived(sender, files, imagesOnly, totalSize) {
|
||||
const displayName = $(sender).ui._displayName()
|
||||
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
|
||||
this._nextFiles();
|
||||
window.blop.play();
|
||||
}
|
||||
|
||||
_nextFiles(sender, nextFiles, nextRequest) {
|
||||
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
|
||||
_nextFiles() {
|
||||
if (this._busy) return;
|
||||
this._busy = true;
|
||||
const {peerId, files, request} = this._filesQueue.shift();
|
||||
this._displayFiles(peerId, files, request);
|
||||
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
|
||||
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
// Todo: change count in document.title and move '- PairDrop' to back
|
||||
if (!this._filesQueue.length) { // nothing to do
|
||||
this._busy = false;
|
||||
return;
|
||||
@@ -520,7 +646,6 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let element = document.createElement(previewElement[mime]);
|
||||
element.src = URL.createObjectURL(file);
|
||||
element.controls = true;
|
||||
element.classList.add('element-preview');
|
||||
element.onload = _ => {
|
||||
this.$previewBox.appendChild(element);
|
||||
resolve(true)
|
||||
@@ -531,30 +656,32 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
});
|
||||
}
|
||||
|
||||
async _displayFiles(peerId, files, request) {
|
||||
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
|
||||
|
||||
let url;
|
||||
let title;
|
||||
let filenameDownload;
|
||||
|
||||
let descriptor = request.imagesOnly ? "Image" : "File";
|
||||
|
||||
let size = this._formatFileSize(request.totalSize);
|
||||
let description = files[0].name;
|
||||
|
||||
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
|
||||
this._parseFileData(displayName, files, imagesOnly, totalSize);
|
||||
|
||||
let descriptor, url, filenameDownload;
|
||||
if (files.length === 1) {
|
||||
url = URL.createObjectURL(files[0])
|
||||
title = `PairDrop - ${descriptor} Received`
|
||||
filenameDownload = files[0].name;
|
||||
descriptor = imagesOnly ? 'Image' : 'File';
|
||||
} else {
|
||||
title = `PairDrop - ${files.length} ${descriptor}s Received`
|
||||
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
|
||||
if(files.length>2) description += "s";
|
||||
descriptor = imagesOnly ? 'Images' : 'Files';
|
||||
}
|
||||
this.$receiveTitle.innerText = `${descriptor} Received`;
|
||||
|
||||
if(!shareInsteadOfDownload) {
|
||||
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
||||
if (canShare) {
|
||||
this.$shareBtn.removeAttribute('hidden');
|
||||
this.$shareBtn.onclick = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let downloadZipped = false;
|
||||
if (files.length > 1) {
|
||||
downloadZipped = true;
|
||||
try {
|
||||
let bytesCompleted = 0;
|
||||
zipper.createNewZipWriter();
|
||||
for (let i=0; i<files.length; i++) {
|
||||
@@ -562,7 +689,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
onprogress: (progress) => {
|
||||
Events.fire('set-progress', {
|
||||
peerId: peerId,
|
||||
progress: (bytesCompleted + progress) / request.totalSize,
|
||||
progress: (bytesCompleted + progress) / totalSize,
|
||||
status: 'process'
|
||||
})
|
||||
}
|
||||
@@ -582,47 +709,58 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
let minutes = now.getMinutes().toString();
|
||||
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
||||
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
downloadZipped = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.$receiveTitleNode.textContent = title;
|
||||
this.$fileDescriptionNode.textContent = description;
|
||||
this.$fileSizeNode.textContent = size;
|
||||
|
||||
if (shareInsteadOfDownload) {
|
||||
this.$shareOrDownloadBtn.innerText = "Share";
|
||||
this.continue = _ => {
|
||||
navigator.share({files: files})
|
||||
.catch(err => console.error(err));
|
||||
this.$downloadBtn.innerText = "Download";
|
||||
this.$downloadBtn.onclick = _ => {
|
||||
if (downloadZipped) {
|
||||
let tmpZipBtn = document.createElement("a");
|
||||
tmpZipBtn.download = filenameDownload;
|
||||
tmpZipBtn.href = url;
|
||||
tmpZipBtn.click();
|
||||
} else {
|
||||
this._downloadFilesIndividually(files);
|
||||
}
|
||||
this.continueCallback = _ => this.continue();
|
||||
} else {
|
||||
this.$shareOrDownloadBtn.innerText = "Download again";
|
||||
this.continue = _ => {
|
||||
let tmpBtn = document.createElement("a");
|
||||
tmpBtn.download = filenameDownload;
|
||||
tmpBtn.href = url;
|
||||
tmpBtn.click();
|
||||
};
|
||||
this.continueCallback = _ => {
|
||||
this.continue();
|
||||
this.hide();
|
||||
};
|
||||
}
|
||||
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
|
||||
|
||||
if (!canShare) {
|
||||
this.$downloadBtn.innerText = "Download again";
|
||||
}
|
||||
Events.fire('notify-user', `${descriptor} downloaded successfully`);
|
||||
this.$downloadBtn.style.pointerEvents = "none";
|
||||
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
|
||||
};
|
||||
|
||||
this.createPreviewElement(files[0]).finally(_ => {
|
||||
document.title = `PairDrop - ${files.length} Files received`;
|
||||
document.title = files.length === 1
|
||||
? 'File received - PairDrop'
|
||||
: `${files.length} Files received - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
|
||||
this.continue();
|
||||
this.show();
|
||||
|
||||
if (canShare) {
|
||||
this.$shareBtn.click();
|
||||
} else {
|
||||
this.$downloadBtn.click();
|
||||
}
|
||||
}).catch(r => console.error(r));
|
||||
}
|
||||
|
||||
_downloadFilesIndividually(files) {
|
||||
let tmpBtn = document.createElement("a");
|
||||
for (let i=0; i<files.length; i++) {
|
||||
tmpBtn.download = files[i].name;
|
||||
tmpBtn.href = URL.createObjectURL(files[i]);
|
||||
tmpBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$shareOrDownloadBtn.removeAttribute('href');
|
||||
this.$shareOrDownloadBtn.removeAttribute('download');
|
||||
this.$shareBtn.setAttribute('hidden', '');
|
||||
this.$previewBox.innerHTML = '';
|
||||
super.hide();
|
||||
this._dequeueFile();
|
||||
@@ -632,15 +770,10 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveRequestDialog');
|
||||
super('receive-request-dialog');
|
||||
|
||||
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName');
|
||||
this.$fileStemNode = this.$el.querySelector('#fileStem');
|
||||
this.$fileExtensionNode = this.$el.querySelector('#fileExtension');
|
||||
this.$fileOtherNode = this.$el.querySelector('#fileOther');
|
||||
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#declineRequest');
|
||||
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
||||
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
||||
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
||||
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
|
||||
|
||||
@@ -670,32 +803,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
_showRequestDialog(request, peerId) {
|
||||
this.correspondingPeerId = peerId;
|
||||
|
||||
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
const fileName = request.header[0].name;
|
||||
const fileNameSplit = fileName.split('.');
|
||||
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
||||
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
||||
this.$fileExtensionNode.innerText = fileExtension
|
||||
|
||||
if (request.header.length >= 2) {
|
||||
let fileOtherText = ` and ${request.header.length - 1} other `;
|
||||
fileOtherText += request.imagesOnly ? 'image' : 'file';
|
||||
if (request.header.length > 2) fileOtherText += "s";
|
||||
this.$fileOtherNode.innerText = fileOtherText;
|
||||
}
|
||||
|
||||
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
|
||||
const displayName = $(peerId).ui._displayName();
|
||||
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
|
||||
|
||||
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
|
||||
let element = document.createElement('img');
|
||||
element.src = request.thumbnailDataUrl;
|
||||
element.classList.add('element-preview');
|
||||
|
||||
this.$previewBox.appendChild(element)
|
||||
}
|
||||
|
||||
document.title = 'PairDrop - File Transfer Requested';
|
||||
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
|
||||
|
||||
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
@@ -721,16 +840,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pairDeviceDialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
|
||||
super('pair-device-dialog');
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
this.$roomKey = this.$el.querySelector('#roomKey');
|
||||
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
|
||||
this.$roomKey = this.$el.querySelector('#room-key');
|
||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||
this.$pairDeviceBtn = $('pair-device');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
||||
this.$createJoinForm = this.$el.querySelector('form');
|
||||
|
||||
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
|
||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||
@@ -800,7 +921,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
evaluateRoomKeyChars() {
|
||||
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
|
||||
if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) {
|
||||
this.$submitBtn.setAttribute("disabled", "");
|
||||
} else {
|
||||
this.inputRoomKey = "";
|
||||
@@ -809,7 +930,7 @@ class PairDeviceDialog extends Dialog {
|
||||
})
|
||||
this.$submitBtn.removeAttribute("disabled");
|
||||
if (document.activeElement === this.$inputRoomKeyChars[5]) {
|
||||
this._onSubmit();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -823,6 +944,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
@@ -840,11 +962,11 @@ class PairDeviceDialog extends Dialog {
|
||||
// Display the QR code for the url
|
||||
const qr = new QRCode({
|
||||
content: this._getShareRoomURL(),
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: 150,
|
||||
height: 150,
|
||||
padding: 0,
|
||||
background: "transparent",
|
||||
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
|
||||
color: `rgb(var(--text-color))`,
|
||||
ecl: "L",
|
||||
join: true
|
||||
});
|
||||
@@ -859,7 +981,8 @@ class PairDeviceDialog extends Dialog {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
|
||||
@@ -936,23 +1059,29 @@ class PairDeviceDialog extends Dialog {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
|
||||
}
|
||||
Events.fire('bg-resize');
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clearDevicesDialog');
|
||||
super('clear-devices-dialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
||||
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._clearRoomSecrets();
|
||||
}
|
||||
|
||||
_clearRoomSecrets() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
}
|
||||
@@ -960,12 +1089,13 @@ class ClearDevicesDialog extends Dialog {
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail));
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
super('send-text-dialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
||||
this.$text = this.$el.querySelector('#text-input');
|
||||
this.$peerDisplayName = this.$el.querySelector('.display-name');
|
||||
this.$form = this.$el.querySelector('form');
|
||||
this.$submit = this.$el.querySelector('button[type="submit"]');
|
||||
this.$form.addEventListener('submit', _ => this._send());
|
||||
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$text.addEventListener('input', e => this._onChange(e));
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
}
|
||||
@@ -993,8 +1123,9 @@ class SendTextDialog extends Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
_onRecipient(peerId) {
|
||||
_onRecipient(peerId, deviceName) {
|
||||
this.correspondingPeerId = peerId;
|
||||
this.$peerDisplayName.innerText = deviceName;
|
||||
this.show();
|
||||
|
||||
const range = document.createRange();
|
||||
@@ -1006,6 +1137,11 @@ class SendTextDialog extends Dialog {
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._send();
|
||||
}
|
||||
|
||||
_send() {
|
||||
Events.fire('send-text', {
|
||||
to: this.correspondingPeerId,
|
||||
@@ -1018,7 +1154,7 @@ class SendTextDialog extends Dialog {
|
||||
|
||||
class ReceiveTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('receiveTextDialog');
|
||||
super('receive-text-dialog');
|
||||
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
|
||||
this.$text = this.$el.querySelector('#text');
|
||||
this.$copy = this.$el.querySelector('#copy');
|
||||
@@ -1029,7 +1165,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
|
||||
Events.on("keydown", e => this._onKeyDown(e));
|
||||
|
||||
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName');
|
||||
this.$displayNameNode = this.$el.querySelector('.display-name');
|
||||
this._receiveTextQueue = [];
|
||||
}
|
||||
|
||||
@@ -1047,6 +1183,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
_onText(text, peerId) {
|
||||
window.blop.play();
|
||||
this._receiveTextQueue.push({text: text, peerId: peerId});
|
||||
this._setDocumentTitleMessages();
|
||||
if (this.$el.attributes["show"]) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
@@ -1058,23 +1195,35 @@ class ReceiveTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
_showReceiveTextDialog(text, peerId) {
|
||||
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
|
||||
this.$displayNameNode.innerText = $(peerId).ui._displayName();
|
||||
|
||||
if (isURL(text)) {
|
||||
const $a = document.createElement('a');
|
||||
$a.href = text;
|
||||
$a.target = '_blank';
|
||||
$a.textContent = text;
|
||||
this.$text.innerHTML = '';
|
||||
this.$text.appendChild($a);
|
||||
} else {
|
||||
this.$text.textContent = text;
|
||||
this.$text.innerText = text;
|
||||
this.$text.classList.remove('text-center');
|
||||
|
||||
// Beautify text if text is short
|
||||
if (text.length < 2000) {
|
||||
// replace urls with actual links
|
||||
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
});
|
||||
|
||||
if (!/\s/.test(text)) {
|
||||
this.$text.classList.add('text-center');
|
||||
}
|
||||
}
|
||||
document.title = 'PairDrop - Message Received';
|
||||
|
||||
this._setDocumentTitleMessages();
|
||||
|
||||
document.changeFavicon("images/favicon-96x96-notification.png");
|
||||
this.show();
|
||||
}
|
||||
|
||||
_setDocumentTitleMessages() {
|
||||
document.title = !this._receiveTextQueue.length
|
||||
? 'Message Received - PairDrop'
|
||||
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
|
||||
}
|
||||
|
||||
async _onCopy() {
|
||||
await navigator.clipboard.writeText(this.$text.textContent);
|
||||
Events.fire('notify-user', 'Copied to clipboard');
|
||||
@@ -1090,13 +1239,13 @@ class ReceiveTextDialog extends Dialog {
|
||||
class Base64ZipDialog extends Dialog {
|
||||
|
||||
constructor() {
|
||||
super('base64PasteDialog');
|
||||
super('base64-paste-dialog');
|
||||
const urlParams = new URL(window.location).searchParams;
|
||||
const base64Text = urlParams.get('base64text');
|
||||
const base64Zip = urlParams.get('base64zip');
|
||||
const base64Hash = window.location.hash.substring(1);
|
||||
|
||||
this.$pasteBtn = this.$el.querySelector('#base64PasteBtn');
|
||||
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
||||
|
||||
if (base64Text) {
|
||||
this.show();
|
||||
@@ -1147,7 +1296,7 @@ class Base64ZipDialog extends Dialog {
|
||||
}
|
||||
|
||||
_setPasteBtnToProcessing() {
|
||||
this.$pasteBtn.pointerEvents = "none";
|
||||
this.$pasteBtn.style.pointerEvents = "none";
|
||||
this.$pasteBtn.innerText = "Processing...";
|
||||
}
|
||||
|
||||
@@ -1247,6 +1396,7 @@ class Notifications {
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||
}
|
||||
// Todo: fix Notifications
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
|
||||
Events.on('files-received', e => this._downloadNotification(e.detail.files));
|
||||
}
|
||||
@@ -1291,7 +1441,7 @@ class Notifications {
|
||||
_messageNotification(message, peerId) {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
const peerDisplayName = $(peerId).ui._displayName();
|
||||
if (isURL(message)) {
|
||||
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
|
||||
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
|
||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||
} else {
|
||||
@@ -1322,7 +1472,7 @@ class Notifications {
|
||||
}
|
||||
|
||||
_download(notification) {
|
||||
$('shareOrDownload').click();
|
||||
$('share-or-download').click();
|
||||
notification.close();
|
||||
}
|
||||
|
||||
@@ -1352,7 +1502,7 @@ class NetworkStatusUI {
|
||||
constructor() {
|
||||
Events.on('offline', _ => this._showOfflineMessage());
|
||||
Events.on('online', _ => this._showOnlineMessage());
|
||||
Events.on('ws-connected', _ => this._showOnlineMessage());
|
||||
Events.on('ws-connected', _ => this._onWsConnected());
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
if (!navigator.onLine) this._showOfflineMessage();
|
||||
}
|
||||
@@ -1363,17 +1513,16 @@ class NetworkStatusUI {
|
||||
}
|
||||
|
||||
_showOnlineMessage() {
|
||||
window.animateBackground(true);
|
||||
if (!this.firstConnect) {
|
||||
this.firstConnect = true;
|
||||
return;
|
||||
}
|
||||
Events.fire('notify-user', 'You are back online');
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
window.animateBackground(false);
|
||||
if (!this.firstConnect) this.firstConnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1652,6 +1801,23 @@ class PersistentStorage {
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||
}
|
||||
|
||||
_broadcastMessage(message) {
|
||||
this.bc.postMessage(message);
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast message received:', e.data)
|
||||
Events.fire(e.data.type, e.data.detail);
|
||||
}
|
||||
}
|
||||
|
||||
class PairDrop {
|
||||
constructor() {
|
||||
Events.on('load', _ => {
|
||||
@@ -1671,6 +1837,7 @@ class PairDrop {
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
const webFileHandlersUI = new WebFileHandlersUI();
|
||||
const noSleepUI = new NoSleepUI();
|
||||
const broadCast = new Broadcast();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1711,23 +1878,18 @@ Events.on('load', () => {
|
||||
let x0, y0, w, h, dw, offset;
|
||||
|
||||
function init() {
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
w = document.documentElement.clientWidth;
|
||||
h = document.documentElement.clientHeight;
|
||||
c.width = w;
|
||||
c.height = h;
|
||||
offset = h > 800
|
||||
? 116
|
||||
: h > 380
|
||||
? 100
|
||||
: 65;
|
||||
|
||||
if (w < 420) offset += 20;
|
||||
offset = $$('footer').offsetHeight - 32;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.max(w, h, 1000) / 13;
|
||||
drawCircles();
|
||||
}
|
||||
window.onresize = init;
|
||||
Events.on('bg-resize', _ => init());
|
||||
window.onresize = _ => Events.fire('bg-resize');
|
||||
|
||||
function drawCircle(radius) {
|
||||
ctx.beginPath();
|
||||
@@ -1792,9 +1954,3 @@ Notifications permission has been blocked
|
||||
as the user has dismissed the permission prompt several times.
|
||||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = _ => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.1.0';
|
||||
const cacheVersion = 'v1.4.1';
|
||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -11,25 +11,25 @@
|
||||
|
||||
/* Layout */
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
overscroll-behavior: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: hidden;
|
||||
min-height: 100vh;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
@@ -71,10 +71,7 @@ body {
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
@@ -117,9 +114,9 @@ h3 {
|
||||
}
|
||||
|
||||
.font-subheading {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
line-height: 18px;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
@@ -197,20 +194,160 @@ body>header a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#center {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 146px;
|
||||
max-height: calc(100vh - 56px - var(--footer-height));
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 402px) and (max-width: 425px) {
|
||||
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 164px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 402px) {
|
||||
#center {
|
||||
--footer-height: 184px;
|
||||
}
|
||||
}
|
||||
/* Peers List */
|
||||
|
||||
#x-peers-filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
x-peers {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-grow: 1;
|
||||
align-items: start !important;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 2;
|
||||
transition: color 300ms;
|
||||
transition: --bg-color 0.5s ease;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-x: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
--peers-per-row: 6; /* default if browser does not support :has selector */
|
||||
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
|
||||
width: var(--x-peers-width);
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
x-peers.overflowing {
|
||||
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
|
||||
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
|
||||
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||
|
||||
/* Opera doesn't support this in the shorthand */
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
|
||||
/* peers-per-row if height is too small for 2 rows */
|
||||
@media screen and (min-height: 538px) and (max-height: 683px) and (max-width: 402px),
|
||||
screen and (min-height: 517px) and (max-height: 664px) and (max-width: 426px),
|
||||
screen and (min-height: 501px) and (max-height: 647px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(7)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
/* peers-per-row if height is too small for 3 rows */
|
||||
@media screen and (min-height: 683px) and (max-width: 402px),
|
||||
screen and (min-height: 664px) and (max-width: 426px),
|
||||
screen and (min-height: 647px) and (min-width: 426px) {
|
||||
x-peers:has(> x-peer) {
|
||||
--peers-per-row: 3;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||
--peers-per-row: 4;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||
--peers-per-row: 5;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||
--peers-per-row: 6;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||
--peers-per-row: 7;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||
--peers-per-row: 8;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||
--peers-per-row: 9;
|
||||
}
|
||||
|
||||
x-peers:has(> x-peer:nth-of-type(28)) {
|
||||
--peers-per-row: 10;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Empty Peers List */
|
||||
|
||||
x-no-peers {
|
||||
height: 114px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
@@ -252,25 +389,19 @@ x-no-peers[drop-bg] * {
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 8px;
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
x-peer .name {
|
||||
width: var(--peer-width);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
@@ -278,27 +409,43 @@ input[type="file"] {
|
||||
|
||||
x-peer x-icon {
|
||||
--icon-size: 40px;
|
||||
margin-bottom: 4px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
x-peer .icon-wrapper {
|
||||
width: var(--icon-size);
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip) x-icon {
|
||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-peer.ws-peer x-icon {
|
||||
border: solid 4px var(--ws-peer-color);
|
||||
x-peer x-icon > .highlight-wrapper {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
margin: 7px auto 0;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
x-peer.ws-peer .progress {
|
||||
margin-top: 4px;
|
||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||
background-color: var(--paired-device-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer:not([status]):hover x-icon,
|
||||
@@ -312,6 +459,36 @@ x-peer[status] x-icon {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
|
||||
x-peer.ws-peer {
|
||||
margin-top: -1.5px;
|
||||
}
|
||||
|
||||
x-peer.ws-peer .progress {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
x-peer.ws-peer .icon-wrapper{
|
||||
border: solid 3px var(--ws-peer-color);
|
||||
}
|
||||
|
||||
x-peer.ws-peer .highlight-wrapper {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status,
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
@@ -377,20 +554,20 @@ x-peer[drop] x-icon {
|
||||
/* Footer */
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
z-index: 2;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 80px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
@@ -408,6 +585,39 @@ footer .font-body2 {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#display-name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
max-width: 15em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -6px;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.1rem;
|
||||
border-radius: 1.3rem/30%;
|
||||
border-right: solid 1rem transparent;
|
||||
border-left: solid 1rem transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(var(--text-color), 43%);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
@@ -415,7 +625,8 @@ x-dialog x-background {
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
padding: 35px;
|
||||
padding: 15px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
@@ -425,11 +636,22 @@ x-dialog x-paper {
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -442,12 +664,6 @@ x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 0;
|
||||
margin-bottom: -16px;
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
x-dialog a {
|
||||
color: var(--primary-color);
|
||||
@@ -459,13 +675,13 @@ x-dialog .font-subheading {
|
||||
|
||||
/* PairDevicesDialog */
|
||||
|
||||
#keyInputContainer {
|
||||
#key-input-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input {
|
||||
#key-input-container>input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
@@ -481,15 +697,15 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#keyInputContainer>input + * {
|
||||
#key-input-container>input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#keyInputContainer>input:nth-of-type(4) {
|
||||
margin-left: 18px;
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#roomKey {
|
||||
#room-key {
|
||||
font-size: 50px;
|
||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||
display: inline-block;
|
||||
@@ -497,19 +713,15 @@ x-dialog .font-subheading {
|
||||
margin: 15px -15px;
|
||||
}
|
||||
|
||||
#roomKeyQrCode {
|
||||
padding: inherit;
|
||||
margin: auto;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
#room-key-qr-code {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
#pair-device-dialog hr {
|
||||
margin: 40px -24px;
|
||||
}
|
||||
|
||||
#pairDeviceDialog x-background {
|
||||
#pair-device-dialog x-background {
|
||||
padding: 16px!important;
|
||||
}
|
||||
|
||||
@@ -520,29 +732,24 @@ x-dialog .row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#receiveRequestDialog h2,
|
||||
#receiveFileDialog h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse {
|
||||
margin: 40px -24px auto;
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: solid 1.25px var(--border-color);
|
||||
margin-bottom: -16px;
|
||||
x-paper > div:last-child > .button {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
word-break: break-word;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
@@ -554,52 +761,52 @@ x-dialog .row-reverse {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
#fileName {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#fileStem {
|
||||
max-width: 80%;
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.file-size{
|
||||
margin-bottom: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
#textInput {
|
||||
min-height: 120px;
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
|
||||
#receiveTextDialog #text {
|
||||
#receive-text-dialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
white-space: pre-wrap;
|
||||
margin-top:36px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a {
|
||||
#receive-text-dialog #text a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a:hover {
|
||||
#receive-text-dialog #text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#receiveTextDialog h3 {
|
||||
#receive-text-dialog h3 {
|
||||
/* Select the received text when double-clicking the dialog */
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
@@ -607,28 +814,25 @@ x-dialog .row-reverse {
|
||||
|
||||
.row-separator {
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
margin: auto -25px;
|
||||
margin: auto -24px;
|
||||
}
|
||||
|
||||
#receiveTextDescriptionContainer {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#base64PasteBtn {
|
||||
#base64-paste-btn {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
}
|
||||
|
||||
#base64PasteDialog button {
|
||||
#base64-paste-dialog button {
|
||||
margin: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64PasteDialog button[close] {
|
||||
#base64-paste-dialog button[close] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#base64PasteDialog button[close]:before {
|
||||
|
||||
#base64-paste-dialog button[close]:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -638,7 +842,6 @@ x-dialog .row-reverse {
|
||||
padding: 2px 16px 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
@@ -649,6 +852,7 @@ x-dialog .row-reverse {
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@@ -686,16 +890,18 @@ x-dialog .row-reverse {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancelPasteModeBtn {
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: 56px;
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
background-color: var(--primary-color);
|
||||
color: rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.button:focus:before,
|
||||
@@ -711,7 +917,6 @@ button::-moz-focus-inner {
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -721,10 +926,7 @@ button::-moz-focus-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -738,9 +940,8 @@ button::-moz-focus-inner {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 254px);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -799,6 +1000,13 @@ button::-moz-focus-inner {
|
||||
margin: 8px 8px -16px;
|
||||
}
|
||||
|
||||
#about section {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#about header {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* Loading Indicator */
|
||||
|
||||
@@ -806,7 +1014,7 @@ button::-moz-focus-inner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -8px;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
@@ -848,11 +1056,11 @@ button::-moz-focus-inner {
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background-color: rgb(var(--text-color));
|
||||
color: rgb(var(--bg-color));
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
@@ -866,20 +1074,23 @@ x-toast {
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
|
||||
|
||||
/* Instructions */
|
||||
|
||||
x-instructions {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
z-index: -1;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
@@ -896,92 +1107,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
|
||||
|
||||
x-instructions p {
|
||||
display: none;
|
||||
margin: 0 auto auto;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.websocket-fallback {
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
x-peer {
|
||||
transform: scale(0.95);
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#websocket-fallback {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
#websocket-fallback>span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#websocket-fallback > span > span {
|
||||
border-bottom: solid 4px var(--ws-peer-color);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
|
||||
@media (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px),
|
||||
screen and (min-width: 1100px) {
|
||||
@media screen and (min-height: 800px) {
|
||||
#websocket-fallback {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
content: attr(desktop);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 420px) {
|
||||
x-instructions {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
|
||||
|
||||
html {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Color Themes
|
||||
*/
|
||||
|
||||
/* Default colors */
|
||||
body {
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-test: 18,18,18;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
body.dark-theme {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Colored Elements */
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
color: rgb(var(--text-color));
|
||||
background-color: rgb(var(--bg-color));
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
background-color: var(--bg-color);
|
||||
background-color: rgb(var(--bg-color));
|
||||
}
|
||||
|
||||
.textarea {
|
||||
color: var(--text-color) !important;
|
||||
color: rgb(var(--text-color)) !important;
|
||||
background-color: var(--bg-color-secondary) !important;
|
||||
}
|
||||
|
||||
@@ -1007,7 +1218,9 @@ x-dialog x-paper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.element-preview {
|
||||
.file-preview > img,
|
||||
.file-preview > audio,
|
||||
.file-preview > video {
|
||||
max-width: 100%;
|
||||
max-height: 40vh;
|
||||
margin: auto;
|
||||
@@ -1019,16 +1232,16 @@ x-dialog x-paper {
|
||||
|
||||
/* defaults to dark theme */
|
||||
body {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
}
|
||||
|
||||
/* Override dark mode with light mode styles if the user decides to swap */
|
||||
body.light-theme {
|
||||
--text-color: #333;
|
||||
--bg-color: #fafafa;
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
}
|
||||
@@ -1046,6 +1259,15 @@ x-dialog x-paper {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
html {
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
}
|
||||
|
||||
/* webkit scrollbar style*/
|
||||
|
||||
::-webkit-scrollbar{
|
||||
|
||||
16
rtc_config_example.json
Normal file
16
rtc_config_example.json
Normal file
@@ -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