mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-06 18:03:48 +00:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f4ce63a0c | ||
|
|
b42c8a0b1a | ||
|
|
4c7bdd3a0f | ||
|
|
3f72fa1160 | ||
|
|
5c3f5ece7d | ||
|
|
8de899f124 | ||
|
|
87097e9cd4 | ||
|
|
b2fc6415da | ||
|
|
2d8bbd5a79 | ||
|
|
cae3bb7c7b | ||
|
|
59dca141b6 | ||
|
|
d0046e83cb | ||
|
|
2aeadb44e2 | ||
|
|
7827a47d29 | ||
|
|
4edc9c9b22 | ||
|
|
398a69d7a0 | ||
|
|
dfe69cc873 | ||
|
|
f5fde731b0 | ||
|
|
d6eee480b3 | ||
|
|
f6ad85a744 | ||
|
|
d50480b2f8 | ||
|
|
ac1e88b6a0 | ||
|
|
0fe36e132c | ||
|
|
ab08091f5d | ||
|
|
19a78a5239 | ||
|
|
d0b2c81582 | ||
|
|
10a0aaf896 | ||
|
|
34ebd60304 | ||
|
|
251df2fbff | ||
|
|
1bb8a63eed | ||
|
|
7d581ca858 | ||
|
|
dcc4e8b747 | ||
|
|
ce549adf22 | ||
|
|
bdb39a1d2c | ||
|
|
195dfd0bb3 | ||
|
|
680ed81bd7 | ||
|
|
f120677393 | ||
|
|
3f85d266b3 | ||
|
|
01cd670afc | ||
|
|
3f0909637b | ||
|
|
1f97c12562 | ||
|
|
17abc91c86 | ||
|
|
4a0cd1f49a | ||
|
|
b7781e2bab | ||
|
|
4e0fb89720 | ||
|
|
5a290718b6 | ||
|
|
6c6f288c3d | ||
|
|
fea15d3ee1 | ||
|
|
028752a809 | ||
|
|
1093f4d246 | ||
|
|
7ddd600b0c | ||
|
|
715356aafb | ||
|
|
490e4db380 | ||
|
|
11a988e550 | ||
|
|
ff8f28660a | ||
|
|
5fc8e85f75 | ||
|
|
5eeaae01fe | ||
|
|
660e523263 | ||
|
|
cdfbc7a2df | ||
|
|
c9dca7e083 | ||
|
|
79af04d95a | ||
|
|
954e9c7c3a | ||
|
|
c0d504f6a8 | ||
|
|
36e152dc7c | ||
|
|
fdf024f378 | ||
|
|
9f2e4c5f8f | ||
|
|
8e219914ec | ||
|
|
d1273ef9cc | ||
|
|
27ac7786d0 | ||
|
|
edf2ab5eb3 | ||
|
|
c3863a9dd3 | ||
|
|
5934e94761 | ||
|
|
1bc23dc4b3 | ||
|
|
cc78b34d2e | ||
|
|
f34f5bd4b2 | ||
|
|
b2f6a75c99 | ||
|
|
82138c06f3 | ||
|
|
ee820ed6e0 | ||
|
|
b7e7fd1b68 | ||
|
|
96ed0e53b1 | ||
|
|
77b76a3b8d | ||
|
|
e37f9bd9fb | ||
|
|
67a1b04da2 | ||
|
|
8b2eb67266 | ||
|
|
827b10219d | ||
|
|
e7ab5e26cc | ||
|
|
451173caac | ||
|
|
8bcaa3f60f | ||
|
|
c0a4224a59 | ||
|
|
460e8ec79c | ||
|
|
002b31a113 | ||
|
|
1e35bab327 | ||
|
|
bb0493d071 | ||
|
|
bfb5aa8546 | ||
|
|
a9d7960a59 | ||
|
|
39ca5b2d21 | ||
|
|
cf715b2872 | ||
|
|
bbb8c1b10f | ||
|
|
d6ef5887dd | ||
|
|
f9f1abef7a | ||
|
|
d244f5fa47 | ||
|
|
3a2d8c75f7 | ||
|
|
545cdc2459 | ||
|
|
1eb53498b1 | ||
|
|
de76da52fe | ||
|
|
d56ee87437 | ||
|
|
66359da2ca | ||
|
|
74b88c2e7d |
2
.github/ISSUE_TEMPLATE/bug-report.md
vendored
2
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
about: Create a report to help us improve. Please check the FAQ first.
|
||||
title: 'Bug:/Enhancement:/Feature Request: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
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 }}
|
||||
|
||||
@@ -9,3 +9,6 @@ RUN npm ci
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1
|
||||
|
||||
24
README.md
24
README.md
@@ -6,7 +6,7 @@
|
||||
<h1>PairDrop</h1>
|
||||
|
||||
<p>
|
||||
Local file sharing in your browser. Inspired by Apple's Airdrop.
|
||||
Local file sharing in your browser. Inspired by Apple's AirDrop.
|
||||
<br />
|
||||
<a href="https://pairdrop.net"><strong>Explore »</strong></a>
|
||||
<br />
|
||||
@@ -33,18 +33,18 @@ 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 they are downloaded automatically if possible.
|
||||
* Multiple files are downloaded as ZIP file
|
||||
* On iOS and Android the devices share menu is opened instead of downloading the files
|
||||
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
||||
* Multiple files are downloaded as a ZIP file
|
||||
* On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu.
|
||||
* Multiple files are transferred at once with an overall progress indicator
|
||||
|
||||
### Send Files or Text Directly From Share Menu, Context Menu or CLI
|
||||
@@ -54,15 +54,18 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||
|
||||
### Other changes
|
||||
* [Paste Mode](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||
* Change your display name permanently to easily differentiate your devices
|
||||
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
|
||||
* Warn user before PairDrop is closed on file transfer
|
||||
* 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))
|
||||
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
||||
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
|
||||
|
||||
## Screenshots
|
||||
<div align="center">
|
||||
@@ -78,6 +81,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
|
||||
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
||||
* [cyrb53](https://github.com/bryc) super fast hash function
|
||||
|
||||
Have any questions? Read our [FAQ](/docs/faq.md).
|
||||
|
||||
@@ -85,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
|
||||
43
docs/docker-swarm-usage.md
Normal file
43
docs/docker-swarm-usage.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Docker Swarm Usage
|
||||
|
||||
## Healthcheck
|
||||
|
||||
The [Docker Image](../Dockerfile) includes a Healthcheck with the following options:
|
||||
|
||||
```
|
||||
--interval=30s
|
||||
```
|
||||
> Specifies the time interval at which the health check should be performed. In this case, the health check will be performed every 30 seconds.
|
||||
|
||||
<br>
|
||||
|
||||
```
|
||||
--timeout=10s
|
||||
```
|
||||
> Specifies the amount of time to wait for a response from the health check command. If the response does not arrive within 10 seconds, the health check will be considered a failure.
|
||||
|
||||
<br>
|
||||
|
||||
```
|
||||
--start-period=5s
|
||||
```
|
||||
> Specifies the amount of time to wait before starting the health check process. In this case, the health check process will begin 5 seconds after the container is started.
|
||||
|
||||
<br>
|
||||
|
||||
```
|
||||
--retries=3
|
||||
```
|
||||
> Specifies the number of times Docker should retry the health check before considering the container to be unhealthy.
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
The CMD instruction is used to define the command that will be run as part of the health check.
|
||||
In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. This command will attempt to connect to `http://localhost:3000/`
|
||||
and if it fails it will exit with a status code of `1`. If this command returns a status code other than `0`, the health check will be considered a failure.
|
||||
|
||||
Overall, this HEALTHCHECK instruction is defining a health check process that will run every 30 seconds, wait up to 10 seconds for a response,
|
||||
begin 5 seconds after the container is started, and retry up to 3 times.
|
||||
The health check will consist of attempting to connect to http://localhost:3000/ and will consider the container to be unhealthy if it is unable to connect.
|
||||
|
||||
184
docs/faq.md
184
docs/faq.md
@@ -1,31 +1,48 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
### Instructions / Discussions
|
||||
* [Video Instructions](https://www.youtube.com/watch?v=4XN02GkcHUM) (Big thanks to [TheiTeckHq](https://www.youtube.com/channel/UC_DUzWMb8gZZnAbISQjmAfQ))
|
||||
* [idownloadblog](http://www.idownloadblog.com/2015/12/29/snapdrop/)
|
||||
* [thenextweb](http://thenextweb.com/insider/2015/12/27/snapdrop-is-a-handy-web-based-replacement-for-apples-fiddly-airdrop-file-transfer-tool/)
|
||||
* [winboard](http://www.winboard.org/artikel-ratgeber/6253-dateien-vom-desktop-pc-mit-anderen-plattformen-teilen-mit-snapdrop.html)
|
||||
* [免費資源網路社群](https://free.com.tw/snapdrop/)
|
||||
* [Hackernews](https://news.ycombinator.com/front?day=2020-12-24)
|
||||
* [Reddit](https://www.reddit.com/r/Android/comments/et4qny/snapdrop_is_a_free_open_source_cross_platform/)
|
||||
* [Producthunt](https://www.producthunt.com/posts/snapdrop)
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Help! I can't install the PWA!
|
||||
</summary>
|
||||
|
||||
### Help! I can't install the PWA!
|
||||
if you are using a Chromium-based browser (Chrome, Edge, Brave, etc.), you can easily install PairDrop PWA on your desktop
|
||||
by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net).
|
||||
|
||||
<img src="pwa-install.png" alt="Example on how to install a pwa with Edge">
|
||||
<img width="400" src="pwa-install.png" alt="Example on how to install a pwa with Edge">
|
||||
|
||||
On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/)
|
||||
|
||||
### Are there any shortcuts?
|
||||
Sure!
|
||||
<br>
|
||||
|
||||
<b>Self-Hosted Instance?</b>
|
||||
|
||||
To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs).
|
||||
See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more information.
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Shortcuts?
|
||||
</summary>
|
||||
|
||||
Shortcuts!
|
||||
- Send a message with `CTRL + ENTER`
|
||||
- Close all send and pair dialogs by pressing `Escape`.
|
||||
- Copy a received message to clipboard with `CTRL/⌘ + C`.
|
||||
- Accept file transfer request with `Enter` and decline with `Escape`.
|
||||
|
||||
### When I receive images on iOS I cannot add them directly to the gallery?
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
How to save images directly to the gallery on iOS?
|
||||
</summary>
|
||||
|
||||
Apparently, iOS does not allow images shared from a website to be saved to the gallery directly.
|
||||
It simply does not offer the option for images shared from a website.
|
||||
|
||||
@@ -33,33 +50,108 @@ iOS Shortcuts to the win:
|
||||
I created a simple iOS shortcut that takes your photos and saves them to your gallery:
|
||||
https://routinehub.co/shortcut/13988/
|
||||
|
||||
### Is it possible to send files or text directly from the context or share menu?
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Is it possible to send files or text directly from the context or share menu?
|
||||
</summary>
|
||||
|
||||
Yes, it finally is!
|
||||
* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
|
||||
* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
|
||||
* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
|
||||
|
||||
### Is it possible to send files or text directly via CLI?
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Is it possible to send files or text directly via CLI?
|
||||
</summary>
|
||||
|
||||
Yes, it is!
|
||||
|
||||
* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||
### What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server?
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### What about privacy? Will files be saved on third-party-servers?
|
||||
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).
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Are there any Third-Party Apps?
|
||||
</summary>
|
||||
|
||||
Here's a list of some third-party apps compatible with PairDrop:
|
||||
|
||||
1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android)
|
||||
2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon)
|
||||
3. Feel free to make one :)
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
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 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 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>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
What about privacy? Will files be saved on third-party-servers?
|
||||
</summary>
|
||||
|
||||
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>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
What about security? Are my files encrypted while being sent between the computers?
|
||||
</summary>
|
||||
|
||||
### What about security? Are my files encrypted while being sent between the computers?
|
||||
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
|
||||
|
||||
### Transferring many files with paired devices takes too long
|
||||
Naturally, if traffic needs to be routed through the turn server transfer speed decreases.
|
||||
As a workaround you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster.
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Transferring many files with paired devices takes too long
|
||||
</summary>
|
||||
|
||||
Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases.
|
||||
|
||||
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)
|
||||
@@ -68,22 +160,52 @@ As a workaround you can open a hotspot on one of your devices to bridge the conn
|
||||
You can also use mobile hotspots on phones to do that.
|
||||
Then, all data should be sent directly between devices and your data plan should not be charged.
|
||||
|
||||
### Why don't you implement feature xyz?
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Why don't you implement feature xyz?
|
||||
</summary>
|
||||
|
||||
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*.
|
||||
|
||||
|
||||
### Snapdrop and PairDrop are awesome! How can I support them?
|
||||
* [Buy me a cover to support open source software](https://www.buymeacoffee.com/pairdrop)
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Snapdrop and PairDrop are awesome! How can I support them?
|
||||
</summary>
|
||||
|
||||
* [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.
|
||||
* Do security analysis and suggestions
|
||||
* To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop)
|
||||
|
||||
|
||||
### How does it work?
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
How does it work?
|
||||
</summary>
|
||||
|
||||
[See here for Information about the Technical Implementation](/docs/technical-documentation.md)
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
[< Back](/README.md)
|
||||
|
||||
@@ -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
|
||||
@@ -17,7 +23,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ls
|
||||
Set options by using the following flags in the `docker run` command:
|
||||
|
||||
##### Port
|
||||
```
|
||||
```bash
|
||||
-p 127.0.0.1:8080:3000
|
||||
```
|
||||
> Specify the port used by the docker image
|
||||
@@ -30,7 +36,7 @@ Set options by using the following flags in the `docker run` command:
|
||||
> Limits clients to 1000 requests per 5 min
|
||||
|
||||
##### Websocket Fallback (for VPN)
|
||||
```
|
||||
```bash
|
||||
-e WS_FALLBACK=true
|
||||
```
|
||||
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||
@@ -42,6 +48,30 @@ Set options by using the following flags in the `docker run` command:
|
||||
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
|
||||
> Additionally, beware that all traffic using this fallback debits the servers data plan.
|
||||
|
||||
##### Specify STUN/TURN Servers
|
||||
```bash
|
||||
-e RTC_CONFIG="rtc_config.json"
|
||||
```
|
||||
|
||||
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
|
||||
> You can use `pairdrop/rtc_config_example.json` as a starting point.
|
||||
>
|
||||
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
|
||||
>
|
||||
> Default configuration:
|
||||
> ```json
|
||||
> {
|
||||
> "sdpSemantics": "unified-plan",
|
||||
> "iceServers": [
|
||||
> {
|
||||
> "urls": "stun:stun.l.google.com:19302"
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
|
||||
<br>
|
||||
|
||||
### Docker Image from GHCR
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
|
||||
@@ -52,6 +82,8 @@ 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)
|
||||
|
||||
### Docker Image self-built
|
||||
#### Build the image
|
||||
```bash
|
||||
@@ -71,6 +103,8 @@ 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)
|
||||
|
||||
<br>
|
||||
|
||||
## Deployment with Docker Compose
|
||||
@@ -141,6 +175,32 @@ $env:PORT=3010; npm start
|
||||
```
|
||||
> Specify the port PairDrop is running on. (Default: 3000)
|
||||
|
||||
#### Specify STUN/TURN Server
|
||||
On Unix based systems
|
||||
```bash
|
||||
RTC_CONFIG="rtc_config.json" npm start
|
||||
```
|
||||
On Windows
|
||||
```bash
|
||||
$env:RTC_CONFIG="rtc_config.json"; npm start
|
||||
```
|
||||
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
|
||||
> You can use `pairdrop/rtc_config_example.json` as a starting point.
|
||||
>
|
||||
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
|
||||
>
|
||||
> Default configuration:
|
||||
> ```json
|
||||
> {
|
||||
> "sdpSemantics": "unified-plan",
|
||||
> "iceServers": [
|
||||
> {
|
||||
> "urls": "stun:stun.l.google.com:19302"
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
|
||||
### Options / Flags
|
||||
#### Local Run
|
||||
```bash
|
||||
@@ -262,13 +322,13 @@ server {
|
||||
|
||||
### Using Apache
|
||||
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
|
||||
```shell
|
||||
```bash
|
||||
a2enmod proxy
|
||||
```
|
||||
```shell
|
||||
```bash
|
||||
a2enmod proxy_http
|
||||
```
|
||||
```shell
|
||||
```bash
|
||||
a2enmod proxy_wstunnel
|
||||
```
|
||||
|
||||
@@ -278,7 +338,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
|
||||
**pairdrop.conf**
|
||||
#### Allow http and https requests
|
||||
```
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
ProxyPass / http://127.0.0.1:3000/
|
||||
RewriteEngine on
|
||||
@@ -295,7 +355,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
</VirtualHost>
|
||||
```
|
||||
#### Automatic http to https redirect:
|
||||
```
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
Redirect permanent / https://127.0.0.1:3000/
|
||||
</VirtualHost>
|
||||
@@ -308,10 +368,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
</VirtualHost>
|
||||
```
|
||||
Activate the new virtual host and reload apache:
|
||||
```shell
|
||||
```bash
|
||||
a2ensite pairdrop
|
||||
```
|
||||
```shell
|
||||
```bash
|
||||
service apache2 reload
|
||||
```
|
||||
|
||||
@@ -322,7 +382,7 @@ All files needed for developing are available on the branch `dev`.
|
||||
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
|
||||
|
||||
Then, clone the repository and run docker-compose:
|
||||
```shell
|
||||
```bash
|
||||
git clone https://github.com/schlagmichdoch/PairDrop.git
|
||||
|
||||
cd PairDrop
|
||||
@@ -347,7 +407,7 @@ The nginx container creates a CA certificate and a website certificate for you.
|
||||
|
||||
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
|
||||
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
|
||||
- On MacOS, double click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
|
||||
- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
|
||||
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
|
||||
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
104
index.js
104
index.js
@@ -1,6 +1,13 @@
|
||||
const process = require('process')
|
||||
const crypto = require('crypto')
|
||||
const {spawn} = require('child_process')
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const parser = require('ua-parser-js');
|
||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
||||
const express = require('express');
|
||||
const RateLimit = require('express-rate-limit');
|
||||
const http = require('http');
|
||||
|
||||
// Handle SIGINT
|
||||
process.on('SIGINT', () => {
|
||||
@@ -49,9 +56,16 @@ if (process.argv.includes('--auto-restart')) {
|
||||
);
|
||||
}
|
||||
|
||||
const express = require('express');
|
||||
const RateLimit = require('express-rate-limit');
|
||||
const http = require('http');
|
||||
const rtcConfig = process.env.RTC_CONFIG
|
||||
? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
|
||||
: {
|
||||
"sdpSemantics": "unified-plan",
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": "stun:stun.l.google.com:19302"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -93,13 +107,9 @@ if (process.argv.includes('--localhost-only')) {
|
||||
server.listen(port);
|
||||
}
|
||||
|
||||
const parser = require('ua-parser-js');
|
||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
||||
|
||||
class PairDropServer {
|
||||
|
||||
constructor() {
|
||||
const WebSocket = require('ws');
|
||||
this._wss = new WebSocket.Server({ server });
|
||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||
|
||||
@@ -110,10 +120,14 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_onConnection(peer) {
|
||||
this._joinRoom(peer);
|
||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||
peer.socket.onerror = e => console.error(e);
|
||||
this._keepAlive(peer);
|
||||
this._send(peer, {
|
||||
type: 'rtc-config',
|
||||
config: rtcConfig
|
||||
});
|
||||
this._joinRoom(peer);
|
||||
|
||||
// send displayName
|
||||
this._send(peer, {
|
||||
@@ -121,7 +135,8 @@ class PairDropServer {
|
||||
message: {
|
||||
displayName: peer.name.displayName,
|
||||
deviceName: peer.name.deviceName,
|
||||
peerId: peer.id
|
||||
peerId: peer.id,
|
||||
peerIdHash: peer.id.hashCode128BitSalted()
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -317,6 +332,10 @@ class PairDropServer {
|
||||
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
|
||||
if (this._rooms[room] && this._rooms[room][peer.id]) {
|
||||
this._leaveRoom(peer, roomType, roomSecret);
|
||||
}
|
||||
|
||||
// if room doesn't exist, create it
|
||||
if (!this._rooms[room]) {
|
||||
this._rooms[room] = {};
|
||||
@@ -526,9 +545,11 @@ class Peer {
|
||||
}
|
||||
|
||||
_setPeerId(request) {
|
||||
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id");
|
||||
if (peer_id && Peer.isValidUuid(peer_id)) {
|
||||
this.id = peer_id;
|
||||
const searchParams = new URL(request.url, "http://server").searchParams;
|
||||
let peerId = searchParams.get("peer_id");
|
||||
let peerIdHash = searchParams.get("peer_id_hash");
|
||||
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
|
||||
this.id = peerId;
|
||||
} else {
|
||||
this.id = crypto.randomUUID();
|
||||
}
|
||||
@@ -587,6 +608,10 @@ class Peer {
|
||||
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
|
||||
}
|
||||
|
||||
isPeerIdHashValid(peerId, peerIdHash) {
|
||||
return peerIdHash === peerId.hashCode128BitSalted();
|
||||
}
|
||||
|
||||
addRoomSecret(roomSecret) {
|
||||
if (!(roomSecret in this.roomSecrets)) {
|
||||
this.roomSecrets.push(roomSecret);
|
||||
@@ -602,14 +627,55 @@ class Peer {
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode', {
|
||||
value: function() {
|
||||
var hash = 0, i, chr;
|
||||
for (i = 0; i < this.length; i++) {
|
||||
chr = this.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
return cyrb53(this);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
|
||||
value: function() {
|
||||
return hasher.hashCode128BitSalted(this);
|
||||
}
|
||||
});
|
||||
|
||||
const hasher = (() => {
|
||||
let seeds;
|
||||
return {
|
||||
hashCode128BitSalted(str) {
|
||||
if (!seeds) {
|
||||
// seeds are created on first call to salt hash.
|
||||
seeds = [4];
|
||||
for (let i=0; i<4; i++) {
|
||||
const randomBuffer = new Uint32Array(1);
|
||||
crypto.webcrypto.getRandomValues(randomBuffer);
|
||||
seeds[i] = randomBuffer[0];
|
||||
}
|
||||
}
|
||||
let hashCode = "";
|
||||
for (let i=0; i<4; i++) {
|
||||
hashCode += cyrb53(str, seeds[i]);
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
/*
|
||||
cyrb53 (c) 2018 bryc (github.com/bryc)
|
||||
A fast and simple hash function with decent collision resistance.
|
||||
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||
Public domain. Attribution appreciated.
|
||||
*/
|
||||
const cyrb53 = function(str, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1>>>0);
|
||||
};
|
||||
|
||||
new PairDropServer();
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.2.0",
|
||||
"version": "1.6.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pairdrop",
|
||||
"version": "1.2.0",
|
||||
"version": "1.6.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"ua-parser-js": "^1.0.33",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"unique-names-generator": "^4.3.0",
|
||||
"ws": "^8.12.1"
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15"
|
||||
@@ -583,9 +583,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "1.0.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
|
||||
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
|
||||
"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",
|
||||
@@ -633,9 +633,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz",
|
||||
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==",
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -1070,9 +1070,9 @@
|
||||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "1.0.33",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
|
||||
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ=="
|
||||
"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",
|
||||
@@ -1095,9 +1095,9 @@
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz",
|
||||
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==",
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.2.0",
|
||||
"version": "1.6.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -12,9 +12,9 @@
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"ua-parser-js": "^1.0.33",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"unique-names-generator": "^4.3.0",
|
||||
"ws": "^8.12.1"
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15"
|
||||
|
||||
@@ -106,7 +106,11 @@ sendFiles()
|
||||
zip -q -b /tmp/ -r "$zipPath" "$path"
|
||||
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
|
||||
|
||||
hash=$(base64 -w 0 "$zipPathTemp")
|
||||
if [[ $OS == "Mac" ]];then
|
||||
hash=$(base64 -i "$zipPathTemp")
|
||||
else
|
||||
hash=$(base64 -w 0 "$zipPathTemp")
|
||||
fi
|
||||
|
||||
# remove temporary temp file
|
||||
rm "$zipPathTemp"
|
||||
@@ -116,7 +120,11 @@ sendFiles()
|
||||
# Create zip file temporarily to send file
|
||||
zip -q -b /tmp/ "$zipPath" "$path"
|
||||
|
||||
hash=$(base64 -w 0 "$zipPath")
|
||||
if [[ $OS == "Mac" ]];then
|
||||
hash=$(base64 -i "$zipPath")
|
||||
else
|
||||
hash=$(base64 -w 0 "$zipPath")
|
||||
fi
|
||||
fi
|
||||
|
||||
# remove temporary temp file
|
||||
|
||||
@@ -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" >
|
||||
</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="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||
</header>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
@@ -89,7 +103,13 @@
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="display-name" placeholder=" "></div>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
@@ -106,18 +126,17 @@
|
||||
<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 id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" 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="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -130,9 +149,9 @@
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -142,25 +161,23 @@
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">PairDrop</h2>
|
||||
<div class="text-center file-description">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span id="requesting-peer-display-name"></span>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
</div>
|
||||
<div id="file-name" class="row" >
|
||||
<span id="file-stem"></span>
|
||||
<span id="file-extension"></span>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="file-other"></span>
|
||||
<div class="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<div class="separator"></div>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -170,13 +187,23 @@
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 id="receive-title" class="center"></h2>
|
||||
<div class="text-center file-description"></div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<a id="share-or-download" class="button" autofocus></a>
|
||||
<div class="separator"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -187,16 +214,16 @@
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="text-center">PairDrop</h2>
|
||||
<div class="text-center">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span id="text-send-peer-display-name"></span>
|
||||
<span class="display-name"></span>
|
||||
</div>
|
||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="row-reverse">
|
||||
<div class="row-separator"></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>
|
||||
<div class="separator"></div>
|
||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -206,16 +233,15 @@
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Message Received</h2>
|
||||
<div id="receive-text-description-container">
|
||||
<span id="receive-text-peer-display-name"></span>
|
||||
<span>sent the following message:</span>
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<div class="separator"></div>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -226,6 +252,7 @@
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||
<button class="button center" close>Close</button>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -236,14 +263,14 @@
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<section class="center column fade-in">
|
||||
<header class="row-reverse">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
@@ -315,7 +342,13 @@
|
||||
<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">
|
||||
@@ -326,13 +359,17 @@
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||
</symbol>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
<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:
|
||||
@@ -21,10 +21,10 @@ class ServerConnection {
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
_connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(await this._endpoint());
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => this._onOpen();
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
@@ -36,6 +36,7 @@ class ServerConnection {
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
Events.fire('ws-connected');
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
@@ -52,16 +53,23 @@ class ServerConnection {
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
}
|
||||
|
||||
_setRtcConfig(config) {
|
||||
window.rtcConfig = config;
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'rtc-config':
|
||||
this._setRtcConfig(msg.config);
|
||||
break;
|
||||
case 'peers':
|
||||
Events.fire('peers', msg);
|
||||
break;
|
||||
@@ -110,34 +118,24 @@ class ServerConnection {
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
if (!peerId) {
|
||||
// save peerId to indexedDB to retrieve after PWA is installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
});
|
||||
}
|
||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
async _endpoint() {
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = await this._peerId();
|
||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
}
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
async _peerId() {
|
||||
// make peerId persistent when pwa is installed
|
||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
? await PersistentStorage.get('peerId')
|
||||
: sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
@@ -145,15 +143,17 @@ class ServerConnection {
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
@@ -194,6 +194,10 @@ class Peer {
|
||||
this._send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
sendDisplayName(displayName) {
|
||||
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||
}
|
||||
|
||||
async createHeader(file) {
|
||||
return {
|
||||
name: file.name,
|
||||
@@ -319,26 +323,25 @@ class Peer {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
const messageJSON = JSON.parse(message);
|
||||
switch (messageJSON.type) {
|
||||
case 'request':
|
||||
this._onFilesTransferRequest(message);
|
||||
this._onFilesTransferRequest(messageJSON);
|
||||
break;
|
||||
case 'header':
|
||||
this._onFilesHeader(message);
|
||||
this._onFileHeader(messageJSON);
|
||||
break;
|
||||
case 'partition':
|
||||
this._onReceivedPartitionEnd(message);
|
||||
this._onReceivedPartitionEnd(messageJSON);
|
||||
break;
|
||||
case 'partition-received':
|
||||
this._sendNextPartition();
|
||||
break;
|
||||
case 'progress':
|
||||
this._onDownloadProgress(message.progress);
|
||||
this._onDownloadProgress(messageJSON.progress);
|
||||
break;
|
||||
case 'files-transfer-response':
|
||||
this._onFileTransferRequestResponded(message);
|
||||
this._onFileTransferRequestResponded(messageJSON);
|
||||
break;
|
||||
case 'file-transfer-complete':
|
||||
this._onFileTransferCompleted();
|
||||
@@ -347,7 +350,10 @@ class Peer {
|
||||
this._onMessageTransferCompleted();
|
||||
break;
|
||||
case 'text':
|
||||
this._onTextReceived(message);
|
||||
this._onTextReceived(messageJSON);
|
||||
break;
|
||||
case 'display-name-changed':
|
||||
this._onDisplayNameChanged(messageJSON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -383,8 +389,8 @@ class Peer {
|
||||
this._requestPending = null;
|
||||
}
|
||||
|
||||
_onFilesHeader(header) {
|
||||
if (this._requestAccepted?.header.length) {
|
||||
_onFileHeader(header) {
|
||||
if (this._requestAccepted && this._requestAccepted.header.length) {
|
||||
this._lastProgress = 0;
|
||||
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
|
||||
this._requestAccepted.totalSize,
|
||||
@@ -437,11 +443,14 @@ class Peer {
|
||||
this._abortTransfer();
|
||||
}
|
||||
|
||||
// include for compatibility with Snapdrop for Android app
|
||||
Events.fire('file-received', fileBlob);
|
||||
|
||||
this._filesReceived.push(fileBlob);
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -486,12 +495,19 @@ class Peer {
|
||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName || this._displayName === message.displayName) return;
|
||||
this._displayName = message.displayName;
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
@@ -509,8 +525,9 @@ class RTCPeer extends Peer {
|
||||
_openConnection(peerId, isCaller) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
||||
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onicecandidateerror = e => this._onError(e);
|
||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||
}
|
||||
@@ -558,14 +575,21 @@ class RTCPeer extends Peer {
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = _ => this._onChannelClosed();
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._closeChannel());
|
||||
this._channel = channel;
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._onPageHide());
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message === 'string') {
|
||||
console.log('RTC:', JSON.parse(message));
|
||||
}
|
||||
super._onMessage(message);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
@@ -601,10 +625,16 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
}
|
||||
|
||||
_closeChannel() {
|
||||
if (this._channel) this._channel.onclose = null;
|
||||
if (this._conn) this._conn.close();
|
||||
this._conn = null;
|
||||
_onPageHide() {
|
||||
this._disconnect();
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
if (this._conn && this._channel) {
|
||||
this._channel.onclose = null;
|
||||
this._channel.close();
|
||||
}
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
@@ -618,9 +648,11 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection disconnected');
|
||||
break;
|
||||
case 'failed':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection failed');
|
||||
break;
|
||||
}
|
||||
@@ -666,6 +698,11 @@ class RTCPeer extends Peer {
|
||||
_isConnecting() {
|
||||
return this._channel && this._channel.readyState === 'connecting';
|
||||
}
|
||||
|
||||
sendDisplayName(displayName) {
|
||||
if (!this._isConnected()) return;
|
||||
super.sendDisplayName(displayName);
|
||||
}
|
||||
}
|
||||
|
||||
class PeersManager {
|
||||
@@ -679,8 +716,12 @@ class PeersManager {
|
||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
@@ -704,10 +745,6 @@ class PeersManager {
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
@@ -739,6 +776,10 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
@@ -756,6 +797,23 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeerDisplayNameChanged(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (!peer) return;
|
||||
this.peers[peerId].sendDisplayName(this._displayName);
|
||||
}
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
@@ -844,28 +902,11 @@ class Events {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback) {
|
||||
return window.addEventListener(type, callback, false);
|
||||
static on(type, callback, options = false) {
|
||||
return window.addEventListener(type, callback, options);
|
||||
}
|
||||
|
||||
static off(type, callback) {
|
||||
return window.removeEventListener(type, callback, false);
|
||||
static off(type, callback, options = false) {
|
||||
return window.removeEventListener(type, callback, options);
|
||||
}
|
||||
}
|
||||
|
||||
RTCPeer.config = {
|
||||
'sdpSemantics': 'unified-plan',
|
||||
'iceServers': [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
},
|
||||
{
|
||||
urls: 'stun:openrelay.metered.ca:80'
|
||||
},
|
||||
{
|
||||
urls: 'turn:openrelay.metered.ca:443',
|
||||
username: 'openrelayproject',
|
||||
credential: 'openrelayproject',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.2.0';
|
||||
const cacheVersion = 'v1.6.2';
|
||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
@@ -71,30 +71,11 @@ const update = request =>
|
||||
self.addEventListener('fetch', function(event) {
|
||||
if (event.request.method === "POST") {
|
||||
// Requests related to Web Share Target.
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const formData = await event.request.formData();
|
||||
const title = formData.get("title");
|
||||
const text = formData.get("text");
|
||||
const url = formData.get("url");
|
||||
const files = formData.get("files");
|
||||
let share_url = "/";
|
||||
if (files.length > 0) {
|
||||
share_url = "/?share-target=files";
|
||||
const db = await window.indexedDB.open('pairdrop_store');
|
||||
const tx = db.transaction('share_target_files', 'readwrite');
|
||||
const store = tx.objectStore('share_target_files');
|
||||
for (let i=0; i<files.length; i++) {
|
||||
await store.add(files[i]);
|
||||
}
|
||||
await tx.complete
|
||||
db.close()
|
||||
} else if (title.length > 0 || text.length > 0 || url.length) {
|
||||
share_url = `/?share-target=text&title=${title}&text=${text}&url=${url}`;
|
||||
}
|
||||
return Response.redirect(encodeURI(share_url), 303);
|
||||
})()
|
||||
);
|
||||
event.respondWith((async () => {
|
||||
let share_url = await evaluateRequestData(event.request);
|
||||
share_url = event.request.url + share_url;
|
||||
return Response.redirect(encodeURI(share_url), 302);
|
||||
})());
|
||||
} else {
|
||||
// Regular requests not related to Web Share Target.
|
||||
event.respondWith(
|
||||
@@ -119,3 +100,49 @@ self.addEventListener('activate', evt =>
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const evaluateRequestData = async function (request) {
|
||||
const formData = await request.formData();
|
||||
const title = formData.get("title");
|
||||
const text = formData.get("text");
|
||||
const url = formData.get("url");
|
||||
const files = formData.getAll("allfiles");
|
||||
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
if (files && files.length > 0) {
|
||||
let fileObjects = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileObjects.push({
|
||||
name: files[i].name,
|
||||
buffer: await files[i].arrayBuffer()
|
||||
});
|
||||
}
|
||||
|
||||
const DBOpenRequest = indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
for (let i = 0; i < fileObjects.length; i++) {
|
||||
const transaction = db.transaction('share_target_files', 'readwrite');
|
||||
const objectStore = transaction.objectStore('share_target_files');
|
||||
|
||||
const objectStoreRequest = objectStore.add(fileObjects[i]);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
if (i === fileObjects.length - 1) resolve('?share-target=files');
|
||||
}
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = _ => {
|
||||
resolve('');
|
||||
}
|
||||
} else {
|
||||
let share_url = '?share-target=text';
|
||||
|
||||
if (title) share_url += `&title=${title}`;
|
||||
if (text) share_url += `&text=${text}`;
|
||||
if (url) share_url += `&url=${url}`;
|
||||
|
||||
resolve(share_url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,13 +22,18 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||
min-height: fill-available;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
height: 100%;
|
||||
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||
min-height: fill-available;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
@@ -70,11 +75,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] {
|
||||
@@ -135,7 +204,8 @@ body {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
a,
|
||||
.icon-button {
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
@@ -187,15 +257,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;
|
||||
@@ -340,10 +405,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,
|
||||
@@ -450,10 +515,11 @@ x-peer[status] x-icon {
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
.device-descriptor > div {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -533,6 +599,7 @@ footer {
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
@@ -557,6 +624,39 @@ footer .font-body2 {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#display-name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
max-width: 15em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -6px;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.1rem;
|
||||
border-radius: 1.3rem/30%;
|
||||
border-right: solid 1rem transparent;
|
||||
border-left: solid 1rem transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(var(--text-color), 43%);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
@@ -564,7 +664,7 @@ x-dialog x-background {
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
padding: 35px;
|
||||
padding: 15px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
@@ -575,19 +675,20 @@ x-dialog x-paper {
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper {
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
height: 650px;
|
||||
margin-top: -325px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
@@ -602,12 +703,6 @@ x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 0;
|
||||
margin-bottom: -16px;
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
x-dialog a {
|
||||
color: var(--primary-color);
|
||||
@@ -646,7 +741,7 @@ x-dialog .font-subheading {
|
||||
}
|
||||
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
margin-left: 18px;
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#room-key {
|
||||
@@ -658,16 +753,11 @@ x-dialog .font-subheading {
|
||||
}
|
||||
|
||||
#room-key-qr-code {
|
||||
padding: inherit;
|
||||
margin: auto;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pair-device-dialog hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
width: 100%;
|
||||
margin: 40px -24px;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-background {
|
||||
@@ -681,29 +771,24 @@ x-dialog .row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#receive-request-dialog h2,
|
||||
#receive-file-dialog h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse {
|
||||
margin: 40px -24px 0;
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: solid 1.25px var(--border-color);
|
||||
margin-bottom: -16px;
|
||||
x-paper > div:last-child > .button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
word-break: break-word;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
@@ -715,26 +800,26 @@ x-dialog .row-reverse {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
#file-name {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#file-stem {
|
||||
max-width: 80%;
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.file-size{
|
||||
margin-bottom: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
min-height: 120px;
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
@@ -742,14 +827,14 @@ x-dialog .row-reverse {
|
||||
#receive-text-dialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
white-space: pre-wrap;
|
||||
margin-top:36px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receive-text-dialog #text a {
|
||||
@@ -768,17 +853,32 @@ x-dialog .row-reverse {
|
||||
|
||||
.row-separator {
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
margin: auto -25px;
|
||||
margin: auto -24px;
|
||||
}
|
||||
|
||||
#receive-text-description-container {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#base64-paste-btn {
|
||||
#base64-paste-btn,
|
||||
#base64-paste-dialog .textarea {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#base64-paste-dialog .textarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#base64-paste-dialog .textarea::before {
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-color);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
content: attr(placeholder);
|
||||
}
|
||||
|
||||
#base64-paste-dialog button {
|
||||
@@ -800,7 +900,6 @@ x-dialog .row-reverse {
|
||||
padding: 2px 16px 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
@@ -811,6 +910,7 @@ x-dialog .row-reverse {
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@@ -848,7 +948,7 @@ x-dialog .row-reverse {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancel-paste-mode-btn {
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -875,7 +975,6 @@ button::-moz-focus-inner {
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -885,10 +984,7 @@ button::-moz-focus-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -902,9 +998,8 @@ button::-moz-focus-inner {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 254px);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -939,8 +1034,8 @@ button::-moz-focus-inner {
|
||||
|
||||
#about x-background {
|
||||
position: absolute;
|
||||
top: calc(32px - 250px);
|
||||
right: calc(32px - 250px);
|
||||
top: calc(28px - 250px);
|
||||
right: calc(36px - 250px);
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
border-radius: 50%;
|
||||
@@ -963,6 +1058,13 @@ button::-moz-focus-inner {
|
||||
margin: 8px 8px -16px;
|
||||
}
|
||||
|
||||
#about section {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#about header {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* Loading Indicator */
|
||||
|
||||
@@ -1012,11 +1114,11 @@ button::-moz-focus-inner {
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background-color: rgb(var(--text-color));
|
||||
color: rgb(var(--bg-color));
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
@@ -1030,7 +1132,7 @@ x-toast {
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
|
||||
|
||||
@@ -1094,6 +1196,14 @@ x-peers:empty~x-instructions {
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px) {
|
||||
footer {
|
||||
@@ -1166,7 +1276,9 @@ x-dialog x-paper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.element-preview {
|
||||
.file-preview > img,
|
||||
.file-preview > audio,
|
||||
.file-preview > video {
|
||||
max-width: 100%;
|
||||
max-height: 40vh;
|
||||
margin: auto;
|
||||
|
||||
@@ -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" >
|
||||
</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="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#clear-pair-devices-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||
</header>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
@@ -89,7 +103,13 @@
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="display-name" placeholder=" "></div>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
@@ -109,18 +129,17 @@
|
||||
<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 id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" 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="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<div class="separator"></div>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -133,9 +152,9 @@
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Unpair Devices</h2>
|
||||
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit">Unpair Devices</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -145,25 +164,23 @@
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">PairDrop</h2>
|
||||
<div class="text-center file-description">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span id="requesting-peer-display-name"></span>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
</div>
|
||||
<div id="file-name" class="row" >
|
||||
<span id="file-stem"></span>
|
||||
<span id="file-extension"></span>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span id="file-other"></span>
|
||||
<div class="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<div class="separator"></div>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -173,13 +190,23 @@
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 id="receive-title" class="center"></h2>
|
||||
<div class="text-center file-description"></div>
|
||||
<div class="font-body2 text-center file-size"></div>
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse space-between">
|
||||
<a id="share-or-download" class="button" autofocus></a>
|
||||
<div class="separator"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -190,16 +217,16 @@
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="text-center">PairDrop</h2>
|
||||
<div class="text-center">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span id="text-send-peer-display-name"></span>
|
||||
<span class="display-name"></span>
|
||||
</div>
|
||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="row-reverse">
|
||||
<div class="row-separator"></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>
|
||||
<div class="separator"></div>
|
||||
<a class="button" title="ESCAPE" close>Cancel</a>
|
||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -209,16 +236,15 @@
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2>PairDrop - Message Received</h2>
|
||||
<div id="receive-text-description-container">
|
||||
<span id="receive-text-peer-display-name"></span>
|
||||
<span>sent the following message:</span>
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<div class="separator"></div>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -229,6 +255,7 @@
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||
<button class="button center" close>Close</button>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -239,14 +266,14 @@
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<section class="center column fade-in">
|
||||
<header class="row-reverse">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
@@ -318,7 +345,13 @@
|
||||
<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">
|
||||
@@ -329,13 +362,17 @@
|
||||
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
|
||||
</symbol>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
<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:
|
||||
@@ -19,10 +19,10 @@ class ServerConnection {
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
_connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(await this._endpoint());
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => this._onOpen();
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
@@ -34,6 +34,7 @@ class ServerConnection {
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
Events.fire('ws-connected');
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
@@ -50,16 +51,23 @@ class ServerConnection {
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
}
|
||||
|
||||
_setRtcConfig(config) {
|
||||
window.rtcConfig = config;
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'rtc-config':
|
||||
this._setRtcConfig(msg.config);
|
||||
break;
|
||||
case 'peers':
|
||||
Events.fire('peers', msg);
|
||||
break;
|
||||
@@ -105,6 +113,7 @@ class ServerConnection {
|
||||
case 'file-transfer-complete':
|
||||
case 'message-transfer-complete':
|
||||
case 'text':
|
||||
case 'display-name-changed':
|
||||
case 'ws-chunk':
|
||||
Events.fire('ws-relay', JSON.stringify(msg));
|
||||
break;
|
||||
@@ -120,34 +129,24 @@ class ServerConnection {
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
if (!peerId) {
|
||||
// save peerId to indexedDB to retrieve after PWA is installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
});
|
||||
}
|
||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
async _endpoint() {
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = await this._peerId();
|
||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
}
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
async _peerId() {
|
||||
// make peerId persistent when pwa is installed
|
||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
? await PersistentStorage.get('peerId')
|
||||
: sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
@@ -155,15 +154,17 @@ class ServerConnection {
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
@@ -204,6 +205,10 @@ class Peer {
|
||||
this._send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
sendDisplayName(displayName) {
|
||||
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||
}
|
||||
|
||||
async createHeader(file) {
|
||||
return {
|
||||
name: file.name,
|
||||
@@ -324,31 +329,30 @@ class Peer {
|
||||
this.sendJSON({ type: 'progress', progress: progress });
|
||||
}
|
||||
|
||||
_onMessage(message, logMessage = true) {
|
||||
_onMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
if (logMessage) console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
const messageJSON = JSON.parse(message);
|
||||
switch (messageJSON.type) {
|
||||
case 'request':
|
||||
this._onFilesTransferRequest(message);
|
||||
this._onFilesTransferRequest(messageJSON);
|
||||
break;
|
||||
case 'header':
|
||||
this._onFilesHeader(message);
|
||||
this._onFileHeader(messageJSON);
|
||||
break;
|
||||
case 'partition':
|
||||
this._onReceivedPartitionEnd(message);
|
||||
this._onReceivedPartitionEnd(messageJSON);
|
||||
break;
|
||||
case 'partition-received':
|
||||
this._sendNextPartition();
|
||||
break;
|
||||
case 'progress':
|
||||
this._onDownloadProgress(message.progress);
|
||||
this._onDownloadProgress(messageJSON.progress);
|
||||
break;
|
||||
case 'files-transfer-response':
|
||||
this._onFileTransferRequestResponded(message);
|
||||
this._onFileTransferRequestResponded(messageJSON);
|
||||
break;
|
||||
case 'file-transfer-complete':
|
||||
this._onFileTransferCompleted();
|
||||
@@ -357,7 +361,10 @@ class Peer {
|
||||
this._onMessageTransferCompleted();
|
||||
break;
|
||||
case 'text':
|
||||
this._onTextReceived(message);
|
||||
this._onTextReceived(messageJSON);
|
||||
break;
|
||||
case 'display-name-changed':
|
||||
this._onDisplayNameChanged(messageJSON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -393,8 +400,8 @@ class Peer {
|
||||
this._requestPending = null;
|
||||
}
|
||||
|
||||
_onFilesHeader(header) {
|
||||
if (this._requestAccepted?.header.length) {
|
||||
_onFileHeader(header) {
|
||||
if (this._requestAccepted && this._requestAccepted.header.length) {
|
||||
this._lastProgress = 0;
|
||||
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
|
||||
this._requestAccepted.totalSize,
|
||||
@@ -447,11 +454,14 @@ class Peer {
|
||||
this._abortTransfer();
|
||||
}
|
||||
|
||||
// include for compatibility with Snapdrop for Android app
|
||||
Events.fire('file-received', fileBlob);
|
||||
|
||||
this._filesReceived.push(fileBlob);
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -496,12 +506,19 @@ class Peer {
|
||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName || this._displayName === message.displayName) return;
|
||||
this._displayName = message.displayName;
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = true;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
@@ -519,8 +536,9 @@ class RTCPeer extends Peer {
|
||||
_openConnection(peerId, isCaller) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
||||
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onicecandidateerror = e => this._onError(e);
|
||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||
}
|
||||
@@ -568,14 +586,21 @@ class RTCPeer extends Peer {
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = _ => this._onChannelClosed();
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._closeChannel());
|
||||
this._channel = channel;
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._onPageHide());
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message === 'string') {
|
||||
console.log('RTC:', JSON.parse(message));
|
||||
}
|
||||
super._onMessage(message);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
@@ -611,10 +636,16 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
}
|
||||
|
||||
_closeChannel() {
|
||||
if (this._channel) this._channel.onclose = null;
|
||||
if (this._conn) this._conn.close();
|
||||
this._conn = null;
|
||||
_onPageHide() {
|
||||
this._disconnect();
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
if (this._conn && this._channel) {
|
||||
this._channel.onclose = null;
|
||||
this._channel.close();
|
||||
}
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
@@ -628,9 +659,11 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection disconnected');
|
||||
break;
|
||||
case 'failed':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection failed');
|
||||
break;
|
||||
}
|
||||
@@ -676,13 +709,20 @@ class RTCPeer extends Peer {
|
||||
_isConnecting() {
|
||||
return this._channel && this._channel.readyState === 'connecting';
|
||||
}
|
||||
|
||||
sendDisplayName(displayName) {
|
||||
if (!this._isConnected()) return;
|
||||
super.sendDisplayName(displayName);
|
||||
}
|
||||
}
|
||||
|
||||
class WSPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = false;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._isCaller = true;
|
||||
this._sendSignal();
|
||||
}
|
||||
|
||||
@@ -694,21 +734,22 @@ class WSPeer extends Peer {
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
console.debug(message)
|
||||
message.to = this._peerId;
|
||||
message.roomType = this._roomType;
|
||||
message.roomSecret = this._roomSecret;
|
||||
this._server.send(message);
|
||||
}
|
||||
|
||||
_sendSignal() {
|
||||
this.sendJSON({type: 'signal'});
|
||||
_sendSignal(connected = false) {
|
||||
this.sendJSON({type: 'signal', connected: connected});
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (this._peerId) return;
|
||||
this._peerId = message.sender.id;
|
||||
this._sendSignal();
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (message.connected) return;
|
||||
this._sendSignal(true);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
@@ -728,8 +769,13 @@ class PeersManager {
|
||||
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||
}
|
||||
|
||||
@@ -749,7 +795,7 @@ class PeersManager {
|
||||
_onWsRelay(message) {
|
||||
const messageJSON = JSON.parse(message)
|
||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
||||
this.peers[messageJSON.sender.id]._onMessage(message, false)
|
||||
this.peers[messageJSON.sender.id]._onMessage(message)
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
@@ -768,10 +814,6 @@ class PeersManager {
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
@@ -797,15 +839,28 @@ class PeersManager {
|
||||
}
|
||||
|
||||
_onPeerLeft(msg) {
|
||||
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) {
|
||||
console.log('WSPeer left:', msg.peerId)
|
||||
Events.fire('peer-disconnected', msg.peerId)
|
||||
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
console.log('WSPeer left:', msg.peerId);
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
} else if (msg.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', msg.peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
for (const peerId in this.peers) {
|
||||
console.debug(this.peers[peerId].rtcSupported);
|
||||
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
|
||||
Events.fire('peer-disconnected', peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
@@ -823,6 +878,23 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeerDisplayNameChanged(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (!peer) return;
|
||||
this.peers[peerId].sendDisplayName(this._displayName);
|
||||
}
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
@@ -911,28 +983,11 @@ class Events {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback) {
|
||||
return window.addEventListener(type, callback, false);
|
||||
static on(type, callback, options = false) {
|
||||
return window.addEventListener(type, callback, options);
|
||||
}
|
||||
|
||||
static off(type, callback) {
|
||||
return window.removeEventListener(type, callback, false);
|
||||
static off(type, callback, options = false) {
|
||||
return window.removeEventListener(type, callback, options);
|
||||
}
|
||||
}
|
||||
|
||||
RTCPeer.config = {
|
||||
'sdpSemantics': 'unified-plan',
|
||||
'iceServers': [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
},
|
||||
{
|
||||
urls: 'stun:openrelay.metered.ca:80'
|
||||
},
|
||||
{
|
||||
urls: 'turn:openrelay.metered.ca:443',
|
||||
username: 'openrelayproject',
|
||||
credential: 'openrelayproject',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.2.0';
|
||||
const cacheVersion = 'v1.6.2';
|
||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
@@ -71,30 +71,11 @@ const update = request =>
|
||||
self.addEventListener('fetch', function(event) {
|
||||
if (event.request.method === "POST") {
|
||||
// Requests related to Web Share Target.
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const formData = await event.request.formData();
|
||||
const title = formData.get("title");
|
||||
const text = formData.get("text");
|
||||
const url = formData.get("url");
|
||||
const files = formData.get("files");
|
||||
let share_url = "/";
|
||||
if (files.length > 0) {
|
||||
share_url = "/?share-target=files";
|
||||
const db = await window.indexedDB.open('pairdrop_store');
|
||||
const tx = db.transaction('share_target_files', 'readwrite');
|
||||
const store = tx.objectStore('share_target_files');
|
||||
for (let i=0; i<files.length; i++) {
|
||||
await store.add(files[i]);
|
||||
}
|
||||
await tx.complete
|
||||
db.close()
|
||||
} else if (title.length > 0 || text.length > 0 || url.length) {
|
||||
share_url = `/?share-target=text&title=${title}&text=${text}&url=${url}`;
|
||||
}
|
||||
return Response.redirect(encodeURI(share_url), 303);
|
||||
})()
|
||||
);
|
||||
event.respondWith((async () => {
|
||||
let share_url = await evaluateRequestData(event.request);
|
||||
share_url = event.request.url + share_url;
|
||||
return Response.redirect(encodeURI(share_url), 302);
|
||||
})());
|
||||
} else {
|
||||
// Regular requests not related to Web Share Target.
|
||||
event.respondWith(
|
||||
@@ -119,3 +100,49 @@ self.addEventListener('activate', evt =>
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const evaluateRequestData = async function (request) {
|
||||
const formData = await request.formData();
|
||||
const title = formData.get("title");
|
||||
const text = formData.get("text");
|
||||
const url = formData.get("url");
|
||||
const files = formData.getAll("allfiles");
|
||||
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
if (files && files.length > 0) {
|
||||
let fileObjects = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileObjects.push({
|
||||
name: files[i].name,
|
||||
buffer: await files[i].arrayBuffer()
|
||||
});
|
||||
}
|
||||
|
||||
const DBOpenRequest = indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
for (let i = 0; i < fileObjects.length; i++) {
|
||||
const transaction = db.transaction('share_target_files', 'readwrite');
|
||||
const objectStore = transaction.objectStore('share_target_files');
|
||||
|
||||
const objectStoreRequest = objectStore.add(fileObjects[i]);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
if (i === fileObjects.length - 1) resolve('?share-target=files');
|
||||
}
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = _ => {
|
||||
resolve('');
|
||||
}
|
||||
} else {
|
||||
let share_url = '?share-target=text';
|
||||
|
||||
if (title) share_url += `&title=${title}`;
|
||||
if (text) share_url += `&text=${text}`;
|
||||
if (url) share_url += `&url=${url}`;
|
||||
|
||||
resolve(share_url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,13 +23,18 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||
min-height: fill-available;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
height: 100%;
|
||||
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||
min-height: fill-available;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
@@ -71,11 +76,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] {
|
||||
@@ -136,7 +205,8 @@ body {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
a,
|
||||
.icon-button {
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
@@ -188,15 +258,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;
|
||||
@@ -350,10 +415,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,
|
||||
@@ -477,10 +542,11 @@ x-peer.ws-peer .highlight-wrapper {
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
.device-descriptor > div {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -559,6 +625,7 @@ footer {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
@@ -583,6 +650,39 @@ footer .font-body2 {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#display-name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
max-width: 15em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -6px;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.1rem;
|
||||
border-radius: 1.3rem/30%;
|
||||
border-right: solid 1rem transparent;
|
||||
border-left: solid 1rem transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(var(--text-color), 43%);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
@@ -590,7 +690,7 @@ x-dialog x-background {
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
padding: 35px;
|
||||
padding: 15px;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
@@ -601,19 +701,20 @@ x-dialog x-paper {
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper {
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
height: 650px;
|
||||
margin-top: -325px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
@@ -628,12 +729,6 @@ x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 0;
|
||||
margin-bottom: -16px;
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
x-dialog a {
|
||||
color: var(--primary-color);
|
||||
@@ -672,7 +767,7 @@ x-dialog .font-subheading {
|
||||
}
|
||||
|
||||
#key-input-container>input:nth-of-type(4) {
|
||||
margin-left: 18px;
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#room-key {
|
||||
@@ -684,16 +779,11 @@ x-dialog .font-subheading {
|
||||
}
|
||||
|
||||
#room-key-qr-code {
|
||||
padding: inherit;
|
||||
margin: auto;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#pair-device-dialog hr {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
width: 100%;
|
||||
margin: 40px -24px;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-background {
|
||||
@@ -707,29 +797,24 @@ x-dialog .row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#receive-request-dialog h2,
|
||||
#receive-file-dialog h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse {
|
||||
margin: 40px -24px 0;
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: solid 1.25px var(--border-color);
|
||||
margin-bottom: -16px;
|
||||
x-paper > div:last-child > .button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
word-break: break-word;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
@@ -741,26 +826,26 @@ x-dialog .row-reverse {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
#file-name {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#file-stem {
|
||||
max-width: 80%;
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.file-size{
|
||||
margin-bottom: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
min-height: 120px;
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
@@ -768,14 +853,14 @@ x-dialog .row-reverse {
|
||||
#receive-text-dialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 393px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
white-space: pre-wrap;
|
||||
margin-top:36px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receive-text-dialog #text a {
|
||||
@@ -794,17 +879,32 @@ x-dialog .row-reverse {
|
||||
|
||||
.row-separator {
|
||||
border-bottom: solid 2.5px var(--border-color);
|
||||
margin: auto -25px;
|
||||
margin: auto -24px;
|
||||
}
|
||||
|
||||
#receive-text-description-container {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#base64-paste-btn {
|
||||
#base64-paste-btn,
|
||||
#base64-paste-dialog .textarea {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#base64-paste-dialog .textarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#base64-paste-dialog .textarea::before {
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-color);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
content: attr(placeholder);
|
||||
}
|
||||
|
||||
#base64-paste-dialog button {
|
||||
@@ -826,7 +926,6 @@ x-dialog .row-reverse {
|
||||
padding: 2px 16px 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
@@ -837,6 +936,7 @@ x-dialog .row-reverse {
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
@@ -874,7 +974,7 @@ x-dialog .row-reverse {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancel-paste-mode-btn {
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -901,7 +1001,6 @@ button::-moz-focus-inner {
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -911,10 +1010,7 @@ button::-moz-focus-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -928,9 +1024,8 @@ button::-moz-focus-inner {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
max-height: 300px;
|
||||
max-height: calc(100vh - 254px);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -989,6 +1084,13 @@ button::-moz-focus-inner {
|
||||
margin: 8px 8px -16px;
|
||||
}
|
||||
|
||||
#about section {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#about header {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* Loading Indicator */
|
||||
|
||||
@@ -1038,11 +1140,11 @@ button::-moz-focus-inner {
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background-color: rgb(var(--text-color));
|
||||
color: rgb(var(--bg-color));
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
@@ -1056,7 +1158,7 @@ x-toast {
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
|
||||
|
||||
@@ -1120,6 +1222,14 @@ x-peers:empty~x-instructions {
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px) {
|
||||
#websocket-fallback {
|
||||
@@ -1192,7 +1302,9 @@ x-dialog x-paper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.element-preview {
|
||||
.file-preview > img,
|
||||
.file-preview > audio,
|
||||
.file-preview > video {
|
||||
max-width: 100%;
|
||||
max-height: 40vh;
|
||||
margin: auto;
|
||||
|
||||
13
rtc_config_example.json
Normal file
13
rtc_config_example.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"sdpSemantics": "unified-plan",
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": "stun:stun.l.google.com:19302"
|
||||
},
|
||||
{
|
||||
"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