Compare commits

...

47 Commits

Author SHA1 Message Date
schlagmichdoch
ab08091f5d increase version to v1.5.2 2023-03-28 20:00:05 +02:00
schlagmichdoch
19a78a5239 Merge pull request #72 from schlagmichdoch/fix_share_target
WIP: Fix share target API
2023-03-28 19:58:34 +02:00
schlagmichdoch
d0b2c81582 Tidy up code 2023-03-28 19:07:33 +02:00
Daniel Pham
10a0aaf896 Fix passed arguments for sharing text 2023-03-28 19:00:15 +02:00
Daniel Pham
34ebd60304 Update service worker
- files array now matches manifest files name
- fixed handling fetch redirect
2023-03-28 19:00:15 +02:00
schlagmichdoch
251df2fbff try to fix share target api 2023-03-28 19:00:05 +02:00
schlagmichdoch
1bb8a63eed increase version to v1.5.1 2023-03-27 02:31:56 +02:00
schlagmichdoch
7d581ca858 Merge pull request #92 from schlagmichdoch/optimize_animation
Optimize background animation to drastically reduce CPU/GPU resources
2023-03-27 02:19:19 +02:00
schlagmichdoch
dcc4e8b747 Optimize background animation drastically by using offscreen canvases to reuse frames. Rewrite animate function to prevent it from being called multiple times 2023-03-27 02:17:36 +02:00
schlagmichdoch
ce549adf22 Merge pull request #82 from kgncloud/patch-1
Add Healthcheck to Dockerfile
2023-03-25 04:09:46 +01:00
schlagmichdoch
bdb39a1d2c add docker-swarm-usage.md reference to host-your-own.md and tidy up docker-swarm-usage.md 2023-03-25 04:08:11 +01:00
schlagmichdoch
195dfd0bb3 Add https requirement for PWAs to faq.md 2023-03-25 03:37:15 +01:00
Kaindl Network
680ed81bd7 Create 2023-03-18 23:52:33 +00:00
schlagmichdoch
f120677393 increase version to v1.5.0 2023-03-14 15:52:15 +01:00
schlagmichdoch
3f85d266b3 Merge pull request #80 from schlagmichdoch/compatibility_snapdrop_android
PairDrop is finally compatible with snapdrop for android! 🎉
2023-03-14 15:49:12 +01:00
schlagmichdoch
01cd670afc Convert FAQ to collapsible sections and add back third-party apps 2023-03-14 15:43:40 +01:00
schlagmichdoch
3f0909637b fix full button width if only one button is shown 2023-03-14 15:12:31 +01:00
schlagmichdoch
1f97c12562 fix overflow of long device names for snapdrop-android 2023-03-14 15:12:31 +01:00
schlagmichdoch
17abc91c86 rename function and add event to achieve compatibility with snapdrop-android app 2023-03-14 15:12:23 +01:00
schlagmichdoch
4a0cd1f49a Update issue templates: Point out the FAQ 2023-03-14 11:48:57 +01:00
Kaindl Network
b7781e2bab Add Healthcheck to Dockerfile 2023-03-13 20:38:36 +01:00
schlagmichdoch
4e0fb89720 replace javascript operators ?? and ?. to support older browsers (see #79) 2023-03-13 14:21:26 +01:00
schlagmichdoch
5a290718b6 Merge pull request #78 from schlagmichdoch/dependabot/npm_and_yarn/ws-8.13.0
Bump ws from 8.12.1 to 8.13.0
2023-03-13 11:35:30 +01:00
dependabot[bot]
6c6f288c3d Bump ws from 8.12.1 to 8.13.0
Bumps [ws](https://github.com/websockets/ws) from 8.12.1 to 8.13.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.12.1...8.13.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 05:17:56 +00:00
schlagmichdoch
fea15d3ee1 increase version to v1.4.5 2023-03-13 00:05:57 +01:00
schlagmichdoch
028752a809 fixes #76. 'File received' dialog not showing on iOS when big videos are sent. 2023-03-13 00:04:48 +01:00
schlagmichdoch
1093f4d246 log error onicecandidateerror 2023-03-10 22:21:19 +01:00
schlagmichdoch
7ddd600b0c fix display name hidden on Firefox for Android 2023-03-10 20:01:59 +01:00
schlagmichdoch
715356aafb Fix AirDrop typo 2023-03-08 11:35:37 +01:00
schlagmichdoch
490e4db380 Add information about specifying TURN servers 2023-03-07 18:25:25 +01:00
schlagmichdoch
11a988e550 increase version to v1.4.4 2023-03-06 16:05:58 +01:00
schlagmichdoch
ff8f28660a prevent buttons from submitting form by adding type="button" 2023-03-06 16:03:34 +01:00
schlagmichdoch
5fc8e85f75 increase version to 1.4.3 2023-03-06 15:40:09 +01:00
schlagmichdoch
5eeaae01fe add connection hash to title of display-name of receive dialogs 2023-03-06 15:39:24 +01:00
schlagmichdoch
660e523263 prevent sending of displayName if RTCPeer is not connected 2023-03-06 15:33:22 +01:00
schlagmichdoch
cdfbc7a2df add missing removal of event listener to ws fallback ui.js 2023-03-06 15:32:58 +01:00
schlagmichdoch
c9dca7e083 fix download notification and add request notification 2023-03-06 15:32:42 +01:00
schlagmichdoch
79af04d95a increase version to v1.4.2 2023-03-06 12:31:44 +01:00
schlagmichdoch
954e9c7c3a Merge pull request #65 from schlagmichdoch/pairdrop_cli_add_firefox_fallback
pairdrop-cli: add fallback if navigator.clipboard.readText() is not available
2023-03-06 12:25:54 +01:00
schlagmichdoch
c0d504f6a8 remove base64 event listeners manually on hide instead of once: true 2023-03-06 12:20:30 +01:00
schlagmichdoch
36e152dc7c add { once: true } to deactivate-paste-mode event listener 2023-03-06 11:59:56 +01:00
schlagmichdoch
fdf024f378 pairdrop-cli: add fallback if navigator.clipboard.readText() is not available 2023-03-06 11:56:17 +01:00
schlagmichdoch
9f2e4c5f8f fix displayName sometimes not exchanged on reload 2023-03-06 11:24:19 +01:00
schlagmichdoch
8e219914ec Merge pull request #66 from schlagmichdoch/dependabot/npm_and_yarn/ua-parser-js-1.0.34
Bump ua-parser-js from 1.0.33 to 1.0.34
2023-03-06 10:53:35 +01:00
dependabot[bot]
d1273ef9cc Bump ua-parser-js from 1.0.33 to 1.0.34
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.33 to 1.0.34.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/1.0.34/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.33...1.0.34)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 05:12:00 +00:00
schlagmichdoch
27ac7786d0 increase version to v1.4.1 2023-03-06 03:48:23 +01:00
schlagmichdoch
edf2ab5eb3 revert some changes to regain stability 2023-03-06 03:47:24 +01:00
19 changed files with 889 additions and 402 deletions

View File

@@ -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: ''

View File

@@ -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

View File

@@ -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 />

View 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.

View File

@@ -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,106 @@ 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?
<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 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?
<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>
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).
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.
### What about security? Are my files encrypted while being sent between the computers?
<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>
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.
As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance.
Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed.
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
@@ -68,22 +158,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).
### 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 to support open source software](https://www.buymeacoffee.com/pairdrop)
* [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)

