mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-22 07:04:53 +08:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fea15d3ee1 | ||
|
|
028752a809 | ||
|
|
1093f4d246 | ||
|
|
7ddd600b0c | ||
|
|
715356aafb | ||
|
|
490e4db380 | ||
|
|
11a988e550 | ||
|
|
ff8f28660a | ||
|
|
5fc8e85f75 | ||
|
|
5eeaae01fe | ||
|
|
660e523263 | ||
|
|
cdfbc7a2df | ||
|
|
c9dca7e083 | ||
|
|
79af04d95a | ||
|
|
954e9c7c3a | ||
|
|
c0d504f6a8 | ||
|
|
36e152dc7c | ||
|
|
fdf024f378 | ||
|
|
9f2e4c5f8f | ||
|
|
8e219914ec | ||
|
|
d1273ef9cc | ||
|
|
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 | ||
|
|
f9e214a1e5 | ||
|
|
b6238b05ae |
4
.github/workflows/github-image.yml
vendored
4
.github/workflows/github-image.yml
vendored
@@ -16,7 +16,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository | downcase }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-image:
|
build-and-push-image:
|
||||||
@@ -48,4 +48,4 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -6,7 +6,7 @@
|
|||||||
<h1>PairDrop</h1>
|
<h1>PairDrop</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Local file sharing in your browser. Inspired by Apple's Airdrop.
|
Local file sharing in your browser. Inspired by Apple's AirDrop.
|
||||||
<br />
|
<br />
|
||||||
<a href="https://pairdrop.net"><strong>Explore »</strong></a>
|
<a href="https://pairdrop.net"><strong>Explore »</strong></a>
|
||||||
<br />
|
<br />
|
||||||
@@ -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/)
|
* 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)
|
### [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.
|
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
||||||
* Multiple files are downloaded as ZIP file
|
* Multiple files are downloaded as a ZIP file
|
||||||
* On iOS and Android the devices share menu is opened instead of downloading the files
|
* 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
|
* Multiple files are transferred at once with an overall progress indicator
|
||||||
|
|
||||||
### Send Files or Text Directly From Share Menu, Context Menu or CLI
|
### 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)
|
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||||
|
|
||||||
### Other changes
|
### 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)
|
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
|
||||||
* Warn user before PairDrop is closed on file transfer
|
* Warn user before PairDrop is closed on file transfer
|
||||||
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
|
* 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))
|
* Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
|
||||||
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
|
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
|
||||||
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
||||||
|
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -78,6 +80,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
|||||||
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
|
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
|
||||||
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
* [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).
|
Have any questions? Read our [FAQ](/docs/faq.md).
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,11 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R
|
|||||||
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
|
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
|
||||||
|
|
||||||
### Transferring many files with paired devices takes too long
|
### Transferring many files with paired devices takes too long
|
||||||
Naturally, if traffic needs to be routed through the turn server transfer speed decreases.
|
Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases.
|
||||||
As a workaround you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster.
|
|
||||||
|
As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance.
|
||||||
|
|
||||||
|
Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed.
|
||||||
|
|
||||||
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
|
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
|
||||||
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
|
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
|
||||||
|
|||||||
@@ -1,33 +1,36 @@
|
|||||||
# Deployment Notes
|
# Deployment Notes
|
||||||
The easiest way to get PairDrop up and running is by using Docker.
|
The easiest way to get PairDrop up and running is by using Docker.
|
||||||
|
|
||||||
## Deployment with Docker from Docker Hub
|
## Deployment with Docker
|
||||||
|
|
||||||
|
### Docker Image from Docker Hub
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
|
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
|
||||||
```
|
```
|
||||||
|
|
||||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||||
>
|
>
|
||||||
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
|
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||||
|
|
||||||
### Options / Flags
|
#### Options / Flags
|
||||||
Set options by using the following flags in the `docker run` command:
|
Set options by using the following flags in the `docker run` command:
|
||||||
|
|
||||||
#### Port
|
##### Port
|
||||||
```
|
```bash
|
||||||
-p 127.0.0.1:8080:3000
|
-p 127.0.0.1:8080:3000
|
||||||
```
|
```
|
||||||
> Specify the port used by the docker image
|
> Specify the port used by the docker image
|
||||||
> - 3000 -> `-p 127.0.0.1:3000:3000`
|
> - 3000 -> `-p 127.0.0.1:3000:3000`
|
||||||
> - 8080 -> `-p 127.0.0.1:8080:3000`
|
> - 8080 -> `-p 127.0.0.1:8080:3000`
|
||||||
#### Rate limiting requests
|
##### Rate limiting requests
|
||||||
```
|
```
|
||||||
-e RATE_LIMIT=true
|
-e RATE_LIMIT=true
|
||||||
```
|
```
|
||||||
> Limits clients to 100 requests per 5 min
|
> Limits clients to 1000 requests per 5 min
|
||||||
|
|
||||||
#### Websocket Fallback (for VPN)
|
##### Websocket Fallback (for VPN)
|
||||||
```
|
```bash
|
||||||
-e WS_FALLBACK=true
|
-e WS_FALLBACK=true
|
||||||
```
|
```
|
||||||
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||||
@@ -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.
|
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
|
||||||
> Additionally, beware that all traffic using this fallback debits the servers data plan.
|
> Additionally, beware that all traffic using this fallback debits the servers data plan.
|
||||||
|
|
||||||
|
##### Specify STUN/TURN Servers
|
||||||
|
```bash
|
||||||
|
-e RTC_CONFIG="rtc_config.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
|
||||||
|
> You can use `pairdrop/rtc_config_example.json` as a starting point.
|
||||||
|
>
|
||||||
|
> Default configuration:
|
||||||
|
> ```json
|
||||||
|
> {
|
||||||
|
> "sdpSemantics": "unified-plan",
|
||||||
|
> "iceServers": [
|
||||||
|
> {
|
||||||
|
> "urls": "stun:stun.l.google.com:19302"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "stun:openrelay.metered.ca:80"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "turn:openrelay.metered.ca:443",
|
||||||
|
> "username": "openrelayproject",
|
||||||
|
> "credential": "openrelayproject"
|
||||||
|
> }
|
||||||
|
> ]
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Deployment with Docker with self-built image
|
### Docker Image from GHCR
|
||||||
### Build the image
|
```bash
|
||||||
|
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
|
||||||
|
```
|
||||||
|
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||||
|
>
|
||||||
|
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||||
|
>
|
||||||
|
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||||
|
|
||||||
|
### Docker Image self-built
|
||||||
|
#### Build the image
|
||||||
```bash
|
```bash
|
||||||
docker build --pull . -f Dockerfile -t pairdrop
|
docker build --pull . -f Dockerfile -t pairdrop
|
||||||
```
|
```
|
||||||
@@ -50,15 +91,45 @@ docker build --pull . -f Dockerfile -t pairdrop
|
|||||||
>
|
>
|
||||||
> `--pull` ensures always the latest node image is used.
|
> `--pull` ensures always the latest node image is used.
|
||||||
|
|
||||||
### Run the image
|
#### Run the image
|
||||||
```bash
|
```bash
|
||||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
|
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
|
||||||
```
|
```
|
||||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||||
>
|
>
|
||||||
> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command.
|
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||||
>
|
>
|
||||||
> To specify options replace `npm run start:prod` according to [the documentation above.](#options--flags)
|
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Deployment with Docker Compose
|
||||||
|
Here's an example docker-compose file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
pairdrop:
|
||||||
|
image: lscr.io/linuxserver/pairdrop:latest
|
||||||
|
container_name: pairdrop
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PUID=1000 # UID to run the application as
|
||||||
|
- PGID=1000 # GID to run the application as
|
||||||
|
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||||
|
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
|
||||||
|
- TZ=Etc/UTC # Time Zone
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:3000:3000 # Web UI
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the compose file with `docker compose up -d`.
|
||||||
|
|
||||||
|
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||||
|
>
|
||||||
|
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
## Deployment with node
|
## Deployment with node
|
||||||
|
|
||||||
@@ -100,6 +171,38 @@ $env:PORT=3010; npm start
|
|||||||
```
|
```
|
||||||
> Specify the port PairDrop is running on. (Default: 3000)
|
> Specify the port PairDrop is running on. (Default: 3000)
|
||||||
|
|
||||||
|
#### Specify STUN/TURN Server
|
||||||
|
On Unix based systems
|
||||||
|
```bash
|
||||||
|
RTC_CONFIG="rtc_config.json" npm start
|
||||||
|
```
|
||||||
|
On Windows
|
||||||
|
```bash
|
||||||
|
$env:RTC_CONFIG="rtc_config.json"; npm start
|
||||||
|
```
|
||||||
|
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
|
||||||
|
> You can use `pairdrop/rtc_config_example.json` as a starting point.
|
||||||
|
>
|
||||||
|
> Default configuration:
|
||||||
|
> ```json
|
||||||
|
> {
|
||||||
|
> "sdpSemantics": "unified-plan",
|
||||||
|
> "iceServers": [
|
||||||
|
> {
|
||||||
|
> "urls": "stun:stun.l.google.com:19302"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "stun:openrelay.metered.ca:80"
|
||||||
|
> },
|
||||||
|
> {
|
||||||
|
> "urls": "turn:openrelay.metered.ca:443",
|
||||||
|
> "username": "openrelayproject",
|
||||||
|
> "credential": "openrelayproject"
|
||||||
|
> }
|
||||||
|
> ]
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
### Options / Flags
|
### Options / Flags
|
||||||
#### Local Run
|
#### Local Run
|
||||||
```bash
|
```bash
|
||||||
@@ -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)).
|
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||||
>
|
>
|
||||||
> Use this when deploying PairDrop with node to prevent bypassing the proxy and reach the docker container directly.
|
> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly.
|
||||||
|
|
||||||
#### Automatic restart on error
|
#### Automatic restart on error
|
||||||
```bash
|
```bash
|
||||||
@@ -123,7 +226,7 @@ npm start -- --auto-restart
|
|||||||
```bash
|
```bash
|
||||||
npm start -- --rate-limit
|
npm start -- --rate-limit
|
||||||
```
|
```
|
||||||
> Limits clients to 100 requests per 5 min
|
> Limits clients to 1000 requests per 5 min
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@@ -221,13 +324,13 @@ server {
|
|||||||
|
|
||||||
### Using Apache
|
### Using Apache
|
||||||
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
|
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
|
||||||
```shell
|
```bash
|
||||||
a2enmod proxy
|
a2enmod proxy
|
||||||
```
|
```
|
||||||
```shell
|
```bash
|
||||||
a2enmod proxy_http
|
a2enmod proxy_http
|
||||||
```
|
```
|
||||||
```shell
|
```bash
|
||||||
a2enmod proxy_wstunnel
|
a2enmod proxy_wstunnel
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -237,7 +340,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
|||||||
|
|
||||||
**pairdrop.conf**
|
**pairdrop.conf**
|
||||||
#### Allow http and https requests
|
#### Allow http and https requests
|
||||||
```
|
```apacheconf
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ProxyPass / http://127.0.0.1:3000/
|
ProxyPass / http://127.0.0.1:3000/
|
||||||
RewriteEngine on
|
RewriteEngine on
|
||||||
@@ -254,7 +357,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
|||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
#### Automatic http to https redirect:
|
#### Automatic http to https redirect:
|
||||||
```
|
```apacheconf
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
Redirect permanent / https://127.0.0.1:3000/
|
Redirect permanent / https://127.0.0.1:3000/
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
@@ -267,10 +370,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
|||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
Activate the new virtual host and reload apache:
|
Activate the new virtual host and reload apache:
|
||||||
```shell
|
```bash
|
||||||
a2ensite pairdrop
|
a2ensite pairdrop
|
||||||
```
|
```
|
||||||
```shell
|
```bash
|
||||||
service apache2 reload
|
service apache2 reload
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -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/)
|
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
Then, clone the repository and run docker-compose:
|
Then, clone the repository and run docker-compose:
|
||||||
```shell
|
```bash
|
||||||
git clone https://github.com/schlagmichdoch/PairDrop.git
|
git clone https://github.com/schlagmichdoch/PairDrop.git
|
||||||
|
|
||||||
cd PairDrop
|
cd PairDrop
|
||||||
@@ -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.
|
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
|
||||||
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
|
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
|
||||||
- On MacOS, double click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
|
- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
|
||||||
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
|
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
|
||||||
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
|
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
|
||||||
|
|
||||||
|
|||||||
112
index.js
112
index.js
@@ -1,6 +1,13 @@
|
|||||||
const process = require('process')
|
const process = require('process')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const {spawn} = require('child_process')
|
const {spawn} = require('child_process')
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const fs = require('fs');
|
||||||
|
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
|
// Handle SIGINT
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
@@ -49,9 +56,24 @@ if (process.argv.includes('--auto-restart')) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const express = require('express');
|
const rtcConfig = process.env.RTC_CONFIG
|
||||||
const RateLimit = require('express-rate-limit');
|
? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
|
||||||
const http = require('http');
|
: {
|
||||||
|
"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();
|
const app = express();
|
||||||
|
|
||||||
@@ -93,13 +115,9 @@ if (process.argv.includes('--localhost-only')) {
|
|||||||
server.listen(port);
|
server.listen(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = require('ua-parser-js');
|
|
||||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
|
||||||
|
|
||||||
class PairDropServer {
|
class PairDropServer {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const WebSocket = require('ws');
|
|
||||||
this._wss = new WebSocket.Server({ server });
|
this._wss = new WebSocket.Server({ server });
|
||||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||||
|
|
||||||
@@ -110,10 +128,14 @@ class PairDropServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onConnection(peer) {
|
_onConnection(peer) {
|
||||||
this._joinRoom(peer);
|
|
||||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||||
peer.socket.onerror = e => console.error(e);
|
peer.socket.onerror = e => console.error(e);
|
||||||
this._keepAlive(peer);
|
this._keepAlive(peer);
|
||||||
|
this._send(peer, {
|
||||||
|
type: 'rtc-config',
|
||||||
|
config: rtcConfig
|
||||||
|
});
|
||||||
|
this._joinRoom(peer);
|
||||||
|
|
||||||
// send displayName
|
// send displayName
|
||||||
this._send(peer, {
|
this._send(peer, {
|
||||||
@@ -121,7 +143,8 @@ class PairDropServer {
|
|||||||
message: {
|
message: {
|
||||||
displayName: peer.name.displayName,
|
displayName: peer.name.displayName,
|
||||||
deviceName: peer.name.deviceName,
|
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 = '') {
|
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||||
|
|
||||||
|
if (this._rooms[room] && this._rooms[room][peer.id]) {
|
||||||
|
this._leaveRoom(peer, roomType, roomSecret);
|
||||||
|
}
|
||||||
|
|
||||||
// if room doesn't exist, create it
|
// if room doesn't exist, create it
|
||||||
if (!this._rooms[room]) {
|
if (!this._rooms[room]) {
|
||||||
this._rooms[room] = {};
|
this._rooms[room] = {};
|
||||||
@@ -526,9 +553,11 @@ class Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setPeerId(request) {
|
_setPeerId(request) {
|
||||||
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id");
|
const searchParams = new URL(request.url, "http://server").searchParams;
|
||||||
if (peer_id && Peer.isValidUuid(peer_id)) {
|
let peerId = searchParams.get("peer_id");
|
||||||
this.id = peer_id;
|
let peerIdHash = searchParams.get("peer_id_hash");
|
||||||
|
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
|
||||||
|
this.id = peerId;
|
||||||
} else {
|
} else {
|
||||||
this.id = crypto.randomUUID();
|
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);
|
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) {
|
addRoomSecret(roomSecret) {
|
||||||
if (!(roomSecret in this.roomSecrets)) {
|
if (!(roomSecret in this.roomSecrets)) {
|
||||||
this.roomSecrets.push(roomSecret);
|
this.roomSecrets.push(roomSecret);
|
||||||
@@ -602,14 +635,55 @@ class Peer {
|
|||||||
|
|
||||||
Object.defineProperty(String.prototype, 'hashCode', {
|
Object.defineProperty(String.prototype, 'hashCode', {
|
||||||
value: function() {
|
value: function() {
|
||||||
var hash = 0, i, chr;
|
return cyrb53(this);
|
||||||
for (i = 0; i < this.length; i++) {
|
|
||||||
chr = this.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + chr;
|
|
||||||
hash |= 0; // Convert to 32bit integer
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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();
|
new PairDropServer();
|
||||||
|
|||||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.1.1",
|
"version": "1.4.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.1.1",
|
"version": "1.4.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"ua-parser-js": "^1.0.33",
|
"ua-parser-js": "^1.0.34",
|
||||||
"unique-names-generator": "^4.3.0",
|
"unique-names-generator": "^4.3.0",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
@@ -583,9 +583,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ua-parser-js": {
|
"node_modules/ua-parser-js": {
|
||||||
"version": "1.0.33",
|
"version": "1.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
||||||
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
|
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1070,9 +1070,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ua-parser-js": {
|
"ua-parser-js": {
|
||||||
"version": "1.0.33",
|
"version": "1.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
||||||
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ=="
|
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
|
||||||
},
|
},
|
||||||
"unique-names-generator": {
|
"unique-names-generator": {
|
||||||
"version": "4.7.1",
|
"version": "4.7.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.1.1",
|
"version": "1.4.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"ua-parser-js": "^1.0.33",
|
"ua-parser-js": "^1.0.34",
|
||||||
"unique-names-generator": "^4.3.0",
|
"unique-names-generator": "^4.3.0",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,7 +106,11 @@ sendFiles()
|
|||||||
zip -q -b /tmp/ -r "$zipPath" "$path"
|
zip -q -b /tmp/ -r "$zipPath" "$path"
|
||||||
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
|
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
|
# remove temporary temp file
|
||||||
rm "$zipPathTemp"
|
rm "$zipPathTemp"
|
||||||
@@ -116,7 +120,11 @@ sendFiles()
|
|||||||
# Create zip file temporarily to send file
|
# Create zip file temporarily to send file
|
||||||
zip -q -b /tmp/ "$zipPath" "$path"
|
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
|
fi
|
||||||
|
|
||||||
# remove temporary temp file
|
# remove temporary temp file
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<use xlink:href="#homescreen" />
|
<use xlink:href="#homescreen" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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">
|
<svg class="icon">
|
||||||
<use xlink:href="#pair-device-icon" />
|
<use xlink:href="#pair-device-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -69,173 +69,194 @@
|
|||||||
<use xlink:href="#clear-pair-devices-icon" />
|
<use xlink:href="#clear-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||||
</header>
|
</header>
|
||||||
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
|
<!-- Center -->
|
||||||
<!-- Peers -->
|
<div id="center">
|
||||||
<x-peers class="center"></x-peers>
|
<!-- Peers -->
|
||||||
<x-no-peers>
|
<div class="x-peers-filler"></div>
|
||||||
<h2>Open PairDrop on other devices to send files</h2>
|
<x-peers class="center"></x-peers>
|
||||||
<div>Pair devices to be discoverable on other networks</div>
|
<x-no-peers>
|
||||||
</x-no-peers>
|
<h2>Open PairDrop on other devices to send files</h2>
|
||||||
<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>Pair devices to be discoverable on other networks</div>
|
||||||
<p id="pasteFilename"></p>
|
</x-no-peers>
|
||||||
</x-instructions>
|
<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 -->
|
||||||
<footer class="column">
|
<footer class="column">
|
||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
<div id="displayName" placeholder=" "></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">
|
<div class="font-body2">
|
||||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
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>
|
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<!-- Pair Device Dialog -->
|
<!-- Pair Device Dialog -->
|
||||||
<x-dialog id="pairDeviceDialog">
|
<x-dialog id="pair-device-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Pair Devices</h2>
|
<h2 class="center">Pair Devices</h2>
|
||||||
<div class="center" id="roomKeyQrCode"></div>
|
<div id="room-key-qr-code" class="center"></div>
|
||||||
<h1 class="center" id="roomKey">000 000</h1>
|
<h1 id="room-key" class="center">000 000</h1>
|
||||||
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div id="keyInputContainer">
|
<div id="key-input-container">
|
||||||
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<button class="button" type="submit" disabled>Pair</button>
|
||||||
<div class="separator"></div>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
<a class="button" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</form>
|
</form>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Clear Devices Dialog -->
|
<!-- Clear Devices Dialog -->
|
||||||
<x-dialog id="clearDevicesDialog">
|
<x-dialog id="clear-devices-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Unpair Devices</h2>
|
<h2 class="center">Unpair Devices</h2>
|
||||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit">Unpair Devices</button>
|
<button class="button" type="submit">Unpair Devices</button>
|
||||||
<a class="button" close>Cancel</a>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</form>
|
</form>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Receive Request Dialog -->
|
<!-- Receive Request Dialog -->
|
||||||
<x-dialog id="receiveRequestDialog">
|
<x-dialog id="receive-request-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">PairDrop</h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description">
|
<div class="center column file-description">
|
||||||
<div>
|
<div>
|
||||||
<span id="requestingPeerDisplayName"></span>
|
<span class="display-name"></span>
|
||||||
<span>would like to share</span>
|
<span>would like to share</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" id="fileName">
|
<div class="row file-name" >
|
||||||
<span id="fileStem"></span>
|
<span class="file-stem"></span>
|
||||||
<span id="fileExtension"></span>
|
<span class="file-extension"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row file-other">
|
||||||
<span id="fileOther"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-body2 text-center file-size"></div>
|
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
|
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||||
<div class="separator"></div>
|
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||||
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Receive File Dialog -->
|
<!-- Receive File Dialog -->
|
||||||
<x-dialog id="receiveFileDialog">
|
<x-dialog id="receive-file-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center" id="receiveTitle"></h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description"></div>
|
<div class="center column file-description">
|
||||||
<div class="font-body2 text-center file-size"></div>
|
<div>
|
||||||
|
<span class="display-name"></span>
|
||||||
|
<span>has sent</span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-name" >
|
||||||
|
<span class="file-stem"></span>
|
||||||
|
<span class="file-extension"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-other"></div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
|
</div>
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<a class="button" id="shareOrDownload" autofocus></a>
|
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||||
<div class="separator"></div>
|
<button id="download-btn" class="button" autofocus>Download</button>
|
||||||
<button class="button" close>Close</button>
|
<button class="button" close>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Send Text Dialog -->
|
<!-- Send Text Dialog -->
|
||||||
<x-dialog id="sendTextDialog">
|
<x-dialog id="send-text-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2>PairDrop - Send a Message</h2>
|
<h2 class="text-center">Send Message</h2>
|
||||||
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div class="dialog-subheader text-center">
|
||||||
<div class="row-reverse">
|
<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>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<div class="separator"></div>
|
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</form>
|
</form>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Receive Text Dialog -->
|
<!-- Receive Text Dialog -->
|
||||||
<x-dialog id="receiveTextDialog">
|
<x-dialog id="receive-text-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2>PairDrop - Message Received</h2>
|
<h2 class="text-center">Message Received</h2>
|
||||||
<div id="receiveTextDescriptionContainer">
|
<div class="text-center dialog-subheader">
|
||||||
<span id="receiveTextPeerDisplayName"></span>
|
<span class="display-name"></span>
|
||||||
<span>sent the following message:</span>
|
<span>has sent:</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row-separator"></div>
|
||||||
<div id="text"></div>
|
<div id="text"></div>
|
||||||
<div class="row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
|
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||||
<div class="separator"></div>
|
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||||
<button class="button" id="close" title="ESCAPE">Close</button>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- base64PasteDialog Dialog -->
|
<!-- base64 Paste Dialog -->
|
||||||
<x-dialog id="base64PasteDialog">
|
<x-dialog id="base64-paste-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
|
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||||
|
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||||
<button class="button center" close>Close</button>
|
<button class="button center" close>Close</button>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Toast -->
|
<!-- Toast -->
|
||||||
<div class="toast-container full center">
|
<div class="toast-container full center">
|
||||||
<x-toast class="row" shadow="1" id="toast"></x-toast>
|
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||||
</div>
|
</div>
|
||||||
<!-- About Page -->
|
<!-- About Page -->
|
||||||
<x-about id="about" class="full center column">
|
<x-about id="about" class="full center column">
|
||||||
|
<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">
|
<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">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -318,6 +339,10 @@
|
|||||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
<!--! 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"/>
|
<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>
|
||||||
|
<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>
|
</svg>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="scripts/util.js"></script>
|
<script src="scripts/util.js"></script>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ class ServerConnection {
|
|||||||
Events.on('online', _ => this._connect());
|
Events.on('online', _ => this._connect());
|
||||||
}
|
}
|
||||||
|
|
||||||
async _connect() {
|
_connect() {
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
if (this._isConnected() || this._isConnecting()) return;
|
if (this._isConnected() || this._isConnecting()) return;
|
||||||
const ws = new WebSocket(await this._endpoint());
|
const ws = new WebSocket(this._endpoint());
|
||||||
ws.binaryType = 'arraybuffer';
|
ws.binaryType = 'arraybuffer';
|
||||||
ws.onopen = _ => this._onOpen();
|
ws.onopen = _ => this._onOpen();
|
||||||
ws.onmessage = e => this._onMessage(e.data);
|
ws.onmessage = e => this._onMessage(e.data);
|
||||||
@@ -36,6 +36,7 @@ class ServerConnection {
|
|||||||
_onOpen() {
|
_onOpen() {
|
||||||
console.log('WS: server connected');
|
console.log('WS: server connected');
|
||||||
Events.fire('ws-connected');
|
Events.fire('ws-connected');
|
||||||
|
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendRoomSecrets(roomSecrets) {
|
_sendRoomSecrets(roomSecrets) {
|
||||||
@@ -52,16 +53,23 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onPairDeviceJoin(roomKey) {
|
_onPairDeviceJoin(roomKey) {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setRtcConfig(config) {
|
||||||
|
window.rtcConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
_onMessage(msg) {
|
_onMessage(msg) {
|
||||||
msg = JSON.parse(msg);
|
msg = JSON.parse(msg);
|
||||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
|
case 'rtc-config':
|
||||||
|
this._setRtcConfig(msg.config);
|
||||||
|
break;
|
||||||
case 'peers':
|
case 'peers':
|
||||||
Events.fire('peers', msg);
|
Events.fire('peers', msg);
|
||||||
break;
|
break;
|
||||||
@@ -110,34 +118,24 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onDisplayName(msg) {
|
_onDisplayName(msg) {
|
||||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||||
PersistentStorage.get('peerId').then(peerId => {
|
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||||
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())
|
|
||||||
Events.fire('display-name', msg);
|
Events.fire('display-name', msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _endpoint() {
|
_endpoint() {
|
||||||
// hack to detect if deployment or development environment
|
// hack to detect if deployment or development environment
|
||||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||||
const peerId = await this._peerId();
|
const peerId = sessionStorage.getItem("peerId");
|
||||||
if (peerId) ws_url.searchParams.append('peer_id', 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();
|
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() {
|
_disconnect() {
|
||||||
this.send({ type: 'disconnect' });
|
this.send({ type: 'disconnect' });
|
||||||
if (this._socket) {
|
if (this._socket) {
|
||||||
@@ -145,15 +143,17 @@ class ServerConnection {
|
|||||||
this._socket.close();
|
this._socket.close();
|
||||||
this._socket = null;
|
this._socket = null;
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisconnect() {
|
_onDisconnect() {
|
||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
Events.fire('notify-user', 'Connecting..');
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onVisibilityChange() {
|
_onVisibilityChange() {
|
||||||
@@ -194,6 +194,10 @@ class Peer {
|
|||||||
this._send(JSON.stringify(message));
|
this._send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||||
|
}
|
||||||
|
|
||||||
async createHeader(file) {
|
async createHeader(file) {
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -319,26 +323,25 @@ class Peer {
|
|||||||
this._onChunkReceived(message);
|
this._onChunkReceived(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message = JSON.parse(message);
|
const messageJSON = JSON.parse(message);
|
||||||
console.log('RTC:', message);
|
switch (messageJSON.type) {
|
||||||
switch (message.type) {
|
|
||||||
case 'request':
|
case 'request':
|
||||||
this._onFilesTransferRequest(message);
|
this._onFilesTransferRequest(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'header':
|
case 'header':
|
||||||
this._onFilesHeader(message);
|
this._onFilesHeader(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition':
|
case 'partition':
|
||||||
this._onReceivedPartitionEnd(message);
|
this._onReceivedPartitionEnd(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition-received':
|
case 'partition-received':
|
||||||
this._sendNextPartition();
|
this._sendNextPartition();
|
||||||
break;
|
break;
|
||||||
case 'progress':
|
case 'progress':
|
||||||
this._onDownloadProgress(message.progress);
|
this._onDownloadProgress(messageJSON.progress);
|
||||||
break;
|
break;
|
||||||
case 'files-transfer-response':
|
case 'files-transfer-response':
|
||||||
this._onFileTransferRequestResponded(message);
|
this._onFileTransferRequestResponded(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'file-transfer-complete':
|
case 'file-transfer-complete':
|
||||||
this._onFileTransferCompleted();
|
this._onFileTransferCompleted();
|
||||||
@@ -347,7 +350,10 @@ class Peer {
|
|||||||
this._onMessageTransferCompleted();
|
this._onMessageTransferCompleted();
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
this._onTextReceived(message);
|
this._onTextReceived(messageJSON);
|
||||||
|
break;
|
||||||
|
case 'display-name-changed':
|
||||||
|
this._onDisplayNameChanged(messageJSON);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,7 +447,7 @@ class Peer {
|
|||||||
if (!this._requestAccepted.header.length) {
|
if (!this._requestAccepted.header.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||||
this._filesReceived = [];
|
this._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
}
|
}
|
||||||
@@ -486,12 +492,19 @@ class Peer {
|
|||||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||||
this.sendJSON({ type: 'message-transfer-complete' });
|
this.sendJSON({ type: 'message-transfer-complete' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDisplayNameChanged(message) {
|
||||||
|
if (!message.displayName || this._displayName === message.displayName) return;
|
||||||
|
this._displayName = message.displayName;
|
||||||
|
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RTCPeer extends Peer {
|
class RTCPeer extends Peer {
|
||||||
|
|
||||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||||
super(serverConnection, peerId, roomType, roomSecret);
|
super(serverConnection, peerId, roomType, roomSecret);
|
||||||
|
this.rtcSupported = true;
|
||||||
if (!peerId) return; // we will listen for a caller
|
if (!peerId) return; // we will listen for a caller
|
||||||
this._connect(peerId, true);
|
this._connect(peerId, true);
|
||||||
}
|
}
|
||||||
@@ -509,8 +522,9 @@ class RTCPeer extends Peer {
|
|||||||
_openConnection(peerId, isCaller) {
|
_openConnection(peerId, isCaller) {
|
||||||
this._isCaller = isCaller;
|
this._isCaller = isCaller;
|
||||||
this._peerId = peerId;
|
this._peerId = peerId;
|
||||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||||
|
this._conn.onicecandidateerror = e => this._onError(e);
|
||||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||||
}
|
}
|
||||||
@@ -558,14 +572,21 @@ class RTCPeer extends Peer {
|
|||||||
|
|
||||||
_onChannelOpened(event) {
|
_onChannelOpened(event) {
|
||||||
console.log('RTC: channel opened with', this._peerId);
|
console.log('RTC: channel opened with', this._peerId);
|
||||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
|
||||||
const channel = event.channel || event.target;
|
const channel = event.channel || event.target;
|
||||||
channel.binaryType = 'arraybuffer';
|
channel.binaryType = 'arraybuffer';
|
||||||
channel.onmessage = e => this._onMessage(e.data);
|
channel.onmessage = e => this._onMessage(e.data);
|
||||||
channel.onclose = _ => this._onChannelClosed();
|
channel.onclose = _ => this._onChannelClosed();
|
||||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
|
||||||
Events.on('pagehide', _ => this._closeChannel());
|
|
||||||
this._channel = channel;
|
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() {
|
getConnectionHash() {
|
||||||
@@ -601,10 +622,16 @@ class RTCPeer extends Peer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_closeChannel() {
|
_onPageHide() {
|
||||||
if (this._channel) this._channel.onclose = null;
|
this._disconnect();
|
||||||
if (this._conn) this._conn.close();
|
}
|
||||||
this._conn = null;
|
|
||||||
|
_disconnect() {
|
||||||
|
if (this._conn && this._channel) {
|
||||||
|
this._channel.onclose = null;
|
||||||
|
this._channel.close();
|
||||||
|
}
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChannelClosed() {
|
_onChannelClosed() {
|
||||||
@@ -618,9 +645,11 @@ class RTCPeer extends Peer {
|
|||||||
console.log('RTC: state changed:', this._conn.connectionState);
|
console.log('RTC: state changed:', this._conn.connectionState);
|
||||||
switch (this._conn.connectionState) {
|
switch (this._conn.connectionState) {
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection disconnected');
|
this._onError('rtc connection disconnected');
|
||||||
break;
|
break;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection failed');
|
this._onError('rtc connection failed');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -666,6 +695,11 @@ class RTCPeer extends Peer {
|
|||||||
_isConnecting() {
|
_isConnecting() {
|
||||||
return this._channel && this._channel.readyState === 'connecting';
|
return this._channel && this._channel.readyState === 'connecting';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
if (!this._isConnected()) return;
|
||||||
|
super.sendDisplayName(displayName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PeersManager {
|
class PeersManager {
|
||||||
@@ -679,8 +713,12 @@ class PeersManager {
|
|||||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||||
Events.on('send-text', e => this._onSendText(e.detail));
|
Events.on('send-text', e => this._onSendText(e.detail));
|
||||||
Events.on('peer-left', e => this._onPeerLeft(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('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||||
|
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||||
|
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||||
|
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessage(message) {
|
_onMessage(message) {
|
||||||
@@ -704,10 +742,6 @@ class PeersManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTo(peerId, message) {
|
|
||||||
this.peers[peerId].send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRespondToFileTransferRequest(detail) {
|
_onRespondToFileTransferRequest(detail) {
|
||||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||||
}
|
}
|
||||||
@@ -739,6 +773,10 @@ class PeersManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPeerConnected(peerId) {
|
||||||
|
this._notifyPeerDisplayNameChanged(peerId);
|
||||||
|
}
|
||||||
|
|
||||||
_onPeerDisconnected(peerId) {
|
_onPeerDisconnected(peerId) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
delete this.peers[peerId];
|
delete this.peers[peerId];
|
||||||
@@ -756,6 +794,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].sendDisplayName(this._displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisplayName(displayName) {
|
||||||
|
this._originalDisplayName = displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileChunker {
|
class FileChunker {
|
||||||
@@ -844,28 +899,11 @@ class Events {
|
|||||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
static on(type, callback) {
|
static on(type, callback, options = false) {
|
||||||
return window.addEventListener(type, callback, false);
|
return window.addEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
static off(type, callback) {
|
static off(type, callback, options = false) {
|
||||||
return window.removeEventListener(type, callback, false);
|
return window.removeEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.1.1';
|
const cacheVersion = 'v1.4.5';
|
||||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
@@ -10,28 +10,30 @@
|
|||||||
|
|
||||||
/* Layout */
|
/* Layout */
|
||||||
|
|
||||||
html {
|
|
||||||
min-height: 100%;
|
|
||||||
height: -webkit-fill-available;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior: none;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
min-height: -webkit-fill-available;
|
/* mobile viewport bug fix */
|
||||||
flex-grow: 1;
|
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||||
align-items: center;
|
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||||
justify-content: center;
|
min-height: fill-available;
|
||||||
overflow-y: hidden;
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||||
|
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||||
|
min-height: fill-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-reverse {
|
.row-reverse {
|
||||||
@@ -73,10 +75,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 56px;
|
height: 56px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -119,9 +118,9 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.font-subheading {
|
.font-subheading {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 18px;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,20 +198,151 @@ body>header a {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#center {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
flex-grow: 1;
|
||||||
|
--footer-height: 132px;
|
||||||
|
max-height: calc(100vh - 56px - var(--footer-height));
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 425px) {
|
||||||
|
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||||
|
--footer-height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Peers List */
|
/* Peers List */
|
||||||
|
|
||||||
|
#x-peers-filler {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
x-peers {
|
x-peers {
|
||||||
width: 100%;
|
position: relative;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: start !important;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transition: color 300ms;
|
transition: --bg-color 0.5s ease;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
--peers-per-row: 6; /* default if browser does not support :has selector */
|
||||||
|
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
|
||||||
|
width: var(--x-peers-width);
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers.overflowing {
|
||||||
|
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
|
||||||
|
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
|
||||||
|
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
|
||||||
|
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||||
|
|
||||||
|
/* Opera doesn't support this in the shorthand */
|
||||||
|
background-attachment: local, local, scroll, scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer) {
|
||||||
|
--peers-per-row: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px),
|
||||||
|
screen and (min-height: 486px) and (max-height: 631px) and (min-width: 426px) {
|
||||||
|
x-peers:has(> x-peer) {
|
||||||
|
--peers-per-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(7)) {
|
||||||
|
--peers-per-row: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||||
|
--peers-per-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||||
|
--peers-per-row: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||||
|
--peers-per-row: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||||
|
--peers-per-row: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||||
|
--peers-per-row: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||||
|
--peers-per-row: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-height: 649px) and (max-width: 425px),
|
||||||
|
screen and (min-height: 631px) and (min-width: 426px) {
|
||||||
|
x-peers:has(> x-peer) {
|
||||||
|
--peers-per-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||||
|
--peers-per-row: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||||
|
--peers-per-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||||
|
--peers-per-row: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||||
|
--peers-per-row: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||||
|
--peers-per-row: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||||
|
--peers-per-row: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(28)) {
|
||||||
|
--peers-per-row: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty Peers List */
|
/* Empty Peers List */
|
||||||
|
|
||||||
x-no-peers {
|
x-no-peers {
|
||||||
height: 114px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* prevent flickering on load */
|
/* prevent flickering on load */
|
||||||
@@ -254,25 +384,19 @@ x-no-peers[drop-bg] * {
|
|||||||
x-peer {
|
x-peer {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
padding: 8px;
|
||||||
|
align-content: start;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer label {
|
x-peer label {
|
||||||
width: var(--peer-width);
|
width: var(--peer-width);
|
||||||
padding: 8px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer .name {
|
|
||||||
width: var(--peer-width);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -280,21 +404,45 @@ input[type="file"] {
|
|||||||
|
|
||||||
x-peer x-icon {
|
x-peer x-icon {
|
||||||
--icon-size: 40px;
|
--icon-size: 40px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: transform 150ms;
|
||||||
|
will-change: transform;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer .icon-wrapper {
|
||||||
width: var(--icon-size);
|
width: var(--icon-size);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
|
||||||
transition: transform 150ms;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer:not(.type-ip) x-icon {
|
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||||
background: var(--paired-device-color);
|
background: var(--paired-device-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x-peer x-icon > .highlight-wrapper {
|
||||||
|
align-self: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 7px auto 0;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||||
|
background-color: var(--paired-device-color);
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
x-peer:not([status]):hover x-icon,
|
x-peer:not([status]):hover x-icon,
|
||||||
x-peer:not([status]):focus x-icon {
|
x-peer:not([status]):focus x-icon {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
@@ -306,6 +454,19 @@ x-peer[status] x-icon {
|
|||||||
transform: scale(1);
|
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,
|
.status,
|
||||||
.device-name,
|
.device-name,
|
||||||
.connection-hash {
|
.connection-hash {
|
||||||
@@ -371,20 +532,21 @@ x-peer[drop] x-icon {
|
|||||||
/* Footer */
|
/* Footer */
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
position: absolute;
|
position: relative;
|
||||||
bottom: 0;
|
margin-top: auto;
|
||||||
left: 0;
|
z-index: 2;
|
||||||
right: 0;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 0 16px 0;
|
padding: 0 0 16px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: color 300ms;
|
transition: color 300ms;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .logo {
|
footer .logo {
|
||||||
--icon-size: 80px;
|
--icon-size: 80px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
margin-top: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .font-body2 {
|
footer .font-body2 {
|
||||||
@@ -402,6 +564,39 @@ footer .font-body2 {
|
|||||||
padding-bottom: 1px;
|
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 */
|
/* Dialog */
|
||||||
|
|
||||||
x-dialog x-background {
|
x-dialog x-background {
|
||||||
@@ -409,7 +604,7 @@ x-dialog x-background {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
padding: 35px;
|
padding: 15px;
|
||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,13 +615,20 @@ x-dialog x-paper {
|
|||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: transform 300ms;
|
transition: transform 300ms;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pair-device-dialog x-paper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: max(50%, 350px);
|
top: max(50%, 350px);
|
||||||
height: 650px;
|
margin-top: -328.5px;
|
||||||
margin-top: -325px;
|
width: calc(100vw - 20px);
|
||||||
|
height: 625px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog:not([show]) {
|
x-dialog:not([show]) {
|
||||||
@@ -441,12 +643,6 @@ x-dialog:not([show]) x-background {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog .row-reverse>.button {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: -16px;
|
|
||||||
width: 50%;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog a {
|
x-dialog a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
@@ -458,13 +654,13 @@ x-dialog .font-subheading {
|
|||||||
|
|
||||||
/* PairDevicesDialog */
|
/* PairDevicesDialog */
|
||||||
|
|
||||||
#keyInputContainer {
|
#key-input-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyInputContainer>input {
|
#key-input-container>input {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
@@ -480,15 +676,15 @@ x-dialog .font-subheading {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyInputContainer>input + * {
|
#key-input-container>input + * {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyInputContainer>input:nth-of-type(4) {
|
#key-input-container>input:nth-of-type(4) {
|
||||||
margin-left: 18px;
|
margin-left: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#roomKey {
|
#room-key {
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -496,19 +692,15 @@ x-dialog .font-subheading {
|
|||||||
margin: 15px -15px;
|
margin: 15px -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#roomKeyQrCode {
|
#room-key-qr-code {
|
||||||
padding: inherit;
|
margin: 16px;
|
||||||
margin: auto;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pairDeviceDialog hr {
|
#pair-device-dialog hr {
|
||||||
margin-top: 40px;
|
margin: 40px -24px;
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pairDeviceDialog x-background {
|
#pair-device-dialog x-background {
|
||||||
padding: 16px!important;
|
padding: 16px!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,29 +711,24 @@ x-dialog .row {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog h2 {
|
/* button row*/
|
||||||
margin-top: 1rem;
|
x-paper > div:last-child {
|
||||||
}
|
margin: auto -24px -15px;
|
||||||
|
|
||||||
#receiveRequestDialog h2,
|
|
||||||
#receiveFileDialog h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog .row-reverse {
|
|
||||||
margin: 40px -24px auto;
|
|
||||||
border-top: solid 2.5px var(--border-color);
|
border-top: solid 2.5px var(--border-color);
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
x-paper > div:last-child > .button {
|
||||||
border: solid 1.25px var(--border-color);
|
height: 100%;
|
||||||
margin-bottom: -16px;
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > div:last-child > .button:not(:last-child) {
|
||||||
|
border-left: solid 2.5px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description {
|
.file-description {
|
||||||
word-break: break-word;
|
margin-bottom: 25px;
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description .row {
|
.file-description .row {
|
||||||
@@ -553,52 +740,52 @@ x-dialog .row-reverse {
|
|||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileName {
|
.file-name {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileStem {
|
.file-stem {
|
||||||
max-width: 80%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
word-break: break-all;
|
white-space: nowrap;
|
||||||
max-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-size{
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send Text Dialog */
|
/* Send Text Dialog */
|
||||||
|
|
||||||
#textInput {
|
x-dialog .dialog-subheader {
|
||||||
min-height: 120px;
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-input {
|
||||||
|
min-height: 200px;
|
||||||
|
margin: 14px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receive Text Dialog */
|
/* Receive Text Dialog */
|
||||||
|
|
||||||
#receiveTextDialog #text {
|
#receive-text-dialog #text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 393px);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-user-select: all;
|
-webkit-user-select: all;
|
||||||
-moz-user-select: all;
|
-moz-user-select: all;
|
||||||
user-select: all;
|
user-select: all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-top:36px;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDialog #text a {
|
#receive-text-dialog #text a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDialog #text a:hover {
|
#receive-text-dialog #text a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDialog h3 {
|
#receive-text-dialog h3 {
|
||||||
/* Select the received text when double-clicking the dialog */
|
/* Select the received text when double-clicking the dialog */
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -606,29 +793,44 @@ x-dialog .row-reverse {
|
|||||||
|
|
||||||
.row-separator {
|
.row-separator {
|
||||||
border-bottom: solid 2.5px var(--border-color);
|
border-bottom: solid 2.5px var(--border-color);
|
||||||
margin: auto -25px;
|
margin: auto -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDescriptionContainer {
|
#base64-paste-btn,
|
||||||
margin-bottom: 25px;
|
#base64-paste-dialog .textarea {
|
||||||
}
|
|
||||||
|
|
||||||
#base64PasteBtn {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
border: solid 12px #438cff;
|
border: solid 12px #438cff;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64PasteDialog button {
|
#base64-paste-dialog .textarea {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog .textarea::before {
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
content: attr(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog button {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64PasteDialog button[close] {
|
#base64-paste-dialog button[close] {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64PasteDialog button[close]:before {
|
#base64-paste-dialog button[close]:before {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,7 +840,6 @@ x-dialog .row-reverse {
|
|||||||
padding: 2px 16px 0;
|
padding: 2px 16px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
min-width: 100px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -649,6 +850,7 @@ x-dialog .row-reverse {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
@@ -686,16 +888,18 @@ x-dialog .row-reverse {
|
|||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cancelPasteModeBtn {
|
#cancel-paste-mode {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
border-bottom: solid 2.5px var(--border-color);
|
background-color: var(--primary-color);
|
||||||
|
color: rgb(238, 238, 238);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus:before,
|
.button:focus:before,
|
||||||
@@ -711,7 +915,6 @@ button::-moz-focus-inner {
|
|||||||
|
|
||||||
|
|
||||||
/* Icon Button */
|
/* Icon Button */
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -721,10 +924,7 @@ button::-moz-focus-inner {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Text Input */
|
/* Text Input */
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -738,9 +938,8 @@ button::-moz-focus-inner {
|
|||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
resize: none;
|
resize: none;
|
||||||
min-height: 40px;
|
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 254px);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,6 +998,13 @@ button::-moz-focus-inner {
|
|||||||
margin: 8px 8px -16px;
|
margin: 8px 8px -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#about section {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#about header {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading Indicator */
|
/* Loading Indicator */
|
||||||
|
|
||||||
@@ -806,7 +1012,7 @@ button::-moz-focus-inner {
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -8px;
|
||||||
clip: rect(0px, 80px, 80px, 40px);
|
clip: rect(0px, 80px, 80px, 40px);
|
||||||
--progress: rotate(0deg);
|
--progress: rotate(0deg);
|
||||||
transition: transform 200ms;
|
transition: transform 200ms;
|
||||||
@@ -848,11 +1054,11 @@ button::-moz-focus-inner {
|
|||||||
x-toast {
|
x-toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
bottom: 24px;
|
top: 50px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 344px;
|
max-width: 344px;
|
||||||
background-color: #323232;
|
background-color: rgb(var(--text-color));
|
||||||
color: rgba(255, 255, 255, 0.95);
|
color: rgb(var(--bg-color));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
@@ -866,20 +1072,23 @@ x-toast {
|
|||||||
|
|
||||||
x-toast:not([show]):not(:hover) {
|
x-toast:not([show]):not(:hover) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(100px);
|
transform: translateY(-100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Instructions */
|
/* Instructions */
|
||||||
|
|
||||||
x-instructions {
|
x-instructions {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 120px;
|
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
z-index: -1;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 80%;
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||||
@@ -896,88 +1105,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
|
|||||||
|
|
||||||
x-instructions p {
|
x-instructions p {
|
||||||
display: none;
|
display: none;
|
||||||
margin: 0 auto auto;
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peers:empty~x-instructions {
|
x-peers:empty~x-instructions {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
x-peer {
|
||||||
|
transform: scale(0.95);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#websocket-fallback {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#websocket-fallback>span {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#websocket-fallback > span > span {
|
||||||
|
border-bottom: solid 4px var(--ws-peer-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Styles */
|
/* Responsive Styles */
|
||||||
|
@media 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 {
|
footer {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-height: 800px),
|
@media (hover: hover) and (pointer: fine) {
|
||||||
screen and (min-width: 1100px) {
|
|
||||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||||
content: attr(desktop);
|
content: attr(desktop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 420px) {
|
|
||||||
x-instructions {
|
|
||||||
top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer .logo {
|
|
||||||
--icon-size: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
iOS specific styles
|
|
||||||
*/
|
|
||||||
@supports (-webkit-overflow-scrolling: touch) {
|
|
||||||
|
|
||||||
|
|
||||||
html {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
|
||||||
content: attr(mobile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Color Themes
|
Color Themes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Default colors */
|
/* Default colors */
|
||||||
body {
|
body {
|
||||||
--text-color: #333;
|
--text-color: 51,51,51;
|
||||||
--bg-color: #fff;
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
|
--bg-color-test: 18,18,18;
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #f1f3f4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: #e7e8e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme colors */
|
/* Dark theme colors */
|
||||||
body.dark-theme {
|
body.dark-theme {
|
||||||
--text-color: #eee;
|
--text-color: 238,238,238;
|
||||||
--bg-color: #121212;
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: #252525;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colored Elements */
|
/* Colored Elements */
|
||||||
body {
|
body {
|
||||||
color: var(--text-color);
|
color: rgb(var(--text-color));
|
||||||
background-color: var(--bg-color);
|
background-color: rgb(var(--bg-color));
|
||||||
transition: background-color 0.5s ease;
|
transition: background-color 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog x-paper {
|
x-dialog x-paper {
|
||||||
background-color: var(--bg-color);
|
background-color: rgb(var(--bg-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
color: var(--text-color) !important;
|
color: rgb(var(--text-color)) !important;
|
||||||
background-color: var(--bg-color-secondary) !important;
|
background-color: var(--bg-color-secondary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1003,7 +1216,9 @@ x-dialog x-paper {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-preview {
|
.file-preview > img,
|
||||||
|
.file-preview > audio,
|
||||||
|
.file-preview > video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -1015,16 +1230,16 @@ x-dialog x-paper {
|
|||||||
|
|
||||||
/* defaults to dark theme */
|
/* defaults to dark theme */
|
||||||
body {
|
body {
|
||||||
--text-color: #eee;
|
--text-color: 238,238,238;
|
||||||
--bg-color: #121212;
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: #252525;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override dark mode with light mode styles if the user decides to swap */
|
/* Override dark mode with light mode styles if the user decides to swap */
|
||||||
body.light-theme {
|
body.light-theme {
|
||||||
--text-color: #333;
|
--text-color: 51,51,51;
|
||||||
--bg-color: #fafafa;
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #f1f3f4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: #e7e8e8;
|
||||||
}
|
}
|
||||||
@@ -1042,6 +1257,15 @@ x-dialog x-paper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
iOS specific styles
|
||||||
|
*/
|
||||||
|
@supports (-webkit-overflow-scrolling: touch) {
|
||||||
|
html {
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* webkit scrollbar style*/
|
/* webkit scrollbar style*/
|
||||||
|
|
||||||
::-webkit-scrollbar{
|
::-webkit-scrollbar{
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<use xlink:href="#homescreen" />
|
<use xlink:href="#homescreen" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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">
|
<svg class="icon">
|
||||||
<use xlink:href="#pair-device-icon" />
|
<use xlink:href="#pair-device-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -69,176 +69,197 @@
|
|||||||
<use xlink:href="#clear-pair-devices-icon" />
|
<use xlink:href="#clear-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||||
</header>
|
</header>
|
||||||
<a id="cancelPasteModeBtn" class="button" close hidden>Done</a>
|
<!-- Center -->
|
||||||
<!-- Peers -->
|
<div id="center">
|
||||||
<x-peers class="center"></x-peers>
|
<!-- Peers -->
|
||||||
<x-no-peers>
|
<div class="x-peers-filler"></div>
|
||||||
<h2>Open PairDrop on other devices to send files</h2>
|
<x-peers class="center"></x-peers>
|
||||||
<div>Pair devices to be discoverable on other networks</div>
|
<x-no-peers>
|
||||||
<br>
|
<h2>Open PairDrop on other devices to send files</h2>
|
||||||
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
|
<div>Pair devices to be discoverable on other networks</div>
|
||||||
</x-no-peers>
|
</x-no-peers>
|
||||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||||
<div>A <span class="websocket-fallback">websocket fallback</span> is implemented on this instance. Use only if you trust the server!</div>
|
<p id="paste-filename"></p>
|
||||||
<p id="pasteFilename"></p>
|
</x-instructions>
|
||||||
</x-instructions>
|
</div>
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="column">
|
<footer class="column">
|
||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
<div id="displayName" placeholder=" "></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">
|
<div class="font-body2">
|
||||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
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>
|
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="websocket-fallback">
|
||||||
|
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<!-- Pair Device Dialog -->
|
<!-- Pair Device Dialog -->
|
||||||
<x-dialog id="pairDeviceDialog">
|
<x-dialog id="pair-device-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Pair Devices</h2>
|
<h2 class="center">Pair Devices</h2>
|
||||||
<div class="center" id="roomKeyQrCode"></div>
|
<div id="room-key-qr-code" class="center"></div>
|
||||||
<h1 class="center" id="roomKey">000 000</h1>
|
<h1 id="room-key" class="center">000 000</h1>
|
||||||
<div id="pairInstructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div id="keyInputContainer">
|
<div id="key-input-container">
|
||||||
<input type="tel" id="char0" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char1" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char2" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char3" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char4" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
<input type="tel" id="char5" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<button class="button" type="submit" disabled>Pair</button>
|
||||||
<div class="separator"></div>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
<a class="button" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</form>
|
</form>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Clear Devices Dialog -->
|
<!-- Clear Devices Dialog -->
|
||||||
<x-dialog id="clearDevicesDialog">
|
<x-dialog id="clear-devices-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Unpair Devices</h2>
|
<h2 class="center">Unpair Devices</h2>
|
||||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit">Unpair Devices</button>
|
<button class="button" type="submit">Unpair Devices</button>
|
||||||
<a class="button" close>Cancel</a>
|
<button class="button" type="button" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</form>
|
</form>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Receive Request Dialog -->
|
<!-- Receive Request Dialog -->
|
||||||
<x-dialog id="receiveRequestDialog">
|
<x-dialog id="receive-request-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">PairDrop</h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description">
|
<div class="center column file-description">
|
||||||
<div>
|
<div>
|
||||||
<span id="requestingPeerDisplayName"></span>
|
<span class="display-name"></span>
|
||||||
<span>would like to share</span>
|
<span>would like to share</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" id="fileName">
|
<div class="row file-name" >
|
||||||
<span id="fileStem"></span>
|
<span class="file-stem"></span>
|
||||||
<span id="fileExtension"></span>
|
<span class="file-extension"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row file-other">
|
||||||
<span id="fileOther"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-body2 text-center file-size"></div>
|
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<button class="button" id="acceptRequest" title="ENTER" autofocus>Accept</button>
|
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||||
<div class="separator"></div>
|
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||||
<button class="button" id="declineRequest" title="ESCAPE">Decline</button>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Receive File Dialog -->
|
<!-- Receive File Dialog -->
|
||||||
<x-dialog id="receiveFileDialog">
|
<x-dialog id="receive-file-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center" id="receiveTitle"></h2>
|
<h2 class="center"></h2>
|
||||||
<div class="text-center file-description"></div>
|
<div class="center column file-description">
|
||||||
<div class="font-body2 text-center file-size"></div>
|
<div>
|
||||||
|
<span class="display-name"></span>
|
||||||
|
<span>has sent</span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-name" >
|
||||||
|
<span class="file-stem"></span>
|
||||||
|
<span class="file-extension"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-other"></div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
|
</div>
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="row-reverse space-between">
|
<div class="center row-reverse">
|
||||||
<a class="button" id="shareOrDownload" autofocus></a>
|
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||||
<div class="separator"></div>
|
<button id="download-btn" class="button" autofocus>Download</button>
|
||||||
<button class="button" close>Close</button>
|
<button class="button" close>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Send Text Dialog -->
|
<!-- Send Text Dialog -->
|
||||||
<x-dialog id="sendTextDialog">
|
<x-dialog id="send-text-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2>PairDrop - Send a Message</h2>
|
<h2 class="text-center">Send Message</h2>
|
||||||
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div class="dialog-subheader text-center">
|
||||||
<div class="row-reverse">
|
<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>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<div class="separator"></div>
|
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</form>
|
</form>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Receive Text Dialog -->
|
<!-- Receive Text Dialog -->
|
||||||
<x-dialog id="receiveTextDialog">
|
<x-dialog id="receive-text-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2>PairDrop - Message Received</h2>
|
<h2 class="text-center">Message Received</h2>
|
||||||
<div id="receiveTextDescriptionContainer">
|
<div class="text-center dialog-subheader">
|
||||||
<span id="receiveTextPeerDisplayName"></span>
|
<span class="display-name"></span>
|
||||||
<span>sent the following message:</span>
|
<span>has sent:</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row-separator"></div>
|
||||||
<div id="text"></div>
|
<div id="text"></div>
|
||||||
<div class="row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" id="copy" title="CTRL/⌘ + C">Copy</button>
|
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||||
<div class="separator"></div>
|
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||||
<button class="button" id="close" title="ESCAPE">Close</button>
|
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- base64PasteDialog Dialog -->
|
<!-- base64 Paste Dialog -->
|
||||||
<x-dialog id="base64PasteDialog">
|
<x-dialog id="base64-paste-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<button class="button center" id="base64PasteBtn" title="Paste">Tap here to paste files</button>
|
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||||
|
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||||
<button class="button center" close>Close</button>
|
<button class="button center" close>Close</button>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Toast -->
|
<!-- Toast -->
|
||||||
<div class="toast-container full center">
|
<div class="toast-container full center">
|
||||||
<x-toast class="row" shadow="1" id="toast"></x-toast>
|
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||||
</div>
|
</div>
|
||||||
<!-- About Page -->
|
<!-- About Page -->
|
||||||
<x-about id="about" class="full center column">
|
<x-about id="about" class="full center column">
|
||||||
|
<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">
|
<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">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -321,6 +342,10 @@
|
|||||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
<!--! 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"/>
|
<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>
|
||||||
|
<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>
|
</svg>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="scripts/util.js"></script>
|
<script src="scripts/util.js"></script>
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ class ServerConnection {
|
|||||||
Events.on('online', _ => this._connect());
|
Events.on('online', _ => this._connect());
|
||||||
}
|
}
|
||||||
|
|
||||||
async _connect() {
|
_connect() {
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
if (this._isConnected() || this._isConnecting()) return;
|
if (this._isConnected() || this._isConnecting()) return;
|
||||||
const ws = new WebSocket(await this._endpoint());
|
const ws = new WebSocket(this._endpoint());
|
||||||
ws.binaryType = 'arraybuffer';
|
ws.binaryType = 'arraybuffer';
|
||||||
ws.onopen = _ => this._onOpen();
|
ws.onopen = _ => this._onOpen();
|
||||||
ws.onmessage = e => this._onMessage(e.data);
|
ws.onmessage = e => this._onMessage(e.data);
|
||||||
@@ -34,6 +34,7 @@ class ServerConnection {
|
|||||||
_onOpen() {
|
_onOpen() {
|
||||||
console.log('WS: server connected');
|
console.log('WS: server connected');
|
||||||
Events.fire('ws-connected');
|
Events.fire('ws-connected');
|
||||||
|
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendRoomSecrets(roomSecrets) {
|
_sendRoomSecrets(roomSecrets) {
|
||||||
@@ -50,16 +51,23 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onPairDeviceJoin(roomKey) {
|
_onPairDeviceJoin(roomKey) {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setRtcConfig(config) {
|
||||||
|
window.rtcConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
_onMessage(msg) {
|
_onMessage(msg) {
|
||||||
msg = JSON.parse(msg);
|
msg = JSON.parse(msg);
|
||||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
|
case 'rtc-config':
|
||||||
|
this._setRtcConfig(msg.config);
|
||||||
|
break;
|
||||||
case 'peers':
|
case 'peers':
|
||||||
Events.fire('peers', msg);
|
Events.fire('peers', msg);
|
||||||
break;
|
break;
|
||||||
@@ -105,6 +113,7 @@ class ServerConnection {
|
|||||||
case 'file-transfer-complete':
|
case 'file-transfer-complete':
|
||||||
case 'message-transfer-complete':
|
case 'message-transfer-complete':
|
||||||
case 'text':
|
case 'text':
|
||||||
|
case 'display-name-changed':
|
||||||
case 'ws-chunk':
|
case 'ws-chunk':
|
||||||
Events.fire('ws-relay', JSON.stringify(msg));
|
Events.fire('ws-relay', JSON.stringify(msg));
|
||||||
break;
|
break;
|
||||||
@@ -120,34 +129,24 @@ class ServerConnection {
|
|||||||
|
|
||||||
_onDisplayName(msg) {
|
_onDisplayName(msg) {
|
||||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||||
PersistentStorage.get('peerId').then(peerId => {
|
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||||
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())
|
|
||||||
Events.fire('display-name', msg);
|
Events.fire('display-name', msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _endpoint() {
|
_endpoint() {
|
||||||
// hack to detect if deployment or development environment
|
// hack to detect if deployment or development environment
|
||||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||||
const peerId = await this._peerId();
|
const peerId = sessionStorage.getItem("peerId");
|
||||||
if (peerId) ws_url.searchParams.append('peer_id', 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();
|
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() {
|
_disconnect() {
|
||||||
this.send({ type: 'disconnect' });
|
this.send({ type: 'disconnect' });
|
||||||
if (this._socket) {
|
if (this._socket) {
|
||||||
@@ -155,15 +154,17 @@ class ServerConnection {
|
|||||||
this._socket.close();
|
this._socket.close();
|
||||||
this._socket = null;
|
this._socket = null;
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisconnect() {
|
_onDisconnect() {
|
||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
Events.fire('notify-user', 'Connecting..');
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
this._isReconnect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onVisibilityChange() {
|
_onVisibilityChange() {
|
||||||
@@ -204,6 +205,10 @@ class Peer {
|
|||||||
this._send(JSON.stringify(message));
|
this._send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||||
|
}
|
||||||
|
|
||||||
async createHeader(file) {
|
async createHeader(file) {
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -324,31 +329,30 @@ class Peer {
|
|||||||
this.sendJSON({ type: 'progress', progress: progress });
|
this.sendJSON({ type: 'progress', progress: progress });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessage(message, logMessage = true) {
|
_onMessage(message) {
|
||||||
if (typeof message !== 'string') {
|
if (typeof message !== 'string') {
|
||||||
this._onChunkReceived(message);
|
this._onChunkReceived(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message = JSON.parse(message);
|
const messageJSON = JSON.parse(message);
|
||||||
if (logMessage) console.log('RTC:', message);
|
switch (messageJSON.type) {
|
||||||
switch (message.type) {
|
|
||||||
case 'request':
|
case 'request':
|
||||||
this._onFilesTransferRequest(message);
|
this._onFilesTransferRequest(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'header':
|
case 'header':
|
||||||
this._onFilesHeader(message);
|
this._onFilesHeader(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition':
|
case 'partition':
|
||||||
this._onReceivedPartitionEnd(message);
|
this._onReceivedPartitionEnd(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'partition-received':
|
case 'partition-received':
|
||||||
this._sendNextPartition();
|
this._sendNextPartition();
|
||||||
break;
|
break;
|
||||||
case 'progress':
|
case 'progress':
|
||||||
this._onDownloadProgress(message.progress);
|
this._onDownloadProgress(messageJSON.progress);
|
||||||
break;
|
break;
|
||||||
case 'files-transfer-response':
|
case 'files-transfer-response':
|
||||||
this._onFileTransferRequestResponded(message);
|
this._onFileTransferRequestResponded(messageJSON);
|
||||||
break;
|
break;
|
||||||
case 'file-transfer-complete':
|
case 'file-transfer-complete':
|
||||||
this._onFileTransferCompleted();
|
this._onFileTransferCompleted();
|
||||||
@@ -357,7 +361,10 @@ class Peer {
|
|||||||
this._onMessageTransferCompleted();
|
this._onMessageTransferCompleted();
|
||||||
break;
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
this._onTextReceived(message);
|
this._onTextReceived(messageJSON);
|
||||||
|
break;
|
||||||
|
case 'display-name-changed':
|
||||||
|
this._onDisplayNameChanged(messageJSON);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,7 +458,7 @@ class Peer {
|
|||||||
if (!this._requestAccepted.header.length) {
|
if (!this._requestAccepted.header.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||||
this._filesReceived = [];
|
this._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
}
|
}
|
||||||
@@ -496,12 +503,19 @@ class Peer {
|
|||||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||||
this.sendJSON({ type: 'message-transfer-complete' });
|
this.sendJSON({ type: 'message-transfer-complete' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDisplayNameChanged(message) {
|
||||||
|
if (!message.displayName || this._displayName === message.displayName) return;
|
||||||
|
this._displayName = message.displayName;
|
||||||
|
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RTCPeer extends Peer {
|
class RTCPeer extends Peer {
|
||||||
|
|
||||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||||
super(serverConnection, peerId, roomType, roomSecret);
|
super(serverConnection, peerId, roomType, roomSecret);
|
||||||
|
this.rtcSupported = true;
|
||||||
if (!peerId) return; // we will listen for a caller
|
if (!peerId) return; // we will listen for a caller
|
||||||
this._connect(peerId, true);
|
this._connect(peerId, true);
|
||||||
}
|
}
|
||||||
@@ -519,8 +533,9 @@ class RTCPeer extends Peer {
|
|||||||
_openConnection(peerId, isCaller) {
|
_openConnection(peerId, isCaller) {
|
||||||
this._isCaller = isCaller;
|
this._isCaller = isCaller;
|
||||||
this._peerId = peerId;
|
this._peerId = peerId;
|
||||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||||
|
this._conn.onicecandidateerror = e => this._onError(e);
|
||||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||||
}
|
}
|
||||||
@@ -568,14 +583,21 @@ class RTCPeer extends Peer {
|
|||||||
|
|
||||||
_onChannelOpened(event) {
|
_onChannelOpened(event) {
|
||||||
console.log('RTC: channel opened with', this._peerId);
|
console.log('RTC: channel opened with', this._peerId);
|
||||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
|
||||||
const channel = event.channel || event.target;
|
const channel = event.channel || event.target;
|
||||||
channel.binaryType = 'arraybuffer';
|
channel.binaryType = 'arraybuffer';
|
||||||
channel.onmessage = e => this._onMessage(e.data);
|
channel.onmessage = e => this._onMessage(e.data);
|
||||||
channel.onclose = _ => this._onChannelClosed();
|
channel.onclose = _ => this._onChannelClosed();
|
||||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
|
||||||
Events.on('pagehide', _ => this._closeChannel());
|
|
||||||
this._channel = channel;
|
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() {
|
getConnectionHash() {
|
||||||
@@ -611,10 +633,16 @@ class RTCPeer extends Peer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_closeChannel() {
|
_onPageHide() {
|
||||||
if (this._channel) this._channel.onclose = null;
|
this._disconnect();
|
||||||
if (this._conn) this._conn.close();
|
}
|
||||||
this._conn = null;
|
|
||||||
|
_disconnect() {
|
||||||
|
if (this._conn && this._channel) {
|
||||||
|
this._channel.onclose = null;
|
||||||
|
this._channel.close();
|
||||||
|
}
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChannelClosed() {
|
_onChannelClosed() {
|
||||||
@@ -628,9 +656,11 @@ class RTCPeer extends Peer {
|
|||||||
console.log('RTC: state changed:', this._conn.connectionState);
|
console.log('RTC: state changed:', this._conn.connectionState);
|
||||||
switch (this._conn.connectionState) {
|
switch (this._conn.connectionState) {
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection disconnected');
|
this._onError('rtc connection disconnected');
|
||||||
break;
|
break;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
|
Events.fire('peer-disconnected', this._peerId);
|
||||||
this._onError('rtc connection failed');
|
this._onError('rtc connection failed');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -676,13 +706,20 @@ class RTCPeer extends Peer {
|
|||||||
_isConnecting() {
|
_isConnecting() {
|
||||||
return this._channel && this._channel.readyState === 'connecting';
|
return this._channel && this._channel.readyState === 'connecting';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendDisplayName(displayName) {
|
||||||
|
if (!this._isConnected()) return;
|
||||||
|
super.sendDisplayName(displayName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WSPeer extends Peer {
|
class WSPeer extends Peer {
|
||||||
|
|
||||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||||
super(serverConnection, peerId, roomType, roomSecret);
|
super(serverConnection, peerId, roomType, roomSecret);
|
||||||
|
this.rtcSupported = false;
|
||||||
if (!peerId) return; // we will listen for a caller
|
if (!peerId) return; // we will listen for a caller
|
||||||
|
this._isCaller = true;
|
||||||
this._sendSignal();
|
this._sendSignal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,21 +731,22 @@ class WSPeer extends Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendJSON(message) {
|
sendJSON(message) {
|
||||||
|
console.debug(message)
|
||||||
message.to = this._peerId;
|
message.to = this._peerId;
|
||||||
message.roomType = this._roomType;
|
message.roomType = this._roomType;
|
||||||
message.roomSecret = this._roomSecret;
|
message.roomSecret = this._roomSecret;
|
||||||
this._server.send(message);
|
this._server.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendSignal() {
|
_sendSignal(connected = false) {
|
||||||
this.sendJSON({type: 'signal'});
|
this.sendJSON({type: 'signal', connected: connected});
|
||||||
}
|
}
|
||||||
|
|
||||||
onServerMessage(message) {
|
onServerMessage(message) {
|
||||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
|
||||||
if (this._peerId) return;
|
|
||||||
this._peerId = message.sender.id;
|
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() {
|
getConnectionHash() {
|
||||||
@@ -728,8 +766,13 @@ class PeersManager {
|
|||||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||||
Events.on('send-text', e => this._onSendText(e.detail));
|
Events.on('send-text', e => this._onSendText(e.detail));
|
||||||
Events.on('peer-left', e => this._onPeerLeft(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('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||||
|
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||||
|
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||||
|
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
|
||||||
|
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,7 +792,7 @@ class PeersManager {
|
|||||||
_onWsRelay(message) {
|
_onWsRelay(message) {
|
||||||
const messageJSON = JSON.parse(message)
|
const messageJSON = JSON.parse(message)
|
||||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
||||||
this.peers[messageJSON.sender.id]._onMessage(message, false)
|
this.peers[messageJSON.sender.id]._onMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPeers(msg) {
|
_onPeers(msg) {
|
||||||
@@ -768,10 +811,6 @@ class PeersManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTo(peerId, message) {
|
|
||||||
this.peers[peerId].send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRespondToFileTransferRequest(detail) {
|
_onRespondToFileTransferRequest(detail) {
|
||||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||||
}
|
}
|
||||||
@@ -797,15 +836,28 @@ class PeersManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onPeerLeft(msg) {
|
_onPeerLeft(msg) {
|
||||||
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) {
|
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
|
||||||
console.log('WSPeer left:', msg.peerId)
|
console.log('WSPeer left:', msg.peerId);
|
||||||
Events.fire('peer-disconnected', msg.peerId)
|
Events.fire('peer-disconnected', msg.peerId);
|
||||||
} else if (msg.disconnect === true) {
|
} else if (msg.disconnect === true) {
|
||||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||||
Events.fire('peer-disconnected', msg.peerId);
|
Events.fire('peer-disconnected', msg.peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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) {
|
_onPeerDisconnected(peerId) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
delete this.peers[peerId];
|
delete this.peers[peerId];
|
||||||
@@ -823,6 +875,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].sendDisplayName(this._displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisplayName(displayName) {
|
||||||
|
this._originalDisplayName = displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileChunker {
|
class FileChunker {
|
||||||
@@ -911,28 +980,11 @@ class Events {
|
|||||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
static on(type, callback) {
|
static on(type, callback, options = false) {
|
||||||
return window.addEventListener(type, callback, false);
|
return window.addEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
static off(type, callback) {
|
static off(type, callback, options = false) {
|
||||||
return window.removeEventListener(type, callback, false);
|
return window.removeEventListener(type, callback, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.1.1';
|
const cacheVersion = 'v1.4.5';
|
||||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
@@ -11,28 +11,30 @@
|
|||||||
|
|
||||||
/* Layout */
|
/* Layout */
|
||||||
|
|
||||||
html {
|
|
||||||
min-height: 100%;
|
|
||||||
height: -webkit-fill-available;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior: none;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
min-height: -webkit-fill-available;
|
/* mobile viewport bug fix */
|
||||||
flex-grow: 1;
|
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||||
align-items: center;
|
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||||
justify-content: center;
|
min-height: fill-available;
|
||||||
overflow-y: hidden;
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||||
|
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||||
|
min-height: fill-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-reverse {
|
.row-reverse {
|
||||||
@@ -74,10 +76,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 56px;
|
height: 56px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -120,9 +119,9 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.font-subheading {
|
.font-subheading {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 18px;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,20 +199,160 @@ body>header a {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#center {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
flex-grow: 1;
|
||||||
|
--footer-height: 146px;
|
||||||
|
max-height: calc(100vh - 56px - var(--footer-height));
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 402px) and (max-width: 425px) {
|
||||||
|
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||||
|
--footer-height: 164px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 402px) {
|
||||||
|
#center {
|
||||||
|
--footer-height: 184px;
|
||||||
|
}
|
||||||
|
}
|
||||||
/* Peers List */
|
/* Peers List */
|
||||||
|
|
||||||
|
#x-peers-filler {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
x-peers {
|
x-peers {
|
||||||
width: 100%;
|
position: relative;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: start !important;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transition: color 300ms;
|
transition: --bg-color 0.5s ease;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
--peers-per-row: 6; /* default if browser does not support :has selector */
|
||||||
|
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
|
||||||
|
width: var(--x-peers-width);
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers.overflowing {
|
||||||
|
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
|
||||||
|
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
|
||||||
|
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
|
||||||
|
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||||
|
|
||||||
|
/* Opera doesn't support this in the shorthand */
|
||||||
|
background-attachment: local, local, scroll, scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer) {
|
||||||
|
--peers-per-row: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* peers-per-row if height is too small for 2 rows */
|
||||||
|
@media screen and (min-height: 538px) and (max-height: 683px) and (max-width: 402px),
|
||||||
|
screen and (min-height: 517px) and (max-height: 664px) and (max-width: 426px),
|
||||||
|
screen and (min-height: 501px) and (max-height: 647px) and (min-width: 426px) {
|
||||||
|
x-peers:has(> x-peer) {
|
||||||
|
--peers-per-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(7)) {
|
||||||
|
--peers-per-row: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||||
|
--peers-per-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||||
|
--peers-per-row: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||||
|
--peers-per-row: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||||
|
--peers-per-row: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||||
|
--peers-per-row: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||||
|
--peers-per-row: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* peers-per-row if height is too small for 3 rows */
|
||||||
|
@media screen and (min-height: 683px) and (max-width: 402px),
|
||||||
|
screen and (min-height: 664px) and (max-width: 426px),
|
||||||
|
screen and (min-height: 647px) and (min-width: 426px) {
|
||||||
|
x-peers:has(> x-peer) {
|
||||||
|
--peers-per-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(10)) {
|
||||||
|
--peers-per-row: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(13)) {
|
||||||
|
--peers-per-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(16)) {
|
||||||
|
--peers-per-row: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(19)) {
|
||||||
|
--peers-per-row: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(22)) {
|
||||||
|
--peers-per-row: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(25)) {
|
||||||
|
--peers-per-row: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peers:has(> x-peer:nth-of-type(28)) {
|
||||||
|
--peers-per-row: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty Peers List */
|
/* Empty Peers List */
|
||||||
|
|
||||||
x-no-peers {
|
x-no-peers {
|
||||||
height: 114px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* prevent flickering on load */
|
/* prevent flickering on load */
|
||||||
@@ -255,25 +394,19 @@ x-no-peers[drop-bg] * {
|
|||||||
x-peer {
|
x-peer {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
padding: 8px;
|
||||||
|
align-content: start;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer label {
|
x-peer label {
|
||||||
width: var(--peer-width);
|
width: var(--peer-width);
|
||||||
padding: 8px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer .name {
|
|
||||||
width: var(--peer-width);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -281,27 +414,43 @@ input[type="file"] {
|
|||||||
|
|
||||||
x-peer x-icon {
|
x-peer x-icon {
|
||||||
--icon-size: 40px;
|
--icon-size: 40px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: transform 150ms;
|
||||||
|
will-change: transform;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer .icon-wrapper {
|
||||||
width: var(--icon-size);
|
width: var(--icon-size);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
|
||||||
transition: transform 150ms;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer:not(.type-ip) x-icon {
|
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||||
background: var(--paired-device-color);
|
background: var(--paired-device-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer.ws-peer x-icon {
|
x-peer x-icon > .highlight-wrapper {
|
||||||
border: solid 4px var(--ws-peer-color);
|
align-self: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 7px auto 0;
|
||||||
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer.ws-peer .progress {
|
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||||
margin-top: 4px;
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||||
|
background-color: var(--paired-device-color);
|
||||||
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer:not([status]):hover x-icon,
|
x-peer:not([status]):hover x-icon,
|
||||||
@@ -315,6 +464,36 @@ x-peer[status] x-icon {
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
x-peer.ws-peer {
|
||||||
|
margin-top: -1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer.ws-peer .progress {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer.ws-peer .icon-wrapper{
|
||||||
|
border: solid 3px var(--ws-peer-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer.ws-peer .highlight-wrapper {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-descriptor {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.status,
|
.status,
|
||||||
.device-name,
|
.device-name,
|
||||||
.connection-hash {
|
.connection-hash {
|
||||||
@@ -380,20 +559,20 @@ x-peer[drop] x-icon {
|
|||||||
/* Footer */
|
/* Footer */
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
position: absolute;
|
position: relative;
|
||||||
bottom: 0;
|
margin-top: auto;
|
||||||
left: 0;
|
z-index: 2;
|
||||||
right: 0;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 0 16px 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: color 300ms;
|
transition: color 300ms;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .logo {
|
footer .logo {
|
||||||
--icon-size: 80px;
|
--icon-size: 80px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
margin-top: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .font-body2 {
|
footer .font-body2 {
|
||||||
@@ -411,6 +590,39 @@ footer .font-body2 {
|
|||||||
padding-bottom: 1px;
|
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 */
|
/* Dialog */
|
||||||
|
|
||||||
x-dialog x-background {
|
x-dialog x-background {
|
||||||
@@ -418,7 +630,7 @@ x-dialog x-background {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
padding: 35px;
|
padding: 15px;
|
||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,13 +641,20 @@ x-dialog x-paper {
|
|||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: transform 300ms;
|
transition: transform 300ms;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pair-device-dialog x-paper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: max(50%, 350px);
|
top: max(50%, 350px);
|
||||||
height: 650px;
|
margin-top: -328.5px;
|
||||||
margin-top: -325px;
|
width: calc(100vw - 20px);
|
||||||
|
height: 625px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog:not([show]) {
|
x-dialog:not([show]) {
|
||||||
@@ -450,12 +669,6 @@ x-dialog:not([show]) x-background {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog .row-reverse>.button {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: -16px;
|
|
||||||
width: 50%;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog a {
|
x-dialog a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
@@ -467,13 +680,13 @@ x-dialog .font-subheading {
|
|||||||
|
|
||||||
/* PairDevicesDialog */
|
/* PairDevicesDialog */
|
||||||
|
|
||||||
#keyInputContainer {
|
#key-input-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyInputContainer>input {
|
#key-input-container>input {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
@@ -489,15 +702,15 @@ x-dialog .font-subheading {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyInputContainer>input + * {
|
#key-input-container>input + * {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyInputContainer>input:nth-of-type(4) {
|
#key-input-container>input:nth-of-type(4) {
|
||||||
margin-left: 18px;
|
margin-left: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#roomKey {
|
#room-key {
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -505,19 +718,15 @@ x-dialog .font-subheading {
|
|||||||
margin: 15px -15px;
|
margin: 15px -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#roomKeyQrCode {
|
#room-key-qr-code {
|
||||||
padding: inherit;
|
margin: 16px;
|
||||||
margin: auto;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pairDeviceDialog hr {
|
#pair-device-dialog hr {
|
||||||
margin-top: 40px;
|
margin: 40px -24px;
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pairDeviceDialog x-background {
|
#pair-device-dialog x-background {
|
||||||
padding: 16px!important;
|
padding: 16px!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,29 +737,24 @@ x-dialog .row {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog h2 {
|
/* button row*/
|
||||||
margin-top: 1rem;
|
x-paper > div:last-child {
|
||||||
}
|
margin: auto -24px -15px;
|
||||||
|
|
||||||
#receiveRequestDialog h2,
|
|
||||||
#receiveFileDialog h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-dialog .row-reverse {
|
|
||||||
margin: 40px -24px auto;
|
|
||||||
border-top: solid 2.5px var(--border-color);
|
border-top: solid 2.5px var(--border-color);
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
x-paper > div:last-child > .button {
|
||||||
border: solid 1.25px var(--border-color);
|
height: 100%;
|
||||||
margin-bottom: -16px;
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > div:last-child > .button:not(:last-child) {
|
||||||
|
border-left: solid 2.5px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description {
|
.file-description {
|
||||||
word-break: break-word;
|
margin-bottom: 25px;
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description .row {
|
.file-description .row {
|
||||||
@@ -562,52 +766,52 @@ x-dialog .row-reverse {
|
|||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileName {
|
.file-name {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileStem {
|
.file-stem {
|
||||||
max-width: 80%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
word-break: break-all;
|
white-space: nowrap;
|
||||||
max-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-size{
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send Text Dialog */
|
/* Send Text Dialog */
|
||||||
|
|
||||||
#textInput {
|
x-dialog .dialog-subheader {
|
||||||
min-height: 120px;
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-input {
|
||||||
|
min-height: 200px;
|
||||||
|
margin: 14px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receive Text Dialog */
|
/* Receive Text Dialog */
|
||||||
|
|
||||||
#receiveTextDialog #text {
|
#receive-text-dialog #text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 393px);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-user-select: all;
|
-webkit-user-select: all;
|
||||||
-moz-user-select: all;
|
-moz-user-select: all;
|
||||||
user-select: all;
|
user-select: all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-top:36px;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDialog #text a {
|
#receive-text-dialog #text a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDialog #text a:hover {
|
#receive-text-dialog #text a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDialog h3 {
|
#receive-text-dialog h3 {
|
||||||
/* Select the received text when double-clicking the dialog */
|
/* Select the received text when double-clicking the dialog */
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -615,29 +819,44 @@ x-dialog .row-reverse {
|
|||||||
|
|
||||||
.row-separator {
|
.row-separator {
|
||||||
border-bottom: solid 2.5px var(--border-color);
|
border-bottom: solid 2.5px var(--border-color);
|
||||||
margin: auto -25px;
|
margin: auto -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#receiveTextDescriptionContainer {
|
#base64-paste-btn,
|
||||||
margin-bottom: 25px;
|
#base64-paste-dialog .textarea {
|
||||||
}
|
|
||||||
|
|
||||||
#base64PasteBtn {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
border: solid 12px #438cff;
|
border: solid 12px #438cff;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64PasteDialog button {
|
#base64-paste-dialog .textarea {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog .textarea::before {
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
content: attr(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
#base64-paste-dialog button {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64PasteDialog button[close] {
|
#base64-paste-dialog button[close] {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64PasteDialog button[close]:before {
|
#base64-paste-dialog button[close]:before {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +866,6 @@ x-dialog .row-reverse {
|
|||||||
padding: 2px 16px 0;
|
padding: 2px 16px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
min-width: 100px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -658,6 +876,7 @@ x-dialog .row-reverse {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
@@ -695,16 +914,18 @@ x-dialog .row-reverse {
|
|||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cancelPasteModeBtn {
|
#cancel-paste-mode {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
border-bottom: solid 2.5px var(--border-color);
|
background-color: var(--primary-color);
|
||||||
|
color: rgb(238, 238, 238);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus:before,
|
.button:focus:before,
|
||||||
@@ -720,7 +941,6 @@ button::-moz-focus-inner {
|
|||||||
|
|
||||||
|
|
||||||
/* Icon Button */
|
/* Icon Button */
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -730,10 +950,7 @@ button::-moz-focus-inner {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Text Input */
|
/* Text Input */
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -747,9 +964,8 @@ button::-moz-focus-inner {
|
|||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
resize: none;
|
resize: none;
|
||||||
min-height: 40px;
|
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
max-height: 300px;
|
max-height: calc(100vh - 254px);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,6 +1024,13 @@ button::-moz-focus-inner {
|
|||||||
margin: 8px 8px -16px;
|
margin: 8px 8px -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#about section {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#about header {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading Indicator */
|
/* Loading Indicator */
|
||||||
|
|
||||||
@@ -815,7 +1038,7 @@ button::-moz-focus-inner {
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -8px;
|
||||||
clip: rect(0px, 80px, 80px, 40px);
|
clip: rect(0px, 80px, 80px, 40px);
|
||||||
--progress: rotate(0deg);
|
--progress: rotate(0deg);
|
||||||
transition: transform 200ms;
|
transition: transform 200ms;
|
||||||
@@ -857,11 +1080,11 @@ button::-moz-focus-inner {
|
|||||||
x-toast {
|
x-toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
bottom: 24px;
|
top: 50px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 344px;
|
max-width: 344px;
|
||||||
background-color: #323232;
|
background-color: rgb(var(--text-color));
|
||||||
color: rgba(255, 255, 255, 0.95);
|
color: rgb(var(--bg-color));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
@@ -875,20 +1098,23 @@ x-toast {
|
|||||||
|
|
||||||
x-toast:not([show]):not(:hover) {
|
x-toast:not([show]):not(:hover) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(100px);
|
transform: translateY(-100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Instructions */
|
/* Instructions */
|
||||||
|
|
||||||
x-instructions {
|
x-instructions {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 120px;
|
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
z-index: -1;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 80%;
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||||
@@ -905,92 +1131,92 @@ x-instructions[drop-bg]:not([drop-peer]):before {
|
|||||||
|
|
||||||
x-instructions p {
|
x-instructions p {
|
||||||
display: none;
|
display: none;
|
||||||
margin: 0 auto auto;
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peers:empty~x-instructions {
|
x-peers:empty~x-instructions {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.websocket-fallback {
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
x-peer {
|
||||||
|
transform: scale(0.95);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#websocket-fallback {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#websocket-fallback>span {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#websocket-fallback > span > span {
|
||||||
border-bottom: solid 4px var(--ws-peer-color);
|
border-bottom: solid 4px var(--ws-peer-color);
|
||||||
padding-bottom: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Styles */
|
/* Responsive Styles */
|
||||||
|
@media screen and (max-width: 360px) {
|
||||||
@media (min-height: 800px) {
|
x-dialog x-paper {
|
||||||
footer {
|
padding: 15px;
|
||||||
margin-bottom: 16px;
|
}
|
||||||
|
x-paper > div:last-child {
|
||||||
|
margin: auto -15px -15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-height: 800px),
|
@media screen and (min-height: 800px) {
|
||||||
screen and (min-width: 1100px) {
|
#websocket-fallback {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||||
content: attr(desktop);
|
content: attr(desktop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 420px) {
|
|
||||||
x-instructions {
|
|
||||||
top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer .logo {
|
|
||||||
--icon-size: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
iOS specific styles
|
|
||||||
*/
|
|
||||||
@supports (-webkit-overflow-scrolling: touch) {
|
|
||||||
|
|
||||||
|
|
||||||
html {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
x-instructions:not([drop-peer]):not([drop-bg]):before {
|
|
||||||
content: attr(mobile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Color Themes
|
Color Themes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Default colors */
|
/* Default colors */
|
||||||
body {
|
body {
|
||||||
--text-color: #333;
|
--text-color: 51,51,51;
|
||||||
--bg-color: #fff;
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
|
--bg-color-test: 18,18,18;
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #f1f3f4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: #e7e8e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme colors */
|
/* Dark theme colors */
|
||||||
body.dark-theme {
|
body.dark-theme {
|
||||||
--text-color: #eee;
|
--text-color: 238,238,238;
|
||||||
--bg-color: #121212;
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: #252525;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colored Elements */
|
/* Colored Elements */
|
||||||
body {
|
body {
|
||||||
color: var(--text-color);
|
color: rgb(var(--text-color));
|
||||||
background-color: var(--bg-color);
|
background-color: rgb(var(--bg-color));
|
||||||
transition: background-color 0.5s ease;
|
transition: background-color 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog x-paper {
|
x-dialog x-paper {
|
||||||
background-color: var(--bg-color);
|
background-color: rgb(var(--bg-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
color: var(--text-color) !important;
|
color: rgb(var(--text-color)) !important;
|
||||||
background-color: var(--bg-color-secondary) !important;
|
background-color: var(--bg-color-secondary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1016,7 +1242,9 @@ x-dialog x-paper {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-preview {
|
.file-preview > img,
|
||||||
|
.file-preview > audio,
|
||||||
|
.file-preview > video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -1028,16 +1256,16 @@ x-dialog x-paper {
|
|||||||
|
|
||||||
/* defaults to dark theme */
|
/* defaults to dark theme */
|
||||||
body {
|
body {
|
||||||
--text-color: #eee;
|
--text-color: 238,238,238;
|
||||||
--bg-color: #121212;
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: #252525;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override dark mode with light mode styles if the user decides to swap */
|
/* Override dark mode with light mode styles if the user decides to swap */
|
||||||
body.light-theme {
|
body.light-theme {
|
||||||
--text-color: #333;
|
--text-color: 51,51,51;
|
||||||
--bg-color: #fafafa;
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #f1f3f4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: #e7e8e8;
|
||||||
}
|
}
|
||||||
@@ -1055,6 +1283,15 @@ x-dialog x-paper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
iOS specific styles
|
||||||
|
*/
|
||||||
|
@supports (-webkit-overflow-scrolling: touch) {
|
||||||
|
html {
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* webkit scrollbar style*/
|
/* webkit scrollbar style*/
|
||||||
|
|
||||||
::-webkit-scrollbar{
|
::-webkit-scrollbar{
|
||||||
|
|||||||
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