mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-22 23:20:54 +08:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59dca141b6 | |||
| d0046e83cb | |||
| 2aeadb44e2 | |||
| 7827a47d29 | |||
| 4edc9c9b22 | |||
| 398a69d7a0 | |||
| dfe69cc873 | |||
| f5fde731b0 | |||
| d6eee480b3 | |||
| f6ad85a744 | |||
| d50480b2f8 | |||
| ac1e88b6a0 | |||
| 0fe36e132c | |||
| ab08091f5d | |||
| 19a78a5239 | |||
| d0b2c81582 | |||
| 10a0aaf896 | |||
| 34ebd60304 | |||
| 251df2fbff | |||
| 1bb8a63eed | |||
| 7d581ca858 | |||
| dcc4e8b747 | |||
| ce549adf22 | |||
| bdb39a1d2c | |||
| 195dfd0bb3 | |||
| 680ed81bd7 | |||
| b7781e2bab |
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
|||||||
* 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))
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
+46
@@ -11,6 +11,16 @@ by clicking the install-button in the top-right corner while on [pairdrop.net](h
|
|||||||
<img width="400" 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/)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<b>Self-Hosted Instance?</b>
|
||||||
|
|
||||||
|
To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs).
|
||||||
|
See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more information.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -23,6 +33,9 @@ Shortcuts!
|
|||||||
- 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`.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -37,6 +50,9 @@ 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/
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -49,6 +65,9 @@ Yes, it finally is!
|
|||||||
* [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)
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -60,6 +79,9 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -72,6 +94,9 @@ Here's a list of some third-party apps compatible with PairDrop:
|
|||||||
1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android)
|
1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android)
|
||||||
2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon)
|
2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon)
|
||||||
3. Feel free to make one :)
|
3. Feel free to make one :)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -83,6 +108,9 @@ It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a S
|
|||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -95,6 +123,9 @@ 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 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.
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -104,6 +135,9 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R
|
|||||||
|
|
||||||
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
|
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -124,6 +158,9 @@ Alternatively, you can open a hotspot on one of your devices to bridge the conne
|
|||||||
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.
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -136,6 +173,9 @@ We are not trying to optimize for some edge-cases. We are optimizing the user fl
|
|||||||
|
|
||||||
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](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).
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -150,6 +190,9 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob
|
|||||||
* 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)
|
* To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop)
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -158,6 +201,9 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob
|
|||||||
</summary>
|
</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>
|
</details>
|
||||||
|
|
||||||
[< Back](/README.md)
|
[< Back](/README.md)
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 gh
|
|||||||
>
|
>
|
||||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
> 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
|
||||||
@@ -101,6 +103,8 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -i
|
|||||||
>
|
>
|
||||||
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
|
> 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
|
||||||
|
|||||||
+3
-3
@@ -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.
|
||||||
|
|||||||
@@ -63,14 +63,6 @@ const rtcConfig = process.env.RTC_CONFIG
|
|||||||
"iceServers": [
|
"iceServers": [
|
||||||
{
|
{
|
||||||
"urls": "stun:stun.l.google.com:19302"
|
"urls": "stun:stun.l.google.com:19302"
|
||||||
},
|
|
||||||
{
|
|
||||||
"urls": "stun:openrelay.metered.ca:80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"urls": "turn:openrelay.metered.ca:443",
|
|
||||||
"username": "openrelayproject",
|
|
||||||
"credential": "openrelayproject"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+9
-9
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"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.34",
|
"ua-parser-js": "^1.0.35",
|
||||||
"unique-names-generator": "^4.3.0",
|
"unique-names-generator": "^4.3.0",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
@@ -583,9 +583,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ua-parser-js": {
|
"node_modules/ua-parser-js": {
|
||||||
"version": "1.0.34",
|
"version": "1.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||||
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
|
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1070,9 +1070,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ua-parser-js": {
|
"ua-parser-js": {
|
||||||
"version": "1.0.34",
|
"version": "1.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||||
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
|
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA=="
|
||||||
},
|
},
|
||||||
"unique-names-generator": {
|
"unique-names-generator": {
|
||||||
"version": "4.7.1",
|
"version": "4.7.1",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pairdrop",
|
"name": "pairdrop",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"ua-parser-js": "^1.0.34",
|
"ua-parser-js": "^1.0.35",
|
||||||
"unique-names-generator": "^4.3.0",
|
"unique-names-generator": "^4.3.0",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+43
-23
@@ -44,32 +44,46 @@
|
|||||||
<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 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 Light" >
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlink:href="#icon-theme-light" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="theme-dark" class="icon-button" title="Always Dark" >
|
||||||
|
<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" hidden>
|
<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="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#clear-pair-devices-icon" />
|
<use xlink:href="#clear-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</div>
|
||||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||||
</header>
|
</header>
|
||||||
<!-- Center -->
|
<!-- Center -->
|
||||||
<div id="center">
|
<div id="center">
|
||||||
@@ -112,12 +126,12 @@
|
|||||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
<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 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 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 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 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 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 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="center row-reverse">
|
<div class="center row-reverse">
|
||||||
@@ -206,7 +220,7 @@
|
|||||||
<span class="display-name"></span>
|
<span class="display-name"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row-separator"></div>
|
||||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||||
@@ -328,7 +342,13 @@
|
|||||||
<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">
|
||||||
@@ -345,11 +365,11 @@
|
|||||||
</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 -->
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
+68
-29
@@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
+69
-44
@@ -1607,27 +1607,33 @@ class WebShareTargetUI {
|
|||||||
let shareTargetText;
|
let shareTargetText;
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable.
|
shareTargetText = url; // we share only the link - no text.
|
||||||
} else if (title && text) {
|
} else if (title && text) {
|
||||||
shareTargetText = title + '\r\n' + text;
|
shareTargetText = title + '\r\n' + text;
|
||||||
} else {
|
} else {
|
||||||
shareTargetText = title + text;
|
shareTargetText = title + text;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Shared Target Text:', '"' + shareTargetText + '"');
|
|
||||||
Events.fire('activate-paste-mode', {files: [], text: shareTargetText})
|
Events.fire('activate-paste-mode', {files: [], text: shareTargetText})
|
||||||
} else if (share_target_type === "files") {
|
} else if (share_target_type === "files") {
|
||||||
const openRequest = window.indexedDB.open('pairdrop_store')
|
let openRequest = window.indexedDB.open('pairdrop_store')
|
||||||
openRequest.onsuccess( db => {
|
openRequest.onsuccess = e => {
|
||||||
|
const db = e.target.result;
|
||||||
const tx = db.transaction('share_target_files', 'readwrite');
|
const tx = db.transaction('share_target_files', 'readwrite');
|
||||||
const store = tx.objectStore('share_target_files');
|
const store = tx.objectStore('share_target_files');
|
||||||
const request = store.getAll();
|
const request = store.getAll();
|
||||||
request.onsuccess = _ => {
|
request.onsuccess = _ => {
|
||||||
Events.fire('activate-paste-mode', {files: request.result, text: ""})
|
const fileObjects = request.result;
|
||||||
|
let filesReceived = [];
|
||||||
|
for (let i=0; i<fileObjects.length; i++) {
|
||||||
|
filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name));
|
||||||
|
}
|
||||||
const clearRequest = store.clear()
|
const clearRequest = store.clear()
|
||||||
clearRequest.onsuccess = _ => db.close();
|
clearRequest.onsuccess = _ => db.close();
|
||||||
|
|
||||||
|
Events.fire('activate-paste-mode', {files: filesReceived, text: ""})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
window.history.replaceState({}, "Rewrite URL", '/');
|
window.history.replaceState({}, "Rewrite URL", '/');
|
||||||
}
|
}
|
||||||
@@ -1684,7 +1690,7 @@ class PersistentStorage {
|
|||||||
PersistentStorage.logBrowserNotCapable();
|
PersistentStorage.logBrowserNotCapable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 2);
|
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
|
||||||
DBOpenRequest.onerror = (e) => {
|
DBOpenRequest.onerror = (e) => {
|
||||||
PersistentStorage.logBrowserNotCapable();
|
PersistentStorage.logBrowserNotCapable();
|
||||||
console.log('Error initializing database: ');
|
console.log('Error initializing database: ');
|
||||||
@@ -1710,7 +1716,10 @@ class PersistentStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.createObjectStore('share_target_files');
|
if (db.objectStoreNames.contains('share_target_files')) {
|
||||||
|
db.deleteObjectStore('share_target_files');
|
||||||
|
}
|
||||||
|
db.createObjectStore('share_target_files', {autoIncrement: true});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Object store named 'share_target_files' already exists")
|
console.log("Object store named 'share_target_files' already exists")
|
||||||
}
|
}
|
||||||
@@ -1943,68 +1952,84 @@ Events.on('load', () => {
|
|||||||
style.zIndex = -1;
|
style.zIndex = -1;
|
||||||
style.top = 0;
|
style.top = 0;
|
||||||
style.left = 0;
|
style.left = 0;
|
||||||
let ctx = c.getContext('2d');
|
let cCtx = c.getContext('2d');
|
||||||
let x0, y0, w, h, dw, offset;
|
let x0, y0, w, h, dw, offset;
|
||||||
|
|
||||||
|
let offscreenCanvases = [];
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
let oldW = w;
|
||||||
|
let oldH = h;
|
||||||
|
let oldOffset = offset
|
||||||
w = document.documentElement.clientWidth;
|
w = document.documentElement.clientWidth;
|
||||||
h = document.documentElement.clientHeight;
|
h = document.documentElement.clientHeight;
|
||||||
c.width = w;
|
|
||||||
c.height = h;
|
|
||||||
offset = $$('footer').offsetHeight - 32;
|
offset = $$('footer').offsetHeight - 32;
|
||||||
if (h > 800) offset += 16;
|
if (h > 800) offset += 16;
|
||||||
|
|
||||||
|
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
|
||||||
|
|
||||||
|
c.width = w;
|
||||||
|
c.height = h;
|
||||||
x0 = w / 2;
|
x0 = w / 2;
|
||||||
y0 = h - offset;
|
y0 = h - offset;
|
||||||
dw = Math.max(w, h, 1000) / 13;
|
dw = Math.round(Math.max(w, h, 1000) / 13);
|
||||||
drawCircles();
|
drawCircles(cCtx, 0);
|
||||||
|
|
||||||
|
// enforce redrawing of frames
|
||||||
|
offscreenCanvases = [];
|
||||||
}
|
}
|
||||||
Events.on('bg-resize', _ => init());
|
Events.on('bg-resize', _ => init());
|
||||||
window.onresize = _ => Events.fire('bg-resize');
|
window.onresize = _ => Events.fire('bg-resize');
|
||||||
|
|
||||||
function drawCircle(radius) {
|
function drawCircle(ctx, radius) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
let color = Math.round(255 * (1 - radius / Math.max(w, h)));
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)';
|
let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h));
|
||||||
|
ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`;
|
||||||
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
|
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.lineWidth = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let step = 0;
|
function drawCircles(ctx, frame) {
|
||||||
|
for (let i = 0; i < 13; i++) {
|
||||||
function drawCircles() {
|
drawCircle(ctx, dw * i + frame);
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
drawCircle(dw * i + step % dw);
|
|
||||||
}
|
|
||||||
step += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let loading = true;
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
if (loading || !finished()) {
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
drawCircles();
|
|
||||||
animate();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function finished() {
|
function createOffscreenCanvas(frame) {
|
||||||
return step % dw >= dw - 5;
|
let canvas = document.createElement("canvas");
|
||||||
|
canvas.width = c.width;
|
||||||
|
canvas.height = c.height;
|
||||||
|
offscreenCanvases[frame] = canvas;
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
drawCircles(ctx, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFrame(frame) {
|
||||||
|
cCtx.clearRect(0, 0, w, h);
|
||||||
|
if (!offscreenCanvases[frame]) {
|
||||||
|
createOffscreenCanvas(frame);
|
||||||
|
}
|
||||||
|
cCtx.drawImage(offscreenCanvases[frame], 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let animate = true;
|
||||||
|
let currentFrame = 0;
|
||||||
|
|
||||||
|
function animateBg() {
|
||||||
|
if (currentFrame + 1 < dw || animate) {
|
||||||
|
currentFrame = (currentFrame + 1) % dw;
|
||||||
|
drawFrame(currentFrame);
|
||||||
|
}
|
||||||
|
setTimeout(_ => animateBg(), 3000 / dw);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.animateBackground = function(l) {
|
window.animateBackground = function(l) {
|
||||||
if (!l) {
|
animate = l;
|
||||||
loading = false;
|
|
||||||
} else if (!loading) {
|
|
||||||
loading = true;
|
|
||||||
if (finished()) animate();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
animate();
|
animateBg();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.changeFavicon = function (src) {
|
document.changeFavicon = function (src) {
|
||||||
|
|||||||
+52
-25
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.5.0';
|
const cacheVersion = 'v1.6.0';
|
||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+71
-12
@@ -75,11 +75,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;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > div: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] {
|
||||||
@@ -192,15 +256,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;
|
||||||
@@ -974,8 +1033,8 @@ button::-moz-focus-inner {
|
|||||||
|
|
||||||
#about x-background {
|
#about x-background {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(32px - 250px);
|
top: calc(28px - 250px);
|
||||||
right: calc(32px - 250px);
|
right: calc(36px - 250px);
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@@ -44,32 +44,46 @@
|
|||||||
<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 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 Light" >
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlink:href="#icon-theme-light" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="theme-dark" class="icon-button" title="Always Dark" >
|
||||||
|
<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" hidden>
|
<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="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#clear-pair-devices-icon" />
|
<use xlink:href="#clear-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</div>
|
||||||
<a id="cancel-paste-mode" class="button" hidden>Done</a>
|
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||||
</header>
|
</header>
|
||||||
<!-- Center -->
|
<!-- Center -->
|
||||||
<div id="center">
|
<div id="center">
|
||||||
@@ -115,12 +129,12 @@
|
|||||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
<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 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 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 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 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 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 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="center row-reverse">
|
<div class="center row-reverse">
|
||||||
@@ -209,7 +223,7 @@
|
|||||||
<span class="display-name"></span>
|
<span class="display-name"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row-separator"></div>
|
||||||
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
<div class="center row-reverse">
|
<div class="center row-reverse">
|
||||||
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
|
||||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||||
@@ -331,7 +345,13 @@
|
|||||||
<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">
|
||||||
@@ -348,11 +368,11 @@
|
|||||||
</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 -->
|
||||||
|
|||||||
Vendored
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1608,27 +1608,33 @@ class WebShareTargetUI {
|
|||||||
let shareTargetText;
|
let shareTargetText;
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable.
|
shareTargetText = url; // we share only the link - no text.
|
||||||
} else if (title && text) {
|
} else if (title && text) {
|
||||||
shareTargetText = title + '\r\n' + text;
|
shareTargetText = title + '\r\n' + text;
|
||||||
} else {
|
} else {
|
||||||
shareTargetText = title + text;
|
shareTargetText = title + text;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Shared Target Text:', '"' + shareTargetText + '"');
|
|
||||||
Events.fire('activate-paste-mode', {files: [], text: shareTargetText})
|
Events.fire('activate-paste-mode', {files: [], text: shareTargetText})
|
||||||
} else if (share_target_type === "files") {
|
} else if (share_target_type === "files") {
|
||||||
const openRequest = window.indexedDB.open('pairdrop_store')
|
let openRequest = window.indexedDB.open('pairdrop_store')
|
||||||
openRequest.onsuccess( db => {
|
openRequest.onsuccess = e => {
|
||||||
|
const db = e.target.result;
|
||||||
const tx = db.transaction('share_target_files', 'readwrite');
|
const tx = db.transaction('share_target_files', 'readwrite');
|
||||||
const store = tx.objectStore('share_target_files');
|
const store = tx.objectStore('share_target_files');
|
||||||
const request = store.getAll();
|
const request = store.getAll();
|
||||||
request.onsuccess = _ => {
|
request.onsuccess = _ => {
|
||||||
Events.fire('activate-paste-mode', {files: request.result, text: ""})
|
const fileObjects = request.result;
|
||||||
|
let filesReceived = [];
|
||||||
|
for (let i=0; i<fileObjects.length; i++) {
|
||||||
|
filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name));
|
||||||
|
}
|
||||||
const clearRequest = store.clear()
|
const clearRequest = store.clear()
|
||||||
clearRequest.onsuccess = _ => db.close();
|
clearRequest.onsuccess = _ => db.close();
|
||||||
|
|
||||||
|
Events.fire('activate-paste-mode', {files: filesReceived, text: ""})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
window.history.replaceState({}, "Rewrite URL", '/');
|
window.history.replaceState({}, "Rewrite URL", '/');
|
||||||
}
|
}
|
||||||
@@ -1685,7 +1691,7 @@ class PersistentStorage {
|
|||||||
PersistentStorage.logBrowserNotCapable();
|
PersistentStorage.logBrowserNotCapable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 2);
|
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
|
||||||
DBOpenRequest.onerror = (e) => {
|
DBOpenRequest.onerror = (e) => {
|
||||||
PersistentStorage.logBrowserNotCapable();
|
PersistentStorage.logBrowserNotCapable();
|
||||||
console.log('Error initializing database: ');
|
console.log('Error initializing database: ');
|
||||||
@@ -1711,7 +1717,10 @@ class PersistentStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.createObjectStore('share_target_files');
|
if (db.objectStoreNames.contains('share_target_files')) {
|
||||||
|
db.deleteObjectStore('share_target_files');
|
||||||
|
}
|
||||||
|
db.createObjectStore('share_target_files', {autoIncrement: true});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Object store named 'share_target_files' already exists")
|
console.log("Object store named 'share_target_files' already exists")
|
||||||
}
|
}
|
||||||
@@ -1944,67 +1953,84 @@ Events.on('load', () => {
|
|||||||
style.zIndex = -1;
|
style.zIndex = -1;
|
||||||
style.top = 0;
|
style.top = 0;
|
||||||
style.left = 0;
|
style.left = 0;
|
||||||
let ctx = c.getContext('2d');
|
let cCtx = c.getContext('2d');
|
||||||
let x0, y0, w, h, dw, offset;
|
let x0, y0, w, h, dw, offset;
|
||||||
|
|
||||||
|
let offscreenCanvases = [];
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
let oldW = w;
|
||||||
|
let oldH = h;
|
||||||
|
let oldOffset = offset
|
||||||
w = document.documentElement.clientWidth;
|
w = document.documentElement.clientWidth;
|
||||||
h = document.documentElement.clientHeight;
|
h = document.documentElement.clientHeight;
|
||||||
|
offset = $$('footer').offsetHeight - 32;
|
||||||
|
if (h > 800) offset += 16;
|
||||||
|
|
||||||
|
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
|
||||||
|
|
||||||
c.width = w;
|
c.width = w;
|
||||||
c.height = h;
|
c.height = h;
|
||||||
offset = $$('footer').offsetHeight - 32;
|
|
||||||
x0 = w / 2;
|
x0 = w / 2;
|
||||||
y0 = h - offset;
|
y0 = h - offset;
|
||||||
dw = Math.max(w, h, 1000) / 13;
|
dw = Math.round(Math.max(w, h, 1000) / 13);
|
||||||
drawCircles();
|
drawCircles(cCtx, 0);
|
||||||
|
|
||||||
|
// enforce redrawing of frames
|
||||||
|
offscreenCanvases = [];
|
||||||
}
|
}
|
||||||
Events.on('bg-resize', _ => init());
|
Events.on('bg-resize', _ => init());
|
||||||
window.onresize = _ => Events.fire('bg-resize');
|
window.onresize = _ => Events.fire('bg-resize');
|
||||||
|
|
||||||
function drawCircle(radius) {
|
function drawCircle(ctx, radius) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
let color = Math.round(255 * (1 - radius / Math.max(w, h)));
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)';
|
let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h));
|
||||||
|
ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`;
|
||||||
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
|
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.lineWidth = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let step = 0;
|
function drawCircles(ctx, frame) {
|
||||||
|
for (let i = 0; i < 13; i++) {
|
||||||
function drawCircles() {
|
drawCircle(ctx, dw * i + frame);
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
drawCircle(dw * i + step % dw);
|
|
||||||
}
|
|
||||||
step += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let loading = true;
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
if (loading || !finished()) {
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
drawCircles();
|
|
||||||
animate();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function finished() {
|
function createOffscreenCanvas(frame) {
|
||||||
return step % dw >= dw - 5;
|
let canvas = document.createElement("canvas");
|
||||||
|
canvas.width = c.width;
|
||||||
|
canvas.height = c.height;
|
||||||
|
offscreenCanvases[frame] = canvas;
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
drawCircles(ctx, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFrame(frame) {
|
||||||
|
cCtx.clearRect(0, 0, w, h);
|
||||||
|
if (!offscreenCanvases[frame]) {
|
||||||
|
createOffscreenCanvas(frame);
|
||||||
|
}
|
||||||
|
cCtx.drawImage(offscreenCanvases[frame], 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let animate = true;
|
||||||
|
let currentFrame = 0;
|
||||||
|
|
||||||
|
function animateBg() {
|
||||||
|
if (currentFrame + 1 < dw || animate) {
|
||||||
|
currentFrame = (currentFrame + 1) % dw;
|
||||||
|
drawFrame(currentFrame);
|
||||||
|
}
|
||||||
|
setTimeout(_ => animateBg(), 3000 / dw);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.animateBackground = function(l) {
|
window.animateBackground = function(l) {
|
||||||
if (!l) {
|
animate = l;
|
||||||
loading = false;
|
|
||||||
} else if (!loading) {
|
|
||||||
loading = true;
|
|
||||||
if (finished()) animate();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
animate();
|
animateBg();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.changeFavicon = function (src) {
|
document.changeFavicon = function (src) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const cacheVersion = 'v1.5.0';
|
const cacheVersion = 'v1.6.0';
|
||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,11 +76,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;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > div: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] {
|
||||||
@@ -193,15 +257,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;
|
||||||
|
|||||||
@@ -3,14 +3,6 @@
|
|||||||
"iceServers": [
|
"iceServers": [
|
||||||
{
|
{
|
||||||
"urls": "stun:stun.l.google.com:19302"
|
"urls": "stun:stun.l.google.com:19302"
|
||||||
},
|
|
||||||
{
|
|
||||||
"urls": "stun:openrelay.metered.ca:80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"urls": "turn:openrelay.metered.ca:443",
|
|
||||||
"username": "openrelayproject",
|
|
||||||
"credential": "openrelayproject"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user