mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-06 09:53:49 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56eb29c91b | ||
|
|
6e4bda0adf | ||
|
|
0baced640a | ||
|
|
3c2e73fc0c | ||
|
|
c629d7cd88 | ||
|
|
ba20c72026 | ||
|
|
347f9b87c0 | ||
|
|
ae9909f596 | ||
|
|
26c1878bb9 | ||
|
|
de0afce4ea | ||
|
|
2a837eb195 | ||
|
|
fdf20cfdd9 | ||
|
|
7606fb398b | ||
|
|
8d640be3a2 | ||
|
|
241ea4f988 | ||
|
|
0ac3c5a11f | ||
|
|
f39bfedf98 | ||
|
|
fafdbcc829 | ||
|
|
b9806d4327 | ||
|
|
fb08bdaf36 | ||
|
|
5a363e90dd | ||
|
|
8f4ce63a0c | ||
|
|
b42c8a0b1a | ||
|
|
4c7bdd3a0f | ||
|
|
3f72fa1160 | ||
|
|
5c3f5ece7d | ||
|
|
8de899f124 | ||
|
|
87097e9cd4 | ||
|
|
b2fc6415da | ||
|
|
2d8bbd5a79 | ||
|
|
cae3bb7c7b | ||
|
|
59dca141b6 | ||
|
|
d0046e83cb | ||
|
|
2aeadb44e2 | ||
|
|
7827a47d29 | ||
|
|
4edc9c9b22 | ||
|
|
398a69d7a0 | ||
|
|
dfe69cc873 | ||
|
|
f5fde731b0 | ||
|
|
d6eee480b3 | ||
|
|
f6ad85a744 | ||
|
|
d50480b2f8 | ||
|
|
ac1e88b6a0 | ||
|
|
0fe36e132c |
7
.github/workflows/github-image.yml
vendored
7
.github/workflows/github-image.yml
vendored
@@ -28,6 +28,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup qemu
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
@@ -46,6 +52,7 @@ jobs:
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
11
README.md
11
README.md
@@ -33,13 +33,13 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
|
||||
## Differences to Snapdrop
|
||||
|
||||
### Device Pairing
|
||||
### Device Pairing / Internet Transfer
|
||||
* Pair devices via 6-digit code or QR-Code
|
||||
* Pair devices outside your local network or in complex network environment (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
|
||||
* Connect to devices on your mobile hotspot.
|
||||
* Paired devices will always find each other via shared secrets even after reopening the browser or the Progressive Web App
|
||||
* You will always discover devices on your local network. Paired devices are shown additionally.
|
||||
* 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 the PairDrop TURN server.
|
||||
|
||||
### [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 files are downloaded automatically if possible.
|
||||
@@ -57,9 +57,10 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* Change your display name permanently to easily differentiate your devices
|
||||
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
|
||||
* Warn user before PairDrop is closed on file transfer
|
||||
* Warn user before PairDrop is closed on file transfer
|
||||
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
|
||||
* [Video and Audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads))
|
||||
* Switch theme back to auto/system after darkmode or lightmode is enabled
|
||||
* Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario))
|
||||
* 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))
|
||||
@@ -88,9 +89,9 @@ You can [host your own instance with Docker](/docs/host-your-own.md).
|
||||
|
||||
|
||||
## Support the Community
|
||||
PairDrop is free and always will be. Still, we have to pay for the domain.
|
||||
PairDrop is free and always will be. Still, we have to pay for the domain and the server.
|
||||
|
||||
To contribute and support me:<br>
|
||||
To contribute and support:<br>
|
||||
<a href="https://www.buymeacoffee.com/pairdrop" target="_blank">
|
||||
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
|
||||
</a>
|
||||
|
||||
19
docker-compose-coturn.yml
Normal file
19
docker-compose-coturn.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: "3"
|
||||
services:
|
||||
node:
|
||||
image: "node:lts-alpine"
|
||||
user: "node"
|
||||
working_dir: /home/node/app
|
||||
volumes:
|
||||
- ./:/home/node/app
|
||||
command: ash -c "npm i && npm run start:prod"
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
coturn_server:
|
||||
image: "coturn/coturn"
|
||||
restart: always
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./turnserver.conf:/etc/coturn/turnserver.conf
|
||||
#you need to copy turnserver_example.conf to turnserver.conf and specify domain, IP address, user and password
|
||||
20
docs/faq.md
20
docs/faq.md
@@ -104,10 +104,13 @@ Here's a list of some third-party apps compatible with PairDrop:
|
||||
What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server?
|
||||
</summary>
|
||||
|
||||
It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer.
|
||||
It uses a WebRTC peer to peer connection. WebRTC needs a Signaling Server that is only used to establish a connection. The server is not involved in the file transfer.
|
||||
|
||||
If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages.
|
||||
If devices are on the same network, none of your files are ever sent to any server.
|
||||
|
||||
If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC.
|
||||
|
||||
If you host your own instance and want to support devices that do not support WebRTC, you can [start the PairDrop instance with an activated Websocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
|
||||
|
||||
<br>
|
||||
|
||||
@@ -118,11 +121,12 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R
|
||||
What about privacy? Will files be saved on third-party-servers?
|
||||
</summary>
|
||||
|
||||
None of your files are ever sent to any server. Files are sent only between peers. PairDrop doesn't even use a database. If you are curious have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
|
||||
Files are sent directly between peers. PairDrop doesn't even use a database. If you are curious, have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
|
||||
WebRTC encrypts the files on transit.
|
||||
|
||||
If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages.
|
||||
If devices are on the same network, none of your files are ever sent to any server.
|
||||
|
||||
If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC.
|
||||
|
||||
<br>
|
||||
|
||||
@@ -147,9 +151,7 @@ Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure
|
||||
|
||||
Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases.
|
||||
|
||||
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.
|
||||
You can open a hotspot on one of your devices to bridge the connection which omits the need of the TURN server.
|
||||
|
||||
- [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)
|
||||
@@ -171,7 +173,7 @@ Then, all data should be sent directly between devices and your data plan should
|
||||
Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer.
|
||||
We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity.
|
||||
|
||||
If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555).
|
||||
If you want to learn more about simplicity you can read *Insanely Simple: The Obsession that Drives Apple's Success* or *Thinking, Fast and Slow*.
|
||||
|
||||
|
||||
<br>
|
||||
@@ -183,7 +185,7 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob
|
||||
Snapdrop and PairDrop are awesome! How can I support them?
|
||||
</summary>
|
||||
|
||||
* [Buy me a coffee to support open source software](https://www.buymeacoffee.com/pairdrop)
|
||||
* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support open source software
|
||||
* [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)
|
||||
* Share PairDrop on social media.
|
||||
* Fix bugs and make a pull request.
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# Deployment Notes
|
||||
The easiest way to get PairDrop up and running is by using Docker.
|
||||
|
||||
> <b>TURN server for Internet Transfer</b>
|
||||
>
|
||||
> Beware that you have to host your own TURN server in order to enable transfers between different networks.
|
||||
>
|
||||
> You can follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) or deploy it via docker-compose (Step 5).
|
||||
|
||||
## Deployment with Docker
|
||||
|
||||
### Docker Image from Docker Hub
|
||||
@@ -24,7 +30,7 @@ Set options by using the following flags in the `docker run` command:
|
||||
> - 3000 -> `-p 127.0.0.1:3000:3000`
|
||||
> - 8080 -> `-p 127.0.0.1:8080:3000`
|
||||
##### Rate limiting requests
|
||||
```
|
||||
```bash
|
||||
-e RATE_LIMIT=true
|
||||
```
|
||||
> Limits clients to 1000 requests per 5 min
|
||||
@@ -50,6 +56,8 @@ Set options by using the following flags in the `docker run` command:
|
||||
> 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.
|
||||
>
|
||||
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
|
||||
>
|
||||
> Default configuration:
|
||||
> ```json
|
||||
> {
|
||||
@@ -57,19 +65,36 @@ Set options by using the following flags in the `docker run` command:
|
||||
> "iceServers": [
|
||||
> {
|
||||
> "urls": "stun:stun.l.google.com:19302"
|
||||
> },
|
||||
> {
|
||||
> "urls": "stun:openrelay.metered.ca:80"
|
||||
> },
|
||||
> {
|
||||
> "urls": "turn:openrelay.metered.ca:443",
|
||||
> "username": "openrelayproject",
|
||||
> "credential": "openrelayproject"
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
|
||||
##### Debug Mode
|
||||
```bash
|
||||
-e DEBUG_MODE="true"
|
||||
```
|
||||
|
||||
> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server)
|
||||
> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status.
|
||||
>
|
||||
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
|
||||
> ```
|
||||
> ----DEBUGGING-PEER-IP-START----
|
||||
> remoteAddress: ::ffff:172.17.0.1
|
||||
> x-forwarded-for: 19.117.63.126
|
||||
> cf-connecting-ip: undefined
|
||||
> PairDrop uses: 19.117.63.126
|
||||
> IP is private: false
|
||||
> if IP is private, '127.0.0.1' is used instead
|
||||
> ----DEBUGGING-PEER-IP-END----
|
||||
> ```
|
||||
> If the IP PairDrop uses is the public IP of your device everything is correctly setup.
|
||||
>To find out your devices public IP visit https://www.whatismyip.com/.
|
||||
>
|
||||
> To preserve your clients' privacy, **never use this flag in production!**
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
### Docker Image from GHCR
|
||||
@@ -82,7 +107,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 gh
|
||||
>
|
||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||
|
||||
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage)
|
||||
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage)
|
||||
|
||||
### Docker Image self-built
|
||||
#### Build the image
|
||||
@@ -103,7 +128,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -i
|
||||
>
|
||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
||||
|
||||
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage)
|
||||
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage)
|
||||
|
||||
<br>
|
||||
|
||||
@@ -186,6 +211,8 @@ $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.
|
||||
>
|
||||
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
|
||||
>
|
||||
> Default configuration:
|
||||
> ```json
|
||||
@@ -194,19 +221,41 @@ $env:RTC_CONFIG="rtc_config.json"; npm start
|
||||
> "iceServers": [
|
||||
> {
|
||||
> "urls": "stun:stun.l.google.com:19302"
|
||||
> },
|
||||
> {
|
||||
> "urls": "stun:openrelay.metered.ca:80"
|
||||
> },
|
||||
> {
|
||||
> "urls": "turn:openrelay.metered.ca:443",
|
||||
> "username": "openrelayproject",
|
||||
> "credential": "openrelayproject"
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
|
||||
#### Debug Mode
|
||||
On Unix based systems
|
||||
```bash
|
||||
DEBUG_MODE="true" npm start
|
||||
```
|
||||
On Windows
|
||||
```bash
|
||||
$env:DEBUG_MODE="true"; npm start
|
||||
```
|
||||
|
||||
> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server)
|
||||
> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status.
|
||||
>
|
||||
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
|
||||
> ```
|
||||
> ----DEBUGGING-PEER-IP-START----
|
||||
> remoteAddress: ::ffff:172.17.0.1
|
||||
> x-forwarded-for: 19.117.63.126
|
||||
> cf-connecting-ip: undefined
|
||||
> PairDrop uses: 19.117.63.126
|
||||
> IP is private: false
|
||||
> if IP is private, '127.0.0.1' is used instead
|
||||
> ----DEBUGGING-PEER-IP-END----
|
||||
> ```
|
||||
> If the IP PairDrop uses is the public IP of your device everything is correctly setup.
|
||||
>To find out your devices public IP visit https://www.whatismyip.com/.
|
||||
>
|
||||
> To preserve your clients' privacy, **never use this flag in production!**
|
||||
|
||||
|
||||
### Options / Flags
|
||||
#### Local Run
|
||||
```bash
|
||||
@@ -263,6 +312,8 @@ npm run start:prod -- --localhost-only --include-ws-fallback
|
||||
## HTTP-Server
|
||||
When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible.
|
||||
|
||||
To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode).
|
||||
|
||||
### Using nginx
|
||||
#### Allow http and https requests
|
||||
```
|
||||
|
||||
@@ -33,10 +33,10 @@ https://routinehub.co/shortcut/13990/
|
||||
|
||||
|
||||
## Send directly from share menu on Android
|
||||
The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented but not yet tested.
|
||||
When the PWA is installed, it should register itself to the share-menu of the device automatically.
|
||||
The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented.
|
||||
|
||||
When the PWA is installed, it will register itself to the share-menu of the device automatically.
|
||||
|
||||
This feature is still under development. Please test this feature and create an issue if it does not work.
|
||||
|
||||
## Send directly via command-line interface
|
||||
Send files or text with PairDrop via command-line interface.
|
||||
|
||||
191
index.js
191
index.js
@@ -63,14 +63,6 @@ const rtcConfig = process.env.RTC_CONFIG
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": "stun:stun.l.google.com:19302"
|
||||
},
|
||||
{
|
||||
"urls": "stun:openrelay.metered.ca:80"
|
||||
},
|
||||
{
|
||||
"urls": "turn:openrelay.metered.ca:443",
|
||||
"username": "openrelayproject",
|
||||
"credential": "openrelayproject"
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -98,6 +90,12 @@ if (process.argv.includes('--include-ws-fallback')) {
|
||||
app.use(express.static('public'));
|
||||
}
|
||||
|
||||
const debugMode = process.env.DEBUG_MODE === "true";
|
||||
|
||||
if (debugMode) {
|
||||
console.log("DEBUG_MODE is active. To protect privacy, do not use in production.")
|
||||
}
|
||||
|
||||
app.use(function(req, res) {
|
||||
res.redirect('/');
|
||||
});
|
||||
@@ -135,7 +133,6 @@ class PairDropServer {
|
||||
type: 'rtc-config',
|
||||
config: rtcConfig
|
||||
});
|
||||
this._joinRoom(peer);
|
||||
|
||||
// send displayName
|
||||
this._send(peer, {
|
||||
@@ -144,7 +141,7 @@ class PairDropServer {
|
||||
displayName: peer.name.displayName,
|
||||
deviceName: peer.name.deviceName,
|
||||
peerId: peer.id,
|
||||
peerIdHash: peer.id.hashCode128BitSalted()
|
||||
peerIdHash: hasher.hashCodeSalted(peer.id)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -164,14 +161,14 @@ class PairDropServer {
|
||||
case 'pong':
|
||||
sender.lastBeat = Date.now();
|
||||
break;
|
||||
case 'join-ip-room':
|
||||
this._joinRoom(sender);
|
||||
break;
|
||||
case 'room-secrets':
|
||||
this._onRoomSecrets(sender, message);
|
||||
break;
|
||||
case 'room-secret-deleted':
|
||||
this._onRoomSecretDeleted(sender, message);
|
||||
break;
|
||||
case 'room-secrets-cleared':
|
||||
this._onRoomSecretsCleared(sender, message);
|
||||
case 'room-secrets-deleted':
|
||||
this._onRoomSecretsDeleted(sender, message);
|
||||
break;
|
||||
case 'pair-device-initiate':
|
||||
this._onPairDeviceInitiate(sender);
|
||||
@@ -182,6 +179,9 @@ class PairDropServer {
|
||||
case 'pair-device-cancel':
|
||||
this._onPairDeviceCancel(sender);
|
||||
break;
|
||||
case 'regenerate-room-secret':
|
||||
this._onRegenerateRoomSecret(sender, message);
|
||||
break
|
||||
case 'resend-peers':
|
||||
this._notifyPeers(sender);
|
||||
break;
|
||||
@@ -215,57 +215,41 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_onRoomSecrets(sender, message) {
|
||||
if (!message.roomSecrets) return;
|
||||
|
||||
const roomSecrets = message.roomSecrets.filter(roomSecret => {
|
||||
return /^[\x00-\x7F]{64}$/.test(roomSecret);
|
||||
return /^[\x00-\x7F]{64,256}$/.test(roomSecret);
|
||||
})
|
||||
|
||||
if (!roomSecrets) return;
|
||||
|
||||
this._joinSecretRooms(sender, roomSecrets);
|
||||
}
|
||||
|
||||
_onRoomSecretDeleted(sender, message) {
|
||||
this._deleteSecretRoom(sender, message.roomSecret)
|
||||
}
|
||||
|
||||
_onRoomSecretsCleared(sender, message) {
|
||||
_onRoomSecretsDeleted(sender, message) {
|
||||
for (let i = 0; i<message.roomSecrets.length; i++) {
|
||||
this._deleteSecretRoom(sender, message.roomSecrets[i]);
|
||||
this._deleteSecretRoom(message.roomSecrets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_deleteSecretRoom(sender, roomSecret) {
|
||||
_deleteSecretRoom(roomSecret) {
|
||||
const room = this._rooms[roomSecret];
|
||||
if (room) {
|
||||
for (const peerId in room) {
|
||||
const peer = room[peerId];
|
||||
this._leaveRoom(peer, 'secret', roomSecret);
|
||||
this._send(peer, {
|
||||
type: 'secret-room-deleted',
|
||||
roomSecret: roomSecret,
|
||||
});
|
||||
}
|
||||
}
|
||||
this._notifyPeers(sender);
|
||||
}
|
||||
if (!room) return;
|
||||
|
||||
getRandomString(length) {
|
||||
let string = "";
|
||||
while (string.length < length) {
|
||||
let arr = new Uint16Array(length);
|
||||
crypto.webcrypto.getRandomValues(arr);
|
||||
arr = Array.apply([], arr); /* turn into non-typed array */
|
||||
arr = arr.map(function (r) {
|
||||
return r % 128
|
||||
})
|
||||
arr = arr.filter(function (r) {
|
||||
/* strip non-printables: if we transform into desirable range we have a propability bias, so I suppose we better skip this character */
|
||||
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
|
||||
for (const peerId in room) {
|
||||
const peer = room[peerId];
|
||||
|
||||
this._leaveRoom(peer, 'secret', roomSecret);
|
||||
|
||||
this._send(peer, {
|
||||
type: 'secret-room-deleted',
|
||||
roomSecret: roomSecret,
|
||||
});
|
||||
string += String.fromCharCode.apply(String, arr);
|
||||
}
|
||||
return string.substring(0, length)
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate(sender) {
|
||||
let roomSecret = this.getRandomString(64);
|
||||
let roomSecret = randomizer.getRandomString(256);
|
||||
let roomKey = this._createRoomKey(sender, roomSecret);
|
||||
if (sender.roomKey) this._removeRoomKey(sender.roomKey);
|
||||
sender.roomKey = roomKey;
|
||||
@@ -278,16 +262,19 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_onPairDeviceJoin(sender, message) {
|
||||
// rate limit implementation: max 10 attempts every 10s
|
||||
if (sender.roomKeyRate >= 10) {
|
||||
this._send(sender, { type: 'pair-device-join-key-rate-limit' });
|
||||
return;
|
||||
}
|
||||
sender.roomKeyRate += 1;
|
||||
setTimeout(_ => sender.roomKeyRate -= 1, 10000);
|
||||
|
||||
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
|
||||
this._send(sender, { type: 'pair-device-join-key-invalid' });
|
||||
return;
|
||||
}
|
||||
|
||||
const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
|
||||
const creator = this._roomSecrets[message.roomKey].creator;
|
||||
this._removeRoomKey(message.roomKey);
|
||||
@@ -306,13 +293,32 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_onPairDeviceCancel(sender) {
|
||||
if (sender.roomKey) {
|
||||
this._send(sender, {
|
||||
type: 'pair-device-canceled',
|
||||
roomKey: sender.roomKey,
|
||||
const roomKey = sender.roomKey
|
||||
|
||||
if (!roomKey) return;
|
||||
|
||||
this._removeRoomKey(roomKey);
|
||||
this._send(sender, {
|
||||
type: 'pair-device-canceled',
|
||||
roomKey: roomKey,
|
||||
});
|
||||
}
|
||||
|
||||
_onRegenerateRoomSecret(sender, message) {
|
||||
const oldRoomSecret = message.roomSecret;
|
||||
const newRoomSecret = randomizer.getRandomString(256);
|
||||
|
||||
// notify all other peers
|
||||
for (const peerId in this._rooms[oldRoomSecret]) {
|
||||
const peer = this._rooms[oldRoomSecret][peerId];
|
||||
this._send(peer, {
|
||||
type: 'room-secret-regenerated',
|
||||
oldRoomSecret: oldRoomSecret,
|
||||
newRoomSecret: newRoomSecret,
|
||||
});
|
||||
this._removeRoomKey(sender.roomKey);
|
||||
peer.removeRoomSecret(oldRoomSecret);
|
||||
}
|
||||
delete this._rooms[oldRoomSecret];
|
||||
}
|
||||
|
||||
_createRoomKey(creator, roomSecret) {
|
||||
@@ -480,7 +486,7 @@ class Peer {
|
||||
this._setIP(request);
|
||||
|
||||
// set peer id
|
||||
this._setPeerId(request)
|
||||
this._setPeerId(request);
|
||||
|
||||
// is WebRTC supported ?
|
||||
this.rtcSupported = request.url.indexOf('webrtc') > -1;
|
||||
@@ -510,6 +516,17 @@ class Peer {
|
||||
if (this.ip.substring(0,7) === "::ffff:")
|
||||
this.ip = this.ip.substring(7);
|
||||
|
||||
if (debugMode) {
|
||||
console.debug("----DEBUGGING-PEER-IP-START----");
|
||||
console.debug("remoteAddress:", request.connection.remoteAddress);
|
||||
console.debug("x-forwarded-for:", request.headers['x-forwarded-for']);
|
||||
console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']);
|
||||
console.debug("PairDrop uses:", this.ip);
|
||||
console.debug("IP is private:", this.ipIsPrivate(this.ip));
|
||||
console.debug("if IP is private, '127.0.0.1' is used instead");
|
||||
console.debug("----DEBUGGING-PEER-IP-END----");
|
||||
}
|
||||
|
||||
// IPv4 and IPv6 use different values to refer to localhost
|
||||
// put all peers on the same network as the server into the same room as well
|
||||
if (this.ip === '::1' || this.ipIsPrivate(this.ip)) {
|
||||
@@ -591,7 +608,7 @@ class Peer {
|
||||
separator: ' ',
|
||||
dictionaries: [colors, animals],
|
||||
style: 'capital',
|
||||
seed: this.id.hashCode()
|
||||
seed: cyrb53(this.id)
|
||||
})
|
||||
|
||||
this.name = {
|
||||
@@ -617,7 +634,7 @@ class Peer {
|
||||
}
|
||||
|
||||
isPeerIdHashValid(peerId, peerIdHash) {
|
||||
return peerIdHash === peerId.hashCode128BitSalted();
|
||||
return peerIdHash === hasher.hashCodeSalted(peerId);
|
||||
}
|
||||
|
||||
addRoomSecret(roomSecret) {
|
||||
@@ -633,39 +650,43 @@ class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode', {
|
||||
value: function() {
|
||||
return cyrb53(this);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
|
||||
value: function() {
|
||||
return hasher.hashCode128BitSalted(this);
|
||||
}
|
||||
});
|
||||
|
||||
const hasher = (() => {
|
||||
let seeds;
|
||||
let password;
|
||||
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];
|
||||
}
|
||||
hashCodeSalted(salt) {
|
||||
if (!password) {
|
||||
// password is created on first call.
|
||||
password = randomizer.getRandomString(128);
|
||||
}
|
||||
let hashCode = "";
|
||||
for (let i=0; i<4; i++) {
|
||||
hashCode += cyrb53(str, seeds[i]);
|
||||
}
|
||||
return hashCode;
|
||||
|
||||
return crypto.createHash("sha3-512")
|
||||
.update(password)
|
||||
.update(crypto.createHash("sha3-512").update(salt, "utf8").digest("hex"))
|
||||
.digest("hex");
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
const randomizer = (() => {
|
||||
return {
|
||||
getRandomString(length) {
|
||||
let string = "";
|
||||
while (string.length < length) {
|
||||
let arr = new Uint16Array(length);
|
||||
crypto.webcrypto.getRandomValues(arr);
|
||||
arr = Array.apply([], arr); /* turn into non-typed array */
|
||||
arr = arr.map(function (r) {
|
||||
return r % 128
|
||||
})
|
||||
arr = arr.filter(function (r) {
|
||||
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
|
||||
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
|
||||
});
|
||||
string += String.fromCharCode.apply(String, arr);
|
||||
}
|
||||
return string.substring(0, length)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
/*
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.5.2",
|
||||
"version": "1.7.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pairdrop",
|
||||
"version": "1.5.2",
|
||||
"version": "1.7.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"ua-parser-js": "^1.0.34",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"unique-names-generator": "^4.3.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
@@ -583,9 +583,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
||||
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
|
||||
"version": "1.0.35",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1070,9 +1070,9 @@
|
||||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
||||
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
|
||||
"version": "1.0.35",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA=="
|
||||
},
|
||||
"unique-names-generator": {
|
||||
"version": "4.7.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.5.2",
|
||||
"version": "1.7.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"ua-parser-js": "^1.0.34",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"unique-names-generator": "^4.3.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<!-- Web App Config -->
|
||||
<title>PairDrop</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#3367d6">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="apple-mobile-web-app-capable" content="no">
|
||||
@@ -39,37 +39,51 @@
|
||||
|
||||
<body translate="no">
|
||||
<header class="row-reverse">
|
||||
<a href="#about" class="icon-button" title="About PairDrop">
|
||||
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="theme" class="icon-button" title="Switch Darkmode/Lightmode" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="notification" class="icon-button" title="Enable Notifications" hidden>
|
||||
<div id="theme-wrapper">
|
||||
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-auto" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-light" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-dark" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="notification" class="icon-button" title="Enable Notifications" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="install" class="icon-button" title="Install PairDrop" hidden>
|
||||
</div>
|
||||
<div id="install" class="icon-button" title="Install PairDrop" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
</div>
|
||||
<div id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
|
||||
</div>
|
||||
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
<use xlink:href="#edit-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||
</header>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
@@ -112,12 +126,12 @@
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="center row-reverse">
|
||||
@@ -128,16 +142,18 @@
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Clear Devices Dialog -->
|
||||
<x-dialog id="clear-devices-dialog">
|
||||
<!-- Edit Paired Devices Dialog -->
|
||||
<x-dialog id="edit-paired-devices-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<h2 class="center">Edit Paired Devices</h2>
|
||||
<div class="paired-devices-wrapper"></div>
|
||||
<div class="font-subheading center">
|
||||
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
|
||||
</div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
<button class="button" type="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -206,9 +222,9 @@
|
||||
<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 id="text-input" title="Message" 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="CTRL/⌘ + ENTER" disabled>Send</button>
|
||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -250,7 +266,7 @@
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button">
|
||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
@@ -328,16 +344,24 @@
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z" />
|
||||
</symbol>
|
||||
<symbol id="icon-theme" viewBox="0 0 24 24">
|
||||
<symbol id="icon-theme-auto" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>
|
||||
</symbol>
|
||||
<symbol id="icon-theme-light" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"/></svg>
|
||||
</symbol>
|
||||
<symbol id="icon-theme-dark" viewBox="0 0 24 24">
|
||||
<rect fill="none" height="24" width="24"/><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/>
|
||||
</symbol>
|
||||
<symbol id="pair-device-icon" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M579.8 267.7c56.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 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.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.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
|
||||
</symbol>
|
||||
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
|
||||
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<!--! edited by @schlagmichdoch -->
|
||||
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.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.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-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.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
|
||||
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
|
||||
</symbol>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
@@ -345,11 +369,11 @@
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
<script src="scripts/theme.js"></script>
|
||||
<script src="scripts/network.js"></script>
|
||||
<script src="scripts/ui.js"></script>
|
||||
<script src="scripts/theme.js" async></script>
|
||||
<script src="scripts/qrcode.js" async></script>
|
||||
<script src="scripts/util.js" async></script>
|
||||
<script src="scripts/QRCode.min.js" async></script>
|
||||
<script src="scripts/zip.min.js" async></script>
|
||||
<script src="scripts/NoSleep.min.js" async></script>
|
||||
<!-- Sounds -->
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -3,16 +3,26 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti
|
||||
|
||||
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
|
||||
|
||||
window.hiddenProperty = 'hidden' in document ? 'hidden' :
|
||||
'webkitHidden' in document ? 'webkitHidden' :
|
||||
'mozHidden' in document ? 'mozHidden' :
|
||||
null;
|
||||
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
|
||||
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
|
||||
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
|
||||
null;
|
||||
|
||||
class ServerConnection {
|
||||
|
||||
constructor() {
|
||||
this._connect();
|
||||
Events.on('pagehide', _ => this._disconnect());
|
||||
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
|
||||
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
|
||||
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
|
||||
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
|
||||
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
|
||||
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
|
||||
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
|
||||
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
||||
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
||||
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
||||
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||
@@ -39,10 +49,6 @@ class ServerConnection {
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
||||
@@ -65,13 +71,13 @@ class ServerConnection {
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
if (msg.type !== 'ping') console.log('WS receive:', msg);
|
||||
switch (msg.type) {
|
||||
case 'rtc-config':
|
||||
this._setRtcConfig(msg.config);
|
||||
break;
|
||||
case 'peers':
|
||||
Events.fire('peers', msg);
|
||||
this._onPeers(msg);
|
||||
break;
|
||||
case 'peer-joined':
|
||||
Events.fire('peer-joined', msg);
|
||||
@@ -106,19 +112,41 @@ class ServerConnection {
|
||||
case 'secret-room-deleted':
|
||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||
break;
|
||||
case 'room-secret-regenerated':
|
||||
Events.fire('room-secret-regenerated', msg);
|
||||
break;
|
||||
default:
|
||||
console.error('WS: unknown message type', msg);
|
||||
console.error('WS receive: unknown message type', msg);
|
||||
}
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
if (!this._isConnected()) return;
|
||||
if (msg.type !== 'pong') console.log("WS send:", msg)
|
||||
this._socket.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
Events.fire('peers', msg);
|
||||
}
|
||||
|
||||
_onDisplayName(msg) {
|
||||
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
|
||||
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
||||
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
||||
if (!peerId) return;
|
||||
console.log("successfully added peerId to localStorage");
|
||||
|
||||
// Only now join rooms
|
||||
Events.fire('join-ip-room');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
});
|
||||
});
|
||||
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
@@ -138,13 +166,19 @@ class ServerConnection {
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
this._socket.onclose = null;
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
||||
console.log("successfully removed peerId from localStorage");
|
||||
});
|
||||
|
||||
if (!this._socket) return;
|
||||
|
||||
this._socket.onclose = null;
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
@@ -157,7 +191,7 @@ class ServerConnection {
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
if (document.hidden) return;
|
||||
if (window.hiddenProperty) return;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
@@ -181,13 +215,18 @@ class ServerConnection {
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
this._server = serverConnection;
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._roomType = roomType;
|
||||
this._roomSecret = roomSecret;
|
||||
this._updateRoomSecret(roomSecret);
|
||||
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
|
||||
// evaluate auto accept
|
||||
this._evaluateAutoAccept();
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
@@ -198,12 +237,47 @@ class Peer {
|
||||
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||
}
|
||||
|
||||
async createHeader(file) {
|
||||
return {
|
||||
name: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
_isSameBrowser() {
|
||||
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
||||
}
|
||||
|
||||
_updateRoomSecret(roomSecret) {
|
||||
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
|
||||
// -> do not delete duplicates and do not regenerate room secrets
|
||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
|
||||
// remove old roomSecrets to prevent multiple pairings with same peer
|
||||
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
|
||||
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
||||
})
|
||||
}
|
||||
|
||||
this._roomSecret = roomSecret;
|
||||
|
||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
|
||||
// increase security by increasing roomSecret length
|
||||
console.log('RoomSecret is regenerated to increase security')
|
||||
Events.fire('regenerate-room-secret', this._roomSecret);
|
||||
}
|
||||
}
|
||||
|
||||
_evaluateAutoAccept() {
|
||||
if (!this._roomSecret) {
|
||||
this._setAutoAccept(false);
|
||||
return;
|
||||
}
|
||||
|
||||
PersistentStorage.getRoomSecretEntry(this._roomSecret)
|
||||
.then(roomSecretEntry => {
|
||||
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
|
||||
this._setAutoAccept(autoAccept);
|
||||
})
|
||||
.catch(_ => {
|
||||
this._setAutoAccept(false);
|
||||
});
|
||||
}
|
||||
|
||||
_setAutoAccept(autoAccept) {
|
||||
this._autoAccept = autoAccept;
|
||||
}
|
||||
|
||||
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||
@@ -248,7 +322,11 @@ class Peer {
|
||||
let imagesOnly = true
|
||||
for (let i=0; i<files.length; i++) {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
|
||||
header.push(await this.createHeader(files[i]));
|
||||
header.push({
|
||||
name: files[i].name,
|
||||
mime: files[i].type,
|
||||
size: files[i].size
|
||||
});
|
||||
totalSize += files[i].size;
|
||||
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
|
||||
}
|
||||
@@ -360,7 +438,7 @@ class Peer {
|
||||
|
||||
_onFilesTransferRequest(request) {
|
||||
if (this._requestPending) {
|
||||
// Only accept one request at a time
|
||||
// Only accept one request at a time per peer
|
||||
this.sendJSON({type: 'files-transfer-response', accepted: false});
|
||||
return;
|
||||
}
|
||||
@@ -372,6 +450,14 @@ class Peer {
|
||||
}
|
||||
|
||||
this._requestPending = request;
|
||||
|
||||
if (this._autoAccept) {
|
||||
// auto accept if set via Edit Paired Devices Dialog
|
||||
this._respondToFileTransferRequest(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// default behavior: show user transfer request
|
||||
Events.fire('files-transfer-request', {
|
||||
request: request,
|
||||
peerId: this._peerId
|
||||
@@ -497,34 +583,39 @@ class Peer {
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName || this._displayName === message.displayName) return;
|
||||
this._displayName = message.displayName;
|
||||
const displayNameHasChanged = this._displayName !== message.displayName
|
||||
|
||||
if (message.displayName && displayNameHasChanged) {
|
||||
this._displayName = message.displayName;
|
||||
}
|
||||
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
|
||||
if (!displayNameHasChanged) return;
|
||||
Events.fire('notify-peer-display-name-changed', this._peerId);
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_connect(peerId, isCaller) {
|
||||
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
|
||||
_connect() {
|
||||
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
|
||||
|
||||
if (isCaller) {
|
||||
if (this._isCaller) {
|
||||
this._openChannel();
|
||||
} else {
|
||||
this._conn.ondatachannel = e => this._onChannelOpened(e);
|
||||
}
|
||||
}
|
||||
|
||||
_openConnection(peerId, isCaller) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
_openConnection() {
|
||||
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onicecandidateerror = e => this._onError(e);
|
||||
@@ -556,7 +647,7 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
if (!this._conn) this._connect(message.sender.id, false);
|
||||
if (!this._conn) this._connect();
|
||||
|
||||
if (message.sdp) {
|
||||
this._conn.setRemoteDescription(message.sdp)
|
||||
@@ -641,7 +732,7 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: channel closed', this._peerId);
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
if (!this._isCaller) return;
|
||||
this._connect(this._peerId, true); // reopen the channel
|
||||
this._connect(); // reopen the channel
|
||||
}
|
||||
|
||||
_onConnectionStateChange() {
|
||||
@@ -688,7 +779,11 @@ class RTCPeer extends Peer {
|
||||
refresh() {
|
||||
// check if channel is open. otherwise create one
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
this._connect(this._peerId, this._isCaller);
|
||||
|
||||
// only reconnect if peer is caller
|
||||
if (!this._isCaller) return;
|
||||
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_isConnected() {
|
||||
@@ -716,32 +811,63 @@ class PeersManager {
|
||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(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('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
|
||||
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
// if different roomType -> abort
|
||||
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
|
||||
if (!this.peers[message.sender.id]) {
|
||||
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
|
||||
}
|
||||
this.peers[message.sender.id].onServerMessage(message);
|
||||
const peerId = message.sender.id;
|
||||
this.peers[peerId].onServerMessage(message);
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
msg.peers.forEach(peer => {
|
||||
if (this.peers[peer.id]) {
|
||||
// if different roomType -> abort
|
||||
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
|
||||
this.peers[peer.id].refresh();
|
||||
return;
|
||||
}
|
||||
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
|
||||
_refreshPeer(peer, roomType, roomSecret) {
|
||||
if (!peer) return false;
|
||||
|
||||
const roomTypeIsSecret = roomType === "secret";
|
||||
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
|
||||
|
||||
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
|
||||
if (roomTypeIsSecret && roomSecretsDiffer) {
|
||||
peer._updateRoomSecret(roomSecret);
|
||||
peer._evaluateAutoAccept();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const roomTypesDiffer = peer._roomType !== roomType;
|
||||
|
||||
// if roomTypes differ peer is already connected -> abort
|
||||
if (roomTypesDiffer) return true;
|
||||
|
||||
peer.refresh();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer) {
|
||||
this._refreshPeer(peer, roomType, roomSecret);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret);
|
||||
}
|
||||
|
||||
_onPeerJoined(message) {
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret);
|
||||
}
|
||||
|
||||
_onPeers(message) {
|
||||
message.peers.forEach(peer => {
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -769,10 +895,19 @@ class PeersManager {
|
||||
this.peers[message.to].sendText(message.text);
|
||||
}
|
||||
|
||||
_onPeerLeft(msg) {
|
||||
if (msg.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
_onPeerLeft(message) {
|
||||
if (message.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', message.peerId);
|
||||
|
||||
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
|
||||
// Tidy up peerIds in localStorage
|
||||
if (Object.keys(this.peers).length === 0) {
|
||||
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
|
||||
if (!peerIds) return;
|
||||
console.log("successfully removed other peerIds from localStorage");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,12 +927,19 @@ class PeersManager {
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer._roomSecret === roomSecret) {
|
||||
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
|
||||
this._onPeerDisconnected(peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomSecretRegenerated(message) {
|
||||
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
|
||||
console.log("successfully regenerated room secret");
|
||||
Events.fire("room-secrets", [message.newRoomSecret]);
|
||||
})
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
@@ -813,6 +955,25 @@ class PeersManager {
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
// if the displayName has not been changed (yet) set the displayName to the original displayName
|
||||
if (!this._displayName) this._displayName = displayName;
|
||||
}
|
||||
|
||||
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
||||
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
|
||||
if (!peerId) return;
|
||||
this.peers[peerId]._setAutoAccept(autoAccept);
|
||||
}
|
||||
|
||||
_getPeerIdFromRoomSecret(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
// peer must have same roomSecret and not be on the same browser.
|
||||
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
|
||||
return peer._peerId;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,78 @@
|
||||
(function(){
|
||||
|
||||
// Select the button
|
||||
const btnTheme = document.getElementById('theme');
|
||||
// Check for dark mode preference at the OS level
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
// Get the user's theme preference from local storage, if it's available
|
||||
const currentTheme = localStorage.getItem('theme');
|
||||
// If the user's preference in localStorage is dark...
|
||||
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
|
||||
const $themeAuto = document.getElementById('theme-auto');
|
||||
const $themeLight = document.getElementById('theme-light');
|
||||
const $themeDark = document.getElementById('theme-dark');
|
||||
|
||||
let currentTheme = localStorage.getItem('theme');
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
// ...let's toggle the .dark-theme class on the body
|
||||
document.body.classList.toggle('dark-theme');
|
||||
// Otherwise, if the user's preference in localStorage is light...
|
||||
setModeToDark();
|
||||
} else if (currentTheme === 'light') {
|
||||
// ...let's toggle the .light-theme class on the body
|
||||
document.body.classList.toggle('light-theme');
|
||||
setModeToLight();
|
||||
}
|
||||
|
||||
// Listen for a click on the button
|
||||
btnTheme.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// If the user's OS setting is dark and matches our .dark-theme class...
|
||||
let theme;
|
||||
if (prefersDarkScheme.matches) {
|
||||
// ...then toggle the light mode class
|
||||
document.body.classList.toggle('light-theme');
|
||||
// ...but use .dark-theme if the .light-theme class is already on the body,
|
||||
theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
|
||||
|
||||
$themeAuto.addEventListener('click', _ => {
|
||||
if (currentTheme) {
|
||||
setModeToAuto();
|
||||
} else {
|
||||
// Otherwise, let's do the same thing, but for .dark-theme
|
||||
document.body.classList.toggle('dark-theme');
|
||||
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||
setModeToDark();
|
||||
}
|
||||
});
|
||||
$themeLight.addEventListener('click', _ => {
|
||||
if (currentTheme !== 'light') {
|
||||
setModeToLight();
|
||||
} else {
|
||||
setModeToAuto();
|
||||
}
|
||||
});
|
||||
$themeDark.addEventListener('click', _ => {
|
||||
if (currentTheme !== 'dark') {
|
||||
setModeToDark();
|
||||
} else {
|
||||
setModeToLight();
|
||||
}
|
||||
// Finally, let's save the current preference to localStorage to keep using it
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
function setModeToDark() {
|
||||
document.body.classList.remove('light-theme');
|
||||
document.body.classList.add('dark-theme');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
currentTheme = 'dark';
|
||||
|
||||
$themeAuto.classList.remove("selected");
|
||||
$themeLight.classList.remove("selected");
|
||||
$themeDark.classList.add("selected");
|
||||
}
|
||||
|
||||
function setModeToLight() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.add('light-theme');
|
||||
localStorage.setItem('theme', 'light');
|
||||
currentTheme = 'light';
|
||||
|
||||
$themeAuto.classList.remove("selected");
|
||||
$themeLight.classList.add("selected");
|
||||
$themeDark.classList.remove("selected");
|
||||
}
|
||||
|
||||
function setModeToAuto() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.remove('light-theme');
|
||||
if (prefersDarkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
} else if (prefersLightTheme) {
|
||||
document.body.classList.add('light-theme');
|
||||
}
|
||||
localStorage.removeItem('theme');
|
||||
currentTheme = undefined;
|
||||
|
||||
$themeAuto.classList.add("selected");
|
||||
$themeLight.classList.remove("selected");
|
||||
$themeDark.classList.remove("selected");
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -50,13 +50,19 @@ class PeersUI {
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
|
||||
|
||||
// Load saved display name on page load
|
||||
this._getSavedDisplayName().then(displayName => {
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||
});
|
||||
|
||||
|
||||
/* prevent animation on load */
|
||||
setTimeout(_ => {
|
||||
this.$xNoPeers.style.animationIterationCount = "1";
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_insertDisplayName(displayName) {
|
||||
@@ -81,26 +87,31 @@ class PeersUI {
|
||||
if (newDisplayName === savedDisplayName) return;
|
||||
|
||||
if (newDisplayName) {
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
}).catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
}).finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName)
|
||||
.then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
})
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
})
|
||||
.finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
} else {
|
||||
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
}).finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
PersistentStorage.delete('editedDisplayName')
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
})
|
||||
.finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +134,12 @@ class PeersUI {
|
||||
this.peers[peerId].name.displayName = displayName;
|
||||
const peerIdNode = $(peerId);
|
||||
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||
this._redrawPeerRoomTypes(peerId);
|
||||
}
|
||||
|
||||
_onPeerDisplayNameChanged(e) {
|
||||
if (!e.detail.displayName) return;
|
||||
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -136,26 +153,47 @@ class PeersUI {
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
|
||||
this._redrawPeer(this.peers[peer.id]);
|
||||
return; // peer already exists
|
||||
const existingPeer = this.peers[peer.id];
|
||||
if (existingPeer) {
|
||||
// peer already exists. Abort but add roomType to GUI and update roomSecret
|
||||
// skip if peer is a tab on the same browser
|
||||
if (!existingPeer.sameBrowser()) {
|
||||
// add roomType to PeerUI
|
||||
if (!existingPeer.roomTypes.includes(roomType)) {
|
||||
existingPeer.roomTypes.push(roomType);
|
||||
}
|
||||
this._redrawPeerRoomTypes(peer.id);
|
||||
|
||||
if (roomType === "secret") existingPeer.roomSecret = roomSecret;
|
||||
}
|
||||
return;
|
||||
}
|
||||
peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id);
|
||||
|
||||
if (!(roomType === "secret" && peer.sameBrowser())) {
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
}
|
||||
|
||||
this.peers[peer.id] = peer;
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId, connectionHash) {
|
||||
if(this.peers[peerId] && !$(peerId))
|
||||
new PeerUI(this.peers[peerId], connectionHash);
|
||||
if (!this.peers[peerId] || $(peerId)) return;
|
||||
|
||||
const peer = this.peers[peerId];
|
||||
|
||||
new PeerUI(peer, connectionHash);
|
||||
}
|
||||
|
||||
_redrawPeer(peer) {
|
||||
const peerNode = $(peer.id);
|
||||
_redrawPeerRoomTypes(peerId) {
|
||||
const peer = this.peers[peerId]
|
||||
const peerNode = $(peerId);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
if (!peer.sameBrowser()) {
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
}
|
||||
}
|
||||
|
||||
evaluateOverflowing() {
|
||||
@@ -175,14 +213,23 @@ class PeersUI {
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
this.evaluateOverflowing();
|
||||
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer.roomSecret === roomSecret) {
|
||||
let index = peer.roomTypes.indexOf('secret');
|
||||
peer.roomTypes.splice(index, 1);
|
||||
peer.roomSecret = "";
|
||||
|
||||
if (peer.roomTypes.length) {
|
||||
this._redrawPeerRoomTypes(peerId)
|
||||
return;
|
||||
}
|
||||
|
||||
this._onPeerDisconnected(peerId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,7 +362,6 @@ class PeerUI {
|
||||
$$('x-peers').appendChild(this.$el)
|
||||
Events.fire('peer-added');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
html() {
|
||||
@@ -360,8 +406,8 @@ class PeerUI {
|
||||
this.$el = document.createElement('x-peer');
|
||||
this.$el.id = this._peer.id;
|
||||
this.$el.ui = this;
|
||||
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
this.$el.classList.add('center');
|
||||
if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
this.html();
|
||||
|
||||
this._callbackInput = e => this._onFilesSelected(e)
|
||||
@@ -531,6 +577,10 @@ class Dialog {
|
||||
if (this.$autoFocus) this.$autoFocus.focus();
|
||||
}
|
||||
|
||||
isShown() {
|
||||
return !!this.$el.attributes["show"];
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$el.removeAttribute('show');
|
||||
if (this.$autoFocus) {
|
||||
@@ -539,10 +589,11 @@ class Dialog {
|
||||
}
|
||||
document.title = 'PairDrop';
|
||||
document.changeFavicon("images/favicon-96x96.png");
|
||||
this.correspondingPeerId = undefined;
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
if (this.correspondingPeerId === peerId) {
|
||||
if (this.isShown() && this.correspondingPeerId === peerId) {
|
||||
this.hide();
|
||||
Events.fire('notify-user', 'Selected peer left.')
|
||||
}
|
||||
@@ -808,14 +859,14 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
if (this.isShown() && e.code === "Escape") {
|
||||
this._respondToFileTransferRequest(false);
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestFileTransfer(request, peerId) {
|
||||
this._filesTransferRequestQueue.push({request: request, peerId: peerId});
|
||||
if (this.$el.attributes["show"]) return;
|
||||
if (this.isShown()) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
|
||||
@@ -858,8 +909,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$previewBox.innerHTML = '';
|
||||
// clear previewBox after dialog is closed
|
||||
setTimeout(_ => this.$previewBox.innerHTML = '', 300);
|
||||
|
||||
super.hide();
|
||||
|
||||
// show next request
|
||||
setTimeout(_ => this._dequeueRequests(), 500);
|
||||
}
|
||||
}
|
||||
@@ -872,7 +927,7 @@ class PairDeviceDialog extends Dialog {
|
||||
this.$roomKey = this.$el.querySelector('#room-key');
|
||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||
this.$pairDeviceBtn = $('pair-device');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$editPairedDevicesBtn = $('edit-paired-devices');
|
||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
this.$createJoinForm = this.$el.querySelector('form');
|
||||
|
||||
@@ -890,14 +945,18 @@ class PairDeviceDialog extends Dialog {
|
||||
Events.on('ws-disconnected', _ => this.hide());
|
||||
Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail));
|
||||
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
|
||||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
|
||||
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
|
||||
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
|
||||
Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
this.$el.addEventListener('paste', e => this._onPaste(e));
|
||||
|
||||
this.evaluateRoomKeyChars();
|
||||
this.evaluateUrlAttributes();
|
||||
|
||||
this.pairPeer = {};
|
||||
}
|
||||
|
||||
_onCharsInput(e) {
|
||||
@@ -913,7 +972,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
if (this.isShown() && e.code === "Escape") {
|
||||
// Timeout to prevent paste mode from getting cancelled simultaneously
|
||||
setTimeout(_ => this._pairDeviceCancel(), 50);
|
||||
}
|
||||
@@ -971,10 +1030,7 @@ class PairDeviceDialog extends Dialog {
|
||||
|
||||
_onWsConnected() {
|
||||
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}
|
||||
|
||||
_pairDeviceInitiate() {
|
||||
@@ -1022,22 +1078,69 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_pairDeviceJoined(peerId, roomSecret) {
|
||||
this.hide();
|
||||
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
|
||||
Events.fire('notify-user', 'Devices paired successfully.');
|
||||
const oldRoomSecret = $(peerId).ui.roomSecret;
|
||||
if (oldRoomSecret) PersistentStorage.deleteRoomSecret(oldRoomSecret);
|
||||
$(peerId).ui.roomSecret = roomSecret;
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).finally(_ => {
|
||||
// skip adding to IndexedDB if peer is another tab on the same browser
|
||||
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
|
||||
this._cleanUp();
|
||||
})
|
||||
.catch(_ => {
|
||||
Events.fire('notify-user', 'Paired devices are not persistent.');
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
this.hide();
|
||||
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.');
|
||||
return;
|
||||
}
|
||||
|
||||
// save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret
|
||||
this.pairPeer = {
|
||||
"peerId": peerId,
|
||||
"roomSecret": roomSecret
|
||||
};
|
||||
}
|
||||
|
||||
_onPeers(message) {
|
||||
if (!Object.keys(this.pairPeer)) return;
|
||||
|
||||
message.peers.forEach(messagePeer => {
|
||||
this._evaluateJoinedPeer(messagePeer.id, message.roomSecret);
|
||||
});
|
||||
}
|
||||
|
||||
_onPeerJoined(message) {
|
||||
if (!Object.keys(this.pairPeer)) return;
|
||||
|
||||
this._evaluateJoinedPeer(message.peer.id, message.roomSecret);
|
||||
}
|
||||
|
||||
_evaluateJoinedPeer(peerId, roomSecret) {
|
||||
const samePeerId = peerId === this.pairPeer.peerId;
|
||||
const sameRoomSecret = roomSecret === this.pairPeer.roomSecret;
|
||||
|
||||
if (!peerId || !roomSecret || !samePeerId || !sameRoomSecret) return;
|
||||
|
||||
this._onPairPeerJoined(peerId, roomSecret);
|
||||
this.pairPeer = {};
|
||||
}
|
||||
|
||||
_onPairPeerJoined(peerId, roomSecret) {
|
||||
// if devices are paired that are already connected we must save the names at this point
|
||||
const $peer = $(peerId);
|
||||
let displayName, deviceName;
|
||||
if ($peer) {
|
||||
displayName = $peer.ui._peer.name.displayName;
|
||||
deviceName = $peer.ui._peer.name.deviceName;
|
||||
}
|
||||
|
||||
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
|
||||
.then(_ => {
|
||||
Events.fire('notify-user', 'Devices paired successfully.');
|
||||
this._evaluateNumberRoomSecrets();
|
||||
})
|
||||
.finally(_ => {
|
||||
this._cleanUp();
|
||||
this.hide();
|
||||
})
|
||||
.catch(_ => {
|
||||
Events.fire('notify-user', 'Paired devices are not persistent.');
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
});
|
||||
}
|
||||
|
||||
_pairDeviceJoinKeyInvalid() {
|
||||
Events.fire('notify-user', 'Key not valid');
|
||||
}
|
||||
@@ -1058,58 +1161,123 @@ class PairDeviceDialog extends Dialog {
|
||||
this.inputRoomKey = '';
|
||||
this.$inputRoomKeyChars.forEach(el => el.value = '');
|
||||
this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", ""));
|
||||
}
|
||||
|
||||
_onClearRoomSecrets() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets-cleared', roomSecrets);
|
||||
PersistentStorage.clearRoomSecrets().finally(_ => {
|
||||
Events.fire('notify-user', 'All Devices unpaired.')
|
||||
this._evaluateNumberRoomSecrets();
|
||||
})
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
this.pairPeer = {};
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
}
|
||||
|
||||
_evaluateNumberRoomSecrets() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
if (roomSecrets.length > 0) {
|
||||
this.$clearSecretsBtn.removeAttribute('hidden');
|
||||
this.$editPairedDevicesBtn.removeAttribute('hidden');
|
||||
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
|
||||
} else {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$editPairedDevicesBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
|
||||
}
|
||||
Events.fire('bg-resize');
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
class EditPairedDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clear-devices-dialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
super('edit-paired-devices-dialog');
|
||||
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
|
||||
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());
|
||||
|
||||
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
_onKeyDown(e) {
|
||||
if (this.isShown() && e.code === "Escape") {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._clearRoomSecrets();
|
||||
async _initDOM() {
|
||||
const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();
|
||||
roomSecretsEntries.forEach(roomSecretsEntry => {
|
||||
let $pairedDevice = document.createElement('div');
|
||||
$pairedDevice.classList = ["paired-device"];
|
||||
|
||||
$pairedDevice.innerHTML = `
|
||||
<div class="display-name">
|
||||
<span>${roomSecretsEntry.display_name}</span>
|
||||
</div>
|
||||
<div class="device-name">
|
||||
<span>${roomSecretsEntry.device_name}</span>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<label class="auto-accept">auto-accept
|
||||
<input type="checkbox" ${roomSecretsEntry.auto_accept ? "checked" : ""}>
|
||||
</label>
|
||||
<button class="button" type="button">unpair</button>
|
||||
</div>`
|
||||
|
||||
$pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => {
|
||||
PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => {
|
||||
Events.fire('auto-accept-updated', {
|
||||
'roomSecret': roomSecretsEntry.entry.secret,
|
||||
'autoAccept': e.target.checked
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$pairedDevice.querySelector('button').addEventListener('click', e => {
|
||||
PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => {
|
||||
Events.fire('room-secrets-deleted', [roomSecret]);
|
||||
Events.fire('evaluate-number-room-secrets');
|
||||
e.target.parentNode.parentNode.remove();
|
||||
});
|
||||
})
|
||||
|
||||
this.$pairedDevicesWrapper.appendChild($pairedDevice)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
hide() {
|
||||
super.hide();
|
||||
setTimeout(_ => {
|
||||
this.$pairedDevicesWrapper.innerHTML = ""
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_onEditPairedDevices() {
|
||||
this._initDOM().then(_ => this.show());
|
||||
}
|
||||
|
||||
_clearRoomSecrets() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
PersistentStorage.getAllRoomSecrets()
|
||||
.then(roomSecrets => {
|
||||
PersistentStorage.clearRoomSecrets().finally(_ => {
|
||||
Events.fire('room-secrets-deleted', roomSecrets);
|
||||
Events.fire('evaluate-number-room-secrets');
|
||||
Events.fire('notify-user', 'All Devices unpaired.');
|
||||
this.hide();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
_onPeerDisplayNameChanged(e) {
|
||||
const peerId = e.detail.peerId;
|
||||
const peerNode = $(peerId);
|
||||
|
||||
if (!peerNode) return;
|
||||
|
||||
const peer = peerNode.ui._peer;
|
||||
|
||||
if (!peer.roomSecret) return;
|
||||
|
||||
PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => {
|
||||
console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,7 +1295,7 @@ class SendTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
async _onKeyDown(e) {
|
||||
if (this.$el.attributes["show"]) {
|
||||
if (this.isShown()) {
|
||||
if (e.code === "Escape") {
|
||||
this.hide();
|
||||
} else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
@@ -1196,7 +1364,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
async _onKeyDown(e) {
|
||||
if (this.$el.attributes["show"]) {
|
||||
if (this.isShown()) {
|
||||
if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) {
|
||||
await this._onCopy()
|
||||
this.hide();
|
||||
@@ -1210,7 +1378,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
window.blop.play();
|
||||
this._receiveTextQueue.push({text: text, peerId: peerId});
|
||||
this._setDocumentTitleMessages();
|
||||
if (this.$el.attributes["show"]) return;
|
||||
if (this.isShown()) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
|
||||
@@ -1571,27 +1739,15 @@ class NetworkStatusUI {
|
||||
constructor() {
|
||||
Events.on('offline', _ => this._showOfflineMessage());
|
||||
Events.on('online', _ => this._showOnlineMessage());
|
||||
Events.on('ws-connected', _ => this._onWsConnected());
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
if (!navigator.onLine) this._showOfflineMessage();
|
||||
}
|
||||
|
||||
_showOfflineMessage() {
|
||||
Events.fire('notify-user', 'You are offline');
|
||||
window.animateBackground(false);
|
||||
}
|
||||
|
||||
_showOnlineMessage() {
|
||||
Events.fire('notify-user', 'You are back online');
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
window.animateBackground(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1690,7 +1846,7 @@ class PersistentStorage {
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
return;
|
||||
}
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
console.log('Error initializing database: ');
|
||||
@@ -1701,27 +1857,32 @@ class PersistentStorage {
|
||||
};
|
||||
DBOpenRequest.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
const txn = e.target.transaction;
|
||||
|
||||
db.onerror = e => console.log('Error loading database: ' + e);
|
||||
try {
|
||||
|
||||
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
|
||||
|
||||
if (e.oldVersion === 0) {
|
||||
// initiate v1
|
||||
db.createObjectStore('keyval');
|
||||
} catch (error) {
|
||||
console.log("Object store named 'keyval' already exists")
|
||||
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
|
||||
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
|
||||
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
|
||||
} catch (error) {
|
||||
console.log("Object store named 'room_secrets' already exists")
|
||||
if (e.oldVersion <= 1) {
|
||||
// migrate to v2
|
||||
db.createObjectStore('share_target_files');
|
||||
}
|
||||
|
||||
try {
|
||||
if (db.objectStoreNames.contains('share_target_files')) {
|
||||
db.deleteObjectStore('share_target_files');
|
||||
}
|
||||
if (e.oldVersion <= 2) {
|
||||
// migrate to v3
|
||||
db.deleteObjectStore('share_target_files');
|
||||
db.createObjectStore('share_target_files', {autoIncrement: true});
|
||||
} catch (error) {
|
||||
console.log("Object store named 'share_target_files' already exists")
|
||||
}
|
||||
if (e.oldVersion <= 3) {
|
||||
// migrate to v4
|
||||
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
|
||||
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
|
||||
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1754,7 +1915,7 @@ class PersistentStorage {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const transaction = db.transaction('keyval', 'readonly');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.get(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
@@ -1787,16 +1948,21 @@ class PersistentStorage {
|
||||
})
|
||||
}
|
||||
|
||||
static addRoomSecret(roomSecret) {
|
||||
static addRoomSecret(roomSecret, displayName, deviceName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.add({'secret': roomSecret});
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. RoomSecret added: ${roomSecret}`);
|
||||
const objectStoreRequest = objectStore.add({
|
||||
'secret': roomSecret,
|
||||
'display_name': displayName,
|
||||
'device_name': deviceName,
|
||||
'auto_accept': false
|
||||
});
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
@@ -1806,21 +1972,31 @@ class PersistentStorage {
|
||||
})
|
||||
}
|
||||
|
||||
static getAllRoomSecrets() {
|
||||
static async getAllRoomSecrets() {
|
||||
try {
|
||||
const roomSecrets = await this.getAllRoomSecretEntries();
|
||||
let secrets = [];
|
||||
for (let i = 0; i < roomSecrets.length; i++) {
|
||||
secrets.push(roomSecrets[i].secret);
|
||||
}
|
||||
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
return(secrets);
|
||||
} catch (e) {
|
||||
this.logBrowserNotCapable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static getAllRoomSecretEntries() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const transaction = db.transaction('room_secrets', 'readonly');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.getAll();
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
let secrets = [];
|
||||
for (let i=0; i<e.target.result.length; i++) {
|
||||
secrets.push(e.target.result[i].secret);
|
||||
}
|
||||
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
resolve(secrets);
|
||||
resolve(e.target.result);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
@@ -1829,24 +2005,59 @@ class PersistentStorage {
|
||||
});
|
||||
}
|
||||
|
||||
static deleteRoomSecret(room_secret) {
|
||||
static getRoomSecretEntry(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readonly');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
const key = e.target.result;
|
||||
if (!key) {
|
||||
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestRetrieval = objectStore.get(key);
|
||||
objectStoreRequestRetrieval.onsuccess = e => {
|
||||
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
|
||||
resolve({
|
||||
"entry": e.target.result,
|
||||
"key": key
|
||||
});
|
||||
}
|
||||
objectStoreRequestRetrieval.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static deleteRoomSecret(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(room_secret);
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
if (!e.target.result) {
|
||||
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
|
||||
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
|
||||
const key = e.target.result;
|
||||
const objectStoreRequestDeletion = objectStore.delete(key);
|
||||
objectStoreRequestDeletion.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
|
||||
resolve();
|
||||
console.log(`Request successful. Deleted room_secret: ${key}`);
|
||||
resolve(roomSecret);
|
||||
}
|
||||
objectStoreRequestDeletion.onerror = (e) => {
|
||||
reject(e);
|
||||
@@ -1877,22 +2088,116 @@ class PersistentStorage {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
|
||||
}
|
||||
|
||||
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
|
||||
}
|
||||
|
||||
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
this.getRoomSecretEntry(roomSecret)
|
||||
.then(roomSecretEntry => {
|
||||
if (!roomSecretEntry) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
|
||||
const updatedRoomSecretEntry = {
|
||||
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
|
||||
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
|
||||
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
|
||||
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
|
||||
};
|
||||
|
||||
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
|
||||
|
||||
objectStoreRequestUpdate.onsuccess = e => {
|
||||
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
|
||||
resolve({
|
||||
"entry": updatedRoomSecretEntry,
|
||||
"key": roomSecretEntry.key
|
||||
});
|
||||
}
|
||||
|
||||
objectStoreRequestUpdate.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
.catch(e => reject(e));
|
||||
};
|
||||
|
||||
DBOpenRequest.onerror = e => reject(e);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast {
|
||||
class BrowserTabsConnector {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
|
||||
}
|
||||
|
||||
_broadcastMessage(message) {
|
||||
_broadcastSend(message) {
|
||||
this.bc.postMessage(message);
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast message received:', e.data)
|
||||
Events.fire(e.data.type, e.data.detail);
|
||||
console.log('Broadcast:', e.data)
|
||||
switch (e.data.type) {
|
||||
case 'self-display-name-changed':
|
||||
Events.fire('self-display-name-changed', e.data.detail);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static peerIsSameBrowser(peerId) {
|
||||
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
|
||||
return peerIdsBrowser
|
||||
? peerIdsBrowser.indexOf(peerId) !== -1
|
||||
: false;
|
||||
}
|
||||
|
||||
static async addPeerIdToLocalStorage() {
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
if (!peerId) return false;
|
||||
|
||||
let peerIdsBrowser = [];
|
||||
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser"));
|
||||
|
||||
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
|
||||
peerIdsBrowser.push(peerId);
|
||||
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
|
||||
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
|
||||
|
||||
return peerIdsBrowser;
|
||||
}
|
||||
|
||||
static async removePeerIdFromLocalStorage(peerId) {
|
||||
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
|
||||
const index = peerIdsBrowser.indexOf(peerId);
|
||||
peerIdsBrowser.splice(index, 1);
|
||||
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
|
||||
return peerId;
|
||||
}
|
||||
|
||||
|
||||
static async removeOtherPeerIdsFromLocalStorage() {
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
if (!peerId) return false;
|
||||
|
||||
let peerIdsBrowser = [peerId];
|
||||
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
|
||||
return peerIdsBrowser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1907,7 +2212,7 @@ class PairDrop {
|
||||
const sendTextDialog = new SendTextDialog();
|
||||
const receiveTextDialog = new ReceiveTextDialog();
|
||||
const pairDeviceDialog = new PairDeviceDialog();
|
||||
const clearDevicesDialog = new ClearDevicesDialog();
|
||||
const clearDevicesDialog = new EditPairedDevicesDialog();
|
||||
const base64ZipDialog = new Base64ZipDialog();
|
||||
const toast = new Toast();
|
||||
const notifications = new Notifications();
|
||||
@@ -1915,7 +2220,7 @@ class PairDrop {
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
const webFileHandlersUI = new WebFileHandlersUI();
|
||||
const noSleepUI = new NoSleepUI();
|
||||
const broadCast = new Broadcast();
|
||||
const broadCast = new BrowserTabsConnector();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1942,28 +2247,26 @@ window.addEventListener('beforeinstallprompt', e => {
|
||||
return e.preventDefault();
|
||||
});
|
||||
|
||||
// Background Animation
|
||||
// Background Circles
|
||||
Events.on('load', () => {
|
||||
let c = document.createElement('canvas');
|
||||
document.body.appendChild(c);
|
||||
let style = c.style;
|
||||
style.width = '100%';
|
||||
style.position = 'absolute';
|
||||
style.zIndex = -1;
|
||||
style.top = 0;
|
||||
style.left = 0;
|
||||
style.animation = "fade-in 800ms";
|
||||
let cCtx = c.getContext('2d');
|
||||
let x0, y0, w, h, dw, offset;
|
||||
|
||||
let offscreenCanvases = [];
|
||||
|
||||
function init() {
|
||||
let oldW = w;
|
||||
let oldH = h;
|
||||
let oldOffset = offset
|
||||
w = document.documentElement.clientWidth;
|
||||
h = document.documentElement.clientHeight;
|
||||
offset = $$('footer').offsetHeight - 32;
|
||||
offset = $$('footer').offsetHeight - 33;
|
||||
if (h > 800) offset += 16;
|
||||
|
||||
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
|
||||
@@ -1973,63 +2276,33 @@ Events.on('load', () => {
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.round(Math.max(w, h, 1000) / 13);
|
||||
drawCircles(cCtx, 0);
|
||||
|
||||
// enforce redrawing of frames
|
||||
offscreenCanvases = [];
|
||||
if (document.body.contains(c)) {
|
||||
document.body.removeChild(c);
|
||||
}
|
||||
drawCircles(cCtx, dw);
|
||||
document.body.appendChild(c);
|
||||
}
|
||||
|
||||
Events.on('bg-resize', _ => init());
|
||||
window.onresize = _ => Events.fire('bg-resize');
|
||||
|
||||
function drawCircle(ctx, radius) {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h));
|
||||
ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`;
|
||||
let opacity = 0.3 * (1 - 1.2 * radius / Math.max(w, h));
|
||||
ctx.strokeStyle = `rgba(128, 128, 128, ${opacity})`;
|
||||
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawCircles(ctx, frame) {
|
||||
for (let i = 0; i < 13; i++) {
|
||||
drawCircle(ctx, dw * i + frame);
|
||||
drawCircle(ctx, dw * i + frame + 33);
|
||||
}
|
||||
}
|
||||
|
||||
function createOffscreenCanvas(frame) {
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = c.width;
|
||||
canvas.height = c.height;
|
||||
offscreenCanvases[frame] = canvas;
|
||||
let ctx = canvas.getContext('2d');
|
||||
drawCircles(ctx, frame);
|
||||
}
|
||||
|
||||
function drawFrame(frame) {
|
||||
cCtx.clearRect(0, 0, w, h);
|
||||
if (!offscreenCanvases[frame]) {
|
||||
createOffscreenCanvas(frame);
|
||||
}
|
||||
cCtx.drawImage(offscreenCanvases[frame], 0, 0);
|
||||
}
|
||||
|
||||
let animate = true;
|
||||
let currentFrame = 0;
|
||||
|
||||
function animateBg() {
|
||||
if (currentFrame + 1 < dw || animate) {
|
||||
currentFrame = (currentFrame + 1) % dw;
|
||||
drawFrame(currentFrame);
|
||||
}
|
||||
setTimeout(_ => animateBg(), 3000 / dw);
|
||||
}
|
||||
|
||||
window.animateBackground = function(l) {
|
||||
animate = l;
|
||||
};
|
||||
|
||||
init();
|
||||
animateBg();
|
||||
});
|
||||
|
||||
document.changeFavicon = function (src) {
|
||||
|
||||
@@ -398,3 +398,7 @@ const cyrb53 = function(str, seed = 0) {
|
||||
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1>>>0);
|
||||
};
|
||||
|
||||
function onlyUnique (value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.5.2';
|
||||
const cacheVersion = 'v1.7.2';
|
||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -19,6 +19,10 @@ body {
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
overflow-y: hidden;
|
||||
/* Only allow selection on message and pair key */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -75,11 +79,75 @@ html {
|
||||
}
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
position: absolute;
|
||||
align-items: baseline;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
header > a,
|
||||
header > div {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
header > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
header > div .icon-button {
|
||||
height: 40px;
|
||||
transition: all 300ms;
|
||||
}
|
||||
|
||||
header > div > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header > div:not(:hover) .icon-button:not(.selected) {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#theme-wrapper:hover::before {
|
||||
border-radius: 20px;
|
||||
background: currentColor;
|
||||
opacity: 0.1;
|
||||
transition: opacity 300ms;
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
header > div:hover .icon-button.selected::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
header > div:hover .icon-button.selected:hover::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
header > div .icon-button:not(.selected) {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
header > div > div {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
@@ -140,7 +208,8 @@ body {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
a,
|
||||
.icon-button {
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
@@ -150,6 +219,14 @@ hr {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
min-width: 13px;
|
||||
}
|
||||
|
||||
x-noscript {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
@@ -192,15 +269,10 @@ x-noscript {
|
||||
}
|
||||
}
|
||||
|
||||
/* Main Header */
|
||||
|
||||
body>header a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#center {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin-top: 56px;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 132px;
|
||||
@@ -213,7 +285,7 @@ body>header a {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 425px) {
|
||||
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||
header:has(#edit-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 150px;
|
||||
}
|
||||
}
|
||||
@@ -345,10 +417,10 @@ x-no-peers {
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
animation: fade-in 300ms;
|
||||
animation-delay: 500ms;
|
||||
animation-fill-mode: backwards;
|
||||
/* prevent flickering on load */
|
||||
animation-iteration-count: 0;
|
||||
}
|
||||
|
||||
x-no-peers h2,
|
||||
@@ -382,8 +454,6 @@ x-no-peers[drop-bg] * {
|
||||
/* Peer */
|
||||
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 8px;
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
@@ -470,7 +540,6 @@ x-peer[status] x-icon {
|
||||
.status,
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
height: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -631,6 +700,12 @@ x-dialog x-paper {
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
#pair-device-dialog ::-moz-selection,
|
||||
#pair-device-dialog ::selection {
|
||||
color: black;
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -652,7 +727,7 @@ x-dialog .font-subheading {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* PairDevicesDialog */
|
||||
/* Pair Devices Dialog */
|
||||
|
||||
#key-input-container {
|
||||
width: 100%;
|
||||
@@ -660,7 +735,7 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#key-input-container>input {
|
||||
#key-input-container > input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
@@ -676,15 +751,18 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#key-input-container>input + * {
|
||||
#key-input-container > input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
#key-input-container > input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#room-key {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
font-size: 50px;
|
||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||
display: inline-block;
|
||||
@@ -696,14 +774,106 @@ x-dialog .font-subheading {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pair-device-dialog hr {
|
||||
margin: 40px -24px;
|
||||
x-dialog hr {
|
||||
margin: 40px -24px 30px -24px;
|
||||
border: solid 1.25px var(--border-color);
|
||||
}
|
||||
|
||||
#pair-device-dialog x-background {
|
||||
padding: 16px!important;
|
||||
}
|
||||
|
||||
/* Edit Paired Devices Dialog */
|
||||
.paired-devices-wrapper:empty:before {
|
||||
content: "No paired devices.";
|
||||
}
|
||||
|
||||
.paired-devices-wrapper:empty {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.paired-devices-wrapper {
|
||||
border-top: solid 4px var(--paired-device-color);
|
||||
border-bottom: solid 4px var(--paired-device-color);
|
||||
max-height: 65vh;
|
||||
overflow: scroll;
|
||||
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), .3), rgba(var(--text-color), 0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
|
||||
|
||||
/* Opera doesn't support this in the shorthand */
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
.paired-device {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.paired-device:not(:last-child) {
|
||||
border-bottom: solid 4px var(--paired-device-color);
|
||||
}
|
||||
|
||||
.paired-device > .display-name,
|
||||
.paired-device > .device-name {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
.paired-device span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper > label,
|
||||
.paired-device > .button-wrapper > button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
justify-content: center;
|
||||
width: 50%;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper > :not(:last-child) {
|
||||
border-right: solid 1px rgba(128, 128, 128, 0.5);
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper > :not(:first-child) {
|
||||
border-left: solid 1px rgba(128, 128, 128, 0.5);
|
||||
}
|
||||
|
||||
.paired-device * {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.paired-device > .auto-accept {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Receive Dialog */
|
||||
|
||||
x-dialog .row {
|
||||
@@ -752,7 +922,7 @@ x-paper > div:last-child > .button:not(:last-child) {
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
/* Todo: add pair underline to send / receive dialogs displayName */
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
@@ -770,9 +940,9 @@ x-dialog .dialog-subheader {
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
@@ -974,10 +1144,12 @@ button::-moz-focus-inner {
|
||||
|
||||
#about x-background {
|
||||
position: absolute;
|
||||
top: calc(32px - 250px);
|
||||
right: calc(32px - 250px);
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
--size: max(max(230vw, 230vh), calc(150vh + 150vw));
|
||||
--size-half: calc(var(--size)/2);
|
||||
top: calc(28px - var(--size-half));
|
||||
right: calc(36px - var(--size-half));
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
transform: scale(0);
|
||||
@@ -991,7 +1163,7 @@ button::-moz-focus-inner {
|
||||
}
|
||||
|
||||
#about:target x-background {
|
||||
transform: scale(10);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
#about .row a {
|
||||
@@ -1114,27 +1286,10 @@ x-peers:empty~x-instructions {
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
x-peer {
|
||||
transform: scale(0.95);
|
||||
padding: 4px 0;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
#websocket-fallback {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
#websocket-fallback>span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#websocket-fallback > span > span {
|
||||
border-bottom: solid 4px var(--ws-peer-color);
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
@@ -1277,3 +1432,9 @@ x-dialog x-paper {
|
||||
background: #bfbfbf;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-moz-selection,
|
||||
::selection {
|
||||
color: black;
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<!-- Web App Config -->
|
||||
<title>PairDrop</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#3367d6">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="apple-mobile-web-app-capable" content="no">
|
||||
@@ -39,37 +39,51 @@
|
||||
|
||||
<body translate="no">
|
||||
<header class="row-reverse">
|
||||
<a href="#about" class="icon-button" title="About PairDrop">
|
||||
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="theme" class="icon-button" title="Switch Darkmode/Lightmode" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="notification" class="icon-button" title="Enable Notifications" hidden>
|
||||
<div id="theme-wrapper">
|
||||
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-auto" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-light" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-dark" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="notification" class="icon-button" title="Enable Notifications" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="install" class="icon-button" title="Install PairDrop" hidden>
|
||||
</div>
|
||||
<div id="install" class="icon-button" title="Install PairDrop" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
</div>
|
||||
<div id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
|
||||
</div>
|
||||
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
<use xlink:href="#edit-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||
</header>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
@@ -115,12 +129,12 @@
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="center row-reverse">
|
||||
@@ -131,16 +145,18 @@
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Clear Devices Dialog -->
|
||||
<x-dialog id="clear-devices-dialog">
|
||||
<!-- Edit Paired Devices Dialog -->
|
||||
<x-dialog id="edit-paired-devices-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<h2 class="center">Edit Paired Devices</h2>
|
||||
<div class="paired-devices-wrapper"></div>
|
||||
<div class="font-subheading center">
|
||||
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
|
||||
</div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
<button class="button" type="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -209,9 +225,9 @@
|
||||
<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 id="text-input" title="Message" 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="CTRL/⌘ + ENTER" disabled>Send</button>
|
||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -253,7 +269,7 @@
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button">
|
||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
@@ -331,16 +347,24 @@
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z" />
|
||||
</symbol>
|
||||
<symbol id="icon-theme" viewBox="0 0 24 24">
|
||||
<symbol id="icon-theme-auto" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>
|
||||
</symbol>
|
||||
<symbol id="icon-theme-light" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-54 -54 620 620"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"/></svg>
|
||||
</symbol>
|
||||
<symbol id="icon-theme-dark" viewBox="0 0 24 24">
|
||||
<rect fill="none" height="24" width="24"/><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/>
|
||||
</symbol>
|
||||
<symbol id="pair-device-icon" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M579.8 267.7c56.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 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.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.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
|
||||
</symbol>
|
||||
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
|
||||
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<!--! edited by @schlagmichdoch -->
|
||||
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.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.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-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.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
|
||||
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
|
||||
</symbol>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
@@ -348,11 +372,11 @@
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
<script src="scripts/theme.js"></script>
|
||||
<script src="scripts/network.js"></script>
|
||||
<script src="scripts/ui.js"></script>
|
||||
<script src="scripts/theme.js" async></script>
|
||||
<script src="scripts/qrcode.js" async></script>
|
||||
<script src="scripts/util.js" async></script>
|
||||
<script src="scripts/QRCode.min.js" async></script>
|
||||
<script src="scripts/zip.min.js" async></script>
|
||||
<script src="scripts/NoSleep.min.js" async></script>
|
||||
<!-- Sounds -->
|
||||
|
||||
2
public_included_ws_fallback/robots.txt
Normal file
2
public_included_ws_fallback/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,16 +1,26 @@
|
||||
window.URL = window.URL || window.webkitURL;
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
window.hiddenProperty = 'hidden' in document ? 'hidden' :
|
||||
'webkitHidden' in document ? 'webkitHidden' :
|
||||
'mozHidden' in document ? 'mozHidden' :
|
||||
null;
|
||||
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
|
||||
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
|
||||
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
|
||||
null;
|
||||
|
||||
class ServerConnection {
|
||||
|
||||
constructor() {
|
||||
this._connect();
|
||||
Events.on('pagehide', _ => this._disconnect());
|
||||
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
|
||||
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
|
||||
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
|
||||
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
|
||||
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
|
||||
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
|
||||
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
|
||||
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
||||
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
||||
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
||||
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||
@@ -37,10 +47,6 @@ class ServerConnection {
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
||||
@@ -63,13 +69,13 @@ class ServerConnection {
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
if (msg.type !== 'ping') console.log('WS receive:', msg);
|
||||
switch (msg.type) {
|
||||
case 'rtc-config':
|
||||
this._setRtcConfig(msg.config);
|
||||
break;
|
||||
case 'peers':
|
||||
Events.fire('peers', msg);
|
||||
this._onPeers(msg);
|
||||
break;
|
||||
case 'peer-joined':
|
||||
Events.fire('peer-joined', msg);
|
||||
@@ -104,6 +110,9 @@ class ServerConnection {
|
||||
case 'secret-room-deleted':
|
||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||
break;
|
||||
case 'room-secret-regenerated':
|
||||
Events.fire('room-secret-regenerated', msg);
|
||||
break;
|
||||
case 'request':
|
||||
case 'header':
|
||||
case 'partition':
|
||||
@@ -118,18 +127,43 @@ class ServerConnection {
|
||||
Events.fire('ws-relay', JSON.stringify(msg));
|
||||
break;
|
||||
default:
|
||||
console.error('WS: unknown message type', msg);
|
||||
console.error('WS receive: unknown message type', msg);
|
||||
}
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
if (!this._isConnected()) return;
|
||||
if (msg.type !== 'pong') console.log("WS send:", msg)
|
||||
this._socket.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
Events.fire('peers', msg);
|
||||
if (msg.roomType === "ip" && msg.peers.length === 0) {
|
||||
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => {
|
||||
if (!peerId) return;
|
||||
console.log("successfully removed other peerIds from localStorage");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onDisplayName(msg) {
|
||||
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
|
||||
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
||||
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
||||
if (!peerId) return;
|
||||
console.log("successfully added peerId to localStorage");
|
||||
|
||||
// Only now join rooms
|
||||
Events.fire('join-ip-room');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
});
|
||||
});
|
||||
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
@@ -149,13 +183,19 @@ class ServerConnection {
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
this._socket.onclose = null;
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
||||
console.log("successfully removed peerId from localStorage");
|
||||
});
|
||||
|
||||
if (!this._socket) return;
|
||||
|
||||
this._socket.onclose = null;
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
@@ -168,7 +208,7 @@ class ServerConnection {
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
if (document.hidden) return;
|
||||
if (window.hiddenProperty) return;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
@@ -192,13 +232,18 @@ class ServerConnection {
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
this._server = serverConnection;
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._roomType = roomType;
|
||||
this._roomSecret = roomSecret;
|
||||
this._updateRoomSecret(roomSecret);
|
||||
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
|
||||
// evaluate auto accept
|
||||
this._evaluateAutoAccept();
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
@@ -209,12 +254,47 @@ class Peer {
|
||||
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||
}
|
||||
|
||||
async createHeader(file) {
|
||||
return {
|
||||
name: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
_isSameBrowser() {
|
||||
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
||||
}
|
||||
|
||||
_updateRoomSecret(roomSecret) {
|
||||
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
|
||||
// -> do not delete duplicates and do not regenerate room secrets
|
||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
|
||||
// remove old roomSecrets to prevent multiple pairings with same peer
|
||||
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
|
||||
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
||||
})
|
||||
}
|
||||
|
||||
this._roomSecret = roomSecret;
|
||||
|
||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
|
||||
// increase security by increasing roomSecret length
|
||||
console.log('RoomSecret is regenerated to increase security')
|
||||
Events.fire('regenerate-room-secret', this._roomSecret);
|
||||
}
|
||||
}
|
||||
|
||||
_evaluateAutoAccept() {
|
||||
if (!this._roomSecret) {
|
||||
this._setAutoAccept(false);
|
||||
return;
|
||||
}
|
||||
|
||||
PersistentStorage.getRoomSecretEntry(this._roomSecret)
|
||||
.then(roomSecretEntry => {
|
||||
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
|
||||
this._setAutoAccept(autoAccept);
|
||||
})
|
||||
.catch(_ => {
|
||||
this._setAutoAccept(false);
|
||||
});
|
||||
}
|
||||
|
||||
_setAutoAccept(autoAccept) {
|
||||
this._autoAccept = autoAccept;
|
||||
}
|
||||
|
||||
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||
@@ -259,7 +339,11 @@ class Peer {
|
||||
let imagesOnly = true
|
||||
for (let i=0; i<files.length; i++) {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
|
||||
header.push(await this.createHeader(files[i]));
|
||||
header.push({
|
||||
name: files[i].name,
|
||||
mime: files[i].type,
|
||||
size: files[i].size
|
||||
});
|
||||
totalSize += files[i].size;
|
||||
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
|
||||
}
|
||||
@@ -371,7 +455,7 @@ class Peer {
|
||||
|
||||
_onFilesTransferRequest(request) {
|
||||
if (this._requestPending) {
|
||||
// Only accept one request at a time
|
||||
// Only accept one request at a time per peer
|
||||
this.sendJSON({type: 'files-transfer-response', accepted: false});
|
||||
return;
|
||||
}
|
||||
@@ -383,6 +467,14 @@ class Peer {
|
||||
}
|
||||
|
||||
this._requestPending = request;
|
||||
|
||||
if (this._autoAccept) {
|
||||
// auto accept if set via Edit Paired Devices Dialog
|
||||
this._respondToFileTransferRequest(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// default behavior: show user transfer request
|
||||
Events.fire('files-transfer-request', {
|
||||
request: request,
|
||||
peerId: this._peerId
|
||||
@@ -508,34 +600,39 @@ class Peer {
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName || this._displayName === message.displayName) return;
|
||||
this._displayName = message.displayName;
|
||||
const displayNameHasChanged = this._displayName !== message.displayName
|
||||
|
||||
if (message.displayName && displayNameHasChanged) {
|
||||
this._displayName = message.displayName;
|
||||
}
|
||||
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
|
||||
if (!displayNameHasChanged) return;
|
||||
Events.fire('notify-peer-display-name-changed', this._peerId);
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_connect(peerId, isCaller) {
|
||||
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
|
||||
_connect() {
|
||||
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
|
||||
|
||||
if (isCaller) {
|
||||
if (this._isCaller) {
|
||||
this._openChannel();
|
||||
} else {
|
||||
this._conn.ondatachannel = e => this._onChannelOpened(e);
|
||||
}
|
||||
}
|
||||
|
||||
_openConnection(peerId, isCaller) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
_openConnection() {
|
||||
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onicecandidateerror = e => this._onError(e);
|
||||
@@ -567,7 +664,7 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
if (!this._conn) this._connect(message.sender.id, false);
|
||||
if (!this._conn) this._connect();
|
||||
|
||||
if (message.sdp) {
|
||||
this._conn.setRemoteDescription(message.sdp)
|
||||
@@ -652,7 +749,7 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: channel closed', this._peerId);
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
if (!this._isCaller) return;
|
||||
this._connect(this._peerId, true); // reopen the channel
|
||||
this._connect(); // reopen the channel
|
||||
}
|
||||
|
||||
_onConnectionStateChange() {
|
||||
@@ -699,7 +796,11 @@ class RTCPeer extends Peer {
|
||||
refresh() {
|
||||
// check if channel is open. otherwise create one
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
this._connect(this._peerId, this._isCaller);
|
||||
|
||||
// only reconnect if peer is caller
|
||||
if (!this._isCaller) return;
|
||||
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_isConnected() {
|
||||
@@ -718,11 +819,10 @@ class RTCPeer extends Peer {
|
||||
|
||||
class WSPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = false;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._isCaller = true;
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._sendSignal();
|
||||
}
|
||||
|
||||
@@ -734,7 +834,6 @@ class WSPeer extends Peer {
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
console.debug(message)
|
||||
message.to = this._peerId;
|
||||
message.roomType = this._roomType;
|
||||
message.roomSecret = this._roomSecret;
|
||||
@@ -769,49 +868,76 @@ class PeersManager {
|
||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(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('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
|
||||
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
// if different roomType -> abort
|
||||
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
|
||||
if (!this.peers[message.sender.id]) {
|
||||
if (window.isRtcSupported && message.sender.rtcSupported) {
|
||||
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
|
||||
} else {
|
||||
this.peers[message.sender.id] = new WSPeer(this._server, undefined, message.roomType, message.roomSecret);
|
||||
}
|
||||
const peerId = message.sender.id;
|
||||
this.peers[peerId].onServerMessage(message);
|
||||
}
|
||||
|
||||
_refreshPeer(peer, roomType, roomSecret) {
|
||||
if (!peer) return false;
|
||||
|
||||
const roomTypeIsSecret = roomType === "secret";
|
||||
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
|
||||
|
||||
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
|
||||
if (roomTypeIsSecret && roomSecretsDiffer) {
|
||||
peer._updateRoomSecret(roomSecret);
|
||||
peer._evaluateAutoAccept();
|
||||
|
||||
return true;
|
||||
}
|
||||
this.peers[message.sender.id].onServerMessage(message);
|
||||
|
||||
const roomTypesDiffer = peer._roomType !== roomType;
|
||||
|
||||
// if roomTypes differ peer is already connected -> abort
|
||||
if (roomTypesDiffer) return true;
|
||||
|
||||
peer.refresh();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret, rtcSupported) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer) {
|
||||
this._refreshPeer(peer, roomType, roomSecret);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.isRtcSupported && rtcSupported) {
|
||||
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret);
|
||||
} else {
|
||||
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret);
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerJoined(message) {
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported);
|
||||
}
|
||||
|
||||
_onPeers(message) {
|
||||
message.peers.forEach(peer => {
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported);
|
||||
})
|
||||
}
|
||||
|
||||
_onWsRelay(message) {
|
||||
const messageJSON = JSON.parse(message)
|
||||
const messageJSON = JSON.parse(message);
|
||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
||||
this.peers[messageJSON.sender.id]._onMessage(message)
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
msg.peers.forEach(peer => {
|
||||
if (this.peers[peer.id]) {
|
||||
// if different roomType -> abort
|
||||
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
|
||||
this.peers[peer.id].refresh();
|
||||
return;
|
||||
}
|
||||
if (window.isRtcSupported && peer.rtcSupported) {
|
||||
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
|
||||
} else {
|
||||
this.peers[peer.id] = new WSPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
|
||||
}
|
||||
})
|
||||
this.peers[messageJSON.sender.id]._onMessage(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
@@ -838,13 +964,22 @@ class PeersManager {
|
||||
this.peers[message.to].sendText(message.text);
|
||||
}
|
||||
|
||||
_onPeerLeft(msg) {
|
||||
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
console.log('WSPeer left:', msg.peerId);
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
} else if (msg.disconnect === true) {
|
||||
_onPeerLeft(message) {
|
||||
if (this.peers[message.peerId] && (!this.peers[message.peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
console.log('WSPeer left:', message.peerId);
|
||||
}
|
||||
if (message.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
Events.fire('peer-disconnected', message.peerId);
|
||||
|
||||
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
|
||||
// Tidy up peerIds in localStorage
|
||||
if (Object.keys(this.peers).length === 0) {
|
||||
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
|
||||
if (!peerIds) return;
|
||||
console.log("successfully removed other peerIds from localStorage");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,7 +989,6 @@ class PeersManager {
|
||||
|
||||
_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);
|
||||
}
|
||||
@@ -873,12 +1007,19 @@ class PeersManager {
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer._roomSecret === roomSecret) {
|
||||
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
|
||||
this._onPeerDisconnected(peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomSecretRegenerated(message) {
|
||||
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
|
||||
console.log("successfully regenerated room secret");
|
||||
Events.fire("room-secrets", [message.newRoomSecret]);
|
||||
})
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
@@ -894,6 +1035,25 @@ class PeersManager {
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
// if the displayName has not been changed (yet) set the displayName to the original displayName
|
||||
if (!this._displayName) this._displayName = displayName;
|
||||
}
|
||||
|
||||
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
||||
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
|
||||
if (!peerId) return;
|
||||
this.peers[peerId]._setAutoAccept(autoAccept);
|
||||
}
|
||||
|
||||
_getPeerIdFromRoomSecret(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
// peer must have same roomSecret and not be on the same browser.
|
||||
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
|
||||
return peer._peerId;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,78 @@
|
||||
(function(){
|
||||
|
||||
// Select the button
|
||||
const btnTheme = document.getElementById('theme');
|
||||
// Check for dark mode preference at the OS level
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
// Get the user's theme preference from local storage, if it's available
|
||||
const currentTheme = localStorage.getItem('theme');
|
||||
// If the user's preference in localStorage is dark...
|
||||
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
|
||||
const $themeAuto = document.getElementById('theme-auto');
|
||||
const $themeLight = document.getElementById('theme-light');
|
||||
const $themeDark = document.getElementById('theme-dark');
|
||||
|
||||
let currentTheme = localStorage.getItem('theme');
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
// ...let's toggle the .dark-theme class on the body
|
||||
document.body.classList.toggle('dark-theme');
|
||||
// Otherwise, if the user's preference in localStorage is light...
|
||||
setModeToDark();
|
||||
} else if (currentTheme === 'light') {
|
||||
// ...let's toggle the .light-theme class on the body
|
||||
document.body.classList.toggle('light-theme');
|
||||
setModeToLight();
|
||||
}
|
||||
|
||||
// Listen for a click on the button
|
||||
btnTheme.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// If the user's OS setting is dark and matches our .dark-theme class...
|
||||
let theme;
|
||||
if (prefersDarkScheme.matches) {
|
||||
// ...then toggle the light mode class
|
||||
document.body.classList.toggle('light-theme');
|
||||
// ...but use .dark-theme if the .light-theme class is already on the body,
|
||||
theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
|
||||
|
||||
$themeAuto.addEventListener('click', _ => {
|
||||
if (currentTheme) {
|
||||
setModeToAuto();
|
||||
} else {
|
||||
// Otherwise, let's do the same thing, but for .dark-theme
|
||||
document.body.classList.toggle('dark-theme');
|
||||
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||
setModeToDark();
|
||||
}
|
||||
});
|
||||
$themeLight.addEventListener('click', _ => {
|
||||
if (currentTheme !== 'light') {
|
||||
setModeToLight();
|
||||
} else {
|
||||
setModeToAuto();
|
||||
}
|
||||
});
|
||||
$themeDark.addEventListener('click', _ => {
|
||||
if (currentTheme !== 'dark') {
|
||||
setModeToDark();
|
||||
} else {
|
||||
setModeToLight();
|
||||
}
|
||||
// Finally, let's save the current preference to localStorage to keep using it
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
function setModeToDark() {
|
||||
document.body.classList.remove('light-theme');
|
||||
document.body.classList.add('dark-theme');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
currentTheme = 'dark';
|
||||
|
||||
$themeAuto.classList.remove("selected");
|
||||
$themeLight.classList.remove("selected");
|
||||
$themeDark.classList.add("selected");
|
||||
}
|
||||
|
||||
function setModeToLight() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.add('light-theme');
|
||||
localStorage.setItem('theme', 'light');
|
||||
currentTheme = 'light';
|
||||
|
||||
$themeAuto.classList.remove("selected");
|
||||
$themeLight.classList.add("selected");
|
||||
$themeDark.classList.remove("selected");
|
||||
}
|
||||
|
||||
function setModeToAuto() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.remove('light-theme');
|
||||
if (prefersDarkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
} else if (prefersLightTheme) {
|
||||
document.body.classList.add('light-theme');
|
||||
}
|
||||
localStorage.removeItem('theme');
|
||||
currentTheme = undefined;
|
||||
|
||||
$themeAuto.classList.add("selected");
|
||||
$themeLight.classList.remove("selected");
|
||||
$themeDark.classList.remove("selected");
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -50,13 +50,19 @@ class PeersUI {
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
|
||||
|
||||
// Load saved display name on page load
|
||||
this._getSavedDisplayName().then(displayName => {
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||
});
|
||||
|
||||
|
||||
/* prevent animation on load */
|
||||
setTimeout(_ => {
|
||||
this.$xNoPeers.style.animationIterationCount = "1";
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_insertDisplayName(displayName) {
|
||||
@@ -81,26 +87,31 @@ class PeersUI {
|
||||
if (newDisplayName === savedDisplayName) return;
|
||||
|
||||
if (newDisplayName) {
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
}).catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
}).finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName)
|
||||
.then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
})
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
})
|
||||
.finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
} else {
|
||||
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
}).finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
PersistentStorage.delete('editedDisplayName')
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
})
|
||||
.finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +134,12 @@ class PeersUI {
|
||||
this.peers[peerId].name.displayName = displayName;
|
||||
const peerIdNode = $(peerId);
|
||||
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||
this._redrawPeerRoomTypes(peerId);
|
||||
}
|
||||
|
||||
_onPeerDisplayNameChanged(e) {
|
||||
if (!e.detail.displayName) return;
|
||||
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -136,26 +153,47 @@ class PeersUI {
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
|
||||
this._redrawPeer(this.peers[peer.id]);
|
||||
return; // peer already exists
|
||||
const existingPeer = this.peers[peer.id];
|
||||
if (existingPeer) {
|
||||
// peer already exists. Abort but add roomType to GUI and update roomSecret
|
||||
// skip if peer is a tab on the same browser
|
||||
if (!existingPeer.sameBrowser()) {
|
||||
// add roomType to PeerUI
|
||||
if (!existingPeer.roomTypes.includes(roomType)) {
|
||||
existingPeer.roomTypes.push(roomType);
|
||||
}
|
||||
this._redrawPeerRoomTypes(peer.id);
|
||||
|
||||
if (roomType === "secret") existingPeer.roomSecret = roomSecret;
|
||||
}
|
||||
return;
|
||||
}
|
||||
peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id);
|
||||
|
||||
if (!(roomType === "secret" && peer.sameBrowser())) {
|
||||
peer.roomTypes = [roomType];
|
||||
peer.roomSecret = roomSecret;
|
||||
}
|
||||
|
||||
this.peers[peer.id] = peer;
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId, connectionHash) {
|
||||
if(this.peers[peerId] && !$(peerId))
|
||||
new PeerUI(this.peers[peerId], connectionHash);
|
||||
if (!this.peers[peerId] || $(peerId)) return;
|
||||
|
||||
const peer = this.peers[peerId];
|
||||
|
||||
new PeerUI(peer, connectionHash);
|
||||
}
|
||||
|
||||
_redrawPeer(peer) {
|
||||
const peerNode = $(peer.id);
|
||||
_redrawPeerRoomTypes(peerId) {
|
||||
const peer = this.peers[peerId]
|
||||
const peerNode = $(peerId);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
if (!peer.sameBrowser()) {
|
||||
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
|
||||
}
|
||||
}
|
||||
|
||||
evaluateOverflowing() {
|
||||
@@ -175,14 +213,23 @@ class PeersUI {
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
this.evaluateOverflowing();
|
||||
if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer.roomSecret === roomSecret) {
|
||||
let index = peer.roomTypes.indexOf('secret');
|
||||
peer.roomTypes.splice(index, 1);
|
||||
peer.roomSecret = "";
|
||||
|
||||
if (peer.roomTypes.length) {
|
||||
this._redrawPeerRoomTypes(peerId)
|
||||
return;
|
||||
}
|
||||
|
||||
this._onPeerDisconnected(peerId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,7 +362,6 @@ class PeerUI {
|
||||
$$('x-peers').appendChild(this.$el)
|
||||
Events.fire('peer-added');
|
||||
this.$xInstructions = $$('x-instructions');
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
html() {
|
||||
@@ -360,9 +406,9 @@ class PeerUI {
|
||||
this.$el = document.createElement('x-peer');
|
||||
this.$el.id = this._peer.id;
|
||||
this.$el.ui = this;
|
||||
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
this.$el.classList.add('center');
|
||||
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer')
|
||||
if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
||||
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer');
|
||||
this.html();
|
||||
|
||||
this._callbackInput = e => this._onFilesSelected(e)
|
||||
@@ -532,6 +578,10 @@ class Dialog {
|
||||
if (this.$autoFocus) this.$autoFocus.focus();
|
||||
}
|
||||
|
||||
isShown() {
|
||||
return !!this.$el.attributes["show"];
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$el.removeAttribute('show');
|
||||
if (this.$autoFocus) {
|
||||
@@ -540,10 +590,11 @@ class Dialog {
|
||||
}
|
||||
document.title = 'PairDrop';
|
||||
document.changeFavicon("images/favicon-96x96.png");
|
||||
this.correspondingPeerId = undefined;
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
if (this.correspondingPeerId === peerId) {
|
||||
if (this.isShown() && this.correspondingPeerId === peerId) {
|
||||
this.hide();
|
||||
Events.fire('notify-user', 'Selected peer left.')
|
||||
}
|
||||
@@ -809,14 +860,14 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
if (this.isShown() && e.code === "Escape") {
|
||||
this._respondToFileTransferRequest(false);
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestFileTransfer(request, peerId) {
|
||||
this._filesTransferRequestQueue.push({request: request, peerId: peerId});
|
||||
if (this.$el.attributes["show"]) return;
|
||||
if (this.isShown()) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
|
||||
@@ -859,8 +910,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$previewBox.innerHTML = '';
|
||||
// clear previewBox after dialog is closed
|
||||
setTimeout(_ => this.$previewBox.innerHTML = '', 300);
|
||||
|
||||
super.hide();
|
||||
|
||||
// show next request
|
||||
setTimeout(_ => this._dequeueRequests(), 500);
|
||||
}
|
||||
}
|
||||
@@ -873,7 +928,7 @@ class PairDeviceDialog extends Dialog {
|
||||
this.$roomKey = this.$el.querySelector('#room-key');
|
||||
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
|
||||
this.$pairDeviceBtn = $('pair-device');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$editPairedDevicesBtn = $('edit-paired-devices');
|
||||
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
this.$createJoinForm = this.$el.querySelector('form');
|
||||
|
||||
@@ -891,14 +946,18 @@ class PairDeviceDialog extends Dialog {
|
||||
Events.on('ws-disconnected', _ => this.hide());
|
||||
Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail));
|
||||
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
|
||||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
|
||||
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
|
||||
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
|
||||
Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
this.$el.addEventListener('paste', e => this._onPaste(e));
|
||||
|
||||
this.evaluateRoomKeyChars();
|
||||
this.evaluateUrlAttributes();
|
||||
|
||||
this.pairPeer = {};
|
||||
}
|
||||
|
||||
_onCharsInput(e) {
|
||||
@@ -914,7 +973,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
if (this.isShown() && e.code === "Escape") {
|
||||
// Timeout to prevent paste mode from getting cancelled simultaneously
|
||||
setTimeout(_ => this._pairDeviceCancel(), 50);
|
||||
}
|
||||
@@ -972,10 +1031,7 @@ class PairDeviceDialog extends Dialog {
|
||||
|
||||
_onWsConnected() {
|
||||
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}
|
||||
|
||||
_pairDeviceInitiate() {
|
||||
@@ -1023,22 +1079,69 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_pairDeviceJoined(peerId, roomSecret) {
|
||||
this.hide();
|
||||
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
|
||||
Events.fire('notify-user', 'Devices paired successfully.');
|
||||
const oldRoomSecret = $(peerId).ui.roomSecret;
|
||||
if (oldRoomSecret) PersistentStorage.deleteRoomSecret(oldRoomSecret);
|
||||
$(peerId).ui.roomSecret = roomSecret;
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).finally(_ => {
|
||||
// skip adding to IndexedDB if peer is another tab on the same browser
|
||||
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
|
||||
this._cleanUp();
|
||||
})
|
||||
.catch(_ => {
|
||||
Events.fire('notify-user', 'Paired devices are not persistent.');
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
this.hide();
|
||||
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.');
|
||||
return;
|
||||
}
|
||||
|
||||
// save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret
|
||||
this.pairPeer = {
|
||||
"peerId": peerId,
|
||||
"roomSecret": roomSecret
|
||||
};
|
||||
}
|
||||
|
||||
_onPeers(message) {
|
||||
if (!Object.keys(this.pairPeer)) return;
|
||||
|
||||
message.peers.forEach(messagePeer => {
|
||||
this._evaluateJoinedPeer(messagePeer.id, message.roomSecret);
|
||||
});
|
||||
}
|
||||
|
||||
_onPeerJoined(message) {
|
||||
if (!Object.keys(this.pairPeer)) return;
|
||||
|
||||
this._evaluateJoinedPeer(message.peer.id, message.roomSecret);
|
||||
}
|
||||
|
||||
_evaluateJoinedPeer(peerId, roomSecret) {
|
||||
const samePeerId = peerId === this.pairPeer.peerId;
|
||||
const sameRoomSecret = roomSecret === this.pairPeer.roomSecret;
|
||||
|
||||
if (!peerId || !roomSecret || !samePeerId || !sameRoomSecret) return;
|
||||
|
||||
this._onPairPeerJoined(peerId, roomSecret);
|
||||
this.pairPeer = {};
|
||||
}
|
||||
|
||||
_onPairPeerJoined(peerId, roomSecret) {
|
||||
// if devices are paired that are already connected we must save the names at this point
|
||||
const $peer = $(peerId);
|
||||
let displayName, deviceName;
|
||||
if ($peer) {
|
||||
displayName = $peer.ui._peer.name.displayName;
|
||||
deviceName = $peer.ui._peer.name.deviceName;
|
||||
}
|
||||
|
||||
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
|
||||
.then(_ => {
|
||||
Events.fire('notify-user', 'Devices paired successfully.');
|
||||
this._evaluateNumberRoomSecrets();
|
||||
})
|
||||
.finally(_ => {
|
||||
this._cleanUp();
|
||||
this.hide();
|
||||
})
|
||||
.catch(_ => {
|
||||
Events.fire('notify-user', 'Paired devices are not persistent.');
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
});
|
||||
}
|
||||
|
||||
_pairDeviceJoinKeyInvalid() {
|
||||
Events.fire('notify-user', 'Key not valid');
|
||||
}
|
||||
@@ -1059,58 +1162,123 @@ class PairDeviceDialog extends Dialog {
|
||||
this.inputRoomKey = '';
|
||||
this.$inputRoomKeyChars.forEach(el => el.value = '');
|
||||
this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", ""));
|
||||
}
|
||||
|
||||
_onClearRoomSecrets() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets-cleared', roomSecrets);
|
||||
PersistentStorage.clearRoomSecrets().finally(_ => {
|
||||
Events.fire('notify-user', 'All Devices unpaired.')
|
||||
this._evaluateNumberRoomSecrets();
|
||||
})
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
this.pairPeer = {};
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
}
|
||||
|
||||
_evaluateNumberRoomSecrets() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
if (roomSecrets.length > 0) {
|
||||
this.$clearSecretsBtn.removeAttribute('hidden');
|
||||
this.$editPairedDevicesBtn.removeAttribute('hidden');
|
||||
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
|
||||
} else {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$editPairedDevicesBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
|
||||
}
|
||||
Events.fire('bg-resize');
|
||||
}).catch(_ => PersistentStorage.logBrowserNotCapable());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
class EditPairedDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clear-devices-dialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
super('edit-paired-devices-dialog');
|
||||
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
|
||||
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());
|
||||
|
||||
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
_onKeyDown(e) {
|
||||
if (this.isShown() && e.code === "Escape") {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._clearRoomSecrets();
|
||||
async _initDOM() {
|
||||
const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();
|
||||
roomSecretsEntries.forEach(roomSecretsEntry => {
|
||||
let $pairedDevice = document.createElement('div');
|
||||
$pairedDevice.classList = ["paired-device"];
|
||||
|
||||
$pairedDevice.innerHTML = `
|
||||
<div class="display-name">
|
||||
<span>${roomSecretsEntry.display_name}</span>
|
||||
</div>
|
||||
<div class="device-name">
|
||||
<span>${roomSecretsEntry.device_name}</span>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<label class="auto-accept">auto-accept
|
||||
<input type="checkbox" ${roomSecretsEntry.auto_accept ? "checked" : ""}>
|
||||
</label>
|
||||
<button class="button" type="button">unpair</button>
|
||||
</div>`
|
||||
|
||||
$pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => {
|
||||
PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => {
|
||||
Events.fire('auto-accept-updated', {
|
||||
'roomSecret': roomSecretsEntry.entry.secret,
|
||||
'autoAccept': e.target.checked
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$pairedDevice.querySelector('button').addEventListener('click', e => {
|
||||
PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => {
|
||||
Events.fire('room-secrets-deleted', [roomSecret]);
|
||||
Events.fire('evaluate-number-room-secrets');
|
||||
e.target.parentNode.parentNode.remove();
|
||||
});
|
||||
})
|
||||
|
||||
this.$pairedDevicesWrapper.appendChild($pairedDevice)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
hide() {
|
||||
super.hide();
|
||||
setTimeout(_ => {
|
||||
this.$pairedDevicesWrapper.innerHTML = ""
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_onEditPairedDevices() {
|
||||
this._initDOM().then(_ => this.show());
|
||||
}
|
||||
|
||||
_clearRoomSecrets() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
PersistentStorage.getAllRoomSecrets()
|
||||
.then(roomSecrets => {
|
||||
PersistentStorage.clearRoomSecrets().finally(_ => {
|
||||
Events.fire('room-secrets-deleted', roomSecrets);
|
||||
Events.fire('evaluate-number-room-secrets');
|
||||
Events.fire('notify-user', 'All Devices unpaired.');
|
||||
this.hide();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
_onPeerDisplayNameChanged(e) {
|
||||
const peerId = e.detail.peerId;
|
||||
const peerNode = $(peerId);
|
||||
|
||||
if (!peerNode) return;
|
||||
|
||||
const peer = peerNode.ui._peer;
|
||||
|
||||
if (!peer.roomSecret) return;
|
||||
|
||||
PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => {
|
||||
console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1128,7 +1296,7 @@ class SendTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
async _onKeyDown(e) {
|
||||
if (this.$el.attributes["show"]) {
|
||||
if (this.isShown()) {
|
||||
if (e.code === "Escape") {
|
||||
this.hide();
|
||||
} else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
@@ -1197,7 +1365,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
}
|
||||
|
||||
async _onKeyDown(e) {
|
||||
if (this.$el.attributes["show"]) {
|
||||
if (this.isShown()) {
|
||||
if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) {
|
||||
await this._onCopy()
|
||||
this.hide();
|
||||
@@ -1211,7 +1379,7 @@ class ReceiveTextDialog extends Dialog {
|
||||
window.blop.play();
|
||||
this._receiveTextQueue.push({text: text, peerId: peerId});
|
||||
this._setDocumentTitleMessages();
|
||||
if (this.$el.attributes["show"]) return;
|
||||
if (this.isShown()) return;
|
||||
this._dequeueRequests();
|
||||
}
|
||||
|
||||
@@ -1572,27 +1740,15 @@ class NetworkStatusUI {
|
||||
constructor() {
|
||||
Events.on('offline', _ => this._showOfflineMessage());
|
||||
Events.on('online', _ => this._showOnlineMessage());
|
||||
Events.on('ws-connected', _ => this._onWsConnected());
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
if (!navigator.onLine) this._showOfflineMessage();
|
||||
}
|
||||
|
||||
_showOfflineMessage() {
|
||||
Events.fire('notify-user', 'You are offline');
|
||||
window.animateBackground(false);
|
||||
}
|
||||
|
||||
_showOnlineMessage() {
|
||||
Events.fire('notify-user', 'You are back online');
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
window.animateBackground(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1691,7 +1847,7 @@ class PersistentStorage {
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
return;
|
||||
}
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
console.log('Error initializing database: ');
|
||||
@@ -1702,27 +1858,32 @@ class PersistentStorage {
|
||||
};
|
||||
DBOpenRequest.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
const txn = e.target.transaction;
|
||||
|
||||
db.onerror = e => console.log('Error loading database: ' + e);
|
||||
try {
|
||||
|
||||
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
|
||||
|
||||
if (e.oldVersion === 0) {
|
||||
// initiate v1
|
||||
db.createObjectStore('keyval');
|
||||
} catch (error) {
|
||||
console.log("Object store named 'keyval' already exists")
|
||||
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
|
||||
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
|
||||
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
|
||||
} catch (error) {
|
||||
console.log("Object store named 'room_secrets' already exists")
|
||||
if (e.oldVersion <= 1) {
|
||||
// migrate to v2
|
||||
db.createObjectStore('share_target_files');
|
||||
}
|
||||
|
||||
try {
|
||||
if (db.objectStoreNames.contains('share_target_files')) {
|
||||
db.deleteObjectStore('share_target_files');
|
||||
}
|
||||
if (e.oldVersion <= 2) {
|
||||
// migrate to v3
|
||||
db.deleteObjectStore('share_target_files');
|
||||
db.createObjectStore('share_target_files', {autoIncrement: true});
|
||||
} catch (error) {
|
||||
console.log("Object store named 'share_target_files' already exists")
|
||||
}
|
||||
if (e.oldVersion <= 3) {
|
||||
// migrate to v4
|
||||
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
|
||||
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
|
||||
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1755,7 +1916,7 @@ class PersistentStorage {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const transaction = db.transaction('keyval', 'readonly');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.get(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
@@ -1788,16 +1949,21 @@ class PersistentStorage {
|
||||
})
|
||||
}
|
||||
|
||||
static addRoomSecret(roomSecret) {
|
||||
static addRoomSecret(roomSecret, displayName, deviceName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.add({'secret': roomSecret});
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. RoomSecret added: ${roomSecret}`);
|
||||
const objectStoreRequest = objectStore.add({
|
||||
'secret': roomSecret,
|
||||
'display_name': displayName,
|
||||
'device_name': deviceName,
|
||||
'auto_accept': false
|
||||
});
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
@@ -1807,21 +1973,31 @@ class PersistentStorage {
|
||||
})
|
||||
}
|
||||
|
||||
static getAllRoomSecrets() {
|
||||
static async getAllRoomSecrets() {
|
||||
try {
|
||||
const roomSecrets = await this.getAllRoomSecretEntries();
|
||||
let secrets = [];
|
||||
for (let i = 0; i < roomSecrets.length; i++) {
|
||||
secrets.push(roomSecrets[i].secret);
|
||||
}
|
||||
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
return(secrets);
|
||||
} catch (e) {
|
||||
this.logBrowserNotCapable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static getAllRoomSecretEntries() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const transaction = db.transaction('room_secrets', 'readonly');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.getAll();
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
let secrets = [];
|
||||
for (let i=0; i<e.target.result.length; i++) {
|
||||
secrets.push(e.target.result[i].secret);
|
||||
}
|
||||
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
resolve(secrets);
|
||||
resolve(e.target.result);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
@@ -1830,24 +2006,59 @@ class PersistentStorage {
|
||||
});
|
||||
}
|
||||
|
||||
static deleteRoomSecret(room_secret) {
|
||||
static getRoomSecretEntry(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readonly');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
const key = e.target.result;
|
||||
if (!key) {
|
||||
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestRetrieval = objectStore.get(key);
|
||||
objectStoreRequestRetrieval.onsuccess = e => {
|
||||
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
|
||||
resolve({
|
||||
"entry": e.target.result,
|
||||
"key": key
|
||||
});
|
||||
}
|
||||
objectStoreRequestRetrieval.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static deleteRoomSecret(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(room_secret);
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
if (!e.target.result) {
|
||||
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
|
||||
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
|
||||
const key = e.target.result;
|
||||
const objectStoreRequestDeletion = objectStore.delete(key);
|
||||
objectStoreRequestDeletion.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
|
||||
resolve();
|
||||
console.log(`Request successful. Deleted room_secret: ${key}`);
|
||||
resolve(roomSecret);
|
||||
}
|
||||
objectStoreRequestDeletion.onerror = (e) => {
|
||||
reject(e);
|
||||
@@ -1878,22 +2089,116 @@ class PersistentStorage {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
|
||||
}
|
||||
|
||||
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
|
||||
}
|
||||
|
||||
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
this.getRoomSecretEntry(roomSecret)
|
||||
.then(roomSecretEntry => {
|
||||
if (!roomSecretEntry) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
|
||||
const updatedRoomSecretEntry = {
|
||||
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
|
||||
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
|
||||
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
|
||||
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
|
||||
};
|
||||
|
||||
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
|
||||
|
||||
objectStoreRequestUpdate.onsuccess = e => {
|
||||
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
|
||||
resolve({
|
||||
"entry": updatedRoomSecretEntry,
|
||||
"key": roomSecretEntry.key
|
||||
});
|
||||
}
|
||||
|
||||
objectStoreRequestUpdate.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
.catch(e => reject(e));
|
||||
};
|
||||
|
||||
DBOpenRequest.onerror = e => reject(e);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast {
|
||||
class BrowserTabsConnector {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
|
||||
}
|
||||
|
||||
_broadcastMessage(message) {
|
||||
_broadcastSend(message) {
|
||||
this.bc.postMessage(message);
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast message received:', e.data)
|
||||
Events.fire(e.data.type, e.data.detail);
|
||||
console.log('Broadcast:', e.data)
|
||||
switch (e.data.type) {
|
||||
case 'self-display-name-changed':
|
||||
Events.fire('self-display-name-changed', e.data.detail);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static peerIsSameBrowser(peerId) {
|
||||
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
|
||||
return peerIdsBrowser
|
||||
? peerIdsBrowser.indexOf(peerId) !== -1
|
||||
: false;
|
||||
}
|
||||
|
||||
static async addPeerIdToLocalStorage() {
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
if (!peerId) return false;
|
||||
|
||||
let peerIdsBrowser = [];
|
||||
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser"));
|
||||
|
||||
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
|
||||
peerIdsBrowser.push(peerId);
|
||||
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
|
||||
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
|
||||
|
||||
return peerIdsBrowser;
|
||||
}
|
||||
|
||||
static async removePeerIdFromLocalStorage(peerId) {
|
||||
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
|
||||
const index = peerIdsBrowser.indexOf(peerId);
|
||||
peerIdsBrowser.splice(index, 1);
|
||||
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
|
||||
return peerId;
|
||||
}
|
||||
|
||||
|
||||
static async removeOtherPeerIdsFromLocalStorage() {
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
if (!peerId) return false;
|
||||
|
||||
let peerIdsBrowser = [peerId];
|
||||
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
|
||||
return peerIdsBrowser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1908,7 +2213,7 @@ class PairDrop {
|
||||
const sendTextDialog = new SendTextDialog();
|
||||
const receiveTextDialog = new ReceiveTextDialog();
|
||||
const pairDeviceDialog = new PairDeviceDialog();
|
||||
const clearDevicesDialog = new ClearDevicesDialog();
|
||||
const clearDevicesDialog = new EditPairedDevicesDialog();
|
||||
const base64ZipDialog = new Base64ZipDialog();
|
||||
const toast = new Toast();
|
||||
const notifications = new Notifications();
|
||||
@@ -1916,7 +2221,7 @@ class PairDrop {
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
const webFileHandlersUI = new WebFileHandlersUI();
|
||||
const noSleepUI = new NoSleepUI();
|
||||
const broadCast = new Broadcast();
|
||||
const broadCast = new BrowserTabsConnector();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1943,29 +2248,26 @@ window.addEventListener('beforeinstallprompt', e => {
|
||||
return e.preventDefault();
|
||||
});
|
||||
|
||||
// Background Animation
|
||||
// Background Circles
|
||||
Events.on('load', () => {
|
||||
let c = document.createElement('canvas');
|
||||
document.body.appendChild(c);
|
||||
let style = c.style;
|
||||
style.width = '100%';
|
||||
style.position = 'absolute';
|
||||
style.zIndex = -1;
|
||||
style.top = 0;
|
||||
style.left = 0;
|
||||
style.animation = "fade-in 800ms";
|
||||
let cCtx = c.getContext('2d');
|
||||
let x0, y0, w, h, dw, offset;
|
||||
|
||||
let offscreenCanvases = [];
|
||||
|
||||
function init() {
|
||||
let oldW = w;
|
||||
let oldH = h;
|
||||
let oldOffset = offset
|
||||
w = document.documentElement.clientWidth;
|
||||
h = document.documentElement.clientHeight;
|
||||
offset = $$('footer').offsetHeight - 32;
|
||||
if (h > 800) offset += 16;
|
||||
offset = $$('footer').offsetHeight - 33;
|
||||
|
||||
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
|
||||
|
||||
@@ -1974,63 +2276,33 @@ Events.on('load', () => {
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.round(Math.max(w, h, 1000) / 13);
|
||||
drawCircles(cCtx, 0);
|
||||
|
||||
// enforce redrawing of frames
|
||||
offscreenCanvases = [];
|
||||
if (document.body.contains(c)) {
|
||||
document.body.removeChild(c);
|
||||
}
|
||||
drawCircles(cCtx, dw);
|
||||
document.body.appendChild(c);
|
||||
}
|
||||
|
||||
Events.on('bg-resize', _ => init());
|
||||
window.onresize = _ => Events.fire('bg-resize');
|
||||
|
||||
function drawCircle(ctx, radius) {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h));
|
||||
ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`;
|
||||
let opacity = 0.3 * (1 - 1.2 * radius / Math.max(w, h));
|
||||
ctx.strokeStyle = `rgba(128, 128, 128, ${opacity})`;
|
||||
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawCircles(ctx, frame) {
|
||||
for (let i = 0; i < 13; i++) {
|
||||
drawCircle(ctx, dw * i + frame);
|
||||
drawCircle(ctx, dw * i + frame + 33);
|
||||
}
|
||||
}
|
||||
|
||||
function createOffscreenCanvas(frame) {
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = c.width;
|
||||
canvas.height = c.height;
|
||||
offscreenCanvases[frame] = canvas;
|
||||
let ctx = canvas.getContext('2d');
|
||||
drawCircles(ctx, frame);
|
||||
}
|
||||
|
||||
function drawFrame(frame) {
|
||||
cCtx.clearRect(0, 0, w, h);
|
||||
if (!offscreenCanvases[frame]) {
|
||||
createOffscreenCanvas(frame);
|
||||
}
|
||||
cCtx.drawImage(offscreenCanvases[frame], 0, 0);
|
||||
}
|
||||
|
||||
let animate = true;
|
||||
let currentFrame = 0;
|
||||
|
||||
function animateBg() {
|
||||
if (currentFrame + 1 < dw || animate) {
|
||||
currentFrame = (currentFrame + 1) % dw;
|
||||
drawFrame(currentFrame);
|
||||
}
|
||||
setTimeout(_ => animateBg(), 3000 / dw);
|
||||
}
|
||||
|
||||
window.animateBackground = function(l) {
|
||||
animate = l;
|
||||
};
|
||||
|
||||
init();
|
||||
animateBg();
|
||||
});
|
||||
|
||||
document.changeFavicon = function (src) {
|
||||
|
||||
@@ -399,6 +399,10 @@ const cyrb53 = function(str, seed = 0) {
|
||||
return 4294967296 * (2097151 & h2) + (h1>>>0);
|
||||
};
|
||||
|
||||
function onlyUnique (value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
var binary = '';
|
||||
var bytes = new Uint8Array(buffer);
|
||||
@@ -410,10 +414,10 @@ function arrayBufferToBase64(buffer) {
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(base64) {
|
||||
var binary_string = window.atob(base64);
|
||||
var binary_string = window.atob(base64);
|
||||
var len = binary_string.length;
|
||||
var bytes = new Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
for (var i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.5.2';
|
||||
const cacheVersion = 'v1.7.2';
|
||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -20,6 +20,10 @@ body {
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
overflow-y: hidden;
|
||||
/* Only allow selection on message and pair key */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -76,11 +80,75 @@ html {
|
||||
}
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
position: absolute;
|
||||
align-items: baseline;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
header > a,
|
||||
header > div {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
header > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
header > div .icon-button {
|
||||
height: 40px;
|
||||
transition: all 300ms;
|
||||
}
|
||||
|
||||
header > div > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header > div:not(:hover) .icon-button:not(.selected) {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#theme-wrapper:hover::before {
|
||||
border-radius: 20px;
|
||||
background: currentColor;
|
||||
opacity: 0.1;
|
||||
transition: opacity 300ms;
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
header > div:hover .icon-button.selected::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
header > div:hover .icon-button.selected:hover::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
header > div .icon-button:not(.selected) {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
header > div > div {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
@@ -141,7 +209,8 @@ body {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
a,
|
||||
.icon-button {
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
@@ -151,6 +220,14 @@ hr {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
min-width: 13px;
|
||||
}
|
||||
|
||||
x-noscript {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
@@ -193,15 +270,10 @@ x-noscript {
|
||||
}
|
||||
}
|
||||
|
||||
/* Main Header */
|
||||
|
||||
body>header a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#center {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin-top: 56px;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 146px;
|
||||
@@ -214,7 +286,7 @@ body>header a {
|
||||
}
|
||||
|
||||
@media screen and (min-width: 402px) and (max-width: 425px) {
|
||||
header:has(#clear-pair-devices:not([hidden]))~#center {
|
||||
header:has(#edit-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 164px;
|
||||
}
|
||||
}
|
||||
@@ -355,10 +427,10 @@ x-no-peers {
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
animation: fade-in 300ms;
|
||||
animation-delay: 500ms;
|
||||
animation-fill-mode: backwards;
|
||||
/* prevent flickering on load */
|
||||
animation-iteration-count: 0;
|
||||
}
|
||||
|
||||
x-no-peers h2,
|
||||
@@ -392,8 +464,6 @@ x-no-peers[drop-bg] * {
|
||||
/* Peer */
|
||||
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 8px;
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
@@ -497,7 +567,6 @@ x-peer.ws-peer .highlight-wrapper {
|
||||
.status,
|
||||
.device-name,
|
||||
.connection-hash {
|
||||
height: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -657,6 +726,12 @@ x-dialog x-paper {
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
#pair-device-dialog ::-moz-selection,
|
||||
#pair-device-dialog ::selection {
|
||||
color: black;
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -678,7 +753,7 @@ x-dialog .font-subheading {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* PairDevicesDialog */
|
||||
/* Pair Devices Dialog */
|
||||
|
||||
#key-input-container {
|
||||
width: 100%;
|
||||
@@ -686,7 +761,7 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#key-input-container>input {
|
||||
#key-input-container > input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
@@ -702,15 +777,18 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#key-input-container>input + * {
|
||||
#key-input-container > input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
#key-input-container > input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#room-key {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
font-size: 50px;
|
||||
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
|
||||
display: inline-block;
|
||||
@@ -722,14 +800,106 @@ x-dialog .font-subheading {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pair-device-dialog hr {
|
||||
margin: 40px -24px;
|
||||
x-dialog hr {
|
||||
margin: 40px -24px 30px -24px;
|
||||
border: solid 1.25px var(--border-color);
|
||||
}
|
||||
|
||||
#pair-device-dialog x-background {
|
||||
padding: 16px!important;
|
||||
}
|
||||
|
||||
/* Edit Paired Devices Dialog */
|
||||
.paired-devices-wrapper:empty:before {
|
||||
content: "No paired devices.";
|
||||
}
|
||||
|
||||
.paired-devices-wrapper:empty {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.paired-devices-wrapper {
|
||||
border-top: solid 4px var(--paired-device-color);
|
||||
border-bottom: solid 4px var(--paired-device-color);
|
||||
max-height: 65vh;
|
||||
overflow: scroll;
|
||||
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), .3), rgba(var(--text-color), 0)),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
|
||||
|
||||
/* Opera doesn't support this in the shorthand */
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
.paired-device {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.paired-device:not(:last-child) {
|
||||
border-bottom: solid 4px var(--paired-device-color);
|
||||
}
|
||||
|
||||
.paired-device > .display-name,
|
||||
.paired-device > .device-name {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
.paired-device span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper > label,
|
||||
.paired-device > .button-wrapper > button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
justify-content: center;
|
||||
width: 50%;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper > :not(:last-child) {
|
||||
border-right: solid 1px rgba(128, 128, 128, 0.5);
|
||||
}
|
||||
|
||||
.paired-device > .button-wrapper > :not(:first-child) {
|
||||
border-left: solid 1px rgba(128, 128, 128, 0.5);
|
||||
}
|
||||
|
||||
.paired-device * {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.paired-device > .auto-accept {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Receive Dialog */
|
||||
|
||||
x-dialog .row {
|
||||
@@ -778,7 +948,7 @@ x-paper > div:last-child > .button:not(:last-child) {
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
/* Todo: add pair underline to send / receive dialogs displayName */
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
@@ -796,9 +966,9 @@ x-dialog .dialog-subheader {
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
@@ -1000,10 +1170,12 @@ button::-moz-focus-inner {
|
||||
|
||||
#about x-background {
|
||||
position: absolute;
|
||||
top: calc(32px - 250px);
|
||||
right: calc(32px - 250px);
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
--size: max(max(230vw, 230vh), calc(150vh + 150vw));
|
||||
--size-half: calc(var(--size)/2);
|
||||
top: calc(28px - var(--size-half));
|
||||
right: calc(36px - var(--size-half));
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
transform: scale(0);
|
||||
@@ -1017,7 +1189,7 @@ button::-moz-focus-inner {
|
||||
}
|
||||
|
||||
#about:target x-background {
|
||||
transform: scale(10);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
#about .row a {
|
||||
@@ -1140,7 +1312,7 @@ x-peers:empty~x-instructions {
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
x-peer {
|
||||
transform: scale(0.95);
|
||||
padding: 4px 0;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1153,7 +1325,7 @@ x-peers:empty~x-instructions {
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
#websocket-fallback>span {
|
||||
#websocket-fallback > span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
@@ -1173,7 +1345,7 @@ x-peers:empty~x-instructions {
|
||||
|
||||
@media screen and (min-height: 800px) {
|
||||
#websocket-fallback {
|
||||
padding-bottom: 15px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1303,3 +1475,9 @@ x-dialog x-paper {
|
||||
background: #bfbfbf;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-moz-selection,
|
||||
::selection {
|
||||
color: black;
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
"urls": "stun:stun.l.google.com:19302"
|
||||
},
|
||||
{
|
||||
"urls": "stun:openrelay.metered.ca:80"
|
||||
},
|
||||
{
|
||||
"urls": "turn:openrelay.metered.ca:443",
|
||||
"username": "openrelayproject",
|
||||
"credential": "openrelayproject"
|
||||
"urls": "turn:example.com:3478",
|
||||
"username": "username",
|
||||
"credential": "password"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
38
turnserver_example.conf
Normal file
38
turnserver_example.conf
Normal file
@@ -0,0 +1,38 @@
|
||||
# TURN server name and realm
|
||||
realm=<DOMAIN>
|
||||
server-name=pairdrop
|
||||
|
||||
# IPs the TURN server listens to
|
||||
listening-ip=0.0.0.0
|
||||
|
||||
# External IP-Address of the TURN server
|
||||
external-ip=<IP_ADDRESS>
|
||||
|
||||
# Main listening port
|
||||
listening-port=3478
|
||||
|
||||
# Further ports that are open for communication
|
||||
min-port=10000
|
||||
max-port=20000
|
||||
|
||||
# Use fingerprint in TURN message
|
||||
fingerprint
|
||||
|
||||
# Log file path
|
||||
log-file=/var/log/turnserver.log
|
||||
|
||||
# Enable verbose logging
|
||||
verbose
|
||||
|
||||
# Specify the user for the TURN authentification
|
||||
user=user:password
|
||||
|
||||
# Enable long-term credential mechanism
|
||||
lt-cred-mech
|
||||
|
||||
# SSL certificates
|
||||
cert=/etc/letsencrypt/live/<DOMAIN>/cert.pem
|
||||
pkey=/etc/letsencrypt/live/<DOMAIN>/privkey.pem
|
||||
|
||||
# 443 for TURN over TLS, which can bypass firewalls
|
||||
tls-listening-port=443
|
||||
Reference in New Issue
Block a user