View File

@@ -82,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
@@ -101,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

View File

@@ -368,6 +368,10 @@ class PairDropServer {
// delete the peer
delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room];

32
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "pairdrop",
"version": "1.4.0",
"version": "1.5.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.4.0",
"version": "1.5.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.34",
"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.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
"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.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
},
"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": {}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.4.0",
"version": "1.5.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.34",
"unique-names-generator": "^4.3.0",
"ws": "^8.12.1"
"ws": "^8.13.0"
},
"engines": {
"node": ">=15"

View File

@@ -122,7 +122,7 @@
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -137,7 +137,7 @@
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -209,7 +209,7 @@
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" title="ESCAPE" close>Cancel</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -238,6 +238,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>

View File

@@ -8,7 +8,6 @@ class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
Events.on('beforeunload', _ => this._onBeforeUnload());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
@@ -137,7 +136,8 @@ class ServerConnection {
return ws_url.toString();
}
_onBeforeUnload() {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
@@ -147,10 +147,6 @@ class ServerConnection {
}
}
_disconnect() {
this.send({ type: 'disconnect' });
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..');
@@ -198,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,
@@ -329,7 +329,7 @@ class Peer {
this._onFilesTransferRequest(messageJSON);
break;
case 'header':
this._onFilesHeader(messageJSON);
this._onFileHeader(messageJSON);
break;
case 'partition':
this._onReceivedPartitionEnd(messageJSON);
@@ -389,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,
@@ -443,6 +443,9 @@ 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;
@@ -494,7 +497,8 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName) return;
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
}
}
@@ -523,6 +527,7 @@ class RTCPeer extends Peer {
this._peerId = peerId;
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);
}
@@ -573,7 +578,7 @@ class RTCPeer extends Peer {
const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data);
channel.onclose = e => this._onChannelClosed(e);
channel.onclose = _ => this._onChannelClosed();
this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
@@ -617,8 +622,6 @@ class RTCPeer extends Peer {
if (this._busy) {
e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?";
} else {
this._disconnect();
}
}
@@ -695,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 {
@@ -713,6 +721,7 @@ class PeersManager {
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) {
@@ -798,8 +807,8 @@ class PeersManager {
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return;
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
if (!peer) return;
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
@@ -893,11 +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);
}
}

