Compare commits

..

129 Commits

Author SHA1 Message Date
schlagmichdoch 56eb29c91b increase version to v1.7.2 2023-05-16 02:35:03 +02:00
schlagmichdoch 6e4bda0adf Fix message sending via submit button.
Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com>
2023-05-16 02:25:50 +02:00
Lopolin-LP 0baced640a Fix About Background not filling up full viewport under certain circumstances (#109)
* Fix About Background Not filling up full viewport under certain circumstances

It is now based on vw/vh instead of px. It can also easily be adjusted, mostly. There is no way it will not fill up the viewport.

* add fix for about bg size to websocket fallback too and tidy up

---------

Co-authored-by: schlagmichdoch <schlagmichdoch@users.noreply.github.com>
2023-05-16 01:50:12 +02:00
schlagmichdoch 3c2e73fc0c fix position of about background circle 2023-05-12 04:59:44 +02:00
schlagmichdoch c629d7cd88 increase version to v1.7.1 2023-05-12 01:41:10 +02:00
schlagmichdoch ba20c72026 fix error on empty roomSecrets 2023-05-12 01:16:37 +02:00
schlagmichdoch 347f9b87c0 fix check whether peer is same browser 2023-05-12 01:16:37 +02:00
schlagmichdoch ae9909f596 fix notification "Key null invalidated" on cancel device pairing 2023-05-11 19:56:47 +02:00
schlagmichdoch 26c1878bb9 increase version to v1.7.0 2023-05-11 19:23:39 +02:00
schlagmichdoch de0afce4ea Merge pull request #107 from schlagmichdoch/add_auto_accept
Add auto accept functionality via Edit Paired Devices Dialog + implement pair secret regeneration functionality
2023-05-11 19:21:26 +02:00
schlagmichdoch 2a837eb195 add 'visbilitychange' event support for older browsers 2023-05-10 21:59:45 +02:00
schlagmichdoch fdf20cfdd9 save roomSecret and notify user that the pairing is successful only after the corresponding pairPeer has joined. 2023-05-10 21:59:45 +02:00
schlagmichdoch 7606fb398b Fix: notify user that "Selected peer left." only if dialog is shown. 2023-05-10 21:59:45 +02:00
schlagmichdoch 8d640be3a2 increase roomSecret length to 264 chars and implement roomSecret regeneration functionality 2023-05-10 21:59:45 +02:00
schlagmichdoch 241ea4f988 implement auto_accept (#91) and manual unpairing via new Edit Paired Devices Dialog and a BrowserTabsConnector 2023-05-10 21:59:43 +02:00
schlagmichdoch 0ac3c5a11f remove debugging logs 2023-05-04 17:39:40 +02:00
schlagmichdoch f39bfedf98 use sha3-512 hash instead of cyrb53 to authenticate peerIds on reconnect 2023-05-04 17:34:33 +02:00
schlagmichdoch fafdbcc829 increase version to v1.6.3 2023-04-27 19:17:41 +02:00
schlagmichdoch b9806d4327 Merge pull request #70 from schlagmichdoch/add_ip_debugging_flag
Add debug mode to enable debugging auto discovery
2023-04-27 18:17:27 +02:00
schlagmichdoch fb08bdaf36 add environment variable DEBUG_MODE to docs 2023-04-27 18:14:45 +02:00
schlagmichdoch 5a363e90dd add debug mode to enable debugging auto discovery 2023-04-24 17:00:03 +02:00
schlagmichdoch 8f4ce63a0c increase version to v1.6.2 2023-04-20 22:04:57 +02:00
schlagmichdoch b42c8a0b1a remove background animation in favor of speed and efficiency 2023-04-20 22:02:00 +02:00
schlagmichdoch 4c7bdd3a0f move robots.txt into correct folder 2023-04-20 21:57:31 +02:00
schlagmichdoch 3f72fa1160 remove fade-in from description (LCP) on page load 2023-04-20 21:57:24 +02:00
schlagmichdoch 5c3f5ece7d increase seo by adding an aria-label and removing 'user-scalable=no' 2023-04-20 21:57:01 +02:00
schlagmichdoch 8de899f124 increase version to v1.6.1 2023-04-19 21:16:43 +02:00
schlagmichdoch 87097e9cd4 fix header btn shadow styling 2023-04-19 21:15:03 +02:00
schlagmichdoch b2fc6415da include example files to run an own TURN server via coturn or via docker-compose 2023-04-19 17:38:14 +02:00
schlagmichdoch 2d8bbd5a79 Change docs to include the usage of our own TURN server instead of the TURN server of the Open Relay Project 2023-04-19 16:50:22 +02:00
schlagmichdoch cae3bb7c7b Add server costs to README.md 2023-04-18 13:06:21 +02:00
schlagmichdoch 59dca141b6 increase version to v1.6.0 2023-04-17 15:25:52 +02:00
schlagmichdoch d0046e83cb remove openrelayproject from rtc_config 2023-04-17 15:24:31 +02:00
schlagmichdoch 2aeadb44e2 Merge pull request #68
Increase SEO
2023-04-17 15:21:04 +02:00
schlagmichdoch 7827a47d29 increase seo with recommendations from PageSpeed Insights 2023-04-17 15:19:54 +02:00
schlagmichdoch 4edc9c9b22 Merge pull request #95 from robvanoostenrijk/ghcr-multi-architecture
Build multi-architecture image
2023-04-16 15:35:22 +02:00
schlagmichdoch 398a69d7a0 Merge pull request #97 from schlagmichdoch/dependabot/npm_and_yarn/ua-parser-js-1.0.35
Bump ua-parser-js from 1.0.34 to 1.0.35
2023-04-07 18:07:43 +02:00
dependabot[bot] dfe69cc873 Bump ua-parser-js from 1.0.34 to 1.0.35
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.34 to 1.0.35.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.34...1.0.35)

---
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-04-03 05:05:12 +00:00
Rob van Oostenrijk f5fde731b0 Build multi-architecture image
Build image output both for amd64 & arm64
2023-03-31 08:37:11 +04:00
schlagmichdoch d6eee480b3 Add darkmode/lightmode/auto toggle to "Other changes" 2023-03-29 17:10:20 +02:00
schlagmichdoch f6ad85a744 increase version to v1.5.3 2023-03-29 16:24:24 +02:00
schlagmichdoch d50480b2f8 Merge pull request #94 from schlagmichdoch/add_theme_menu
Add theme menu to toggle between auto, light and dark mode.
2023-03-29 16:22:42 +02:00
schlagmichdoch ac1e88b6a0 Add possibility to reset theme to auto 2023-03-29 01:39:45 +02:00
schlagmichdoch 0fe36e132c Remove the "under development" message from the share-menu section 2023-03-28 20:24:46 +02:00
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
schlagmichdoch c3863a9dd3 increase version to v1.4.0 2023-03-06 02:19:41 +01:00
schlagmichdoch 5934e94761 edit some styling of the display-name edit field 2023-03-06 00:40:53 +01:00
schlagmichdoch 1bc23dc4b3 fix read rtcConfig.json must be parsed as JSON.. 2023-03-06 00:16:33 +01:00
schlagmichdoch cc78b34d2e Revert making peerId ephemeral to prevent duplication of shown peers on reconnect. Implement peerIdHash to prevent rogue users from overtaking peerIds 2023-03-06 00:07:21 +01:00
schlagmichdoch f34f5bd4b2 tidy up code, add tooltip to device name and change color and bg-color of device-name 2023-03-06 00:07:21 +01:00
schlagmichdoch b2f6a75c99 Merge pull request #60 from ChaosExAnima/master
Updates CLI to work with OSX base64
2023-03-05 00:43:37 +01:00
Echo 82138c06f3 Updates CLI to work with OSX base64 2023-03-04 15:53:13 -05:00
schlagmichdoch ee820ed6e0 Merge pull request #57 from schlagmichdoch/enable_renaming
New feature: You can now change your display name
2023-03-04 20:53:41 +01:00
schlagmichdoch b7e7fd1b68 Merge branch 'master' into enable_renaming 2023-03-04 20:52:10 +01:00
schlagmichdoch 96ed0e53b1 apply styling to clarify that the display-name is editable 2023-03-04 20:50:52 +01:00
schlagmichdoch 77b76a3b8d reduce reconnect timers to 1s 2023-03-04 15:46:26 +01:00
schlagmichdoch e37f9bd9fb fix display name offset in styles.css 2023-03-04 15:44:42 +01:00
schlagmichdoch 67a1b04da2 increase version to v1.3.0 2023-03-03 19:45:04 +01:00
schlagmichdoch 8b2eb67266 fix position of close btn on about page 2023-03-03 19:43:31 +01:00
schlagmichdoch 827b10219d Merge pull request #58 from schlagmichdoch/define_turn_config_dynamically
STUN/TURN config can now be changed dynamically via environment variable 🎉
2023-03-03 19:42:52 +01:00
schlagmichdoch e7ab5e26cc Add dynamic stun/turn config as new feature to README.md 2023-03-03 19:41:55 +01:00
schlagmichdoch 451173caac Add possibility to change the display name to the README.md 2023-03-03 19:10:24 +01:00
schlagmichdoch 8bcaa3f60f Fix header hierarchy for dynamic stun/turn in docs 2023-03-03 18:28:49 +01:00
schlagmichdoch c0a4224a59 merge master into branch 2023-03-03 18:01:24 +01:00
schlagmichdoch 460e8ec79c change cursor to clarify that the display name is editable 2023-03-03 17:43:03 +01:00
schlagmichdoch 002b31a113 merge master into branch 2023-03-03 17:40:10 +01:00
schlagmichdoch 1e35bab327 increase version to v1.2.2 2023-03-03 17:07:02 +01:00
schlagmichdoch bb0493d071 Make user notifications and document titles more concise. 2023-03-03 17:03:10 +01:00
schlagmichdoch bfb5aa8546 fix overwrite method _onMessage of class RTCPeer 2023-03-03 16:36:55 +01:00
schlagmichdoch a9d7960a59 increase version to v1.2.1 2023-03-03 13:12:06 +01:00
schlagmichdoch 39ca5b2d21 ws-fallback: remove all WSPeers when server connection disconnects + fix onPeerLeft 2023-03-03 13:10:14 +01:00
schlagmichdoch cf715b2872 stability on reconnect: prevent "peer-left" signal after "peer-joined" by leaving rooms first before reentering them, clear _keepAlive timeout before joining ip room and not manually terminating sockets 2023-03-03 13:10:14 +01:00
schlagmichdoch bbb8c1b10f ws-fallback: prevent signaling from stopping on reconnect. Do not stop to signal until both devices have sent event "peer-connected" 2023-03-03 13:10:13 +01:00
schlagmichdoch d6ef5887dd move logging of rtc message from class Peer class to overwritten method in class RTCPeer 2023-03-03 12:38:34 +01:00
schlagmichdoch f9f1abef7a Replace all urls in received messages with links. Center the message if it does not include any whitespace. 2023-03-03 12:28:50 +01:00
schlagmichdoch d244f5fa47 fix circles position on ios safari are shifted by url bar 2023-03-03 12:03:20 +01:00
schlagmichdoch 3a2d8c75f7 - restructure and unify dialogs to use less space on mobile and be clearer
- give user option both options "share" and "download" on mobile
- add fallback if zipper fails that downloads files individually
- fix dequeuing of message queue not possible if sending peer has left
2023-03-03 12:01:43 +01:00
schlagmichdoch 545cdc2459 Fix browser reloading when first message is sent by preventing event default on submit 2023-03-02 16:30:47 +01:00
schlagmichdoch 1eb53498b1 add localStorage fallback to fix renaming on private tabs and fix Firefox inserting linebreaks into edited divs 2023-03-02 15:06:22 +01:00
schlagmichdoch de76da52fe merge master into branch 2023-03-01 21:55:50 +01:00
schlagmichdoch d56ee87437 - Enable renaming of own display name permanently via UI
- Make peerId completely ephemeral
- Stabilize RTCConnection by closing connections cleanly
2023-03-01 21:38:36 +01:00
schlagmichdoch 66359da2ca get rtcConfig dynamically from the server 2023-02-24 18:08:48 +01:00
schlagmichdoch 74b88c2e7d fix dialog heights 2023-02-24 16:53:13 +01:00
33 changed files with 4145 additions and 1604 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: Bug Report 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: ' title: 'Bug:/Enhancement:/Feature Request: '
labels: '' labels: ''
assignees: '' assignees: ''
+7
View File
@@ -29,6 +29,12 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 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 - name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with: with:
@@ -46,6 +52,7 @@ jobs:
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+3
View File
@@ -9,3 +9,6 @@ RUN npm ci
COPY . . COPY . .
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1
+13 -9
View File
@@ -6,7 +6,7 @@
<h1>PairDrop</h1> <h1>PairDrop</h1>
<p> <p>
Local file sharing in your browser. Inspired by Apple's Airdrop. Local file sharing in your browser. Inspired by Apple's AirDrop.
<br /> <br />
<a href="https://pairdrop.net"><strong>Explore »</strong></a> <a href="https://pairdrop.net"><strong>Explore »</strong></a>
<br /> <br />
@@ -33,18 +33,18 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
## Differences to Snapdrop ## Differences to Snapdrop
### Device Pairing ### Device Pairing / Internet Transfer
* Pair devices via 6-digit code or QR-Code * 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.). * 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. * 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 * 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. * 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) ### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
* Files are transferred only after a request is accepted first. On transfer completion they are downloaded automatically if possible. * Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
* Multiple files are downloaded as ZIP file * Multiple files are downloaded as a ZIP file
* On iOS and Android the devices share menu is opened instead of downloading the files * On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu.
* Multiple files are transferred at once with an overall progress indicator * Multiple files are transferred at once with an overall progress indicator
### Send Files or Text Directly From Share Menu, Context Menu or CLI ### Send Files or Text Directly From Share Menu, Context Menu or CLI
@@ -54,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) * [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
### Other changes ### Other changes
* [Paste Mode](https://github.com/RobinLinus/snapdrop/pull/534) * Change your display name permanently to easily differentiate your devices
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413) * [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
* Warn user before PairDrop is closed on file transfer * Warn user before PairDrop is closed on file transfer
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101)) * Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
* [Video and Audio preview](https://github.com/RobinLinus/snapdrop/pull/455) (Thanks [@victorwads](https://github.com/victorwads)) * [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)) * Node-only implementation (Thanks [@Bellisario](https://github.com/Bellisario))
* Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87)) * Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101)) * Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558) * To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
## Screenshots ## Screenshots
<div align="center"> <div align="center">
@@ -78,6 +81,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App) * [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) * [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
* [zip.js](https://gildas-lormeau.github.io/zip.js/) * [zip.js](https://gildas-lormeau.github.io/zip.js/)
* [cyrb53](https://github.com/bryc) super fast hash function
Have any questions? Read our [FAQ](/docs/faq.md). Have any questions? Read our [FAQ](/docs/faq.md).
@@ -85,9 +89,9 @@ You can [host your own instance with Docker](/docs/host-your-own.md).
## Support the Community ## 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"> <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;" > <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
</a> </a>
+19
View 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
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.
+153 -31
View File
@@ -1,31 +1,48 @@
# Frequently Asked Questions # Frequently Asked Questions
### Instructions / Discussions <details>
* [Video Instructions](https://www.youtube.com/watch?v=4XN02GkcHUM) (Big thanks to [TheiTeckHq](https://www.youtube.com/channel/UC_DUzWMb8gZZnAbISQjmAfQ)) <summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
* [idownloadblog](http://www.idownloadblog.com/2015/12/29/snapdrop/) Help! I can't install the PWA!
* [thenextweb](http://thenextweb.com/insider/2015/12/27/snapdrop-is-a-handy-web-based-replacement-for-apples-fiddly-airdrop-file-transfer-tool/) </summary>
* [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)
### 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 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). 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/) On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/)
### Are there any shortcuts? <br>
Sure!
<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` - Send a message with `CTRL + ENTER`
- Close all send and pair dialogs by pressing `Escape`. - Close all send and pair dialogs by pressing `Escape`.
- Copy a received message to clipboard with `CTRL/⌘ + C`. - Copy a received message to clipboard with `CTRL/⌘ + C`.
- Accept file transfer request with `Enter` and decline with `Escape`. - 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. 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. 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: I created a simple iOS shortcut that takes your photos and saves them to your gallery:
https://routinehub.co/shortcut/13988/ 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! 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 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 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) * [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! Yes, it is!
* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) * [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? <br>
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).
</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. 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. 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. <br>
As a workaround you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster.
</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 Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) - [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
@@ -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. 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. 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. 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. 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? <br>
* [Buy me a cover to support open source software](https://www.buymeacoffee.com/pairdrop)
</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) * [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)
* Share PairDrop on social media. * Share PairDrop on social media.
* Fix bugs and make a pull request. * Fix bugs and make a pull request.
* Do security analysis and suggestions * 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) [See here for Information about the Technical Implementation](/docs/technical-documentation.md)
<br>
</details>
[< Back](/README.md) [< Back](/README.md)
+129 -12
View File
@@ -1,6 +1,12 @@
# Deployment Notes # Deployment Notes
The easiest way to get PairDrop up and running is by using Docker. The easiest way to get PairDrop up and running is by using Docker.
> <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 ## Deployment with Docker
### Docker Image from Docker Hub ### Docker Image from Docker Hub
@@ -17,20 +23,20 @@ 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: Set options by using the following flags in the `docker run` command:
##### Port ##### Port
``` ```bash
-p 127.0.0.1:8080:3000 -p 127.0.0.1:8080:3000
``` ```
> Specify the port used by the docker image > Specify the port used by the docker image
> - 3000 -> `-p 127.0.0.1:3000:3000` > - 3000 -> `-p 127.0.0.1:3000:3000`
> - 8080 -> `-p 127.0.0.1:8080:3000` > - 8080 -> `-p 127.0.0.1:8080:3000`
##### Rate limiting requests ##### Rate limiting requests
``` ```bash
-e RATE_LIMIT=true -e RATE_LIMIT=true
``` ```
> Limits clients to 1000 requests per 5 min > Limits clients to 1000 requests per 5 min
##### Websocket Fallback (for VPN) ##### Websocket Fallback (for VPN)
``` ```bash
-e WS_FALLBACK=true -e WS_FALLBACK=true
``` ```
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. > Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
@@ -42,6 +48,55 @@ Set options by using the following flags in the `docker run` command:
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. > Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
> Additionally, beware that all traffic using this fallback debits the servers data plan. > Additionally, beware that all traffic using this fallback debits the servers data plan.
##### Specify STUN/TURN Servers
```bash
-e RTC_CONFIG="rtc_config.json"
```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> 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"
> }
> ]
> }
> ```
##### Debug Mode
```bash
-e DEBUG_MODE="true"
```
> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server)
> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device everything is correctly setup.
>To find out your devices public IP visit https://www.whatismyip.com/.
>
> To preserve your clients' privacy, **never use this flag in production!**
<br>
### Docker Image from GHCR ### Docker Image from GHCR
```bash ```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod 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 +107,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) > 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 ### Docker Image self-built
#### Build the image #### Build the image
```bash ```bash
@@ -71,6 +128,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) > 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> <br>
## Deployment with Docker Compose ## Deployment with Docker Compose
@@ -141,6 +200,62 @@ $env:PORT=3010; npm start
``` ```
> Specify the port PairDrop is running on. (Default: 3000) > Specify the port PairDrop is running on. (Default: 3000)
#### Specify STUN/TURN Server
On Unix based systems
```bash
RTC_CONFIG="rtc_config.json" npm start
```
On Windows
```bash
$env:RTC_CONFIG="rtc_config.json"; npm start
```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> 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"
> }
> ]
> }
> ```
#### Debug Mode
On Unix based systems
```bash
DEBUG_MODE="true" npm start
```
On Windows
```bash
$env:DEBUG_MODE="true"; npm start
```
> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server)
> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device everything is correctly setup.
>To find out your devices public IP visit https://www.whatismyip.com/.
>
> To preserve your clients' privacy, **never use this flag in production!**
### Options / Flags ### Options / Flags
#### Local Run #### Local Run
```bash ```bash
@@ -197,6 +312,8 @@ npm run start:prod -- --localhost-only --include-ws-fallback
## HTTP-Server ## HTTP-Server
When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible. When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible.
To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode).
### Using nginx ### Using nginx
#### Allow http and https requests #### Allow http and https requests
``` ```
@@ -262,13 +379,13 @@ server {
### Using Apache ### Using Apache
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel` install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
```shell ```bash
a2enmod proxy a2enmod proxy
``` ```
```shell ```bash
a2enmod proxy_http a2enmod proxy_http
``` ```
```shell ```bash
a2enmod proxy_wstunnel a2enmod proxy_wstunnel
``` ```
@@ -278,7 +395,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
**pairdrop.conf** **pairdrop.conf**
#### Allow http and https requests #### Allow http and https requests
``` ```apacheconf
<VirtualHost *:80> <VirtualHost *:80>
ProxyPass / http://127.0.0.1:3000/ ProxyPass / http://127.0.0.1:3000/
RewriteEngine on RewriteEngine on
@@ -295,7 +412,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
</VirtualHost> </VirtualHost>
``` ```
#### Automatic http to https redirect: #### Automatic http to https redirect:
``` ```apacheconf
<VirtualHost *:80> <VirtualHost *:80>
Redirect permanent / https://127.0.0.1:3000/ Redirect permanent / https://127.0.0.1:3000/
</VirtualHost> </VirtualHost>
@@ -308,10 +425,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
</VirtualHost> </VirtualHost>
``` ```
Activate the new virtual host and reload apache: Activate the new virtual host and reload apache:
```shell ```bash
a2ensite pairdrop a2ensite pairdrop
``` ```
```shell ```bash
service apache2 reload service apache2 reload
``` ```
@@ -322,7 +439,7 @@ All files needed for developing are available on the branch `dev`.
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/) First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
Then, clone the repository and run docker-compose: Then, clone the repository and run docker-compose:
```shell ```bash
git clone https://github.com/schlagmichdoch/PairDrop.git git clone https://github.com/schlagmichdoch/PairDrop.git
cd PairDrop cd PairDrop
@@ -347,7 +464,7 @@ The nginx container creates a CA certificate and a website certificate for you.
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system. If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. - On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
- On MacOS, double click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL. - On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK. - Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data). - When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
+3 -3
View File
@@ -33,10 +33,10 @@ https://routinehub.co/shortcut/13990/
## Send directly from share menu on Android ## 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. The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented.
When the PWA is installed, it should register itself to the share-menu of the device automatically.
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 directly via command-line interface
Send files or text with PairDrop via command-line interface. Send files or text with PairDrop via command-line interface.
+155 -60
View File
@@ -1,6 +1,13 @@
const process = require('process') const process = require('process')
const crypto = require('crypto') const crypto = require('crypto')
const {spawn} = require('child_process') const {spawn} = require('child_process')
const WebSocket = require('ws');
const fs = require('fs');
const parser = require('ua-parser-js');
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
const express = require('express');
const RateLimit = require('express-rate-limit');
const http = require('http');
// Handle SIGINT // Handle SIGINT
process.on('SIGINT', () => { process.on('SIGINT', () => {
@@ -49,9 +56,16 @@ if (process.argv.includes('--auto-restart')) {
); );
} }
const express = require('express'); const rtcConfig = process.env.RTC_CONFIG
const RateLimit = require('express-rate-limit'); ? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
const http = require('http'); : {
"sdpSemantics": "unified-plan",
"iceServers": [
{
"urls": "stun:stun.l.google.com:19302"
}
]
};
const app = express(); const app = express();
@@ -76,6 +90,12 @@ if (process.argv.includes('--include-ws-fallback')) {
app.use(express.static('public')); app.use(express.static('public'));
} }
const debugMode = process.env.DEBUG_MODE === "true";
if (debugMode) {
console.log("DEBUG_MODE is active. To protect privacy, do not use in production.")
}
app.use(function(req, res) { app.use(function(req, res) {
res.redirect('/'); res.redirect('/');
}); });
@@ -93,13 +113,9 @@ if (process.argv.includes('--localhost-only')) {
server.listen(port); server.listen(port);
} }
const parser = require('ua-parser-js');
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
class PairDropServer { class PairDropServer {
constructor() { constructor() {
const WebSocket = require('ws');
this._wss = new WebSocket.Server({ server }); this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
@@ -110,10 +126,13 @@ class PairDropServer {
} }
_onConnection(peer) { _onConnection(peer) {
this._joinRoom(peer);
peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e); peer.socket.onerror = e => console.error(e);
this._keepAlive(peer); this._keepAlive(peer);
this._send(peer, {
type: 'rtc-config',
config: rtcConfig
});
// send displayName // send displayName
this._send(peer, { this._send(peer, {
@@ -121,7 +140,8 @@ class PairDropServer {
message: { message: {
displayName: peer.name.displayName, displayName: peer.name.displayName,
deviceName: peer.name.deviceName, deviceName: peer.name.deviceName,
peerId: peer.id peerId: peer.id,
peerIdHash: hasher.hashCodeSalted(peer.id)
} }
}); });
} }
@@ -141,14 +161,14 @@ class PairDropServer {
case 'pong': case 'pong':
sender.lastBeat = Date.now(); sender.lastBeat = Date.now();
break; break;
case 'join-ip-room':
this._joinRoom(sender);
break;
case 'room-secrets': case 'room-secrets':
this._onRoomSecrets(sender, message); this._onRoomSecrets(sender, message);
break; break;
case 'room-secret-deleted': case 'room-secrets-deleted':
this._onRoomSecretDeleted(sender, message); this._onRoomSecretsDeleted(sender, message);
break;
case 'room-secrets-cleared':
this._onRoomSecretsCleared(sender, message);
break; break;
case 'pair-device-initiate': case 'pair-device-initiate':
this._onPairDeviceInitiate(sender); this._onPairDeviceInitiate(sender);
@@ -159,6 +179,9 @@ class PairDropServer {
case 'pair-device-cancel': case 'pair-device-cancel':
this._onPairDeviceCancel(sender); this._onPairDeviceCancel(sender);
break; break;
case 'regenerate-room-secret':
this._onRegenerateRoomSecret(sender, message);
break
case 'resend-peers': case 'resend-peers':
this._notifyPeers(sender); this._notifyPeers(sender);
break; break;
@@ -192,57 +215,41 @@ class PairDropServer {
} }
_onRoomSecrets(sender, message) { _onRoomSecrets(sender, message) {
if (!message.roomSecrets) return;
const roomSecrets = message.roomSecrets.filter(roomSecret => { const roomSecrets = message.roomSecrets.filter(roomSecret => {
return /^[\x00-\x7F]{64}$/.test(roomSecret); return /^[\x00-\x7F]{64,256}$/.test(roomSecret);
}) })
if (!roomSecrets) return;
this._joinSecretRooms(sender, roomSecrets); this._joinSecretRooms(sender, roomSecrets);
} }
_onRoomSecretDeleted(sender, message) { _onRoomSecretsDeleted(sender, message) {
this._deleteSecretRoom(sender, message.roomSecret)
}
_onRoomSecretsCleared(sender, message) {
for (let i = 0; i<message.roomSecrets.length; i++) { for (let i = 0; i<message.roomSecrets.length; i++) {
this._deleteSecretRoom(sender, message.roomSecrets[i]); this._deleteSecretRoom(message.roomSecrets[i]);
} }
} }
_deleteSecretRoom(sender, roomSecret) { _deleteSecretRoom(roomSecret) {
const room = this._rooms[roomSecret]; const room = this._rooms[roomSecret];
if (room) { if (!room) return;
for (const peerId in room) { for (const peerId in room) {
const peer = room[peerId]; const peer = room[peerId];
this._leaveRoom(peer, 'secret', roomSecret); this._leaveRoom(peer, 'secret', roomSecret);
this._send(peer, { this._send(peer, {
type: 'secret-room-deleted', type: 'secret-room-deleted',
roomSecret: roomSecret, roomSecret: roomSecret,
}); });
} }
} }
this._notifyPeers(sender);
}
getRandomString(length) {
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a propability bias, so I suppose we better skip this character */
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
_onPairDeviceInitiate(sender) { _onPairDeviceInitiate(sender) {
let roomSecret = this.getRandomString(64); let roomSecret = randomizer.getRandomString(256);
let roomKey = this._createRoomKey(sender, roomSecret); let roomKey = this._createRoomKey(sender, roomSecret);
if (sender.roomKey) this._removeRoomKey(sender.roomKey); if (sender.roomKey) this._removeRoomKey(sender.roomKey);
sender.roomKey = roomKey; sender.roomKey = roomKey;
@@ -255,16 +262,19 @@ class PairDropServer {
} }
_onPairDeviceJoin(sender, message) { _onPairDeviceJoin(sender, message) {
// rate limit implementation: max 10 attempts every 10s
if (sender.roomKeyRate >= 10) { if (sender.roomKeyRate >= 10) {
this._send(sender, { type: 'pair-device-join-key-rate-limit' }); this._send(sender, { type: 'pair-device-join-key-rate-limit' });
return; return;
} }
sender.roomKeyRate += 1; sender.roomKeyRate += 1;
setTimeout(_ => sender.roomKeyRate -= 1, 10000); setTimeout(_ => sender.roomKeyRate -= 1, 10000);
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) { if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
this._send(sender, { type: 'pair-device-join-key-invalid' }); this._send(sender, { type: 'pair-device-join-key-invalid' });
return; return;
} }
const roomSecret = this._roomSecrets[message.roomKey].roomSecret; const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
const creator = this._roomSecrets[message.roomKey].creator; const creator = this._roomSecrets[message.roomKey].creator;
this._removeRoomKey(message.roomKey); this._removeRoomKey(message.roomKey);
@@ -283,13 +293,32 @@ class PairDropServer {
} }
_onPairDeviceCancel(sender) { _onPairDeviceCancel(sender) {
if (sender.roomKey) { const roomKey = sender.roomKey
if (!roomKey) return;
this._removeRoomKey(roomKey);
this._send(sender, { this._send(sender, {
type: 'pair-device-canceled', type: 'pair-device-canceled',
roomKey: sender.roomKey, roomKey: roomKey,
}); });
this._removeRoomKey(sender.roomKey);
} }
_onRegenerateRoomSecret(sender, message) {
const oldRoomSecret = message.roomSecret;
const newRoomSecret = randomizer.getRandomString(256);
// notify all other peers
for (const peerId in this._rooms[oldRoomSecret]) {
const peer = this._rooms[oldRoomSecret][peerId];
this._send(peer, {
type: 'room-secret-regenerated',
oldRoomSecret: oldRoomSecret,
newRoomSecret: newRoomSecret,
});
peer.removeRoomSecret(oldRoomSecret);
}
delete this._rooms[oldRoomSecret];
} }
_createRoomKey(creator, roomSecret) { _createRoomKey(creator, roomSecret) {
@@ -317,6 +346,10 @@ class PairDropServer {
_joinRoom(peer, roomType = 'ip', roomSecret = '') { _joinRoom(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.ip : roomSecret; const room = roomType === 'ip' ? peer.ip : roomSecret;
if (this._rooms[room] && this._rooms[room][peer.id]) {
this._leaveRoom(peer, roomType, roomSecret);
}
// if room doesn't exist, create it // if room doesn't exist, create it
if (!this._rooms[room]) { if (!this._rooms[room]) {
this._rooms[room] = {}; this._rooms[room] = {};
@@ -453,7 +486,7 @@ class Peer {
this._setIP(request); this._setIP(request);
// set peer id // set peer id
this._setPeerId(request) this._setPeerId(request);
// is WebRTC supported ? // is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1; this.rtcSupported = request.url.indexOf('webrtc') > -1;
@@ -483,6 +516,17 @@ class Peer {
if (this.ip.substring(0,7) === "::ffff:") if (this.ip.substring(0,7) === "::ffff:")
this.ip = this.ip.substring(7); this.ip = this.ip.substring(7);
if (debugMode) {
console.debug("----DEBUGGING-PEER-IP-START----");
console.debug("remoteAddress:", request.connection.remoteAddress);
console.debug("x-forwarded-for:", request.headers['x-forwarded-for']);
console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']);
console.debug("PairDrop uses:", this.ip);
console.debug("IP is private:", this.ipIsPrivate(this.ip));
console.debug("if IP is private, '127.0.0.1' is used instead");
console.debug("----DEBUGGING-PEER-IP-END----");
}
// IPv4 and IPv6 use different values to refer to localhost // IPv4 and IPv6 use different values to refer to localhost
// put all peers on the same network as the server into the same room as well // put all peers on the same network as the server into the same room as well
if (this.ip === '::1' || this.ipIsPrivate(this.ip)) { if (this.ip === '::1' || this.ipIsPrivate(this.ip)) {
@@ -526,9 +570,11 @@ class Peer {
} }
_setPeerId(request) { _setPeerId(request) {
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id"); const searchParams = new URL(request.url, "http://server").searchParams;
if (peer_id && Peer.isValidUuid(peer_id)) { let peerId = searchParams.get("peer_id");
this.id = peer_id; let peerIdHash = searchParams.get("peer_id_hash");
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
this.id = peerId;
} else { } else {
this.id = crypto.randomUUID(); this.id = crypto.randomUUID();
} }
@@ -562,7 +608,7 @@ class Peer {
separator: ' ', separator: ' ',
dictionaries: [colors, animals], dictionaries: [colors, animals],
style: 'capital', style: 'capital',
seed: this.id.hashCode() seed: cyrb53(this.id)
}) })
this.name = { this.name = {
@@ -587,6 +633,10 @@ class Peer {
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid); return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
} }
isPeerIdHashValid(peerId, peerIdHash) {
return peerIdHash === hasher.hashCodeSalted(peerId);
}
addRoomSecret(roomSecret) { addRoomSecret(roomSecret) {
if (!(roomSecret in this.roomSecrets)) { if (!(roomSecret in this.roomSecrets)) {
this.roomSecrets.push(roomSecret); this.roomSecrets.push(roomSecret);
@@ -600,16 +650,61 @@ class Peer {
} }
} }
Object.defineProperty(String.prototype, 'hashCode', { const hasher = (() => {
value: function() { let password;
var hash = 0, i, chr; return {
for (i = 0; i < this.length; i++) { hashCodeSalted(salt) {
chr = this.charCodeAt(i); if (!password) {
hash = ((hash << 5) - hash) + chr; // password is created on first call.
hash |= 0; // Convert to 32bit integer password = randomizer.getRandomString(128);
} }
return hash;
return crypto.createHash("sha3-512")
.update(password)
.update(crypto.createHash("sha3-512").update(salt, "utf8").digest("hex"))
.digest("hex");
} }
}
})()
const randomizer = (() => {
return {
getRandomString(length) {
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
}); });
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
}
})()
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
A fast and simple hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Public domain. Attribution appreciated.
*/
const cyrb53 = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
new PairDropServer(); new PairDropServer();
+16 -16
View File
@@ -1,19 +1,19 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.0", "version": "1.7.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.0", "version": "1.7.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.35",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.12.1" "ws": "^8.13.0"
}, },
"engines": { "engines": {
"node": ">=15" "node": ">=15"
@@ -583,9 +583,9 @@
} }
}, },
"node_modules/ua-parser-js": { "node_modules/ua-parser-js": {
"version": "1.0.33", "version": "1.0.35",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -633,9 +633,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.12.1", "version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -1070,9 +1070,9 @@
} }
}, },
"ua-parser-js": { "ua-parser-js": {
"version": "1.0.33", "version": "1.0.35",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==" "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA=="
}, },
"unique-names-generator": { "unique-names-generator": {
"version": "4.7.1", "version": "4.7.1",
@@ -1095,9 +1095,9 @@
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}, },
"ws": { "ws": {
"version": "8.12.1", "version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"requires": {} "requires": {}
} }
} }
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.0", "version": "1.7.2",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -12,9 +12,9 @@
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.35",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.12.1" "ws": "^8.13.0"
}, },
"engines": { "engines": {
"node": ">=15" "node": ">=15"
+8
View File
@@ -106,7 +106,11 @@ sendFiles()
zip -q -b /tmp/ -r "$zipPath" "$path" zip -q -b /tmp/ -r "$zipPath" "$path"
zip -q -b /tmp/ "$zipPathTemp" "$zipPath" zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
if [[ $OS == "Mac" ]];then
hash=$(base64 -i "$zipPathTemp")
else
hash=$(base64 -w 0 "$zipPathTemp") hash=$(base64 -w 0 "$zipPathTemp")
fi
# remove temporary temp file # remove temporary temp file
rm "$zipPathTemp" rm "$zipPathTemp"
@@ -116,8 +120,12 @@ sendFiles()
# Create zip file temporarily to send file # Create zip file temporarily to send file
zip -q -b /tmp/ "$zipPath" "$path" zip -q -b /tmp/ "$zipPath" "$path"
if [[ $OS == "Mac" ]];then
hash=$(base64 -i "$zipPath")
else
hash=$(base64 -w 0 "$zipPath") hash=$(base64 -w 0 "$zipPath")
fi fi
fi
# remove temporary temp file # remove temporary temp file
rm "$zipPath" rm "$zipPath"
+114 -73
View File
@@ -6,7 +6,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config --> <!-- Web App Config -->
<title>PairDrop</title> <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="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light"> <meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="no"> <meta name="apple-mobile-web-app-capable" content="no">
@@ -39,37 +39,51 @@
<body translate="no"> <body translate="no">
<header class="row-reverse"> <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"> <svg class="icon">
<use xlink:href="#info-outline" /> <use xlink:href="#info-outline" />
</svg> </svg>
</a> </a>
<a id="theme" class="icon-button" title="Switch Darkmode/Lightmode" > <div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme" /> <use xlink:href="#icon-theme-auto" />
</svg> </svg>
</a> </div>
<a id="notification" class="icon-button" title="Enable Notifications" hidden> <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"> <svg class="icon">
<use xlink:href="#notifications" /> <use xlink:href="#notifications" />
</svg> </svg>
</a> </div>
<a id="install" class="icon-button" title="Install PairDrop" hidden> <div id="install" class="icon-button" title="Install PairDrop" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#homescreen" /> <use xlink:href="#homescreen" />
</svg> </svg>
</a> </div>
<a id="pair-device" class="icon-button" title="Pair Device" > <div id="pair-device" class="icon-button" title="Pair Device" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#pair-device-icon" /> <use xlink:href="#pair-device-icon" />
</svg> </svg>
</a> </div>
<a id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden> <div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#clear-pair-devices-icon" /> <use xlink:href="#edit-pair-devices-icon" />
</svg> </svg>
</a> </div>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a> <div id="cancel-paste-mode" class="button" hidden>Done</div>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
@@ -89,7 +103,13 @@
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
<div id="display-name" placeholder="&nbsp;"></div> <div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" />
</svg>
</div>
<div class="font-body2"> <div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span> You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span> <span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
@@ -106,33 +126,34 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr> <hr>
<div id="key-input-container"> <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 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 id="char1" 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-2" 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 type="tel" class="textarea center" aria-label="pair-key-3" 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 type="tel" class="textarea center" aria-label="pair-key-4" 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 type="tel" class="textarea center" aria-label="pair-key-5" 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-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div> <button class="button" type="button" close>Cancel</button>
<a class="button" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
</form> </form>
</x-dialog> </x-dialog>
<!-- Clear Devices Dialog --> <!-- Edit Paired Devices Dialog -->
<x-dialog id="clear-devices-dialog"> <x-dialog id="edit-paired-devices-dialog">
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Unpair Devices</h2> <h2 class="center">Edit Paired Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div> <div class="paired-devices-wrapper"></div>
<div class="row-reverse space-between"> <div class="font-subheading center">
<button class="button" type="submit">Unpair Devices</button> <p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
<a class="button" close>Cancel</a> </div>
<div class="center row-reverse">
<button class="button" type="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -142,25 +163,23 @@
<x-dialog id="receive-request-dialog"> <x-dialog id="receive-request-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">PairDrop</h2> <h2 class="center"></h2>
<div class="text-center file-description"> <div class="center column file-description">
<div> <div>
<span id="requesting-peer-display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span>would like to share</span>
</div> </div>
<div id="file-name" class="row" > <div class="row file-name" >
<span id="file-stem"></span> <span class="file-stem"></span>
<span id="file-extension"></span> <span class="file-extension"></span>
</div> </div>
<div class="row"> <div class="row file-other">
<span id="file-other"></span>
</div> </div>
<div class="row font-body2 file-size"></div>
</div> </div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div> </div>
</x-paper> </x-paper>
@@ -170,13 +189,23 @@
<x-dialog id="receive-file-dialog"> <x-dialog id="receive-file-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 id="receive-title" class="center"></h2> <h2 class="center"></h2>
<div class="text-center file-description"></div> <div class="center column file-description">
<div class="font-body2 text-center file-size"></div> <div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<a id="share-or-download" class="button" autofocus></a> <button id="share-btn" class="button" autofocus hidden>Share</button>
<div class="separator"></div> <button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@@ -187,16 +216,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">PairDrop</h2> <h2 class="text-center">Send Message</h2>
<div class="text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span>Send a Message to</span>
<span id="text-send-peer-display-name"></span> <span class="display-name"></span>
</div> </div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div class="row-separator"></div>
<div class="row-reverse"> <div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button> <div class="center row-reverse">
<div class="separator"></div> <button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
<a class="button" title="ESCAPE" close>Cancel</a> <button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -206,16 +235,15 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2>PairDrop - Message Received</h2> <h2 class="text-center">Message Received</h2>
<div id="receive-text-description-container"> <div class="text-center dialog-subheader">
<span id="receive-text-peer-display-name"></span> <span class="display-name"></span>
<span>sent the following message:</span> <span>has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE">Close</button>
</div> </div>
</x-paper> </x-paper>
@@ -226,6 +254,7 @@
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button> <button class="button center" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -236,14 +265,14 @@
</div> </div>
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
<section class="center column fade-in"> <header class="row-reverse fade-in">
<header class="row-reverse"> <a href="#" class="close icon-button" aria-label="Close About PairDrop">
<a href="#" class="close icon-button">
<svg class="icon"> <svg class="icon">
<use xlink:href="#close-icon" /> <use xlink:href="#close-icon" />
</svg> </svg>
</a> </a>
</header> </header>
<section class="center column fade-in">
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
@@ -315,24 +344,36 @@
<path d="M0 0h24v24H0z" fill="none" /> <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" /> <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>
<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"/> <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>
<symbol id="pair-device-icon" viewBox="0 0 640 512"> <symbol id="pair-device-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/> <path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</symbol> </symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512"> <symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 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"/> <!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<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> </symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/util.js"></script> <script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script> <script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script> <script src="scripts/ui.js"></script>
<script src="scripts/theme.js" async></script> <script src="scripts/util.js" async></script>
<script src="scripts/qrcode.js" async></script> <script src="scripts/QRCode.min.js" async></script>
<script src="scripts/zip.min.js" async></script> <script src="scripts/zip.min.js" async></script>
<script src="scripts/NoSleep.min.js" async></script> <script src="scripts/NoSleep.min.js" async></script>
<!-- Sounds --> <!-- Sounds -->
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+325 -123
View File
@@ -3,16 +3,26 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work"); if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection { class ServerConnection {
constructor() { constructor() {
this._connect(); this._connect();
Events.on('pagehide', _ => this._disconnect()); Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail})); Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail})); Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'})); Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate()); Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail)); Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
@@ -21,10 +31,10 @@ class ServerConnection {
Events.on('online', _ => this._connect()); Events.on('online', _ => this._connect());
} }
async _connect() { _connect() {
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting()) return;
const ws = new WebSocket(await this._endpoint()); const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer';
ws.onopen = _ => this._onOpen(); ws.onopen = _ => this._onOpen();
ws.onmessage = e => this._onMessage(e.data); ws.onmessage = e => this._onMessage(e.data);
@@ -36,10 +46,7 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
} if (this._isReconnect) Events.fire('notify-user', 'Connected.');
_sendRoomSecrets(roomSecrets) {
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
} }
_onPairDeviceInitiate() { _onPairDeviceInitiate() {
@@ -52,18 +59,25 @@ class ServerConnection {
_onPairDeviceJoin(roomKey) { _onPairDeviceJoin(roomKey) {
if (!this._isConnected()) { if (!this._isConnected()) {
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000); setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
return; return;
} }
this.send({ type: 'pair-device-join', roomKey: roomKey }) this.send({ type: 'pair-device-join', roomKey: roomKey })
} }
_setRtcConfig(config) {
window.rtcConfig = config;
}
_onMessage(msg) { _onMessage(msg) {
msg = JSON.parse(msg); msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg); if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) { switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers': case 'peers':
Events.fire('peers', msg); this._onPeers(msg);
break; break;
case 'peer-joined': case 'peer-joined':
Events.fire('peer-joined', msg); Events.fire('peer-joined', msg);
@@ -98,66 +112,86 @@ class ServerConnection {
case 'secret-room-deleted': case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret); Events.fire('secret-room-deleted', msg.roomSecret);
break; break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
default: default:
console.error('WS: unknown message type', msg); console.error('WS receive: unknown message type', msg);
} }
} }
send(msg) { send(msg) {
if (!this._isConnected()) return; if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg)); this._socket.send(JSON.stringify(msg));
} }
_onDisplayName(msg) { _onPeers(msg) {
sessionStorage.setItem("peerId", msg.message.peerId); Events.fire('peers', msg);
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())
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg); Events.fire('display-name', msg);
} }
async _endpoint() { _endpoint() {
// hack to detect if deployment or development environment // hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws'; const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback'; const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc); let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
const peerId = await this._peerId(); const peerId = sessionStorage.getItem("peerId");
if (peerId) ws_url.searchParams.append('peer_id', peerId) const peerIdHash = sessionStorage.getItem("peerIdHash");
return ws_url.toString(); 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() { _disconnect() {
this.send({ type: 'disconnect' }); this.send({ type: 'disconnect' });
if (this._socket) {
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null; this._socket.onclose = null;
this._socket.close(); this._socket.close();
this._socket = null; this._socket = null;
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
} this._isReconnect = true;
} }
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...'); Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000); this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
if (document.hidden) return; if (window.hiddenProperty) return;
this._connect(); this._connect();
} }
@@ -181,25 +215,69 @@ class ServerConnection {
class Peer { class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
this._server = serverConnection; this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId; this._peerId = peerId;
this._roomType = roomType; this._roomType = roomType;
this._roomSecret = roomSecret; this._updateRoomSecret(roomSecret);
this._filesQueue = []; this._filesQueue = [];
this._busy = false; this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
} }
sendJSON(message) { sendJSON(message) {
this._send(JSON.stringify(message)); this._send(JSON.stringify(message));
} }
async createHeader(file) { sendDisplayName(displayName) {
return { this.sendJSON({type: 'display-name-changed', displayName: displayName});
name: file.name, }
mime: file.type,
size: file.size, _isSameBrowser() {
}; return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
} }
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) { getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -244,7 +322,11 @@ class Peer {
let imagesOnly = true let imagesOnly = true
for (let i=0; i<files.length; i++) { for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'}) Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i])); header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size; totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false; if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
} }
@@ -319,26 +401,25 @@ class Peer {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
console.log('RTC:', message); switch (messageJSON.type) {
switch (message.type) {
case 'request': case 'request':
this._onFilesTransferRequest(message); this._onFilesTransferRequest(messageJSON);
break; break;
case 'header': case 'header':
this._onFilesHeader(message); this._onFileHeader(messageJSON);
break; break;
case 'partition': case 'partition':
this._onReceivedPartitionEnd(message); this._onReceivedPartitionEnd(messageJSON);
break; break;
case 'partition-received': case 'partition-received':
this._sendNextPartition(); this._sendNextPartition();
break; break;
case 'progress': case 'progress':
this._onDownloadProgress(message.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
case 'files-transfer-response': case 'files-transfer-response':
this._onFileTransferRequestResponded(message); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted();
@@ -347,14 +428,17 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break;
case 'display-name-changed':
this._onDisplayNameChanged(messageJSON);
break; break;
} }
} }
_onFilesTransferRequest(request) { _onFilesTransferRequest(request) {
if (this._requestPending) { if (this._requestPending) {
// Only accept one request at a time // Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false}); this.sendJSON({type: 'files-transfer-response', accepted: false});
return; return;
} }
@@ -366,6 +450,14 @@ class Peer {
} }
this._requestPending = request; this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', { Events.fire('files-transfer-request', {
request: request, request: request,
peerId: this._peerId peerId: this._peerId
@@ -383,8 +475,8 @@ class Peer {
this._requestPending = null; this._requestPending = null;
} }
_onFilesHeader(header) { _onFileHeader(header) {
if (this._requestAccepted?.header.length) { if (this._requestAccepted && this._requestAccepted.header.length) {
this._lastProgress = 0; this._lastProgress = 0;
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
this._requestAccepted.totalSize, this._requestAccepted.totalSize,
@@ -437,11 +529,14 @@ class Peer {
this._abortTransfer(); this._abortTransfer();
} }
// include for compatibility with Snapdrop for Android app
Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob); this._filesReceived.push(fileBlob);
if (!this._requestAccepted.header.length) { if (!this._requestAccepted.header.length) {
this._busy = false; this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted}); Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = []; this._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
} }
@@ -486,31 +581,44 @@ class Peer {
Events.fire('text-received', { text: escaped, peerId: this._peerId }); Events.fire('text-received', { text: escaped, peerId: this._peerId });
this.sendJSON({ type: 'message-transfer-complete' }); this.sendJSON({ type: 'message-transfer-complete' });
} }
_onDisplayNameChanged(message) {
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
} }
class RTCPeer extends Peer { class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, isCaller, peerId, roomType, roomSecret);
if (!peerId) return; // we will listen for a caller this.rtcSupported = true;
this._connect(peerId, true); if (!this._isCaller) return; // we will listen for a caller
this._connect();
} }
_connect(peerId, isCaller) { _connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller); if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) { if (this._isCaller) {
this._openChannel(); this._openChannel();
} else { } else {
this._conn.ondatachannel = e => this._onChannelOpened(e); this._conn.ondatachannel = e => this._onChannelOpened(e);
} }
} }
_openConnection(peerId, isCaller) { _openConnection() {
this._isCaller = isCaller; this._conn = new RTCPeerConnection(window.rtcConfig);
this._peerId = peerId;
this._conn = new RTCPeerConnection(RTCPeer.config);
this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
} }
@@ -539,7 +647,7 @@ class RTCPeer extends Peer {
} }
onServerMessage(message) { onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false); if (!this._conn) this._connect();
if (message.sdp) { if (message.sdp) {
this._conn.setRemoteDescription(message.sdp) this._conn.setRemoteDescription(message.sdp)
@@ -558,14 +666,21 @@ class RTCPeer extends Peer {
_onChannelOpened(event) { _onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId); console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = _ => this._onChannelClosed(); channel.onclose = _ => this._onChannelClosed();
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._closeChannel());
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
}
_onMessage(message) {
if (typeof message === 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
} }
getConnectionHash() { getConnectionHash() {
@@ -601,26 +716,34 @@ class RTCPeer extends Peer {
} }
} }
_closeChannel() { _onPageHide() {
if (this._channel) this._channel.onclose = null; this._disconnect();
if (this._conn) this._conn.close(); }
this._conn = null;
_disconnect() {
if (this._conn && this._channel) {
this._channel.onclose = null;
this._channel.close();
}
Events.fire('peer-disconnected', this._peerId);
} }
_onChannelClosed() { _onChannelClosed() {
console.log('RTC: channel closed', this._peerId); console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId); Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return; if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel this._connect(); // reopen the channel
} }
_onConnectionStateChange() { _onConnectionStateChange() {
console.log('RTC: state changed:', this._conn.connectionState); console.log('RTC: state changed:', this._conn.connectionState);
switch (this._conn.connectionState) { switch (this._conn.connectionState) {
case 'disconnected': case 'disconnected':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection disconnected'); this._onError('rtc connection disconnected');
break; break;
case 'failed': case 'failed':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection failed'); this._onError('rtc connection failed');
break; break;
} }
@@ -656,7 +779,11 @@ class RTCPeer extends Peer {
refresh() { refresh() {
// check if channel is open. otherwise create one // check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
} }
_isConnected() { _isConnected() {
@@ -666,6 +793,11 @@ class RTCPeer extends Peer {
_isConnecting() { _isConnecting() {
return this._channel && this._channel.readyState === 'connecting'; return this._channel && this._channel.readyState === 'connecting';
} }
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
} }
class PeersManager { class PeersManager {
@@ -679,33 +811,64 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail)); Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
} }
_onMessage(message) { _onMessage(message) {
// if different roomType -> abort const peerId = message.sender.id;
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return; this.peers[peerId].onServerMessage(message);
if (!this.peers[message.sender.id]) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
this.peers[message.sender.id].onServerMessage(message);
} }
_onPeers(msg) { _refreshPeer(peer, roomType, roomSecret) {
msg.peers.forEach(peer => { if (!peer) return false;
if (this.peers[peer.id]) {
// if different roomType -> abort const roomTypeIsSecret = roomType === "secret";
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return; const roomSecretsDiffer = peer._roomSecret !== roomSecret;
this.peers[peer.id].refresh();
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
}
const roomTypesDiffer = peer._roomType !== roomType;
// if roomTypes differ peer is already connected -> abort
if (roomTypesDiffer) return true;
peer.refresh();
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomSecret);
return; return;
} }
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
}) this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret);
} }
sendTo(peerId, message) { _onPeerJoined(message) {
this.peers[peerId].send(message); this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret);
})
} }
_onRespondToFileTransferRequest(detail) { _onRespondToFileTransferRequest(detail) {
@@ -732,12 +895,25 @@ class PeersManager {
this.peers[message.to].sendText(message.text); this.peers[message.to].sendText(message.text);
} }
_onPeerLeft(msg) { _onPeerLeft(message) {
if (msg.disconnect === true) { if (message.disconnect === true) {
// if user actively disconnected from PairDrop disconnect all peer to peer connections immediately // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId); Events.fire('peer-disconnected', message.peerId);
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
} }
} }
}
_onPeerConnected(peerId) {
this._notifyPeerDisplayNameChanged(peerId);
}
_onPeerDisconnected(peerId) { _onPeerDisconnected(peerId) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
@@ -751,11 +927,54 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) { _onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) { for (const peerId in this.peers) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) { if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId); this._onPeerDisconnected(peerId);
} }
} }
} }
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
this._notifyPeerDisplayNameChanged(peerId);
}
}
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer) return;
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomSecret and not be on the same browser.
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
return peer._peerId;
}
}
return false;
}
} }
class FileChunker { class FileChunker {
@@ -844,28 +1063,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail })); window.dispatchEvent(new CustomEvent(type, { detail: detail }));
} }
static on(type, callback) { static on(type, callback, options = false) {
return window.addEventListener(type, callback, false); return window.addEventListener(type, callback, options);
} }
static off(type, callback) { static off(type, callback, options = false) {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, options);
} }
} }
RTCPeer.config = {
'sdpSemantics': 'unified-plan',
'iceServers': [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'stun:openrelay.metered.ca:80'
},
{
urls: 'turn:openrelay.metered.ca:443',
username: 'openrelayproject',
credential: 'openrelayproject',
},
]
}
+68 -29
View File
@@ -1,39 +1,78 @@
(function(){ (function(){
// Select the button const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
const btnTheme = document.getElementById('theme'); const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
// Check for dark mode preference at the OS level
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); const $themeAuto = document.getElementById('theme-auto');
const $themeLight = document.getElementById('theme-light');
// Get the user's theme preference from local storage, if it's available const $themeDark = document.getElementById('theme-dark');
const currentTheme = localStorage.getItem('theme');
// If the user's preference in localStorage is dark... let currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') { if (currentTheme === 'dark') {
// ...let's toggle the .dark-theme class on the body setModeToDark();
document.body.classList.toggle('dark-theme');
// Otherwise, if the user's preference in localStorage is light...
} else if (currentTheme === 'light') { } else if (currentTheme === 'light') {
// ...let's toggle the .light-theme class on the body setModeToLight();
document.body.classList.toggle('light-theme');
} }
// Listen for a click on the button $themeAuto.addEventListener('click', _ => {
btnTheme.addEventListener('click', function(e) { if (currentTheme) {
e.preventDefault(); setModeToAuto();
// 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';
} else { } else {
// Otherwise, let's do the same thing, but for .dark-theme setModeToDark();
document.body.classList.toggle('dark-theme'); }
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light'; });
$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");
}
})(); })();
+809 -316
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -398,3 +398,7 @@ const cyrb53 = function(str, seed = 0) {
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909); h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0); return 4294967296 * (2097151 & h2) + (h1>>>0);
}; };
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
+52 -25
View File
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.2.0'; const cacheVersion = 'v1.7.2';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
@@ -71,30 +71,11 @@ const update = request =>
self.addEventListener('fetch', function(event) { self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") { if (event.request.method === "POST") {
// Requests related to Web Share Target. // Requests related to Web Share Target.
event.respondWith( event.respondWith((async () => {
(async () => { let share_url = await evaluateRequestData(event.request);
const formData = await event.request.formData(); share_url = event.request.url + share_url;
const title = formData.get("title"); return Response.redirect(encodeURI(share_url), 302);
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);
})()
);
} else { } else {
// Regular requests not related to Web Share Target. // Regular requests not related to Web Share Target.
event.respondWith( 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);
}
});
}
+329 -116
View File
@@ -19,16 +19,25 @@ body {
overflow-x: hidden; overflow-x: hidden;
overscroll-behavior: none; overscroll-behavior: none;
overflow-y: hidden; overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
} }
body { body {
min-height: 100vh; height: 100%;
/* mobile viewport bug fix */ /* 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 { 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 { .row-reverse {
@@ -70,11 +79,75 @@ html {
} }
header { header {
position: relative; position: absolute;
height: 56px; align-items: baseline;
align-items: center; padding: 8px 16px;
padding: 16px;
box-sizing: border-box; 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] { [hidden] {
@@ -135,7 +208,8 @@ body {
line-height: 18px; line-height: 18px;
} }
a { a,
.icon-button {
text-decoration: none; text-decoration: none;
color: currentColor; color: currentColor;
cursor: pointer; cursor: pointer;
@@ -145,6 +219,14 @@ hr {
color: white; color: white;
} }
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript { x-noscript {
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
@@ -187,15 +269,10 @@ x-noscript {
} }
} }
/* Main Header */
body>header a {
margin-left: 8px;
}
#center { #center {
position: relative; position: relative;
display: flex; display: flex;
margin-top: 56px;
flex-direction: column-reverse; flex-direction: column-reverse;
flex-grow: 1; flex-grow: 1;
--footer-height: 132px; --footer-height: 132px;
@@ -208,7 +285,7 @@ body>header a {
} }
@media screen and (max-width: 425px) { @media screen and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center { header:has(#edit-pair-devices:not([hidden]))~#center {
--footer-height: 150px; --footer-height: 150px;
} }
} }
@@ -340,10 +417,10 @@ x-no-peers {
flex-direction: column; flex-direction: column;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
/* prevent flickering on load */
animation: fade-in 300ms; animation: fade-in 300ms;
animation-delay: 500ms;
animation-fill-mode: backwards; animation-fill-mode: backwards;
/* prevent flickering on load */
animation-iteration-count: 0;
} }
x-no-peers h2, x-no-peers h2,
@@ -377,8 +454,6 @@ x-no-peers[drop-bg] * {
/* Peer */ /* Peer */
x-peer { x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px; padding: 8px;
align-content: start; align-content: start;
flex-wrap: wrap; flex-wrap: wrap;
@@ -450,10 +525,11 @@ x-peer[status] x-icon {
} }
.device-descriptor { .device-descriptor {
width: 100%;
text-align: center; text-align: center;
} }
.name { .device-descriptor > div {
width: 100%; width: 100%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -464,7 +540,6 @@ x-peer[status] x-icon {
.status, .status,
.device-name, .device-name,
.connection-hash { .connection-hash {
height: 18px;
opacity: 0.7; opacity: 0.7;
} }
@@ -533,6 +608,7 @@ footer {
padding: 0 0 16px 0; padding: 0 0 16px 0;
text-align: center; text-align: center;
transition: color 300ms; transition: color 300ms;
cursor: default;
} }
footer .logo { footer .logo {
@@ -557,6 +633,39 @@ footer .font-body2 {
padding-bottom: 1px; padding-bottom: 1px;
} }
#display-name {
display: inline-block;
text-align: left;
border: none;
outline: none;
max-width: 15em;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
margin-left: -1rem;
margin-bottom: -6px;
padding-right: 0.3rem;
padding-left: 0.3em;
padding-bottom: 0.1rem;
border-radius: 1.3rem/30%;
border-right: solid 1rem transparent;
border-left: solid 1rem transparent;
background-clip: padding-box;
background-color: rgba(var(--text-color), 43%);
color: white;
transition: background-color 0.5s ease;
overflow: hidden;
}
#edit-pen {
width: 1rem;
height: 1rem;
margin-left: -1rem;
margin-bottom: -2px;
position: relative;
z-index: -1;
}
/* Dialog */ /* Dialog */
x-dialog x-background { x-dialog x-background {
@@ -564,7 +673,7 @@ x-dialog x-background {
z-index: 10; z-index: 10;
transition: opacity 300ms; transition: opacity 300ms;
will-change: opacity; will-change: opacity;
padding: 35px; padding: 15px;
overflow: overlay; overflow: overlay;
} }
@@ -575,19 +684,26 @@ x-dialog x-paper {
padding: 16px 24px; padding: 16px 24px;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
transition: transform 300ms; transition: transform 300ms;
will-change: transform; will-change: transform;
} }
#pair-device-dialog x-paper { #pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
} }
x-dialog:not([show]) { x-dialog:not([show]) {
@@ -602,12 +718,6 @@ x-dialog:not([show]) x-background {
opacity: 0; opacity: 0;
} }
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a { x-dialog a {
color: var(--primary-color); color: var(--primary-color);
@@ -646,10 +756,13 @@ x-dialog .font-subheading {
} }
#key-input-container > input:nth-of-type(4) { #key-input-container > input:nth-of-type(4) {
margin-left: 18px; margin-left: 5%;
} }
#room-key { #room-key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 50px; font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block; display: inline-block;
@@ -658,22 +771,109 @@ x-dialog .font-subheading {
} }
#room-key-qr-code { #room-key-qr-code {
padding: inherit; margin: 16px;
margin: auto;
width: 150px;
height: 150px;
} }
#pair-device-dialog hr { x-dialog hr {
margin-top: 40px; margin: 40px -24px 30px -24px;
margin-bottom: 40px; border: solid 1.25px var(--border-color);
width: 100%;
} }
#pair-device-dialog x-background { #pair-device-dialog x-background {
padding: 16px!important; padding: 16px!important;
} }
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: "No paired devices.";
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
.paired-device > .auto-accept {
cursor: pointer;
}
/* Receive Dialog */ /* Receive Dialog */
x-dialog .row { x-dialog .row {
@@ -681,29 +881,24 @@ x-dialog .row {
margin-bottom: 8px; margin-bottom: 8px;
} }
x-dialog h2 { /* button row*/
margin-top: 1rem; x-paper > div:last-child {
} margin: auto -24px -15px;
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px;
} }
.separator { x-paper > div:last-child > .button {
border: solid 1.25px var(--border-color); height: 100%;
margin-bottom: -16px; width: 100%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
} }
.file-description { .file-description {
word-break: break-word; margin-bottom: 25px;
width: 80%;
margin: auto;
} }
.file-description .row { .file-description .row {
@@ -715,26 +910,26 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#file-name { .file-name {
font-style: italic; font-style: italic;
max-width: 100%;
} }
#file-stem { .file-stem {
max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-all; white-space: nowrap;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
} }
/* Send Text Dialog */ /* Send Text Dialog */
/* Todo: add pair underline to send / receive dialogs displayName */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input { #text-input {
min-height: 120px; min-height: 200px;
margin: 14px auto;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
@@ -742,14 +937,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: calc(100vh - 393px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-user-select: all; -webkit-user-select: text;
-moz-user-select: all; -moz-user-select: text;
user-select: all; user-select: text;
white-space: pre-wrap; white-space: pre-wrap;
margin-top:36px; padding: 15px 0;
} }
#receive-text-dialog #text a { #receive-text-dialog #text a {
@@ -768,17 +963,32 @@ x-dialog .row-reverse {
.row-separator { .row-separator {
border-bottom: solid 2.5px var(--border-color); border-bottom: solid 2.5px var(--border-color);
margin: auto -25px; margin: auto -24px;
} }
#receive-text-description-container { #base64-paste-btn,
margin-bottom: 25px; #base64-paste-dialog .textarea {
}
#base64-paste-btn {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; 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 { #base64-paste-dialog button {
@@ -800,7 +1010,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0; padding: 2px 16px 0;
box-sizing: border-box; box-sizing: border-box;
min-height: 36px; min-height: 36px;
min-width: 100px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
font-weight: 700; font-weight: 700;
@@ -811,6 +1020,7 @@ x-dialog .row-reverse {
user-select: none; user-select: none;
background: inherit; background: inherit;
color: var(--primary-color); color: var(--primary-color);
overflow: hidden;
} }
.button[disabled] { .button[disabled] {
@@ -848,7 +1058,7 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancel-paste-mode-btn { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -875,7 +1085,6 @@ button::-moz-focus-inner {
/* Icon Button */ /* Icon Button */
.icon-button { .icon-button {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -885,10 +1094,7 @@ button::-moz-focus-inner {
border-radius: 50%; border-radius: 50%;
} }
/* Text Input */ /* Text Input */
.textarea { .textarea {
box-sizing: border-box; box-sizing: border-box;
border: none; border: none;
@@ -902,9 +1108,8 @@ button::-moz-focus-inner {
display: block; display: block;
overflow: auto; overflow: auto;
resize: none; resize: none;
min-height: 40px;
line-height: 16px; line-height: 16px;
max-height: 300px; max-height: calc(100vh - 254px);
white-space: pre; white-space: pre;
} }
@@ -939,10 +1144,12 @@ button::-moz-focus-inner {
#about x-background { #about x-background {
position: absolute; position: absolute;
top: calc(32px - 250px); --size: max(max(230vw, 230vh), calc(150vh + 150vw));
right: calc(32px - 250px); --size-half: calc(var(--size)/2);
width: 500px; top: calc(28px - var(--size-half));
height: 500px; right: calc(36px - var(--size-half));
width: var(--size);
height: var(--size);
border-radius: 50%; border-radius: 50%;
background: var(--primary-color); background: var(--primary-color);
transform: scale(0); transform: scale(0);
@@ -956,13 +1163,20 @@ button::-moz-focus-inner {
} }
#about:target x-background { #about:target x-background {
transform: scale(10); transform: scale(1);
} }
#about .row a { #about .row a {
margin: 8px 8px -16px; margin: 8px 8px -16px;
} }
#about section {
flex-grow: 1;
}
#about header {
align-self: end;
}
/* Loading Indicator */ /* Loading Indicator */
@@ -1012,11 +1226,11 @@ button::-moz-focus-inner {
x-toast { x-toast {
position: absolute; position: absolute;
min-height: 48px; min-height: 48px;
bottom: 24px; top: 50px;
width: 100%; width: 100%;
max-width: 344px; max-width: 344px;
background-color: #323232; background-color: rgb(var(--text-color));
color: rgba(255, 255, 255, 0.95); color: rgb(var(--bg-color));
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
padding: 8px 24px; padding: 8px 24px;
@@ -1030,7 +1244,7 @@ x-toast {
x-toast:not([show]):not(:hover) { x-toast:not([show]):not(:hover) {
opacity: 0; opacity: 0;
transform: translateY(100px); transform: translateY(-100px);
} }
@@ -1072,28 +1286,19 @@ x-peers:empty~x-instructions {
@media (hover: none) and (pointer: coarse) { @media (hover: none) and (pointer: coarse) {
x-peer { x-peer {
transform: scale(0.95); transform: scale(0.95);
padding: 4px 0; padding: 4px;
} }
} }
#websocket-fallback {
margin-left: 5px;
margin-right: 5px;
padding: 5px;
text-align: center;
opacity: 0.5;
transition: opacity 300ms;
}
#websocket-fallback>span {
margin: 2px;
}
#websocket-fallback > span > span {
border-bottom: solid 4px var(--ws-peer-color);
}
/* Responsive Styles */ /* 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) { @media screen and (min-height: 800px) {
footer { footer {
@@ -1166,7 +1371,9 @@ x-dialog x-paper {
display: none; display: none;
} }
.element-preview { .file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%; max-width: 100%;
max-height: 40vh; max-height: 40vh;
margin: auto; margin: auto;
@@ -1225,3 +1432,9 @@ x-dialog x-paper {
background: #bfbfbf; background: #bfbfbf;
border-radius: 4px; border-radius: 4px;
} }
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}
+114 -73
View File
@@ -6,7 +6,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config --> <!-- Web App Config -->
<title>PairDrop</title> <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="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light"> <meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="no"> <meta name="apple-mobile-web-app-capable" content="no">
@@ -39,37 +39,51 @@
<body translate="no"> <body translate="no">
<header class="row-reverse"> <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"> <svg class="icon">
<use xlink:href="#info-outline" /> <use xlink:href="#info-outline" />
</svg> </svg>
</a> </a>
<a id="theme" class="icon-button" title="Switch Darkmode/Lightmode" > <div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme" /> <use xlink:href="#icon-theme-auto" />
</svg> </svg>
</a> </div>
<a id="notification" class="icon-button" title="Enable Notifications" hidden> <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"> <svg class="icon">
<use xlink:href="#notifications" /> <use xlink:href="#notifications" />
</svg> </svg>
</a> </div>
<a id="install" class="icon-button" title="Install PairDrop" hidden> <div id="install" class="icon-button" title="Install PairDrop" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#homescreen" /> <use xlink:href="#homescreen" />
</svg> </svg>
</a> </div>
<a id="pair-device" class="icon-button" title="Pair Device" > <div id="pair-device" class="icon-button" title="Pair Device" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#pair-device-icon" /> <use xlink:href="#pair-device-icon" />
</svg> </svg>
</a> </div>
<a id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden> <div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#clear-pair-devices-icon" /> <use xlink:href="#edit-pair-devices-icon" />
</svg> </svg>
</a> </div>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a> <div id="cancel-paste-mode" class="button" hidden>Done</div>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
@@ -89,7 +103,13 @@
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
<div id="display-name" placeholder="&nbsp;"></div> <div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" />
</svg>
</div>
<div class="font-body2"> <div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span> You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span> <span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
@@ -109,33 +129,34 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr> <hr>
<div id="key-input-container"> <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 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 id="char1" 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-2" 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 type="tel" class="textarea center" aria-label="pair-key-3" 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 type="tel" class="textarea center" aria-label="pair-key-4" 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 type="tel" class="textarea center" aria-label="pair-key-5" 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-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div> <button class="button" type="button" close>Cancel</button>
<a class="button" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
</form> </form>
</x-dialog> </x-dialog>
<!-- Clear Devices Dialog --> <!-- Edit Paired Devices Dialog -->
<x-dialog id="clear-devices-dialog"> <x-dialog id="edit-paired-devices-dialog">
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Unpair Devices</h2> <h2 class="center">Edit Paired Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div> <div class="paired-devices-wrapper"></div>
<div class="row-reverse space-between"> <div class="font-subheading center">
<button class="button" type="submit">Unpair Devices</button> <p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
<a class="button" close>Cancel</a> </div>
<div class="center row-reverse">
<button class="button" type="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -145,25 +166,23 @@
<x-dialog id="receive-request-dialog"> <x-dialog id="receive-request-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">PairDrop</h2> <h2 class="center"></h2>
<div class="text-center file-description"> <div class="center column file-description">
<div> <div>
<span id="requesting-peer-display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span>would like to share</span>
</div> </div>
<div id="file-name" class="row" > <div class="row file-name" >
<span id="file-stem"></span> <span class="file-stem"></span>
<span id="file-extension"></span> <span class="file-extension"></span>
</div> </div>
<div class="row"> <div class="row file-other">
<span id="file-other"></span>
</div> </div>
<div class="row font-body2 file-size"></div>
</div> </div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div> </div>
</x-paper> </x-paper>
@@ -173,13 +192,23 @@
<x-dialog id="receive-file-dialog"> <x-dialog id="receive-file-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 id="receive-title" class="center"></h2> <h2 class="center"></h2>
<div class="text-center file-description"></div> <div class="center column file-description">
<div class="font-body2 text-center file-size"></div> <div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<a id="share-or-download" class="button" autofocus></a> <button id="share-btn" class="button" autofocus hidden>Share</button>
<div class="separator"></div> <button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@@ -190,16 +219,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">PairDrop</h2> <h2 class="text-center">Send Message</h2>
<div class="text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span>Send a Message to</span>
<span id="text-send-peer-display-name"></span> <span class="display-name"></span>
</div> </div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div class="row-separator"></div>
<div class="row-reverse"> <div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button> <div class="center row-reverse">
<div class="separator"></div> <button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
<a class="button" title="ESCAPE" close>Cancel</a> <button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -209,16 +238,15 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2>PairDrop - Message Received</h2> <h2 class="text-center">Message Received</h2>
<div id="receive-text-description-container"> <div class="text-center dialog-subheader">
<span id="receive-text-peer-display-name"></span> <span class="display-name"></span>
<span>sent the following message:</span> <span>has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE">Close</button>
</div> </div>
</x-paper> </x-paper>
@@ -229,6 +257,7 @@
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button> <button class="button center" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -239,14 +268,14 @@
</div> </div>
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
<section class="center column fade-in"> <header class="row-reverse fade-in">
<header class="row-reverse"> <a href="#" class="close icon-button" aria-label="Close About PairDrop">
<a href="#" class="close icon-button">
<svg class="icon"> <svg class="icon">
<use xlink:href="#close-icon" /> <use xlink:href="#close-icon" />
</svg> </svg>
</a> </a>
</header> </header>
<section class="center column fade-in">
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
@@ -318,24 +347,36 @@
<path d="M0 0h24v24H0z" fill="none" /> <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" /> <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>
<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"/> <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>
<symbol id="pair-device-icon" viewBox="0 0 640 512"> <symbol id="pair-device-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/> <path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</symbol> </symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512"> <symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 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"/> <!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<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> </symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/util.js"></script> <script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script> <script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script> <script src="scripts/ui.js"></script>
<script src="scripts/theme.js" async></script> <script src="scripts/util.js" async></script>
<script src="scripts/qrcode.js" async></script> <script src="scripts/QRCode.min.js" async></script>
<script src="scripts/zip.min.js" async></script> <script src="scripts/zip.min.js" async></script>
<script src="scripts/NoSleep.min.js" async></script> <script src="scripts/NoSleep.min.js" async></script>
<!-- Sounds --> <!-- Sounds -->
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+357 -142
View File
@@ -1,16 +1,26 @@
window.URL = window.URL || window.webkitURL; window.URL = window.URL || window.webkitURL;
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection { class ServerConnection {
constructor() { constructor() {
this._connect(); this._connect();
Events.on('pagehide', _ => this._disconnect()); Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail})); Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail})); Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'})); Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate()); Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail)); Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
@@ -19,10 +29,10 @@ class ServerConnection {
Events.on('online', _ => this._connect()); Events.on('online', _ => this._connect());
} }
async _connect() { _connect() {
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting()) return;
const ws = new WebSocket(await this._endpoint()); const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer';
ws.onopen = _ => this._onOpen(); ws.onopen = _ => this._onOpen();
ws.onmessage = e => this._onMessage(e.data); ws.onmessage = e => this._onMessage(e.data);
@@ -34,10 +44,7 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
} if (this._isReconnect) Events.fire('notify-user', 'Connected.');
_sendRoomSecrets(roomSecrets) {
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
} }
_onPairDeviceInitiate() { _onPairDeviceInitiate() {
@@ -50,18 +57,25 @@ class ServerConnection {
_onPairDeviceJoin(roomKey) { _onPairDeviceJoin(roomKey) {
if (!this._isConnected()) { if (!this._isConnected()) {
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000); setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
return; return;
} }
this.send({ type: 'pair-device-join', roomKey: roomKey }) this.send({ type: 'pair-device-join', roomKey: roomKey })
} }
_setRtcConfig(config) {
window.rtcConfig = config;
}
_onMessage(msg) { _onMessage(msg) {
msg = JSON.parse(msg); msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg); if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) { switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers': case 'peers':
Events.fire('peers', msg); this._onPeers(msg);
break; break;
case 'peer-joined': case 'peer-joined':
Events.fire('peer-joined', msg); Events.fire('peer-joined', msg);
@@ -96,6 +110,9 @@ class ServerConnection {
case 'secret-room-deleted': case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret); Events.fire('secret-room-deleted', msg.roomSecret);
break; break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
case 'request': case 'request':
case 'header': case 'header':
case 'partition': case 'partition':
@@ -105,69 +122,93 @@ class ServerConnection {
case 'file-transfer-complete': case 'file-transfer-complete':
case 'message-transfer-complete': case 'message-transfer-complete':
case 'text': case 'text':
case 'display-name-changed':
case 'ws-chunk': case 'ws-chunk':
Events.fire('ws-relay', JSON.stringify(msg)); Events.fire('ws-relay', JSON.stringify(msg));
break; break;
default: default:
console.error('WS: unknown message type', msg); console.error('WS receive: unknown message type', msg);
} }
} }
send(msg) { send(msg) {
if (!this._isConnected()) return; if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg)); this._socket.send(JSON.stringify(msg));
} }
_onDisplayName(msg) { _onPeers(msg) {
sessionStorage.setItem("peerId", msg.message.peerId); Events.fire('peers', msg);
PersistentStorage.get('peerId').then(peerId => { if (msg.roomType === "ip" && msg.peers.length === 0) {
if (!peerId) { BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => {
// save peerId to indexedDB to retrieve after PWA is installed if (!peerId) return;
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => { console.log("successfully removed other peerIds from localStorage");
console.log(`peerId saved to indexedDB: ${peerId}`);
}); });
} }
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable()) }
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg); Events.fire('display-name', msg);
} }
async _endpoint() { _endpoint() {
// hack to detect if deployment or development environment // hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws'; const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback'; const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc); let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
const peerId = await this._peerId(); const peerId = sessionStorage.getItem("peerId");
if (peerId) ws_url.searchParams.append('peer_id', peerId) const peerIdHash = sessionStorage.getItem("peerIdHash");
return ws_url.toString(); 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() { _disconnect() {
this.send({ type: 'disconnect' }); this.send({ type: 'disconnect' });
if (this._socket) {
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null; this._socket.onclose = null;
this._socket.close(); this._socket.close();
this._socket = null; this._socket = null;
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
} this._isReconnect = true;
} }
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...'); Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000); this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
if (document.hidden) return; if (window.hiddenProperty) return;
this._connect(); this._connect();
} }
@@ -191,25 +232,69 @@ class ServerConnection {
class Peer { class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
this._server = serverConnection; this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId; this._peerId = peerId;
this._roomType = roomType; this._roomType = roomType;
this._roomSecret = roomSecret; this._updateRoomSecret(roomSecret);
this._filesQueue = []; this._filesQueue = [];
this._busy = false; this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
} }
sendJSON(message) { sendJSON(message) {
this._send(JSON.stringify(message)); this._send(JSON.stringify(message));
} }
async createHeader(file) { sendDisplayName(displayName) {
return { this.sendJSON({type: 'display-name-changed', displayName: displayName});
name: file.name, }
mime: file.type,
size: file.size, _isSameBrowser() {
}; return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
} }
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) { getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -254,7 +339,11 @@ class Peer {
let imagesOnly = true let imagesOnly = true
for (let i=0; i<files.length; i++) { for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'}) Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i])); header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size; totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false; if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
} }
@@ -324,31 +413,30 @@ class Peer {
this.sendJSON({ type: 'progress', progress: progress }); this.sendJSON({ type: 'progress', progress: progress });
} }
_onMessage(message, logMessage = true) { _onMessage(message) {
if (typeof message !== 'string') { if (typeof message !== 'string') {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
if (logMessage) console.log('RTC:', message); switch (messageJSON.type) {
switch (message.type) {
case 'request': case 'request':
this._onFilesTransferRequest(message); this._onFilesTransferRequest(messageJSON);
break; break;
case 'header': case 'header':
this._onFilesHeader(message); this._onFileHeader(messageJSON);
break; break;
case 'partition': case 'partition':
this._onReceivedPartitionEnd(message); this._onReceivedPartitionEnd(messageJSON);
break; break;
case 'partition-received': case 'partition-received':
this._sendNextPartition(); this._sendNextPartition();
break; break;
case 'progress': case 'progress':
this._onDownloadProgress(message.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
case 'files-transfer-response': case 'files-transfer-response':
this._onFileTransferRequestResponded(message); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted();
@@ -357,14 +445,17 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break;
case 'display-name-changed':
this._onDisplayNameChanged(messageJSON);
break; break;
} }
} }
_onFilesTransferRequest(request) { _onFilesTransferRequest(request) {
if (this._requestPending) { if (this._requestPending) {
// Only accept one request at a time // Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false}); this.sendJSON({type: 'files-transfer-response', accepted: false});
return; return;
} }
@@ -376,6 +467,14 @@ class Peer {
} }
this._requestPending = request; this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', { Events.fire('files-transfer-request', {
request: request, request: request,
peerId: this._peerId peerId: this._peerId
@@ -393,8 +492,8 @@ class Peer {
this._requestPending = null; this._requestPending = null;
} }
_onFilesHeader(header) { _onFileHeader(header) {
if (this._requestAccepted?.header.length) { if (this._requestAccepted && this._requestAccepted.header.length) {
this._lastProgress = 0; this._lastProgress = 0;
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
this._requestAccepted.totalSize, this._requestAccepted.totalSize,
@@ -447,11 +546,14 @@ class Peer {
this._abortTransfer(); this._abortTransfer();
} }
// include for compatibility with Snapdrop for Android app
Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob); this._filesReceived.push(fileBlob);
if (!this._requestAccepted.header.length) { if (!this._requestAccepted.header.length) {
this._busy = false; this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted}); Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = []; this._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
} }
@@ -496,31 +598,44 @@ class Peer {
Events.fire('text-received', { text: escaped, peerId: this._peerId }); Events.fire('text-received', { text: escaped, peerId: this._peerId });
this.sendJSON({ type: 'message-transfer-complete' }); this.sendJSON({ type: 'message-transfer-complete' });
} }
_onDisplayNameChanged(message) {
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
} }
class RTCPeer extends Peer { class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, isCaller, peerId, roomType, roomSecret);
if (!peerId) return; // we will listen for a caller this.rtcSupported = true;
this._connect(peerId, true); if (!this._isCaller) return; // we will listen for a caller
this._connect();
} }
_connect(peerId, isCaller) { _connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller); if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) { if (this._isCaller) {
this._openChannel(); this._openChannel();
} else { } else {
this._conn.ondatachannel = e => this._onChannelOpened(e); this._conn.ondatachannel = e => this._onChannelOpened(e);
} }
} }
_openConnection(peerId, isCaller) { _openConnection() {
this._isCaller = isCaller; this._conn = new RTCPeerConnection(window.rtcConfig);
this._peerId = peerId;
this._conn = new RTCPeerConnection(RTCPeer.config);
this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
} }
@@ -549,7 +664,7 @@ class RTCPeer extends Peer {
} }
onServerMessage(message) { onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false); if (!this._conn) this._connect();
if (message.sdp) { if (message.sdp) {
this._conn.setRemoteDescription(message.sdp) this._conn.setRemoteDescription(message.sdp)
@@ -568,14 +683,21 @@ class RTCPeer extends Peer {
_onChannelOpened(event) { _onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId); console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = _ => this._onChannelClosed(); channel.onclose = _ => this._onChannelClosed();
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._closeChannel());
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
}
_onMessage(message) {
if (typeof message === 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
} }
getConnectionHash() { getConnectionHash() {
@@ -611,26 +733,34 @@ class RTCPeer extends Peer {
} }
} }
_closeChannel() { _onPageHide() {
if (this._channel) this._channel.onclose = null; this._disconnect();
if (this._conn) this._conn.close(); }
this._conn = null;
_disconnect() {
if (this._conn && this._channel) {
this._channel.onclose = null;
this._channel.close();
}
Events.fire('peer-disconnected', this._peerId);
} }
_onChannelClosed() { _onChannelClosed() {
console.log('RTC: channel closed', this._peerId); console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId); Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return; if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel this._connect(); // reopen the channel
} }
_onConnectionStateChange() { _onConnectionStateChange() {
console.log('RTC: state changed:', this._conn.connectionState); console.log('RTC: state changed:', this._conn.connectionState);
switch (this._conn.connectionState) { switch (this._conn.connectionState) {
case 'disconnected': case 'disconnected':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection disconnected'); this._onError('rtc connection disconnected');
break; break;
case 'failed': case 'failed':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection failed'); this._onError('rtc connection failed');
break; break;
} }
@@ -666,7 +796,11 @@ class RTCPeer extends Peer {
refresh() { refresh() {
// check if channel is open. otherwise create one // check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
} }
_isConnected() { _isConnected() {
@@ -676,13 +810,19 @@ class RTCPeer extends Peer {
_isConnecting() { _isConnecting() {
return this._channel && this._channel.readyState === 'connecting'; return this._channel && this._channel.readyState === 'connecting';
} }
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
} }
class WSPeer extends Peer { class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, isCaller, peerId, roomType, roomSecret);
if (!peerId) return; // we will listen for a caller this.rtcSupported = false;
if (!this._isCaller) return; // we will listen for a caller
this._sendSignal(); this._sendSignal();
} }
@@ -700,15 +840,15 @@ class WSPeer extends Peer {
this._server.send(message); this._server.send(message);
} }
_sendSignal() { _sendSignal(connected = false) {
this.sendJSON({type: 'signal'}); this.sendJSON({type: 'signal', connected: connected});
} }
onServerMessage(message) { onServerMessage(message) {
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._peerId) return;
this._peerId = message.sender.id; this._peerId = message.sender.id;
this._sendSignal(); Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (message.connected) return;
this._sendSignal(true);
} }
getConnectionHash() { getConnectionHash() {
@@ -728,48 +868,76 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail)); Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail)); Events.on('ws-relay', e => this._onWsRelay(e.detail));
} }
_onMessage(message) { _onMessage(message) {
// if different roomType -> abort const peerId = message.sender.id;
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return; this.peers[peerId].onServerMessage(message);
if (!this.peers[message.sender.id]) {
if (window.isRtcSupported && message.sender.rtcSupported) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
} else {
this.peers[message.sender.id] = new WSPeer(this._server, undefined, message.roomType, message.roomSecret);
}
}
this.peers[message.sender.id].onServerMessage(message);
} }
_onWsRelay(message) { _refreshPeer(peer, roomType, roomSecret) {
const messageJSON = JSON.parse(message) if (!peer) return false;
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message, false) const roomTypeIsSecret = roomType === "secret";
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
} }
_onPeers(msg) { const roomTypesDiffer = peer._roomType !== roomType;
msg.peers.forEach(peer => {
if (this.peers[peer.id]) { // if roomTypes differ peer is already connected -> abort
// if different roomType -> abort if (roomTypesDiffer) return true;
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh(); peer.refresh();
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret, rtcSupported) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomSecret);
return; return;
} }
if (window.isRtcSupported && peer.rtcSupported) {
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret); if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret);
} else { } else {
this.peers[peer.id] = new WSPeer(this._server, peer.id, msg.roomType, msg.roomSecret); this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret);
} }
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported);
}) })
} }
sendTo(peerId, message) { _onWsRelay(message) {
this.peers[peerId].send(message); const messageJSON = JSON.parse(message);
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message);
} }
_onRespondToFileTransferRequest(detail) { _onRespondToFileTransferRequest(detail) {
@@ -796,13 +964,34 @@ class PeersManager {
this.peers[message.to].sendText(message.text); this.peers[message.to].sendText(message.text);
} }
_onPeerLeft(msg) { _onPeerLeft(message) {
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) { if (this.peers[message.peerId] && (!this.peers[message.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId) console.log('WSPeer left:', message.peerId);
Events.fire('peer-disconnected', msg.peerId) }
} else if (msg.disconnect === true) { if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId); Events.fire('peer-disconnected', message.peerId);
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
_onPeerConnected(peerId) {
this._notifyPeerDisplayNameChanged(peerId);
}
_onWsDisconnected() {
for (const peerId in this.peers) {
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
Events.fire('peer-disconnected', peerId);
}
} }
} }
@@ -818,11 +1007,54 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) { _onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) { for (const peerId in this.peers) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) { if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId); this._onPeerDisconnected(peerId);
} }
} }
} }
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
this._notifyPeerDisplayNameChanged(peerId);
}
}
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer) return;
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomSecret and not be on the same browser.
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
return peer._peerId;
}
}
return false;
}
} }
class FileChunker { class FileChunker {
@@ -911,28 +1143,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail })); window.dispatchEvent(new CustomEvent(type, { detail: detail }));
} }
static on(type, callback) { static on(type, callback, options = false) {
return window.addEventListener(type, callback, false); return window.addEventListener(type, callback, options);
} }
static off(type, callback) { static off(type, callback, options = false) {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, options);
} }
} }
RTCPeer.config = {
'sdpSemantics': 'unified-plan',
'iceServers': [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'stun:openrelay.metered.ca:80'
},
{
urls: 'turn:openrelay.metered.ca:443',
username: 'openrelayproject',
credential: 'openrelayproject',
},
]
}
+68 -29
View File
@@ -1,39 +1,78 @@
(function(){ (function(){
// Select the button const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
const btnTheme = document.getElementById('theme'); const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
// Check for dark mode preference at the OS level
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); const $themeAuto = document.getElementById('theme-auto');
const $themeLight = document.getElementById('theme-light');
// Get the user's theme preference from local storage, if it's available const $themeDark = document.getElementById('theme-dark');
const currentTheme = localStorage.getItem('theme');
// If the user's preference in localStorage is dark... let currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') { if (currentTheme === 'dark') {
// ...let's toggle the .dark-theme class on the body setModeToDark();
document.body.classList.toggle('dark-theme');
// Otherwise, if the user's preference in localStorage is light...
} else if (currentTheme === 'light') { } else if (currentTheme === 'light') {
// ...let's toggle the .light-theme class on the body setModeToLight();
document.body.classList.toggle('light-theme');
} }
// Listen for a click on the button $themeAuto.addEventListener('click', _ => {
btnTheme.addEventListener('click', function(e) { if (currentTheme) {
e.preventDefault(); setModeToAuto();
// 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';
} else { } else {
// Otherwise, let's do the same thing, but for .dark-theme setModeToDark();
document.body.classList.toggle('dark-theme'); }
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light'; });
$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
@@ -399,6 +399,10 @@ const cyrb53 = function(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1>>>0); return 4294967296 * (2097151 & h2) + (h1>>>0);
}; };
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function arrayBufferToBase64(buffer) { function arrayBufferToBase64(buffer) {
var binary = ''; var binary = '';
var bytes = new Uint8Array(buffer); var bytes = new Uint8Array(buffer);
+52 -25
View File
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.2.0'; const cacheVersion = 'v1.7.2';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
@@ -71,30 +71,11 @@ const update = request =>
self.addEventListener('fetch', function(event) { self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") { if (event.request.method === "POST") {
// Requests related to Web Share Target. // Requests related to Web Share Target.
event.respondWith( event.respondWith((async () => {
(async () => { let share_url = await evaluateRequestData(event.request);
const formData = await event.request.formData(); share_url = event.request.url + share_url;
const title = formData.get("title"); return Response.redirect(encodeURI(share_url), 302);
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);
})()
);
} else { } else {
// Regular requests not related to Web Share Target. // Regular requests not related to Web Share Target.
event.respondWith( 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);
}
});
}
+330 -100
View File
@@ -20,16 +20,25 @@ body {
overflow-x: hidden; overflow-x: hidden;
overscroll-behavior: none; overscroll-behavior: none;
overflow-y: hidden; overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
} }
body { body {
min-height: 100vh; height: 100%;
/* mobile viewport bug fix */ /* 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 { 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 { .row-reverse {
@@ -71,11 +80,75 @@ html {
} }
header { header {
position: relative; position: absolute;
height: 56px; align-items: baseline;
align-items: center; padding: 8px 16px;
padding: 16px;
box-sizing: border-box; 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] { [hidden] {
@@ -136,7 +209,8 @@ body {
line-height: 18px; line-height: 18px;
} }
a { a,
.icon-button {
text-decoration: none; text-decoration: none;
color: currentColor; color: currentColor;
cursor: pointer; cursor: pointer;
@@ -146,6 +220,14 @@ hr {
color: white; color: white;
} }
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript { x-noscript {
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
@@ -188,15 +270,10 @@ x-noscript {
} }
} }
/* Main Header */
body>header a {
margin-left: 8px;
}
#center { #center {
position: relative; position: relative;
display: flex; display: flex;
margin-top: 56px;
flex-direction: column-reverse; flex-direction: column-reverse;
flex-grow: 1; flex-grow: 1;
--footer-height: 146px; --footer-height: 146px;
@@ -209,7 +286,7 @@ body>header a {
} }
@media screen and (min-width: 402px) and (max-width: 425px) { @media screen and (min-width: 402px) and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center { header:has(#edit-pair-devices:not([hidden]))~#center {
--footer-height: 164px; --footer-height: 164px;
} }
} }
@@ -350,10 +427,10 @@ x-no-peers {
flex-direction: column; flex-direction: column;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
/* prevent flickering on load */
animation: fade-in 300ms; animation: fade-in 300ms;
animation-delay: 500ms;
animation-fill-mode: backwards; animation-fill-mode: backwards;
/* prevent flickering on load */
animation-iteration-count: 0;
} }
x-no-peers h2, x-no-peers h2,
@@ -387,8 +464,6 @@ x-no-peers[drop-bg] * {
/* Peer */ /* Peer */
x-peer { x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px; padding: 8px;
align-content: start; align-content: start;
flex-wrap: wrap; flex-wrap: wrap;
@@ -477,10 +552,11 @@ x-peer.ws-peer .highlight-wrapper {
} }
.device-descriptor { .device-descriptor {
width: 100%;
text-align: center; text-align: center;
} }
.name { .device-descriptor > div {
width: 100%; width: 100%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -491,7 +567,6 @@ x-peer.ws-peer .highlight-wrapper {
.status, .status,
.device-name, .device-name,
.connection-hash { .connection-hash {
height: 18px;
opacity: 0.7; opacity: 0.7;
} }
@@ -559,6 +634,7 @@ footer {
align-items: center; align-items: center;
text-align: center; text-align: center;
transition: color 300ms; transition: color 300ms;
cursor: default;
} }
footer .logo { footer .logo {
@@ -583,6 +659,39 @@ footer .font-body2 {
padding-bottom: 1px; padding-bottom: 1px;
} }
#display-name {
display: inline-block;
text-align: left;
border: none;
outline: none;
max-width: 15em;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
margin-left: -1rem;
margin-bottom: -6px;
padding-right: 0.3rem;
padding-left: 0.3em;
padding-bottom: 0.1rem;
border-radius: 1.3rem/30%;
border-right: solid 1rem transparent;
border-left: solid 1rem transparent;
background-clip: padding-box;
background-color: rgba(var(--text-color), 43%);
color: white;
transition: background-color 0.5s ease;
overflow: hidden;
}
#edit-pen {
width: 1rem;
height: 1rem;
margin-left: -1rem;
margin-bottom: -2px;
position: relative;
z-index: -1;
}
/* Dialog */ /* Dialog */
x-dialog x-background { x-dialog x-background {
@@ -590,7 +699,7 @@ x-dialog x-background {
z-index: 10; z-index: 10;
transition: opacity 300ms; transition: opacity 300ms;
will-change: opacity; will-change: opacity;
padding: 35px; padding: 15px;
overflow: overlay; overflow: overlay;
} }
@@ -601,19 +710,26 @@ x-dialog x-paper {
padding: 16px 24px; padding: 16px 24px;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
transition: transform 300ms; transition: transform 300ms;
will-change: transform; will-change: transform;
} }
#pair-device-dialog x-paper { #pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
} }
x-dialog:not([show]) { x-dialog:not([show]) {
@@ -628,12 +744,6 @@ x-dialog:not([show]) x-background {
opacity: 0; opacity: 0;
} }
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a { x-dialog a {
color: var(--primary-color); color: var(--primary-color);
@@ -672,10 +782,13 @@ x-dialog .font-subheading {
} }
#key-input-container > input:nth-of-type(4) { #key-input-container > input:nth-of-type(4) {
margin-left: 18px; margin-left: 5%;
} }
#room-key { #room-key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 50px; font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block; display: inline-block;
@@ -684,22 +797,109 @@ x-dialog .font-subheading {
} }
#room-key-qr-code { #room-key-qr-code {
padding: inherit; margin: 16px;
margin: auto;
width: 150px;
height: 150px;
} }
#pair-device-dialog hr { x-dialog hr {
margin-top: 40px; margin: 40px -24px 30px -24px;
margin-bottom: 40px; border: solid 1.25px var(--border-color);
width: 100%;
} }
#pair-device-dialog x-background { #pair-device-dialog x-background {
padding: 16px!important; padding: 16px!important;
} }
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: "No paired devices.";
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
.paired-device > .auto-accept {
cursor: pointer;
}
/* Receive Dialog */ /* Receive Dialog */
x-dialog .row { x-dialog .row {
@@ -707,29 +907,24 @@ x-dialog .row {
margin-bottom: 8px; margin-bottom: 8px;
} }
x-dialog h2 { /* button row*/
margin-top: 1rem; x-paper > div:last-child {
} margin: auto -24px -15px;
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px;
} }
.separator { x-paper > div:last-child > .button {
border: solid 1.25px var(--border-color); height: 100%;
margin-bottom: -16px; width: 100%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
} }
.file-description { .file-description {
word-break: break-word; margin-bottom: 25px;
width: 80%;
margin: auto;
} }
.file-description .row { .file-description .row {
@@ -741,26 +936,26 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#file-name { .file-name {
font-style: italic; font-style: italic;
max-width: 100%;
} }
#file-stem { .file-stem {
max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-all; white-space: nowrap;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
} }
/* Send Text Dialog */ /* Send Text Dialog */
/* Todo: add pair underline to send / receive dialogs displayName */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input { #text-input {
min-height: 120px; min-height: 200px;
margin: 14px auto;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
@@ -768,14 +963,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: calc(100vh - 393px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-user-select: all; -webkit-user-select: text;
-moz-user-select: all; -moz-user-select: text;
user-select: all; user-select: text;
white-space: pre-wrap; white-space: pre-wrap;
margin-top:36px; padding: 15px 0;
} }
#receive-text-dialog #text a { #receive-text-dialog #text a {
@@ -794,17 +989,32 @@ x-dialog .row-reverse {
.row-separator { .row-separator {
border-bottom: solid 2.5px var(--border-color); border-bottom: solid 2.5px var(--border-color);
margin: auto -25px; margin: auto -24px;
} }
#receive-text-description-container { #base64-paste-btn,
margin-bottom: 25px; #base64-paste-dialog .textarea {
}
#base64-paste-btn {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; 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 { #base64-paste-dialog button {
@@ -826,7 +1036,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0; padding: 2px 16px 0;
box-sizing: border-box; box-sizing: border-box;
min-height: 36px; min-height: 36px;
min-width: 100px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
font-weight: 700; font-weight: 700;
@@ -837,6 +1046,7 @@ x-dialog .row-reverse {
user-select: none; user-select: none;
background: inherit; background: inherit;
color: var(--primary-color); color: var(--primary-color);
overflow: hidden;
} }
.button[disabled] { .button[disabled] {
@@ -874,7 +1084,7 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancel-paste-mode-btn { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -901,7 +1111,6 @@ button::-moz-focus-inner {
/* Icon Button */ /* Icon Button */
.icon-button { .icon-button {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -911,10 +1120,7 @@ button::-moz-focus-inner {
border-radius: 50%; border-radius: 50%;
} }
/* Text Input */ /* Text Input */
.textarea { .textarea {
box-sizing: border-box; box-sizing: border-box;
border: none; border: none;
@@ -928,9 +1134,8 @@ button::-moz-focus-inner {
display: block; display: block;
overflow: auto; overflow: auto;
resize: none; resize: none;
min-height: 40px;
line-height: 16px; line-height: 16px;
max-height: 300px; max-height: calc(100vh - 254px);
white-space: pre; white-space: pre;
} }
@@ -965,10 +1170,12 @@ button::-moz-focus-inner {
#about x-background { #about x-background {
position: absolute; position: absolute;
top: calc(32px - 250px); --size: max(max(230vw, 230vh), calc(150vh + 150vw));
right: calc(32px - 250px); --size-half: calc(var(--size)/2);
width: 500px; top: calc(28px - var(--size-half));
height: 500px; right: calc(36px - var(--size-half));
width: var(--size);
height: var(--size);
border-radius: 50%; border-radius: 50%;
background: var(--primary-color); background: var(--primary-color);
transform: scale(0); transform: scale(0);
@@ -982,13 +1189,20 @@ button::-moz-focus-inner {
} }
#about:target x-background { #about:target x-background {
transform: scale(10); transform: scale(1);
} }
#about .row a { #about .row a {
margin: 8px 8px -16px; margin: 8px 8px -16px;
} }
#about section {
flex-grow: 1;
}
#about header {
align-self: end;
}
/* Loading Indicator */ /* Loading Indicator */
@@ -1038,11 +1252,11 @@ button::-moz-focus-inner {
x-toast { x-toast {
position: absolute; position: absolute;
min-height: 48px; min-height: 48px;
bottom: 24px; top: 50px;
width: 100%; width: 100%;
max-width: 344px; max-width: 344px;
background-color: #323232; background-color: rgb(var(--text-color));
color: rgba(255, 255, 255, 0.95); color: rgb(var(--bg-color));
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
padding: 8px 24px; padding: 8px 24px;
@@ -1056,7 +1270,7 @@ x-toast {
x-toast:not([show]):not(:hover) { x-toast:not([show]):not(:hover) {
opacity: 0; opacity: 0;
transform: translateY(100px); transform: translateY(-100px);
} }
@@ -1098,7 +1312,7 @@ x-peers:empty~x-instructions {
@media (hover: none) and (pointer: coarse) { @media (hover: none) and (pointer: coarse) {
x-peer { x-peer {
transform: scale(0.95); transform: scale(0.95);
padding: 4px 0; padding: 4px;
} }
} }
@@ -1120,10 +1334,18 @@ x-peers:empty~x-instructions {
} }
/* Responsive Styles */ /* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
padding: 15px;
}
x-paper > div:last-child {
margin: auto -15px -15px;
}
}
@media screen and (min-height: 800px) { @media screen and (min-height: 800px) {
#websocket-fallback { #websocket-fallback {
padding-bottom: 15px; padding-bottom: 16px;
} }
} }
@@ -1192,7 +1414,9 @@ x-dialog x-paper {
display: none; display: none;
} }
.element-preview { .file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%; max-width: 100%;
max-height: 40vh; max-height: 40vh;
margin: auto; margin: auto;
@@ -1251,3 +1475,9 @@ x-dialog x-paper {
background: #bfbfbf; background: #bfbfbf;
border-radius: 4px; border-radius: 4px;
} }
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}
+13
View 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
View 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