View File

@@ -107,8 +107,15 @@ class PeersUI {
_getSavedDisplayName() {
return new Promise((resolve) => {
PersistentStorage.get('editedDisplayName')
.then(displayName => resolve(displayName ?? ""))
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
.then(displayName => {
if (!displayName) displayName = "";
resolve(displayName);
})
.catch(_ => {
let displayName = localStorage.getItem('editedDisplayName');
if (!displayName) displayName = "";
resolve(displayName);
})
});
}
@@ -241,7 +248,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback);
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback));
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -300,7 +307,8 @@ class PeerUI {
constructor(peer, connectionHash) {
this._peer = peer;
this._connectionHash = connectionHash;
this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
this._initDom();
this._bindListeners();
@@ -345,8 +353,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent =
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
}
_initDom() {
@@ -569,7 +576,7 @@ class ReceiveDialog extends Dialog {
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
@@ -586,6 +593,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
@@ -603,8 +611,9 @@ class ReceiveFileDialog extends ReceiveDialog {
}
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
const displayName = $(sender).ui._displayName();
const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
@@ -612,12 +621,11 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
}
_dequeueFile() {
// Todo: change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do
this._busy = false;
return;
@@ -631,32 +639,40 @@ class ReceiveFileDialog extends ReceiveDialog {
createPreviewElement(file) {
return new Promise((resolve, reject) => {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
try {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
console.log('the file is able to preview');
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
};
element.addEventListener('loadeddata', _ => resolve(true));
element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`);
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
let element = document.createElement(previewElement[mime]);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onloadeddata = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onerror = _ => {
reject(`${mime} preview could not be loaded from type ${file.type}`);
};
element.src = URL.createObjectURL(file);
}
} catch (e) {
reject(`preview could not be loaded from type ${file.type}`);
}
});
}
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
@@ -733,20 +749,30 @@ class ReceiveFileDialog extends ReceiveDialog {
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
setTimeout(_ => {
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}, 500);
this.createPreviewElement(files[0])
.then(canPreview => {
if (canPreview) {
console.log('the file is able to preview');
} else {
console.log('the file is not able to preview');
}
})
.catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
@@ -803,9 +829,10 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
const connectionHash = $(peerId).ui._connectionHash;
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
element.src = request.thumbnailDataUrl;
this.$previewBox.appendChild(element)
@@ -1245,21 +1272,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) {
this.show();
if (base64Text === "paste") {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
this.preparePasting("text");
} else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1269,7 +1296,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1282,14 +1309,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.")
console.log("File content incorrect.");
}).finally(_ => {
this.hide();
});
} else {
// ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
this.preparePasting('files');
}
}
}
@@ -1299,39 +1325,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing...";
}
async processClipboard(type) {
if (!navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.');
console.log("navigator.clipboard.readText() is not available on your browser.")
this.hide();
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
preparePasting(type) {
if (navigator.clipboard.readText) {
this.$pasteBtn.innerText = `Tap here to paste ${type}`;
this._clickCallback = _ => this.processClipboard(type);
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
} else {
this.processBase64Zip(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
this.$pasteBtn.setAttribute('hidden', '');
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
this.$fallbackTextarea.removeAttribute('hidden');
this._inputCallback = _ => this.processInput(type);
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
this.$fallbackTextarea.focus();
}
}
async processInput(type) {
const base64 = this.$fallbackTextarea.textContent;
this.$fallbackTextarea.textContent = '';
await this.processBase64(type, base64);
}
async processClipboard(type) {
const base64 = await navigator.clipboard.readText();
await this.processBase64(type, base64);
}
isValidBase64(base64) {
try {
// check if input is base64 encoded
window.atob(base64);
return true;
} catch (e) {
// input is not base64 string.
return false;
}
}
async processBase64(type, base64) {
if (!base64 || !this.isValidBase64(base64)) return;
this._setPasteBtnToProcessing();
try {
if (type === "text") {
await this.processBase64Text(base64);
} else {
await this.processBase64Zip(base64);
}
} catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}
this.hide();
}
processBase64Text(base64Text){
return new Promise((resolve) => {
this._setPasteBtnToProcessing();
@@ -1365,6 +1412,8 @@ class Base64ZipDialog extends Dialog {
hide() {
this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide();
}
}
@@ -1395,9 +1444,9 @@ class Notifications {
this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission());
}
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
}
_requestPermission() {
@@ -1470,8 +1519,29 @@ class Notifications {
}
}
_requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<request.header.length; i++) {
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show');
}
}
_download(notification) {
$('share-or-download').click();
$('download-btn').click();
notification.close();
}
@@ -1537,27 +1607,33 @@ class WebShareTargetUI {
let shareTargetText;
if (url) {
shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable.
shareTargetText = url; // we share only the link - no text.
} else if (title && text) {
shareTargetText = title + '\r\n' + text;
} else {
shareTargetText = title + text;
}
console.log('Shared Target Text:', '"' + shareTargetText + '"');
Events.fire('activate-paste-mode', {files: [], text: shareTargetText})
} else if (share_target_type === "files") {
const openRequest = window.indexedDB.open('pairdrop_store')
openRequest.onsuccess( db => {
let openRequest = window.indexedDB.open('pairdrop_store')
openRequest.onsuccess = e => {
const db = e.target.result;
const tx = db.transaction('share_target_files', 'readwrite');
const store = tx.objectStore('share_target_files');
const request = store.getAll();
request.onsuccess = _ => {
Events.fire('activate-paste-mode', {files: request.result, text: ""})
const fileObjects = request.result;
let filesReceived = [];
for (let i=0; i<fileObjects.length; i++) {
filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name));
}
const clearRequest = store.clear()
clearRequest.onsuccess = _ => db.close();
Events.fire('activate-paste-mode', {files: filesReceived, text: ""})
}
})
}
}
window.history.replaceState({}, "Rewrite URL", '/');
}
@@ -1614,7 +1690,7 @@ class PersistentStorage {
PersistentStorage.logBrowserNotCapable();
return;
}
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 2);
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
DBOpenRequest.onerror = (e) => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
@@ -1640,7 +1716,10 @@ class PersistentStorage {
}
try {
db.createObjectStore('share_target_files');
if (db.objectStoreNames.contains('share_target_files')) {
db.deleteObjectStore('share_target_files');
}
db.createObjectStore('share_target_files', {autoIncrement: true});
} catch (error) {
console.log("Object store named 'share_target_files' already exists")
}
@@ -1873,68 +1952,84 @@ Events.on('load', () => {
style.zIndex = -1;
style.top = 0;
style.left = 0;
let ctx = c.getContext('2d');
let cCtx = c.getContext('2d');
let x0, y0, w, h, dw, offset;
let offscreenCanvases = [];
function init() {
let oldW = w;
let oldH = h;
let oldOffset = offset
w = document.documentElement.clientWidth;
h = document.documentElement.clientHeight;
c.width = w;
c.height = h;
offset = $$('footer').offsetHeight - 32;
if (h > 800) offset += 16;
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
c.width = w;
c.height = h;
x0 = w / 2;
y0 = h - offset;
dw = Math.max(w, h, 1000) / 13;
drawCircles();
dw = Math.round(Math.max(w, h, 1000) / 13);
drawCircles(cCtx, 0);
// enforce redrawing of frames
offscreenCanvases = [];
}
Events.on('bg-resize', _ => init());
window.onresize = _ => Events.fire('bg-resize');
function drawCircle(radius) {
function drawCircle(ctx, radius) {
ctx.beginPath();
let color = Math.round(255 * (1 - radius / Math.max(w, h)));
ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)';
ctx.lineWidth = 2;
let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h));
ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`;
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.lineWidth = 2;
}
let step = 0;
function drawCircles() {
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < 8; i++) {
drawCircle(dw * i + step % dw);
}
step += 1;
}
let loading = true;
function animate() {
if (loading || !finished()) {
requestAnimationFrame(function() {
drawCircles();
animate();
});
function drawCircles(ctx, frame) {
for (let i = 0; i < 13; i++) {
drawCircle(ctx, dw * i + frame);
}
}
function finished() {
return step % dw >= dw - 5;
function createOffscreenCanvas(frame) {
let canvas = document.createElement("canvas");
canvas.width = c.width;
canvas.height = c.height;
offscreenCanvases[frame] = canvas;
let ctx = canvas.getContext('2d');
drawCircles(ctx, frame);
}
function drawFrame(frame) {
cCtx.clearRect(0, 0, w, h);
if (!offscreenCanvases[frame]) {
createOffscreenCanvas(frame);
}
cCtx.drawImage(offscreenCanvases[frame], 0, 0);
}
let animate = true;
let currentFrame = 0;
function animateBg() {
if (currentFrame + 1 < dw || animate) {
currentFrame = (currentFrame + 1) % dw;
drawFrame(currentFrame);
}
setTimeout(_ => animateBg(), 3000 / dw);
}
window.animateBackground = function(l) {
if (!l) {
loading = false;
} else if (!loading) {
loading = true;
if (finished()) animate();
}
animate = l;
};
init();
animate();
animateBg();
});
document.changeFavicon = function (src) {

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.4.0';
const cacheVersion = 'v1.5.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);
}
});
}

View File

@@ -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 {
@@ -454,7 +459,7 @@ x-peer[status] x-icon {
text-align: center;
}
.name {
.device-descriptor > div {
width: 100%;
white-space: nowrap;
overflow: hidden;
@@ -715,7 +720,7 @@ x-paper > div:last-child {
x-paper > div:last-child > .button {
height: 100%;
width: 50%;
width: 100%;
}
x-paper > div:last-child > .button:not(:last-child) {
@@ -791,10 +796,29 @@ x-dialog .dialog-subheader {
margin: auto -24px;
}
#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 {

View File

@@ -125,7 +125,7 @@
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -140,7 +140,7 @@
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -212,7 +212,7 @@
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" title="ESCAPE" close>Cancel</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -241,6 +241,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>

View File

@@ -6,7 +6,6 @@ class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
Events.on('beforeunload', _ => this._onBeforeUnload());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
@@ -148,7 +147,8 @@ class ServerConnection {
return ws_url.toString();
}
_onBeforeUnload() {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
@@ -158,10 +158,6 @@ class ServerConnection {
}
}
_disconnect() {
this.send({ type: 'disconnect' });
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..');
@@ -209,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,
@@ -340,7 +340,7 @@ class Peer {
this._onFilesTransferRequest(messageJSON);
break;
case 'header':
this._onFilesHeader(messageJSON);
this._onFileHeader(messageJSON);
break;
case 'partition':
this._onReceivedPartitionEnd(messageJSON);
@@ -400,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,
@@ -454,6 +454,9 @@ 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;
@@ -505,7 +508,8 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName) return;
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
}
}
@@ -534,6 +538,7 @@ class RTCPeer extends Peer {
this._peerId = peerId;
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);
}
@@ -584,7 +589,7 @@ class RTCPeer extends Peer {
const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data);
channel.onclose = e => this._onChannelClosed(e);
channel.onclose = _ => this._onChannelClosed();
this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
@@ -628,8 +633,6 @@ class RTCPeer extends Peer {
if (this._busy) {
e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?";
} else {
this._disconnect();
}
}
@@ -706,6 +709,11 @@ 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 {
@@ -766,6 +774,7 @@ class PeersManager {
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));
}
@@ -879,8 +888,8 @@ class PeersManager {
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return;
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
if (!peer) return;
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
@@ -974,11 +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);
}
}

View File

@@ -107,8 +107,15 @@ class PeersUI {
_getSavedDisplayName() {
return new Promise((resolve) => {
PersistentStorage.get('editedDisplayName')
.then(displayName => resolve(displayName ?? ""))
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
.then(displayName => {
if (!displayName) displayName = "";
resolve(displayName);
})
.catch(_ => {
let displayName = localStorage.getItem('editedDisplayName');
if (!displayName) displayName = "";
resolve(displayName);
})
});
}
@@ -241,7 +248,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback);
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback));
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -300,7 +307,8 @@ class PeerUI {
constructor(peer, connectionHash) {
this._peer = peer;
this._connectionHash = connectionHash;
this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
this._initDom();
this._bindListeners();
@@ -345,8 +353,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent =
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
}
_initDom() {
@@ -570,7 +577,7 @@ class ReceiveDialog extends Dialog {
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
@@ -587,6 +594,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
@@ -604,8 +612,9 @@ class ReceiveFileDialog extends ReceiveDialog {
}
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
const displayName = $(sender).ui._displayName();
const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
@@ -613,12 +622,11 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
}
_dequeueFile() {
// Todo: change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do
this._busy = false;
return;
@@ -632,32 +640,40 @@ class ReceiveFileDialog extends ReceiveDialog {
createPreviewElement(file) {
return new Promise((resolve, reject) => {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
try {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
console.log('the file is able to preview');
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
};
element.addEventListener('loadeddata', _ => resolve(true));
element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`);
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
let element = document.createElement(previewElement[mime]);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onloadeddata = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onerror = _ => {
reject(`${mime} preview could not be loaded from type ${file.type}`);
};
element.src = URL.createObjectURL(file);
}
} catch (e) {
reject(`preview could not be loaded from type ${file.type}`);
}
});
}
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
@@ -734,20 +750,30 @@ class ReceiveFileDialog extends ReceiveDialog {
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
setTimeout(_ => {
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}, 500);
this.createPreviewElement(files[0])
.then(canPreview => {
if (canPreview) {
console.log('the file is able to preview');
} else {
console.log('the file is not able to preview');
}
})
.catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
@@ -804,9 +830,10 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
const connectionHash = $(peerId).ui._connectionHash;
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
element.src = request.thumbnailDataUrl;
this.$previewBox.appendChild(element)
@@ -1246,21 +1273,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) {
this.show();
if (base64Text === "paste") {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
this.preparePasting("text");
} else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1270,7 +1297,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1283,14 +1310,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.")
console.log("File content incorrect.");
}).finally(_ => {
this.hide();
});
} else {
// ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
this.preparePasting('files');
}
}
}
@@ -1300,39 +1326,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing...";
}
async processClipboard(type) {
if (!navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.');
console.log("navigator.clipboard.readText() is not available on your browser.")
this.hide();
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
preparePasting(type) {
if (navigator.clipboard.readText) {
this.$pasteBtn.innerText = `Tap here to paste ${type}`;
this._clickCallback = _ => this.processClipboard(type);
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
} else {
this.processBase64Zip(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
this.$pasteBtn.setAttribute('hidden', '');
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
this.$fallbackTextarea.removeAttribute('hidden');
this._inputCallback = _ => this.processInput(type);
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
this.$fallbackTextarea.focus();
}
}
async processInput(type) {
const base64 = this.$fallbackTextarea.textContent;
this.$fallbackTextarea.textContent = '';
await this.processBase64(type, base64);
}
async processClipboard(type) {
const base64 = await navigator.clipboard.readText();
await this.processBase64(type, base64);
}
isValidBase64(base64) {
try {
// check if input is base64 encoded
window.atob(base64);
return true;
} catch (e) {
// input is not base64 string.
return false;
}
}
async processBase64(type, base64) {
if (!base64 || !this.isValidBase64(base64)) return;
this._setPasteBtnToProcessing();
try {
if (type === "text") {
await this.processBase64Text(base64);
} else {
await this.processBase64Zip(base64);
}
} catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}
this.hide();
}
processBase64Text(base64Text){
return new Promise((resolve) => {
this._setPasteBtnToProcessing();
@@ -1366,6 +1413,8 @@ class Base64ZipDialog extends Dialog {
hide() {
this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide();
}
}
@@ -1396,9 +1445,9 @@ class Notifications {
this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission());
}
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
}
_requestPermission() {
@@ -1471,8 +1520,29 @@ class Notifications {
}
}
_requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<request.header.length; i++) {
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show');
}
}
_download(notification) {
$('share-or-download').click();
$('download-btn').click();
notification.close();
}
@@ -1538,27 +1608,33 @@ class WebShareTargetUI {
let shareTargetText;
if (url) {
shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable.
shareTargetText = url; // we share only the link - no text.
} else if (title && text) {
shareTargetText = title + '\r\n' + text;
} else {
shareTargetText = title + text;
}
console.log('Shared Target Text:', '"' + shareTargetText + '"');
Events.fire('activate-paste-mode', {files: [], text: shareTargetText})
} else if (share_target_type === "files") {
const openRequest = window.indexedDB.open('pairdrop_store')
openRequest.onsuccess( db => {
let openRequest = window.indexedDB.open('pairdrop_store')
openRequest.onsuccess = e => {
const db = e.target.result;
const tx = db.transaction('share_target_files', 'readwrite');
const store = tx.objectStore('share_target_files');
const request = store.getAll();
request.onsuccess = _ => {
Events.fire('activate-paste-mode', {files: request.result, text: ""})
const fileObjects = request.result;
let filesReceived = [];
for (let i=0; i<fileObjects.length; i++) {
filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name));
}
const clearRequest = store.clear()
clearRequest.onsuccess = _ => db.close();
Events.fire('activate-paste-mode', {files: filesReceived, text: ""})
}
})
}
}
window.history.replaceState({}, "Rewrite URL", '/');
}
@@ -1615,7 +1691,7 @@ class PersistentStorage {
PersistentStorage.logBrowserNotCapable();
return;
}
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 2);
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
DBOpenRequest.onerror = (e) => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
@@ -1641,7 +1717,10 @@ class PersistentStorage {
}
try {
db.createObjectStore('share_target_files');
if (db.objectStoreNames.contains('share_target_files')) {
db.deleteObjectStore('share_target_files');
}
db.createObjectStore('share_target_files', {autoIncrement: true});
} catch (error) {
console.log("Object store named 'share_target_files' already exists")
}
@@ -1874,67 +1953,84 @@ Events.on('load', () => {
style.zIndex = -1;
style.top = 0;
style.left = 0;
let ctx = c.getContext('2d');
let cCtx = c.getContext('2d');
let x0, y0, w, h, dw, offset;
let offscreenCanvases = [];
function init() {
let oldW = w;
let oldH = h;
let oldOffset = offset
w = document.documentElement.clientWidth;
h = document.documentElement.clientHeight;
offset = $$('footer').offsetHeight - 32;
if (h > 800) offset += 16;
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
c.width = w;
c.height = h;
offset = $$('footer').offsetHeight - 32;
x0 = w / 2;
y0 = h - offset;
dw = Math.max(w, h, 1000) / 13;
drawCircles();
dw = Math.round(Math.max(w, h, 1000) / 13);
drawCircles(cCtx, 0);
// enforce redrawing of frames
offscreenCanvases = [];
}
Events.on('bg-resize', _ => init());
window.onresize = _ => Events.fire('bg-resize');
function drawCircle(radius) {
function drawCircle(ctx, radius) {
ctx.beginPath();
let color = Math.round(255 * (1 - radius / Math.max(w, h)));
ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)';
ctx.lineWidth = 2;
let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h));
ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`;
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.lineWidth = 2;
}
let step = 0;
function drawCircles() {
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < 8; i++) {
drawCircle(dw * i + step % dw);
}
step += 1;
}
let loading = true;
function animate() {
if (loading || !finished()) {
requestAnimationFrame(function() {
drawCircles();
animate();
});
function drawCircles(ctx, frame) {
for (let i = 0; i < 13; i++) {
drawCircle(ctx, dw * i + frame);
}
}
function finished() {
return step % dw >= dw - 5;
function createOffscreenCanvas(frame) {
let canvas = document.createElement("canvas");
canvas.width = c.width;
canvas.height = c.height;
offscreenCanvases[frame] = canvas;
let ctx = canvas.getContext('2d');
drawCircles(ctx, frame);
}
function drawFrame(frame) {
cCtx.clearRect(0, 0, w, h);
if (!offscreenCanvases[frame]) {
createOffscreenCanvas(frame);
}
cCtx.drawImage(offscreenCanvases[frame], 0, 0);
}
let animate = true;
let currentFrame = 0;
function animateBg() {
if (currentFrame + 1 < dw || animate) {
currentFrame = (currentFrame + 1) % dw;
drawFrame(currentFrame);
}
setTimeout(_ => animateBg(), 3000 / dw);
}
window.animateBackground = function(l) {
if (!l) {
loading = false;
} else if (!loading) {
loading = true;
if (finished()) animate();
}
animate = l;
};
init();
animate();
animateBg();
});
document.changeFavicon = function (src) {

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.4.0';
const cacheVersion = 'v1.5.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);
}
});
}

View File

@@ -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 {
@@ -481,7 +486,7 @@ x-peer.ws-peer .highlight-wrapper {
text-align: center;
}
.name {
.device-descriptor > div {
width: 100%;
white-space: nowrap;
overflow: hidden;
@@ -741,7 +746,7 @@ x-paper > div:last-child {
x-paper > div:last-child > .button {
height: 100%;
width: 50%;
width: 100%;
}
x-paper > div:last-child > .button:not(:last-child) {
@@ -817,10 +822,29 @@ x-dialog .dialog-subheader {
margin: auto -24px;
}
#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 {