mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-06 09:53:49 +00:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cffc2a834f | ||
|
|
9f8acd6b6a | ||
|
|
60c699f3b2 | ||
|
|
1669fe7d75 | ||
|
|
3c1ef57740 | ||
|
|
faa2030f0b | ||
|
|
34c5392a66 | ||
|
|
d082a885c4 | ||
|
|
fd89aca219 | ||
|
|
82a46ebb84 | ||
|
|
d7e25dc2d4 | ||
|
|
eeccf78fa6 | ||
|
|
d8043bd4e5 | ||
|
|
e6b10bc9cb | ||
|
|
ab4ac5c217 | ||
|
|
c3c3e2685f | ||
|
|
a120d7c1fc | ||
|
|
ce9748fd0e | ||
|
|
d4ff69d215 | ||
|
|
fdf9457a0d | ||
|
|
b537541788 | ||
|
|
49254bcca9 | ||
|
|
9a7ee95d2f | ||
|
|
e0548102d3 | ||
|
|
d7626822fa | ||
|
|
426b75f336 | ||
|
|
f63aebbcb4 | ||
|
|
5e74f515a7 | ||
|
|
e807c3959a | ||
|
|
d77ccdf233 | ||
|
|
d45c6b5bea | ||
|
|
5625bf6f93 | ||
|
|
efec6eaa1a | ||
|
|
2abbfcea75 | ||
|
|
b7abc1d174 | ||
|
|
71d92bca71 | ||
|
|
b4f9f9b048 | ||
|
|
6b98d84697 | ||
|
|
c305996799 | ||
|
|
684e43a352 | ||
|
|
fc190dd121 | ||
|
|
6c07dc3678 | ||
|
|
dbe3381e7d | ||
|
|
0bae7c84bd | ||
|
|
ddd5c7df8c | ||
|
|
33a6a1b8dc | ||
|
|
4c1de16eb6 | ||
|
|
00b95ed16a | ||
|
|
e7e3221e4c | ||
|
|
c7f4166f0b | ||
|
|
292a674482 | ||
|
|
95231d0507 | ||
|
|
c7263c0aa4 | ||
|
|
09583ae6c8 | ||
|
|
b584b6adc1 | ||
|
|
3355351c98 | ||
|
|
3c7218e2d5 | ||
|
|
8caec76ba0 | ||
|
|
4ad54c6023 | ||
|
|
0e9df8cc17 | ||
|
|
7e63994140 | ||
|
|
f92c7acdce | ||
|
|
a51255b84a | ||
|
|
2985bc0d08 | ||
|
|
e34556bc74 | ||
|
|
16fd866445 | ||
|
|
df734a4fc7 | ||
|
|
e9b1c51530 | ||
|
|
c7357e7ecc | ||
|
|
446035d704 | ||
|
|
6492f84c4e | ||
|
|
3f1477f24a | ||
|
|
a314b2c275 | ||
|
|
3faa173dde | ||
|
|
8b30940056 | ||
|
|
de49af3bca | ||
|
|
8d2584fa69 | ||
|
|
bd7b3c6d28 | ||
|
|
c71bf456e3 | ||
|
|
6679ef7529 | ||
|
|
02911804cb | ||
|
|
d689fe28e5 | ||
|
|
1c946fc78e | ||
|
|
17afa18d84 | ||
|
|
4510ba5164 | ||
|
|
d738258869 | ||
|
|
19f56a8499 | ||
|
|
abc06fcc21 | ||
|
|
c2a746d69c | ||
|
|
72f0aff60e | ||
|
|
161bd2be84 | ||
|
|
c36c8dae59 | ||
|
|
22bf1be2b7 | ||
|
|
6db844565e | ||
|
|
efeff84320 | ||
|
|
e0210b0307 | ||
|
|
f95181c057 | ||
|
|
69f1688dfe | ||
|
|
30a1c72a0c | ||
|
|
3c56427bf7 | ||
|
|
d252900055 | ||
|
|
da5038a51a | ||
|
|
b319fbe156 | ||
|
|
714608ce97 | ||
|
|
471278f7b0 | ||
|
|
6d7c13775f | ||
|
|
dccc17400c | ||
|
|
6563ec98b3 | ||
|
|
17a12baa2a | ||
|
|
913b60b712 | ||
|
|
9424f704bf | ||
|
|
044d7aa20d | ||
|
|
9b71d93dd3 | ||
|
|
65ec416646 | ||
|
|
99faa6bbfd | ||
|
|
525fd295b7 | ||
|
|
410936dcd8 | ||
|
|
1d333c850c | ||
|
|
ebde466019 | ||
|
|
ba46befde4 | ||
|
|
f50d7438b6 | ||
|
|
2d0ea9a2f1 |
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve. Please check the FAQ first.
|
||||
title: 'Bug:/Enhancement:/Feature Request: '
|
||||
labels: ''
|
||||
title: '[Bug] '
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -34,12 +34,17 @@ If applicable, add screenshots to help explain your problem.
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Self-Hosted**
|
||||
**Bug occurs on official PairDrop instance https://pairdrop.net/**
|
||||
No | Yes
|
||||
Version: v1.8.3
|
||||
|
||||
**Bug occurs on self-hosted PairDrop instance**
|
||||
No | Yes
|
||||
|
||||
**Self-Hosted Setup**
|
||||
Proxy: Nginx | Apache2
|
||||
Deployment: docker run | docker-compose | npm run start:prod
|
||||
Version: v1.8.3
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Enhancement
|
||||
about: Enhancements and feature requests are always welcome. See discussions regarding central topics.
|
||||
title: '[Enhancement] '
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What problem is solved by the new feature**
|
||||
What's the motivation for this topic
|
||||
|
||||
**Describe the feature**
|
||||
A clear and concise description of what the new feature/enhancement is.
|
||||
|
||||
**Drafts**
|
||||
Screenshots of Draw.io graph or drawn sketch.
|
||||
|
||||
**Additional context**
|
||||
Add any other context here.
|
||||
60
README.md
60
README.md
@@ -20,8 +20,12 @@
|
||||
## Features
|
||||
[PairDrop](https://pairdrop.net) is a sublime alternative to AirDrop that works on all platforms.
|
||||
|
||||
Send images, documents or text via peer to peer connection to devices in the same local network/Wi-Fi or to paired devices.
|
||||
As it is web based, it runs on all devices.
|
||||
- File Sharing on your local network
|
||||
- Send images, documents or text via peer to peer connection to devices on the same local network.
|
||||
- Internet Transfers
|
||||
- Join temporary public rooms to transfer files easily over the internet!
|
||||
- Web-Application
|
||||
- As it is web based, it runs on all devices.
|
||||
|
||||
You want to quickly send a file from your phone to your laptop?
|
||||
<br>You want to share photos in original quality with friends that use a mixture of Android and iOS?
|
||||
@@ -32,14 +36,29 @@ You want to quickly send a file from your phone to your laptop?
|
||||
Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
|
||||
## Differences to Snapdrop
|
||||
<details><summary>Click to expand</summary>
|
||||
|
||||
### Device Pairing / Internet Transfer
|
||||
* Pair devices via 6-digit code or QR-Code
|
||||
* Pair devices outside your local network or in complex network environment (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
|
||||
### Paired Devices and Public Rooms - Internet Transfer
|
||||
* Transfer files over the internet between paired devices or by entering temporary public rooms.
|
||||
* Connect to devices in complex network environments (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
|
||||
* Connect to devices on your mobile hotspot.
|
||||
* Paired devices will always find each other via shared secrets even after reopening the browser or the Progressive Web App
|
||||
* You will always discover devices on your local network. Paired devices are shown additionally.
|
||||
* Paired devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
|
||||
* Devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
|
||||
* Connect to devices on your mobile hotspot.
|
||||
* You will always discover devices on your local network. Paired devices and devices in the same public room are shown additionally.
|
||||
|
||||
#### Persistent Device Pairing
|
||||
* Pair your devices via a 6-digit code or a QR-Code.
|
||||
* Paired devices will always find each other via shared secrets independently of their local network.
|
||||
* Paired devices are persistent. You find your devices even after reopening PairDrop.
|
||||
* You can edit and unpair devices easily
|
||||
* Ideal to always connect easily to your own devices
|
||||
|
||||
#### Temporary Public Rooms
|
||||
* Enter a public room via a 5-letter code or a QR-Code.
|
||||
* Enter a public room to temporarily connect to devices outside your local network.
|
||||
* All devices in the same public room see each other mutually.
|
||||
* Public rooms are temporary. Public rooms are left as soon as PairDrop is closed.
|
||||
* Ideal to connect easily to others in complex network situations or over the internet.
|
||||
|
||||
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
|
||||
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
||||
@@ -66,13 +85,12 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
|
||||
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
||||
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
|
||||
* Built-in translations
|
||||
|
||||
</details>
|
||||
|
||||
## Screenshots
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
<img src="https://raw.githubusercontent.com/schlagmichdoch/PairDrop/master/docs/pairdrop_screenshot_mobile.gif" style="max-height: 50vh">
|
||||
|
||||
## PairDrop is built with the following awesome technologies:
|
||||
* Vanilla HTML5 / ES6 / CSS3 frontend
|
||||
@@ -82,23 +100,29 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
||||
* [cyrb53](https://github.com/bryc) super fast hash function
|
||||
* [Weblate](https://weblate.org/) Web based localization tool
|
||||
|
||||
Have any questions? Read our [FAQ](/docs/faq.md).
|
||||
|
||||
You can [host your own instance with Docker](/docs/host-your-own.md).
|
||||
|
||||
|
||||
## Support the Community
|
||||
PairDrop is free and always will be. Still, we have to pay for the domain and the server.
|
||||
|
||||
To contribute and support:<br>
|
||||
## Support PairDrop
|
||||
<a href="https://www.buymeacoffee.com/pairdrop" target="_blank">
|
||||
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
|
||||
</a>
|
||||
|
||||
PairDrop is free and always will be.
|
||||
Still, we have to pay for the domain and the server.
|
||||
|
||||
To contribute and support, please use BuyMeACoffee via the button above.
|
||||
|
||||
Thanks a lot for supporting free and open software!
|
||||
|
||||
To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop).
|
||||
## Translate PairDrop
|
||||
<a href="https://hosted.weblate.org/engage/pairdrop/">
|
||||
<img src="https://hosted.weblate.org/widget/pairdrop/pairdrop-spa/open-graph.png" alt="Translation status" style="max-height: 30vh" />
|
||||
</a>
|
||||
|
||||
## How to contribute
|
||||
|
||||
|
||||
@@ -2,42 +2,46 @@
|
||||
|
||||
## Healthcheck
|
||||
|
||||
The [Docker Image](../Dockerfile) includes a Healthcheck with the following options:
|
||||
The [Docker Image](../Dockerfile) includes a health check 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.
|
||||
|
||||
> Specifies the time interval to run the health check. \
|
||||
> In this case, the health check is 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.
|
||||
|
||||
> Specifies the amount of time to wait for a response from the \"HEALTHCHECK\" command. \
|
||||
> If the response does not arrive within 10 seconds, the health check fails.
|
||||
<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.
|
||||
|
||||
> 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.
|
||||
|
||||
> 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.
|
||||
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 fails.
|
||||
|
||||
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.
|
||||
Overall, this \"HEALTHCHECK\" instruction is defining a health check process \
|
||||
that runs every 30 seconds, and waits up to 10 seconds for a response, \
|
||||
begins 5 seconds after the container is started, and retries up to 3 times. \
|
||||
The health check attempts to connect to http://localhost:3000/ \
|
||||
and will considers the container unhealthy if unable to connect.
|
||||
|
||||
|
||||
124
docs/faq.md
124
docs/faq.md
@@ -5,20 +5,18 @@
|
||||
Help! I can't install the PWA!
|
||||
</summary>
|
||||
|
||||
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, Vivaldi, Brave, etc.), you can easily install PairDrop PWA on your desktop
|
||||
by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net).
|
||||
|
||||
<img 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/)
|
||||
|
||||
<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.
|
||||
|
||||
See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info.
|
||||
<br>
|
||||
|
||||
</details>
|
||||
@@ -28,12 +26,11 @@ See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob
|
||||
Shortcuts?
|
||||
</summary>
|
||||
|
||||
Shortcuts!
|
||||
Shortcuts
|
||||
- Send a message with `CTRL + ENTER`
|
||||
- Close all send and pair dialogs by pressing `Escape`.
|
||||
- Copy a received message to clipboard with `CTRL/⌘ + C`.
|
||||
- Accept file transfer request with `Enter` and decline with `Escape`.
|
||||
|
||||
- Close all "Send" and "Pair" dialogs by pressing `Esc`.
|
||||
- Copy a received message to the clipboard with `CTRL/⌘ + C`.
|
||||
- Accept file-transfer requests with `Enter` and decline with `Esc`.
|
||||
<br>
|
||||
|
||||
</details>
|
||||
@@ -44,28 +41,24 @@ Shortcuts!
|
||||
</summary>
|
||||
|
||||
Apparently, iOS does not allow images shared from a website to be saved to the gallery directly.
|
||||
It simply does not offer the option for images shared from a website.
|
||||
It simply does not offer that option for images shared from a website.
|
||||
|
||||
iOS Shortcuts to the win:
|
||||
iOS Shortcuts saves the day:
|
||||
I created a simple iOS shortcut that takes your photos and saves them to your gallery:
|
||||
https://routinehub.co/shortcut/13988/
|
||||
|
||||
|
||||
<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?
|
||||
Is it possible to send files or text directly from the "Context" or "Share" menu?
|
||||
</summary>
|
||||
|
||||
Yes, it finally is!
|
||||
* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
|
||||
* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
|
||||
* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
|
||||
|
||||
|
||||
Yes, it finally is.
|
||||
* [Send files directly from the "Context" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows)
|
||||
* [Send directly from the "Share" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios)
|
||||
* [Send directly from the "Share" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
|
||||
<br>
|
||||
|
||||
</details>
|
||||
@@ -75,71 +68,81 @@ Yes, it finally is!
|
||||
Is it possible to send files or text directly via CLI?
|
||||
</summary>
|
||||
|
||||
Yes, it is!
|
||||
|
||||
* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||
|
||||
Yes.
|
||||
|
||||
* [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary style="font-size:1.25em;margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25;">
|
||||
Are there any Third-Party Apps?
|
||||
Are there any third-party Apps?
|
||||
</summary>
|
||||
|
||||
Here's a list of some third-party apps compatible with PairDrop:
|
||||
These third-party apps are 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?
|
||||
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.
|
||||
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 the 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).
|
||||
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?
|
||||
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.
|
||||
Files are sent directly between peers.
|
||||
PairDrop doesn't even use a database.
|
||||
If curious, study [the server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
|
||||
WebRTC encrypts the files in transit.
|
||||
|
||||
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 the 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?
|
||||
What about security? Are my files encrypted while sent between the computers?
|
||||
</summary>
|
||||
|
||||
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
|
||||
|
||||
|
||||
Yes. Your files are sent using WebRTC, encrypting them in transit.
|
||||
To ensure the connection is secure and there is no [MITM](https://wikiless.org/wiki/Man-in-the-middle_attack),
|
||||
compare the security number shown under the device name on both devices.
|
||||
The security number is different for every connection.
|
||||
<br>
|
||||
|
||||
</details>
|
||||
@@ -149,18 +152,18 @@ Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure
|
||||
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.
|
||||
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.
|
||||
You can open a hotspot on one of your devices to bridge the connection,
|
||||
which omits the need of the TURN server.
|
||||
|
||||
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
|
||||
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
|
||||
- [How to open a hotspot on macOS](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
|
||||
- [Library to open a hotspot on Linux](https://github.com/lakinduakash/linux-wifi-hotspot)
|
||||
|
||||
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 not use your data plan.
|
||||
<br>
|
||||
|
||||
</details>
|
||||
@@ -170,10 +173,16 @@ Then, all data should be sent directly between devices and your data plan should
|
||||
Why don't you implement feature xyz?
|
||||
</summary>
|
||||
|
||||
Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer.
|
||||
We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity.
|
||||
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.
|
||||
Not facilitating optimal edge-cases means better flow for average users.
|
||||
Don't be sad. We may 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* or *Thinking, Fast and Slow*.
|
||||
Read *Insanely Simple: The Obsession that Drives Apple's Success*,
|
||||
and/or *Thinking, Fast and Slow* to learn more.
|
||||
|
||||
|
||||
<br>
|
||||
@@ -182,17 +191,15 @@ If you want to learn more about simplicity you can read *Insanely Simple: The Ob
|
||||
|
||||
<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?
|
||||
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
|
||||
* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software.
|
||||
* [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)
|
||||
* Share PairDrop on social media.
|
||||
* Fix bugs and make a pull request.
|
||||
* Do security analysis and suggestions
|
||||
* Do some security analysis and make suggestions.
|
||||
* To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop)
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
</details>
|
||||
@@ -202,8 +209,7 @@ If you want to learn more about simplicity you can read *Insanely Simple: The Ob
|
||||
How does it work?
|
||||
</summary>
|
||||
|
||||
[See here for Information about the Technical Implementation](/docs/technical-documentation.md)
|
||||
|
||||
[See here for info about the technical implementation](/docs/technical-documentation.md)
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
@@ -3,9 +3,14 @@ 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.
|
||||
> Beware that you have to host your own TURN server 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).
|
||||
> 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).
|
||||
|
||||
> <b>PairDrop via HTTPS</b>
|
||||
>
|
||||
> On some browsers PairDrop must be served over TLS in order for some feautures to work properly. These may include copying an incoming message via the 'copy' button, installing PairDrop as PWA, persistent pairing of devices and changing of the display name, and notifications. Naturally, this is also recommended to increase security.
|
||||
|
||||
## Deployment with Docker
|
||||
|
||||
@@ -15,9 +20,11 @@ The easiest way to get PairDrop up and running is by using Docker.
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop
|
||||
```
|
||||
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
> You must use a server proxy to set the X-Forwarded-For \
|
||||
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, \
|
||||
> `127.0.0.1` is specified in the run command.
|
||||
|
||||
#### Options / Flags
|
||||
Set options by using the following flags in the `docker run` command:
|
||||
@@ -39,21 +46,30 @@ Set options by using the following flags in the `docker run` command:
|
||||
```bash
|
||||
-e IPV6_LOCALIZE=4
|
||||
```
|
||||
> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments of the client IPv6 address to be evaluated as the peer's IP. This can be especially useful when using Cloudflare as a proxy.
|
||||
> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments \
|
||||
> of the client IPv6 address to be evaluated as the peer's IP. \
|
||||
> This can be especially useful when using Cloudflare as a proxy.
|
||||
>
|
||||
> The flag must be set to an **integer** between `1` and `7`. The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) to match the client IP against. The most common value would be `4`, which will group peers within the same `/64` subnet.
|
||||
> The flag must be set to an **integer** between `1` and `7`. \
|
||||
> The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) \
|
||||
> to match the client IP against. The most common value would be `4`, \
|
||||
> which will group peers within the same `/64` subnet.
|
||||
|
||||
##### Websocket Fallback (for VPN)
|
||||
```bash
|
||||
-e WS_FALLBACK=true
|
||||
```
|
||||
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||
> Provides PairDrop to clients with an included websocket fallback \
|
||||
> if the peer to peer WebRTC connection is not available to the client.
|
||||
>
|
||||
> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance.
|
||||
> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
|
||||
> This is not used on the official https://pairdrop.net website, \
|
||||
> but you can activate it on your self-hosted instance.
|
||||
> This is especially useful if you connect to your instance via a VPN (as most VPN services block WebRTC completely in order to hide your real IP address). ([Read more here](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
|
||||
>
|
||||
> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer!
|
||||
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
|
||||
> **Warning:** All traffic sent between devices using this fallback \
|
||||
> is routed through the server and therefor not peer to peer! \
|
||||
> Beware that the traffic routed via this fallback is readable by the server. \
|
||||
> Only ever use this on instances you can trust. \
|
||||
> Additionally, beware that all traffic using this fallback debits the servers data plan.
|
||||
|
||||
##### Specify STUN/TURN Servers
|
||||
@@ -61,10 +77,12 @@ Set options by using the following flags in the `docker run` command:
|
||||
-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.
|
||||
> 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/
|
||||
> Alternatively, use a free, pre-configured TURN server like [OpenRelay]([url](https://www.metered.ca/tools/openrelay/))
|
||||
>
|
||||
> Default configuration:
|
||||
> ```json
|
||||
@@ -83,8 +101,10 @@ Set options by using the following flags in the `docker run` command:
|
||||
-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.
|
||||
> 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:
|
||||
> ```
|
||||
@@ -97,7 +117,7 @@ Set options by using the following flags in the `docker run` command:
|
||||
> 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.
|
||||
> If the IP PairDrop uses is the public IP of your device, everything is set up correctly. \
|
||||
>To find out your devices public IP visit https://www.whatismyip.com/.
|
||||
>
|
||||
> To preserve your clients' privacy, **never use this flag in production!**
|
||||
@@ -109,13 +129,17 @@ Set options by using the following flags in the `docker run` command:
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
|
||||
```
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent \
|
||||
> all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
> To prevent bypassing the proxy by reaching the Docker container directly, \
|
||||
> `127.0.0.1` is specified in the run command.
|
||||
>
|
||||
> 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)
|
||||
> The Docker Image includes a healthcheck. \
|
||||
> Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
|
||||
|
||||
### Docker Image self-built
|
||||
#### Build the image
|
||||
@@ -130,13 +154,17 @@ docker build --pull . -f Dockerfile -t pairdrop
|
||||
```bash
|
||||
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod
|
||||
```
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
> You must use a server proxy to set the X-Forwarded-For \
|
||||
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
> To prevent bypassing the proxy by reaching the Docker container \
|
||||
> directly, `127.0.0.1` is specified in the run command.
|
||||
>
|
||||
> 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)
|
||||
> The Docker Image includes a Healthcheck. \
|
||||
Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
|
||||
|
||||
<br>
|
||||
|
||||
@@ -162,9 +190,11 @@ services:
|
||||
|
||||
Run the compose file with `docker compose up -d`.
|
||||
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
> You must use a server proxy to set the X-Forwarded-For \
|
||||
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command.
|
||||
> To prevent bypassing the proxy by reaching the Docker container \
|
||||
> directly, `127.0.0.1` is specified in the run command.
|
||||
|
||||
<br>
|
||||
|
||||
@@ -190,7 +220,7 @@ or
|
||||
npm start
|
||||
```
|
||||
|
||||
> Remember to check your IP Address using your OS command to see where you can access the server.
|
||||
> Remember to check your IP address using your OS command to see where you can access the server.
|
||||
|
||||
> By default, the node server listens on port 3000.
|
||||
|
||||
@@ -212,7 +242,8 @@ $env:PORT=3010; npm start
|
||||
```bash
|
||||
IPV6_LOCALIZE=4
|
||||
```
|
||||
> Truncate a portion of the client IPv6 address to make peers more discoverable. See [Options/Flags](#options--flags) above.
|
||||
> Truncate a portion of the client IPv6 address to make peers more discoverable. \
|
||||
> See [Options/Flags](#options--flags) above.
|
||||
|
||||
#### Specify STUN/TURN Server
|
||||
On Unix based systems
|
||||
@@ -223,10 +254,12 @@ 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.
|
||||
> 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/
|
||||
> To host your own TURN server you can follow this guide: \
|
||||
> https://gabrieltanner.org/blog/turn-server/
|
||||
>
|
||||
> Default configuration:
|
||||
> ```json
|
||||
@@ -250,10 +283,13 @@ On Windows
|
||||
$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.
|
||||
> Use this flag to enable debugging info 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:
|
||||
> 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
|
||||
@@ -264,10 +300,10 @@ $env:DEBUG_MODE="true"; npm start
|
||||
> 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/.
|
||||
> If the IP PairDrop uses is the public IP of your device everything is set up correctly. \
|
||||
>Find your devices public IP by visiting https://www.whatismyip.com/.
|
||||
>
|
||||
> To preserve your clients' privacy, **never use this flag in production!**
|
||||
> Preserve your clients' privacy. **Never use this flag in production!**
|
||||
|
||||
|
||||
### Options / Flags
|
||||
@@ -277,9 +313,11 @@ npm start -- --localhost-only
|
||||
```
|
||||
> Only allow connections from localhost.
|
||||
>
|
||||
> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
> You must use a server proxy to set the X-Forwarded-For \
|
||||
> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)).
|
||||
>
|
||||
> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly.
|
||||
> Use this when deploying PairDrop with node to prevent \
|
||||
> bypassing the proxy by reaching the Docker container directly.
|
||||
|
||||
#### Automatic restart on error
|
||||
```bash
|
||||
@@ -301,13 +339,19 @@ npm start -- --rate-limit
|
||||
```bash
|
||||
npm start -- --include-ws-fallback
|
||||
```
|
||||
> 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.
|
||||
>
|
||||
> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance.
|
||||
> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
|
||||
> This is not used on the official https://pairdrop.net, \
|
||||
but you can activate it on your self-hosted instance. \
|
||||
> This is especially useful if you connect to your instance \
|
||||
> via a VPN as most VPN services block WebRTC completely in order to hide your real IP address.
|
||||
> ([Read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)).
|
||||
>
|
||||
> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer!
|
||||
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
|
||||
> **Warning:** All traffic sent between devices using this fallback \
|
||||
> is routed through the server and therefor not peer to peer! \
|
||||
> 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.
|
||||
|
||||
<br>
|
||||
@@ -321,10 +365,12 @@ npm run start:prod
|
||||
```bash
|
||||
npm run start:prod -- --localhost-only --include-ws-fallback
|
||||
```
|
||||
> To prevent connections to the node server from bypassing the proxy server you should always use "--localhost-only" on production.
|
||||
> To prevent connections to the node server from bypassing \
|
||||
> the proxy server you should always use "--localhost-only" on production.
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -405,10 +451,10 @@ a2enmod proxy_wstunnel
|
||||
|
||||
<br>
|
||||
|
||||
Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
|
||||
|
||||
**pairdrop.conf**
|
||||
#### Allow http and https requests
|
||||
#### Allow HTTP and HTTPS requests
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
ProxyPass / http://127.0.0.1:3000/
|
||||
@@ -425,7 +471,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
|
||||
</VirtualHost>
|
||||
```
|
||||
#### Automatic http to https redirect:
|
||||
#### Automatic HTTP to HTTPS redirect:
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
Redirect permanent / https://127.0.0.1:3000/
|
||||
@@ -438,7 +484,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
|
||||
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
|
||||
</VirtualHost>
|
||||
```
|
||||
Activate the new virtual host and reload apache:
|
||||
Activate the new virtual host and reload Apache:
|
||||
```bash
|
||||
a2ensite pairdrop
|
||||
```
|
||||
@@ -462,28 +508,38 @@ Then, clone the repository and run docker-compose:
|
||||
|
||||
docker-compose up -d
|
||||
```
|
||||
Now point your browser to `http://localhost:8080`.
|
||||
Now point your web browser to `http://localhost:8080`.
|
||||
|
||||
- To restart the containers run `docker-compose restart`.
|
||||
- To stop the containers run `docker-compose stop`.
|
||||
- To debug the NodeJS server run `docker logs pairdrop_node_1`.
|
||||
- To restart the containers, run `docker-compose restart`.
|
||||
- To stop the containers, run `docker-compose stop`.
|
||||
- To debug the NodeJS server, run `docker logs pairdrop_node_1`.
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
## Testing PWA related features
|
||||
PWAs require that the app is served under a correctly set up and trusted TLS endpoint.
|
||||
PWAs requires the app to be served under a correctly set up and trusted TLS endpoint.
|
||||
|
||||
The nginx container creates a CA certificate and a website certificate for you. To correctly set the common name of the certificate, you need to change the FQDN environment variable in `docker/fqdn.env` to the fully qualified domain name of your workstation.
|
||||
The NGINX container creates a CA certificate and a website certificate for you. \
|
||||
To correctly set the common name of the certificate, \
|
||||
you need to change the FQDN environment variable in `docker/fqdn.env` \
|
||||
to the fully qualified domain name of your workstation.
|
||||
|
||||
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
|
||||
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
|
||||
- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
|
||||
- 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).
|
||||
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \
|
||||
For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. \
|
||||
Install that certificate to the trust store of your operating system. \
|
||||
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. \
|
||||
- On macOS, double-click the installed CA certificate in `Keychain Access`, \
|
||||
- expand `Trust`, and select `Always Trust` for SSL. \
|
||||
- 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).
|
||||
|
||||
Please note that the certificates (CA and webserver cert) expire after a day.
|
||||
Also, whenever you restart the nginx docker, container new certificates are created.
|
||||
Also, whenever you restart the NGINX Docker, container new certificates are created.
|
||||
|
||||
The site is served on `https://<Your FQDN>:8443`.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progres
|
||||
|
||||
This is still experimental and must be enabled via a flag **before** the PWA is installed to Windows.
|
||||
1. [Enabled feature in Edge](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files#enable-the-file-handling-api)
|
||||
2. Install PairDrop by visiting https://pairdrop.net/ with the Edge browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-).
|
||||
2. Install PairDrop by visiting https://pairdrop.net/ with the Edge web browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-).
|
||||
3. You are done! You can now send most files one at a time via PairDrop:
|
||||
|
||||
_context menu > Open with > PairDrop_
|
||||
@@ -13,7 +13,8 @@ This is still experimental and must be enabled via a flag **before** the PWA is
|
||||
[//]: # (Todo: add screenshots)
|
||||
|
||||
### Sending multiple files to PairDrop
|
||||
Outstandingly, it is also possible to send multiple files to PairDrop via the context menu by adding PairDrop to the `Send to` menu:
|
||||
Outstandingly, it is also possible to send multiple files to PairDrop \
|
||||
via the context menu by adding PairDrop to the `Send to` menu:
|
||||
1. [Register PairDrop as file handler](#registering-to-open-files-with-pairdrop)
|
||||
2. Hit Windows Key+R, type: `shell:programs` and hit Enter.
|
||||
3. Copy the PairDrop shortcut from the directory
|
||||
@@ -26,7 +27,8 @@ Outstandingly, it is also possible to send multiple files to PairDrop via the co
|
||||
[//]: # (Todo: add screenshots)
|
||||
|
||||
## Send directly from share menu on iOS
|
||||
I created an iOS shortcut to send images, files, folder, URLs or text directly from the share-menu
|
||||
I created an iOS shortcut to send images, files, folder, URLs \
|
||||
or text directly from the share-menu
|
||||
https://routinehub.co/shortcut/13990/
|
||||
|
||||
[//]: # (Todo: add doku with screenshots)
|
||||
@@ -63,7 +65,7 @@ On Windows Command Prompt you need to use bash: `bash pairdrop -h`
|
||||
Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop).
|
||||
|
||||
#### Linux
|
||||
1. Put file in a preferred folder e.g. `/usr/local/bin`
|
||||
1. Put the file in a preferred folder e.g. `/usr/local/bin`
|
||||
2. Make sure the bash file is executable. Otherwise, use `chmod +x pairdrop`
|
||||
3. Add absolute path of the folder to PATH variable to make `pairdrop` available globally by executing
|
||||
`export PATH=$PATH:/opt/pairdrop-cli`
|
||||
@@ -74,7 +76,7 @@ Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop).
|
||||
#### Windows
|
||||
1. Put file in a preferred folder e.g. `C:\Users\Public\pairdrop-cli`
|
||||
2. Search for and open `Edit environment variables for your account`
|
||||
3. Click `Environment Variables...`
|
||||
3. Click `Environment Variables…`
|
||||
4. Under *System Variables* select `Path` and click *Edit...*
|
||||
5. Click *New*, insert the preferred folder (`C:\Users\Public\pairdrop-cli`), click *OK* until all windows are closed
|
||||
6. Reopen Command prompt window
|
||||
|
||||
@@ -3,48 +3,80 @@
|
||||
|
||||
Encryption is mandatory for WebRTC connections and completely done by the browser itself.
|
||||
|
||||
When the peers are first connecting, a channel is created by exchanging their signaling information.
|
||||
This signaling information includes some sort of public key and is specific to the clients ip address.
|
||||
That is what the STUN Server is used for: it simply returns your public IP address as you only know your local ip address
|
||||
When the peers are first connecting, \
|
||||
a channel is created by exchanging their signaling info. \
|
||||
This signaling information includes some sort of public key \
|
||||
and is specific to the clients IP address. \
|
||||
That is what the STUN Server is used for: \
|
||||
it simply returns your public IP address \
|
||||
as you only know your local ip address \
|
||||
if behind a NAT (router).
|
||||
|
||||
The transfer of the signaling information is done by the PairDrop / Snapdrop server using secure websockets.
|
||||
After that the channel itself is completely peer-2-peer and all information can only be decrypted by the receiver.
|
||||
When the two peers are on the same network or when they are not behind any NAT system (which they are always for classic
|
||||
Snapdrop and for not paired users on PairDrop) the files are send directly peer to peer.
|
||||
The transfer of the signaling info is done by the \
|
||||
PairDrop / Snapdrop server using secure websockets. \
|
||||
After that the channel itself is completely peer-to-peer \
|
||||
and all info can only be decrypted by the receiver. \
|
||||
When the two peers are on the same network \
|
||||
or when they are not behind any NAT system \
|
||||
(which they are always for classic \
|
||||
Snapdrop and for not paired users on PairDrop) \
|
||||
the files are send directly peer-to-peer.
|
||||
|
||||
When a user is behind a NAT (behind a router) the contents are channeled through a TURN server.
|
||||
But again, the contents send via the channel can only be decrypted by the receiver. So a rogue TURN server could only
|
||||
see that there is a connection, but not what is sent. Obviously, connections which are channeled through a TURN server
|
||||
are not as fast as peer to peer.
|
||||
When a user is behind a NAT (behind a router) \
|
||||
the contents are channeled through a TURN server. \
|
||||
But again, the contents send via the channel \
|
||||
can only be decrypted by the receiver. \
|
||||
So a rogue TURN server could only \
|
||||
see that there is a connection, but not what is sent. \
|
||||
Obviously, connections which are channeled through a TURN server \
|
||||
are not as fast as peer-to-peer.
|
||||
|
||||
The selection whether a TURN server is needed or not is also done automatically by the browser.
|
||||
It simply iterated through the configured RTC iceServers and checks what works. Only if the STUN server is not sufficient,
|
||||
The selection whether a TURN server is needed \
|
||||
or not is also done automatically by the web browser. \
|
||||
It simply iterated through the configured \
|
||||
RTC iceServers and checks what works. \
|
||||
Only if the STUN server is not sufficient, \
|
||||
the TURN server is used.
|
||||
|
||||

|
||||
_Diagram created by wowza.com_
|
||||
|
||||
Good thing: if your device has an IPv6 address it is uniquely reachable by that address. As I understand it, when both devices are using IPv6 addresses there is no need for a TURN server in any scenario.
|
||||
Good thing: if your device has an IPv6 address \
|
||||
it is uniquely reachable by that address. \
|
||||
As I understand it, when both devices are using \
|
||||
IPv6 addresses there is no need for a TURN server in any scenario.
|
||||
|
||||
To learn more take a look at https://www.wowza.com/blog/webrtc-encryption-and-security which gives a good insight into stun, turn and webrtc
|
||||
Learn more by reading https://www.wowza.com/blog/webrtc-encryption-and-security \
|
||||
which gives a good insight into STUN, TURN and WebRTC.
|
||||
|
||||
|
||||
## Device Pairing
|
||||
|
||||
The pairing functionality uses the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
|
||||
|
||||
It works by creating long secrets that are served by the server to the initiating and requesting pair peer,
|
||||
when the inserted key is correct. These long secrets are then saved to an indexedDB database in the browser.
|
||||
IndexedDB is somewhat the successor of localStorage as saved data is shared between all tabs.
|
||||
It goes one step further by making the data persistent and available offline if implemented to a PWA.
|
||||
It works by creating long secrets that are served \
|
||||
by the server to the initiating and requesting pair peer, \
|
||||
when the inserted key is correct. \
|
||||
These long secrets are then saved to an \
|
||||
indexedDB database in the web browser. \
|
||||
IndexedDB is somewhat the successor of localStorage \
|
||||
as saved data is shared between all tabs. \
|
||||
It goes one step further by making the data persistent \
|
||||
and available offline if implemented to a PWA.
|
||||
|
||||
All secrets a client has saved to its database are send to the PairDrop server. Peers with a common secret are discoverable
|
||||
to each other analog to peers with the same ip-address are discoverable to each other.
|
||||
All secrets a client has saved to its database \
|
||||
are sent to the PairDrop server. \
|
||||
Peers with a common secret are discoverable \
|
||||
to each other analog to peers with the same \
|
||||
IP address are discoverable by each other.
|
||||
|
||||
What I really like about this approach, and the reason why I implemented it, is that devices on the same network are always
|
||||
visible regardless whether any devices are paired or not. The main user flow is never obstructed. Paired devices are simply
|
||||
shown additionally. This makes it in my idea better than the idea of using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214).
|
||||
What I really like about this approach (and the reason I implemented it) \
|
||||
is that devices on the same network are always \
|
||||
visible regardless whether any devices are paired or not. \
|
||||
The main user flow is never obstructed. \
|
||||
Paired devices are simply shown additionally. \
|
||||
This makes it in my idea better than the idea of \
|
||||
using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214).
|
||||
|
||||
|
||||
[< Back](/README.md)
|
||||
|
||||
332
index.js
332
index.js
@@ -124,14 +124,24 @@ if (process.argv.includes('--localhost-only')) {
|
||||
server.listen(port);
|
||||
}
|
||||
|
||||
server.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(err);
|
||||
console.info("Error EADDRINUSE received, exiting process without restarting process...");
|
||||
process.exit(0)
|
||||
}
|
||||
});
|
||||
|
||||
class PairDropServer {
|
||||
|
||||
constructor() {
|
||||
this._wss = new WebSocket.Server({ server });
|
||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||
|
||||
this._rooms = {};
|
||||
this._roomSecrets = {};
|
||||
this._rooms = {}; // { roomId: peers[] }
|
||||
this._roomSecrets = {}; // { pairKey: roomSecret }
|
||||
|
||||
this._keepAliveTimers = {};
|
||||
|
||||
console.log('PairDrop is running on port', port);
|
||||
}
|
||||
@@ -139,7 +149,9 @@ class PairDropServer {
|
||||
_onConnection(peer) {
|
||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||
peer.socket.onerror = e => console.error(e);
|
||||
|
||||
this._keepAlive(peer);
|
||||
|
||||
this._send(peer, {
|
||||
type: 'rtc-config',
|
||||
config: rtcConfig
|
||||
@@ -170,10 +182,10 @@ class PairDropServer {
|
||||
this._onDisconnect(sender);
|
||||
break;
|
||||
case 'pong':
|
||||
sender.lastBeat = Date.now();
|
||||
this._setKeepAliveTimerToNow(sender);
|
||||
break;
|
||||
case 'join-ip-room':
|
||||
this._joinRoom(sender);
|
||||
this._joinIpRoom(sender);
|
||||
break;
|
||||
case 'room-secrets':
|
||||
this._onRoomSecrets(sender, message);
|
||||
@@ -192,9 +204,15 @@ class PairDropServer {
|
||||
break;
|
||||
case 'regenerate-room-secret':
|
||||
this._onRegenerateRoomSecret(sender, message);
|
||||
break
|
||||
case 'resend-peers':
|
||||
this._notifyPeers(sender);
|
||||
break;
|
||||
case 'create-public-room':
|
||||
this._onCreatePublicRoom(sender);
|
||||
break;
|
||||
case 'join-public-room':
|
||||
this._onJoinPublicRoom(sender, message);
|
||||
break;
|
||||
case 'leave-public-room':
|
||||
this._onLeavePublicRoom(sender);
|
||||
break;
|
||||
case 'signal':
|
||||
default:
|
||||
@@ -203,7 +221,9 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_signalAndRelay(sender, message) {
|
||||
const room = message.roomType === 'ip' ? sender.ip : message.roomSecret;
|
||||
const room = message.roomType === 'ip'
|
||||
? sender.ip
|
||||
: message.roomId;
|
||||
|
||||
// relay message to recipient
|
||||
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
|
||||
@@ -223,10 +243,16 @@ class PairDropServer {
|
||||
}
|
||||
|
||||
_disconnect(sender) {
|
||||
this._leaveRoom(sender, 'ip', '', true);
|
||||
this._removePairKey(sender.pairKey);
|
||||
sender.pairKey = null;
|
||||
|
||||
this._cancelKeepAlive(sender);
|
||||
delete this._keepAliveTimers[sender.id];
|
||||
|
||||
this._leaveIpRoom(sender, true);
|
||||
this._leaveAllSecretRooms(sender, true);
|
||||
this._removeRoomKey(sender.roomKey);
|
||||
sender.roomKey = null;
|
||||
this._leavePublicRoom(sender, true);
|
||||
|
||||
sender.socket.terminate();
|
||||
}
|
||||
|
||||
@@ -255,7 +281,7 @@ class PairDropServer {
|
||||
for (const peerId in room) {
|
||||
const peer = room[peerId];
|
||||
|
||||
this._leaveRoom(peer, 'secret', roomSecret);
|
||||
this._leaveSecretRoom(peer, roomSecret, true);
|
||||
|
||||
this._send(peer, {
|
||||
type: 'secret-room-deleted',
|
||||
@@ -266,34 +292,35 @@ class PairDropServer {
|
||||
|
||||
_onPairDeviceInitiate(sender) {
|
||||
let roomSecret = randomizer.getRandomString(256);
|
||||
let roomKey = this._createRoomKey(sender, roomSecret);
|
||||
if (sender.roomKey) this._removeRoomKey(sender.roomKey);
|
||||
sender.roomKey = roomKey;
|
||||
let pairKey = this._createPairKey(sender, roomSecret);
|
||||
|
||||
if (sender.pairKey) {
|
||||
this._removePairKey(sender.pairKey);
|
||||
}
|
||||
sender.pairKey = pairKey;
|
||||
|
||||
this._send(sender, {
|
||||
type: 'pair-device-initiated',
|
||||
roomSecret: roomSecret,
|
||||
roomKey: roomKey
|
||||
pairKey: pairKey
|
||||
});
|
||||
this._joinRoom(sender, 'secret', roomSecret);
|
||||
this._joinSecretRoom(sender, roomSecret);
|
||||
}
|
||||
|
||||
_onPairDeviceJoin(sender, message) {
|
||||
// rate limit implementation: max 10 attempts every 10s
|
||||
if (sender.roomKeyRate >= 10) {
|
||||
this._send(sender, { type: 'pair-device-join-key-rate-limit' });
|
||||
if (sender.rateLimitReached()) {
|
||||
this._send(sender, { type: 'join-key-rate-limit' });
|
||||
return;
|
||||
}
|
||||
sender.roomKeyRate += 1;
|
||||
setTimeout(_ => sender.roomKeyRate -= 1, 10000);
|
||||
|
||||
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
|
||||
if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].creator.id) {
|
||||
this._send(sender, { type: 'pair-device-join-key-invalid' });
|
||||
return;
|
||||
}
|
||||
|
||||
const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
|
||||
const creator = this._roomSecrets[message.roomKey].creator;
|
||||
this._removeRoomKey(message.roomKey);
|
||||
const roomSecret = this._roomSecrets[message.pairKey].roomSecret;
|
||||
const creator = this._roomSecrets[message.pairKey].creator;
|
||||
this._removePairKey(message.pairKey);
|
||||
this._send(sender, {
|
||||
type: 'pair-device-joined',
|
||||
roomSecret: roomSecret,
|
||||
@@ -304,22 +331,53 @@ class PairDropServer {
|
||||
roomSecret: roomSecret,
|
||||
peerId: sender.id
|
||||
});
|
||||
this._joinRoom(sender, 'secret', roomSecret);
|
||||
this._removeRoomKey(sender.roomKey);
|
||||
this._joinSecretRoom(sender, roomSecret);
|
||||
this._removePairKey(sender.pairKey);
|
||||
}
|
||||
|
||||
_onPairDeviceCancel(sender) {
|
||||
const roomKey = sender.roomKey
|
||||
const pairKey = sender.pairKey
|
||||
|
||||
if (!roomKey) return;
|
||||
if (!pairKey) return;
|
||||
|
||||
this._removeRoomKey(roomKey);
|
||||
this._removePairKey(pairKey);
|
||||
this._send(sender, {
|
||||
type: 'pair-device-canceled',
|
||||
roomKey: roomKey,
|
||||
pairKey: pairKey,
|
||||
});
|
||||
}
|
||||
|
||||
_onCreatePublicRoom(sender) {
|
||||
let publicRoomId = randomizer.getRandomString(5, true).toLowerCase();
|
||||
|
||||
this._send(sender, {
|
||||
type: 'public-room-created',
|
||||
roomId: publicRoomId
|
||||
});
|
||||
|
||||
this._joinPublicRoom(sender, publicRoomId);
|
||||
}
|
||||
|
||||
_onJoinPublicRoom(sender, message) {
|
||||
if (sender.rateLimitReached()) {
|
||||
this._send(sender, { type: 'join-key-rate-limit' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) {
|
||||
this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId });
|
||||
return;
|
||||
}
|
||||
|
||||
this._leavePublicRoom(sender);
|
||||
this._joinPublicRoom(sender, message.publicRoomId);
|
||||
}
|
||||
|
||||
_onLeavePublicRoom(sender) {
|
||||
this._leavePublicRoom(sender, true);
|
||||
this._send(sender, { type: 'public-room-left' });
|
||||
}
|
||||
|
||||
_onRegenerateRoomSecret(sender, message) {
|
||||
const oldRoomSecret = message.roomSecret;
|
||||
const newRoomSecret = randomizer.getRandomString(256);
|
||||
@@ -337,122 +395,158 @@ class PairDropServer {
|
||||
delete this._rooms[oldRoomSecret];
|
||||
}
|
||||
|
||||
_createRoomKey(creator, roomSecret) {
|
||||
let roomKey;
|
||||
_createPairKey(creator, roomSecret) {
|
||||
let pairKey;
|
||||
do {
|
||||
// get randomInt until keyRoom not occupied
|
||||
roomKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
|
||||
} while (roomKey in this._roomSecrets)
|
||||
pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
|
||||
} while (pairKey in this._roomSecrets)
|
||||
|
||||
this._roomSecrets[roomKey] = {
|
||||
this._roomSecrets[pairKey] = {
|
||||
roomSecret: roomSecret,
|
||||
creator: creator
|
||||
}
|
||||
|
||||
return roomKey;
|
||||
return pairKey;
|
||||
}
|
||||
|
||||
_removeRoomKey(roomKey) {
|
||||
_removePairKey(roomKey) {
|
||||
if (roomKey in this._roomSecrets) {
|
||||
this._roomSecrets[roomKey].creator.roomKey = null
|
||||
delete this._roomSecrets[roomKey];
|
||||
}
|
||||
}
|
||||
|
||||
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
_joinIpRoom(peer) {
|
||||
this._joinRoom(peer, 'ip', peer.ip);
|
||||
}
|
||||
|
||||
if (this._rooms[room] && this._rooms[room][peer.id]) {
|
||||
_joinSecretRoom(peer, roomSecret) {
|
||||
this._joinRoom(peer, 'secret', roomSecret);
|
||||
|
||||
// add secret to peer
|
||||
peer.addRoomSecret(roomSecret);
|
||||
}
|
||||
|
||||
_joinPublicRoom(peer, publicRoomId) {
|
||||
// prevent joining of 2 public rooms simultaneously
|
||||
this._leavePublicRoom(peer);
|
||||
|
||||
this._joinRoom(peer, 'public-id', publicRoomId);
|
||||
|
||||
peer.publicRoomId = publicRoomId;
|
||||
}
|
||||
|
||||
_joinRoom(peer, roomType, roomId) {
|
||||
// roomType: 'ip', 'secret' or 'public-id'
|
||||
if (this._rooms[roomId] && this._rooms[roomId][peer.id]) {
|
||||
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
|
||||
this._leaveRoom(peer, roomType, roomSecret);
|
||||
this._leaveRoom(peer, roomType, roomId);
|
||||
}
|
||||
|
||||
// if room doesn't exist, create it
|
||||
if (!this._rooms[room]) {
|
||||
this._rooms[room] = {};
|
||||
if (!this._rooms[roomId]) {
|
||||
this._rooms[roomId] = {};
|
||||
}
|
||||
|
||||
this._notifyPeers(peer, roomType, roomSecret);
|
||||
this._notifyPeers(peer, roomType, roomId);
|
||||
|
||||
// add peer to room
|
||||
this._rooms[room][peer.id] = peer;
|
||||
// add secret to peer
|
||||
if (roomType === 'secret') {
|
||||
peer.addRoomSecret(roomSecret);
|
||||
}
|
||||
this._rooms[roomId][peer.id] = peer;
|
||||
}
|
||||
|
||||
_leaveRoom(peer, roomType = 'ip', roomSecret = '', disconnect = false) {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
|
||||
if (!this._rooms[room] || !this._rooms[room][peer.id]) return;
|
||||
this._cancelKeepAlive(this._rooms[room][peer.id]);
|
||||
_leaveIpRoom(peer, disconnect = false) {
|
||||
this._leaveRoom(peer, 'ip', peer.ip, disconnect);
|
||||
}
|
||||
|
||||
// delete the peer
|
||||
delete this._rooms[room][peer.id];
|
||||
_leaveSecretRoom(peer, roomSecret, disconnect = false) {
|
||||
this._leaveRoom(peer, 'secret', roomSecret, disconnect)
|
||||
|
||||
//if room is empty, delete the room
|
||||
if (!Object.keys(this._rooms[room]).length) {
|
||||
delete this._rooms[room];
|
||||
} else {
|
||||
// notify all other peers
|
||||
for (const otherPeerId in this._rooms[room]) {
|
||||
const otherPeer = this._rooms[room][otherPeerId];
|
||||
this._send(otherPeer, {
|
||||
type: 'peer-left',
|
||||
peerId: peer.id,
|
||||
roomType: roomType,
|
||||
roomSecret: roomSecret,
|
||||
disconnect: disconnect
|
||||
});
|
||||
}
|
||||
}
|
||||
//remove secret from peer
|
||||
if (roomType === 'secret') {
|
||||
peer.removeRoomSecret(roomSecret);
|
||||
peer.removeRoomSecret(roomSecret);
|
||||
}
|
||||
|
||||
_leavePublicRoom(peer, disconnect = false) {
|
||||
if (!peer.publicRoomId) return;
|
||||
|
||||
this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect);
|
||||
|
||||
peer.publicRoomId = null;
|
||||
}
|
||||
|
||||
_leaveRoom(peer, roomType, roomId, disconnect = false) {
|
||||
if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return;
|
||||
|
||||
// remove peer from room
|
||||
delete this._rooms[roomId][peer.id];
|
||||
|
||||
// delete room if empty and abort
|
||||
if (!Object.keys(this._rooms[roomId]).length) {
|
||||
delete this._rooms[roomId];
|
||||
return;
|
||||
}
|
||||
|
||||
// notify all other peers that remain in room that peer left
|
||||
for (const otherPeerId in this._rooms[roomId]) {
|
||||
const otherPeer = this._rooms[roomId][otherPeerId];
|
||||
|
||||
let msg = {
|
||||
type: 'peer-left',
|
||||
peerId: peer.id,
|
||||
roomType: roomType,
|
||||
roomId: roomId,
|
||||
disconnect: disconnect
|
||||
};
|
||||
|
||||
this._send(otherPeer, msg);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeers(peer, roomType = 'ip', roomSecret = '') {
|
||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
||||
if (!this._rooms[room]) return;
|
||||
_notifyPeers(peer, roomType, roomId) {
|
||||
if (!this._rooms[roomId]) return;
|
||||
|
||||
// notify all other peers
|
||||
for (const otherPeerId in this._rooms[room]) {
|
||||
// notify all other peers that peer joined
|
||||
for (const otherPeerId in this._rooms[roomId]) {
|
||||
if (otherPeerId === peer.id) continue;
|
||||
const otherPeer = this._rooms[room][otherPeerId];
|
||||
this._send(otherPeer, {
|
||||
const otherPeer = this._rooms[roomId][otherPeerId];
|
||||
|
||||
let msg = {
|
||||
type: 'peer-joined',
|
||||
peer: peer.getInfo(),
|
||||
roomType: roomType,
|
||||
roomSecret: roomSecret
|
||||
});
|
||||
roomId: roomId
|
||||
};
|
||||
|
||||
this._send(otherPeer, msg);
|
||||
}
|
||||
|
||||
// notify peer about the other peers
|
||||
// notify peer about peers already in the room
|
||||
const otherPeers = [];
|
||||
for (const otherPeerId in this._rooms[room]) {
|
||||
for (const otherPeerId in this._rooms[roomId]) {
|
||||
if (otherPeerId === peer.id) continue;
|
||||
otherPeers.push(this._rooms[room][otherPeerId].getInfo());
|
||||
otherPeers.push(this._rooms[roomId][otherPeerId].getInfo());
|
||||
}
|
||||
|
||||
this._send(peer, {
|
||||
let msg = {
|
||||
type: 'peers',
|
||||
peers: otherPeers,
|
||||
roomType: roomType,
|
||||
roomSecret: roomSecret
|
||||
});
|
||||
roomId: roomId
|
||||
};
|
||||
|
||||
this._send(peer, msg);
|
||||
}
|
||||
|
||||
_joinSecretRooms(peer, roomSecrets) {
|
||||
for (let i=0; i<roomSecrets.length; i++) {
|
||||
this._joinRoom(peer, 'secret', roomSecrets[i])
|
||||
this._joinSecretRoom(peer, roomSecrets[i])
|
||||
}
|
||||
}
|
||||
|
||||
_leaveAllSecretRooms(peer, disconnect = false) {
|
||||
for (let i=0; i<peer.roomSecrets.length; i++) {
|
||||
this._leaveRoom(peer, 'secret', peer.roomSecrets[i], disconnect);
|
||||
this._leaveSecretRoom(peer, peer.roomSecrets[i], disconnect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,23 +559,35 @@ class PairDropServer {
|
||||
|
||||
_keepAlive(peer) {
|
||||
this._cancelKeepAlive(peer);
|
||||
let timeout = 500;
|
||||
if (!peer.lastBeat) {
|
||||
peer.lastBeat = Date.now();
|
||||
let timeout = 1000;
|
||||
|
||||
if (!this._keepAliveTimers[peer.id]) {
|
||||
this._keepAliveTimers[peer.id] = {
|
||||
timer: 0,
|
||||
lastBeat: Date.now()
|
||||
};
|
||||
}
|
||||
if (Date.now() - peer.lastBeat > 2 * timeout) {
|
||||
|
||||
if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) {
|
||||
// Disconnect peer if unresponsive for 10s
|
||||
this._disconnect(peer);
|
||||
return;
|
||||
}
|
||||
|
||||
this._send(peer, { type: 'ping' });
|
||||
|
||||
peer.timerId = setTimeout(() => this._keepAlive(peer), timeout);
|
||||
this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout);
|
||||
}
|
||||
|
||||
_cancelKeepAlive(peer) {
|
||||
if (peer && peer.timerId) {
|
||||
clearTimeout(peer.timerId);
|
||||
if (this._keepAliveTimers[peer.id]?.timer) {
|
||||
clearTimeout(this._keepAliveTimers[peer.id].timer);
|
||||
}
|
||||
}
|
||||
|
||||
_setKeepAliveTimerToNow(peer) {
|
||||
if (this._keepAliveTimers[peer.id]?.lastBeat) {
|
||||
this._keepAliveTimers[peer.id].lastBeat = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -506,13 +612,22 @@ class Peer {
|
||||
// set name
|
||||
this._setName(request);
|
||||
|
||||
// for keepalive
|
||||
this.timerId = 0;
|
||||
this.lastBeat = Date.now();
|
||||
this.requestRate = 0;
|
||||
|
||||
this.roomSecrets = [];
|
||||
this.roomKey = null;
|
||||
this.roomKeyRate = 0;
|
||||
|
||||
this.publicRoomId = null;
|
||||
}
|
||||
|
||||
rateLimitReached() {
|
||||
// rate limit implementation: max 10 attempts every 10s
|
||||
if (this.requestRate >= 10) {
|
||||
return true;
|
||||
}
|
||||
this.requestRate += 1;
|
||||
setTimeout(_ => this.requestRate -= 1, 10000);
|
||||
return false;
|
||||
}
|
||||
|
||||
_setIP(request) {
|
||||
@@ -688,8 +803,15 @@ const hasher = (() => {
|
||||
})()
|
||||
|
||||
const randomizer = (() => {
|
||||
let charCodeLettersOnly = r => 65 <= r && r <= 90;
|
||||
let charCodeAllPrintableChars = r => r === 45 || 47 <= r && r <= 57 || 64 <= r && r <= 90 || 97 <= r && r <= 122;
|
||||
|
||||
return {
|
||||
getRandomString(length) {
|
||||
getRandomString(length, lettersOnly = false) {
|
||||
const charCodeCondition = lettersOnly
|
||||
? charCodeLettersOnly
|
||||
: charCodeAllPrintableChars;
|
||||
|
||||
let string = "";
|
||||
while (string.length < length) {
|
||||
let arr = new Uint16Array(length);
|
||||
@@ -700,7 +822,7 @@ const randomizer = (() => {
|
||||
})
|
||||
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;
|
||||
return charCodeCondition(r);
|
||||
});
|
||||
string += String.fromCharCode.apply(String, arr);
|
||||
}
|
||||
|
||||
52
package-lock.json
generated
52
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.7.7",
|
||||
"version": "1.8.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pairdrop",
|
||||
"version": "1.7.7",
|
||||
"version": "1.8.3",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.9.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"express-rate-limit": "^7.1.0",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"unique-names-generator": "^4.3.0",
|
||||
"ws": "^8.13.0"
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15"
|
||||
@@ -204,11 +204,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.9.0.tgz",
|
||||
"integrity": "sha512-AnISR3V8qy4gpKM62/TzYdoFO9NV84fBx0POXzTryHU/qGUJBWuVGd+JhbvtVmKBv37t8/afmqdnv16xWoQxag==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.0.tgz",
|
||||
"integrity": "sha512-pwKOMedrpJJeINON/9jhAa18udV2qwxPZSoklPZK8pmXxUyE5uXaptiwjGw8bZILbxqfUZ/p8pQA99ODjSgA5Q==",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
"node": ">= 16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4 || ^5"
|
||||
@@ -583,9 +583,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "1.0.35",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
|
||||
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -594,6 +594,10 @@
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/faisalman"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
@@ -633,9 +637,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"version": "8.14.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
||||
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -801,9 +805,9 @@
|
||||
}
|
||||
},
|
||||
"express-rate-limit": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.9.0.tgz",
|
||||
"integrity": "sha512-AnISR3V8qy4gpKM62/TzYdoFO9NV84fBx0POXzTryHU/qGUJBWuVGd+JhbvtVmKBv37t8/afmqdnv16xWoQxag==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.0.tgz",
|
||||
"integrity": "sha512-pwKOMedrpJJeINON/9jhAa18udV2qwxPZSoklPZK8pmXxUyE5uXaptiwjGw8bZILbxqfUZ/p8pQA99ODjSgA5Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"finalhandler": {
|
||||
@@ -1070,9 +1074,9 @@
|
||||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "1.0.35",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
|
||||
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA=="
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
|
||||
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw=="
|
||||
},
|
||||
"unique-names-generator": {
|
||||
"version": "4.7.1",
|
||||
@@ -1095,9 +1099,9 @@
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"version": "8.14.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
||||
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.7.7",
|
||||
"version": "1.8.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,10 +11,10 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.9.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"express-rate-limit": "^7.1.0",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"unique-names-generator": "^4.3.0",
|
||||
"ws": "^8.13.0"
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15"
|
||||
|
||||
@@ -39,62 +39,76 @@
|
||||
|
||||
<body translate="no">
|
||||
<header class="row-reverse">
|
||||
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
|
||||
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label" title="About PairDrop" aria-label="Open About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title" title="Select Language">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-language-selector" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="theme-wrapper">
|
||||
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
|
||||
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-auto" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
|
||||
<div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title" 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" >
|
||||
<div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title" 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>
|
||||
<div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" title="Enable Notifications" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="install" class="icon-button" title="Install PairDrop" hidden>
|
||||
<div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" title="Install PairDrop" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title" title="Pair Your Devices Permanently">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
|
||||
<div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" title="Edit Paired Devices" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#edit-pair-devices-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||
<div id="join-public-room" class="icon-button" data-i18n-key="header.join-public-room" data-i18n-attrs="title" title="Join Public Room Temporarily">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#public-room-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden>Done</div>
|
||||
</header>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
<!-- Peers -->
|
||||
<div class="x-peers-filler"></div>
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
<x-no-peers data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg" data-drop-bg="Release to select recipient">
|
||||
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text">Open PairDrop on other devices to send files</h2>
|
||||
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text">Pair devices or enter a public room to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"
|
||||
desktop="Click to send files or right click to send a message"
|
||||
mobile="Tap to send files or long tap to send a message"
|
||||
data-drop-peer="Release to send to peer"
|
||||
data-drop-bg="Release to select recipient">
|
||||
<p id="paste-filename"></p>
|
||||
</x-instructions>
|
||||
</div>
|
||||
@@ -103,40 +117,92 @@
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
<div class="column">
|
||||
<div class="known-as-wrapper">
|
||||
<span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
|
||||
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title"
|
||||
placeholder="Loading..." data-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="discovery-wrapper row">
|
||||
<div class="row center">
|
||||
<span data-i18n-key="footer.discovery" data-i18n-attrs="text">You can be discovered:</span>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title">on this network</span>
|
||||
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden>paired devices</span>
|
||||
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Language Select Dialog -->
|
||||
<x-dialog id="language-select-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<div class="row center">
|
||||
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text">Select Language</h2>
|
||||
</div>
|
||||
<div class="language-buttons">
|
||||
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text">System Language</button>
|
||||
<button class="button fw" value="en">English</button>
|
||||
<button class="button fw" value="de">Deutsch (German)</button>
|
||||
<button class="button fw" value="fr">Français (French)</button>
|
||||
<button class="button fw" value="id">Bahasa Indonesia (Indonesian)</button>
|
||||
<button class="button fw" value="nb">Norsk (Norwegian)</button>
|
||||
<button class="button fw" value="ro">Română (Romanian)</button>
|
||||
<button class="button fw" value="ru">Русский язык (Russian)</button>
|
||||
<button class="button fw" value="zh-CN">中文 (Chinese)</button>
|
||||
</div>
|
||||
<div class="center row-reverse button-row">
|
||||
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Pair Device Dialog -->
|
||||
<x-dialog id="pair-device-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Pair Devices</h2>
|
||||
<div id="room-key-qr-code" class="center"></div>
|
||||
<h1 id="room-key" class="center">000 000</h1>
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<div class="row center">
|
||||
<h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text">Pair Devices</h2>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="center key-qr-code"></div>
|
||||
<h1 class="center key">000 000</h1>
|
||||
<p class="center text-center key-instructions">
|
||||
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text">Input this key on another device</span>
|
||||
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-note">
|
||||
<hr>
|
||||
<div>
|
||||
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="input-key-container six-chars">
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device here.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row row-reverse">
|
||||
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
|
||||
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -147,13 +213,70 @@
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Edit Paired Devices</h2>
|
||||
<div class="paired-devices-wrapper"></div>
|
||||
<div class="font-subheading center">
|
||||
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
|
||||
<div class="row center">
|
||||
<h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text">Edit Paired Devices</h2>
|
||||
</div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="button" close>Close</button>
|
||||
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty" data-empty="No paired devices."></div>
|
||||
<div class="font-subheading center">
|
||||
<p>
|
||||
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text">
|
||||
Activate
|
||||
</span>
|
||||
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text">auto-accept</u>
|
||||
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text">
|
||||
to automatically accept all files sent from that device.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="center row-reverse button-row">
|
||||
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Public Room Dialog -->
|
||||
<x-dialog id="public-room-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center" data-i18n-key="dialogs.temporary-public-room-title" data-i18n-attrs="text">Temporary Public Room</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="center key-qr-code"></div>
|
||||
<h1 class="center key"></h1>
|
||||
<p class="center text-center key-instructions">
|
||||
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text">Input this room id on another device</span>
|
||||
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-note">
|
||||
<hr>
|
||||
<div>
|
||||
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="input-key-container">
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-room-id-from-another-device" data-i18n-attrs="text">Enter room id from another device to join room.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center row-reverse button-row">
|
||||
<button class="button" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled>Join</button>
|
||||
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
<button class="button leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text">Leave</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -163,24 +286,30 @@
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center"></h2>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column center file-description">
|
||||
<div>
|
||||
<span class="display-name badge"></span>
|
||||
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text">would like to share</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="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
<div class="row-reverse center button-row">
|
||||
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -189,24 +318,31 @@
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center"></h2>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column center file-description">
|
||||
<div>
|
||||
<span class="display-name badge"></span>
|
||||
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">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="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
<div class="row-reverse center button-row">
|
||||
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden>Share</button>
|
||||
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
|
||||
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -216,16 +352,27 @@
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span class="display-name"></span>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text">Send Message</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
|
||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||
<div class="row center display-name-wrapper">
|
||||
<div class="column">
|
||||
<div class="text-center">
|
||||
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text">Send a Message to</span>
|
||||
<span class="display-name badge"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column fw">
|
||||
<textarea title="Message" class="textarea" wrap="off" autofocus></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row row-reverse">
|
||||
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
|
||||
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -235,16 +382,23 @@
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
<div class="row center">
|
||||
<h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text">Message Received</h2>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
<div class="row center">
|
||||
<div class="text-center">
|
||||
<span class="display-name badge"></span>
|
||||
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent:</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column fw">
|
||||
<div id="text" class="textarea fw"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-reverse center button-row">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -253,20 +407,22 @@
|
||||
<x-dialog id="base64-paste-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" id="base64-paste-btn" title="Paste"></button>
|
||||
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||
<button class="button center" close>Close</button>
|
||||
<div class="row-reverse center button-row">
|
||||
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||
<x-toast id="toast" class="row center" shadow="1"></x-toast>
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
||||
<a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="aria-label" aria-label="Close About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
@@ -278,26 +434,26 @@
|
||||
</svg>
|
||||
<div class="title-wrapper">
|
||||
<h1>PairDrop</h1>
|
||||
<div class="font-subheading">v1.7.7</div>
|
||||
<div class="font-subheading">v1.8.3</div>
|
||||
</div>
|
||||
<div class="font-subheading">The easiest way to transfer files across devices</div>
|
||||
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text">The easiest way to transfer files across devices</div>
|
||||
<div class="row">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer" data-i18n-key="about.github" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#github" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#monetarization" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#twitter" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#help-outline" />
|
||||
</svg>
|
||||
@@ -371,8 +527,18 @@
|
||||
<!--! 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 id="public-room-icon" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-language-selector" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/localization.js"></script>
|
||||
<script src="scripts/theme.js"></script>
|
||||
<script src="scripts/network.js"></script>
|
||||
<script src="scripts/ui.js"></script>
|
||||
|
||||
160
public/lang/de.json
Normal file
160
public/lang/de.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "Über PairDrop",
|
||||
"notification_title": "Benachrichtigungen aktivieren",
|
||||
"about_aria-label": "Über PairDrop öffnen",
|
||||
"install_title": "PairDrop installieren",
|
||||
"pair-device_title": "Deine Geräte dauerhaft koppeln",
|
||||
"edit-paired-devices_title": "Gekoppelte Geräte bearbeiten",
|
||||
"theme-auto_title": "Systemstil verwenden",
|
||||
"theme-dark_title": "Dunklen Stil verwenden",
|
||||
"theme-light_title": "Hellen Stil verwenden",
|
||||
"cancel-paste-mode": "Fertig",
|
||||
"language-selector_title": "Sprache auswählen",
|
||||
"join-public-room_title": "Öffentlichen Raum temporär betreten"
|
||||
},
|
||||
"dialogs": {
|
||||
"share": "Teilen",
|
||||
"download": "Herunterladen",
|
||||
"pair-devices-title": "Geräte dauerhaft koppeln",
|
||||
"input-key-on-this-device": "Gib diesen Schlüssel auf einem anderen Gerät ein",
|
||||
"enter-key-from-another-device": "Gib den Schlüssel von einem anderen Gerät hier ein.",
|
||||
"pair": "Koppeln",
|
||||
"cancel": "Abbrechen",
|
||||
"edit-paired-devices-title": "Gekoppelte Geräte bearbeiten",
|
||||
"paired-devices-wrapper_data-empty": "Keine gekoppelten Geräte.",
|
||||
"close": "Schließen",
|
||||
"accept": "Akzeptieren",
|
||||
"decline": "Ablehnen",
|
||||
"title-image": "Bild",
|
||||
"title-file": "Datei",
|
||||
"title-image-plural": "Bilder",
|
||||
"title-file-plural": "Dateien",
|
||||
"scan-qr-code": "oder scanne den QR-Code.",
|
||||
"would-like-to-share": "möchte Folgendes teilen",
|
||||
"send": "Senden",
|
||||
"copy": "Kopieren",
|
||||
"receive-text-title": "Textnachricht erhalten",
|
||||
"file-other-description-image-plural": "und {{count}} andere Bilder",
|
||||
"file-other-description-file-plural": "und {{count}} andere Dateien",
|
||||
"auto-accept-instructions-1": "Aktiviere",
|
||||
"auto-accept": "auto-accept",
|
||||
"auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.",
|
||||
"has-sent": "hat Folgendes gesendet:",
|
||||
"send-message-title": "Textnachricht senden",
|
||||
"send-message-to": "Sende eine Textnachricht an",
|
||||
"base64-tap-to-paste": "Hier tippen, um {{type}} einzufügen",
|
||||
"base64-paste-to-send": "Hier einfügen, um {{type}} zu versenden",
|
||||
"base64-text": "Text",
|
||||
"base64-files": "Dateien",
|
||||
"base64-processing": "Bearbeitung läuft…",
|
||||
"file-other-description-image": "und ein anderes Bild",
|
||||
"file-other-description-file": "und eine andere Datei",
|
||||
"receive-title": "{{descriptor}} erhalten",
|
||||
"download-again": "Erneut herunterladen",
|
||||
"system-language": "Systemsprache",
|
||||
"language-selector-title": "Sprache auswählen",
|
||||
"hr-or": "ODER",
|
||||
"input-room-id-on-another-device": "Gib diese Raum-ID auf einem anderen Gerät ein",
|
||||
"unpair": "Entkoppeln",
|
||||
"leave": "Verlassen",
|
||||
"join": "Betreten",
|
||||
"enter-room-id-from-another-device": "Gib die Raum-ID von einem anderen Gerät hier ein.",
|
||||
"temporary-public-room-title": "Temporärer Öffentlicher Raum"
|
||||
},
|
||||
"about": {
|
||||
"tweet_title": "Über PairDrop twittern",
|
||||
"faq_title": "Häufig gestellte Fragen",
|
||||
"close-about_aria-label": "Schließe Über PairDrop",
|
||||
"github_title": "PairDrop auf GitHub",
|
||||
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
|
||||
"claim": "Der einfachste Weg Dateien zwischen Geräten zu teilen"
|
||||
},
|
||||
"footer": {
|
||||
"known-as": "Du wirst angezeigt als:",
|
||||
"display-name_title": "Setze einen permanenten Gerätenamen",
|
||||
"on-this-network": "in diesem Netzwerk",
|
||||
"paired-devices": "für gekoppelte Geräte",
|
||||
"traffic": "Datenverkehr wird",
|
||||
"display-name_placeholder": "Lade…",
|
||||
"routed": "durch den Server geleitet",
|
||||
"webrtc": "wenn WebRTC nicht verfügbar ist.",
|
||||
"display-name_data-placeholder": "Lade…",
|
||||
"public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum unabhängig von deinem Netzwerk gefunden werden.",
|
||||
"paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.",
|
||||
"public-room-devices": "in Raum {{roomId}}",
|
||||
"discovery": "Du bist sichtbar:",
|
||||
"on-this-network_title": "Du kannst von jedem in diesem Netzwerk gefunden werden."
|
||||
},
|
||||
"notifications": {
|
||||
"link-received": "Link von {{name}} empfangen - Klicke um ihn zu öffnen",
|
||||
"message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren",
|
||||
"click-to-download": "Klicken zum Download",
|
||||
"copied-text": "Text in die Zwischenablage kopiert",
|
||||
"connected": "Verbunden.",
|
||||
"pairing-success": "Geräte gekoppelt.",
|
||||
"display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert.",
|
||||
"pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden.",
|
||||
"pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent.",
|
||||
"pairing-key-invalid": "Ungültiger Schlüssel",
|
||||
"pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht.",
|
||||
"copied-to-clipboard": "In die Zwischenablage kopiert",
|
||||
"text-content-incorrect": "Textinhalt ist fehlerhaft.",
|
||||
"clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft.",
|
||||
"copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!",
|
||||
"file-content-incorrect": "Dateiinhalt ist fehlerhaft.",
|
||||
"notifications-enabled": "Benachrichtigungen aktiviert.",
|
||||
"offline": "Du bist offline",
|
||||
"online": "Du bist wieder Online",
|
||||
"unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?",
|
||||
"display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert.",
|
||||
"download-successful": "{{descriptor}} heruntergeladen",
|
||||
"pairing-cleared": "Alle Geräte entkoppelt.",
|
||||
"click-to-show": "Klicken zum Anzeigen",
|
||||
"online-requirement": "Du musst online sein um Geräte zu koppeln.",
|
||||
"display-name-changed-temporarily": "Anzeigename wurde nur für diese Session geändert.",
|
||||
"request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen",
|
||||
"connecting": "Verbindung wird aufgebaut…",
|
||||
"files-incorrect": "Dateien sind fehlerhaft.",
|
||||
"file-transfer-completed": "Dateiübertragung fertiggestellt.",
|
||||
"message-transfer-completed": "Nachrichtenübertragung fertiggestellt.",
|
||||
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
|
||||
"selected-peer-left": "Ausgewählter Peer ist gegangen.",
|
||||
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
|
||||
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
|
||||
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
|
||||
"public-room-id-invalid": "Ungültige Raum-ID",
|
||||
"online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln.",
|
||||
"online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können."
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_desktop": "Klicke, um Dateien zu Senden oder klicke mit der rechten Maustaste, um Textnachrichten zu senden",
|
||||
"no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden",
|
||||
"no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen",
|
||||
"no-peers-subtitle": "Kopple Geräte oder besuche einen öffentlichen Raum, damit du in anderen Netzwerken sichtbar bist",
|
||||
"click-to-send": "Klicke zum Senden von",
|
||||
"tap-to-send": "Tippe zum Senden von",
|
||||
"x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden",
|
||||
"x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen",
|
||||
"x-instructions_mobile": "Tippe zum Senden von Dateien oder tippe lange zum Senden von Nachrichten",
|
||||
"activate-paste-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von",
|
||||
"activate-paste-mode-and-other-files": "und {{count}} anderen Dateien",
|
||||
"activate-paste-mode-shared-text": "freigegebenem Text"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-transfer-requested": "Datenübertagung angefordert",
|
||||
"file-received": "Datei erhalten",
|
||||
"file-received-plural": "{{count}} Dateien erhalten",
|
||||
"message-received": "Nachricht erhalten",
|
||||
"message-received-plural": "{{count}} Nachrichten erhalten"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send": "Klicke um Dateien zu senden oder nutze einen Rechtsklick um eine Textnachricht zu senden",
|
||||
"connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten",
|
||||
"waiting": "Warte…",
|
||||
"click-to-send-paste-mode": "Klicken um {{descriptor}} zu senden",
|
||||
"transferring": "Übertragung läuft…",
|
||||
"processing": "Bearbeitung läuft…",
|
||||
"preparing": "Vorbereitung läuft…"
|
||||
}
|
||||
}
|
||||
159
public/lang/en.json
Normal file
159
public/lang/en.json
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "About PairDrop",
|
||||
"language-selector_title": "Select Language",
|
||||
"about_aria-label": "Open About PairDrop",
|
||||
"theme-auto_title": "Adapt Theme to System",
|
||||
"theme-light_title": "Always Use Light-Theme",
|
||||
"theme-dark_title": "Always Use Dark-Theme",
|
||||
"notification_title": "Enable Notifications",
|
||||
"install_title": "Install PairDrop",
|
||||
"pair-device_title": "Pair Your Devices Permanently",
|
||||
"edit-paired-devices_title": "Edit Paired Devices",
|
||||
"join-public-room_title": "Join Public Room Temporarily",
|
||||
"cancel-paste-mode": "Done"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Release to select recipient",
|
||||
"no-peers-title": "Open PairDrop on other devices to send files",
|
||||
"no-peers-subtitle": "Pair devices or enter a public room to be discoverable on other networks",
|
||||
"x-instructions_desktop": "Click to send files or right click to send a message",
|
||||
"x-instructions_mobile": "Tap to send files or long tap to send a message",
|
||||
"x-instructions_data-drop-peer": "Release to send to peer",
|
||||
"x-instructions_data-drop-bg": "Release to select recipient",
|
||||
"click-to-send": "Click to send",
|
||||
"tap-to-send": "Tap to send",
|
||||
"activate-paste-mode-base": "Open PairDrop on other devices to send",
|
||||
"activate-paste-mode-and-other-files": "and {{count}} other files",
|
||||
"activate-paste-mode-shared-text": "shared text"
|
||||
},
|
||||
"footer": {
|
||||
"known-as": "You are known as:",
|
||||
"display-name_data-placeholder": "Loading…",
|
||||
"display-name_title": "Edit your device name permanently",
|
||||
"discovery": "You can be discovered:",
|
||||
"on-this-network": "on this network",
|
||||
"on-this-network_title": "You can be discovered by everyone on this network.",
|
||||
"paired-devices": "by paired devices",
|
||||
"paired-devices_title": "You can be discovered by paired devices at all times independent of the network.",
|
||||
"public-room-devices": "in room {{roomId}}",
|
||||
"public-room-devices_title": "You can be discovered by devices in this public room independent of the network.",
|
||||
"traffic": "Traffic is",
|
||||
"routed": "routed through the server",
|
||||
"webrtc": "if WebRTC is not available."
|
||||
},
|
||||
"dialogs": {
|
||||
"pair-devices-title": "Pair Devices Permanently",
|
||||
"input-key-on-this-device": "Input this key on another device",
|
||||
"scan-qr-code": "or scan the QR-code.",
|
||||
"enter-key-from-another-device": "Enter key from another device here.",
|
||||
"temporary-public-room-title": "Temporary Public Room",
|
||||
"input-room-id-on-another-device": "Input this room ID on another device",
|
||||
"enter-room-id-from-another-device": "Enter room ID from another device to join room.",
|
||||
"hr-or": "OR",
|
||||
"pair": "Pair",
|
||||
"cancel": "Cancel",
|
||||
"edit-paired-devices-title": "Edit Paired Devices",
|
||||
"unpair": "Unpair",
|
||||
"paired-devices-wrapper_data-empty": "No paired devices.",
|
||||
"auto-accept-instructions-1": "Activate",
|
||||
"auto-accept": "auto-accept",
|
||||
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
|
||||
"close": "Close",
|
||||
"join": "Join",
|
||||
"leave": "Leave",
|
||||
"would-like-to-share": "would like to share",
|
||||
"accept": "Accept",
|
||||
"decline": "Decline",
|
||||
"has-sent": "has sent:",
|
||||
"share": "Share",
|
||||
"download": "Download",
|
||||
"send-message-title": "Send Message",
|
||||
"send-message-to": "Send a Message to",
|
||||
"send": "Send",
|
||||
"receive-text-title": "Message Received",
|
||||
"copy": "Copy",
|
||||
"base64-processing": "Processing…",
|
||||
"base64-tap-to-paste": "Tap here to paste {{type}}",
|
||||
"base64-paste-to-send": "Paste here to send {{type}}",
|
||||
"base64-text": "text",
|
||||
"base64-files": "files",
|
||||
"file-other-description-image": "and 1 other image",
|
||||
"file-other-description-file": "and 1 other file",
|
||||
"file-other-description-image-plural": "and {{count}} other images",
|
||||
"file-other-description-file-plural": "and {{count}} other files",
|
||||
"title-image": "Image",
|
||||
"title-file": "File",
|
||||
"title-image-plural": "Images",
|
||||
"title-file-plural": "Files",
|
||||
"receive-title": "{{descriptor}} Received",
|
||||
"download-again": "Download again",
|
||||
"language-selector-title": "Select Language",
|
||||
"system-language": "System Language"
|
||||
},
|
||||
"about": {
|
||||
"close-about_aria-label": "Close About PairDrop",
|
||||
"claim": "The easiest way to transfer files across devices",
|
||||
"github_title": "PairDrop on GitHub",
|
||||
"buy-me-a-coffee_title": "Buy me a coffee!",
|
||||
"tweet_title": "Tweet about PairDrop",
|
||||
"faq_title": "Frequently asked questions"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "Display name is changed permanently.",
|
||||
"display-name-changed-temporarily": "Display name is changed only for this session.",
|
||||
"display-name-random-again": "Display name is randomly generated again.",
|
||||
"download-successful": "{{descriptor}} downloaded",
|
||||
"pairing-tabs-error": "Pairing two web browser tabs is impossible.",
|
||||
"pairing-success": "Devices paired.",
|
||||
"pairing-not-persistent": "Paired devices are not persistent.",
|
||||
"pairing-key-invalid": "Invalid key",
|
||||
"pairing-key-invalidated": "Key {{key}} invalidated.",
|
||||
"pairing-cleared": "All Devices unpaired.",
|
||||
"public-room-id-invalid": "Invalid room ID",
|
||||
"public-room-left": "Left public room {{publicRoomId}}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"copied-to-clipboard-error": "Copying not possible. Copy manually.",
|
||||
"text-content-incorrect": "Text content is incorrect.",
|
||||
"file-content-incorrect": "File content is incorrect.",
|
||||
"clipboard-content-incorrect": "Clipboard content is incorrect.",
|
||||
"notifications-enabled": "Notifications enabled.",
|
||||
"link-received": "Link received by {{name}} - Click to open",
|
||||
"message-received": "Message received by {{name}} - Click to copy",
|
||||
"click-to-download": "Click to download",
|
||||
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
|
||||
"click-to-show": "Click to show",
|
||||
"copied-text": "Copied text to clipboard",
|
||||
"copied-text-error": "Writing to clipboard failed. Copy manually!",
|
||||
"offline": "You are offline",
|
||||
"online": "You are back online",
|
||||
"connected": "Connected.",
|
||||
"online-requirement-pairing": "You need to be online to pair devices.",
|
||||
"online-requirement-public-room": "You need to be online to create a public room.",
|
||||
"connecting": "Connecting…",
|
||||
"files-incorrect": "Files are incorrect.",
|
||||
"file-transfer-completed": "File transfer completed.",
|
||||
"ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once",
|
||||
"message-transfer-completed": "Message transfer completed.",
|
||||
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
|
||||
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
|
||||
"selected-peer-left": "Selected peer left."
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "File Received",
|
||||
"file-received-plural": "{{count}} Files Received",
|
||||
"file-transfer-requested": "File Transfer Requested",
|
||||
"image-transfer-requested": "Image Transfer Requested",
|
||||
"message-received": "Message Received",
|
||||
"message-received-plural": "{{count}} Messages Received"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "Click to send {{descriptor}}",
|
||||
"click-to-send": "Click to send files or right click to send a message",
|
||||
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
|
||||
"preparing": "Preparing…",
|
||||
"waiting": "Waiting…",
|
||||
"processing": "Processing…",
|
||||
"transferring": "Transferring…"
|
||||
}
|
||||
}
|
||||
160
public/lang/fr.json
Normal file
160
public/lang/fr.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "A propos de",
|
||||
"language-selector_title": "Choix de la langue",
|
||||
"about_aria-label": "Ouvrir à propos de",
|
||||
"theme-auto_title": "Adapter le thème au système",
|
||||
"theme-light_title": "Toujours utiliser le thème clair",
|
||||
"theme-dark_title": "Toujours utiliser le thème sombre",
|
||||
"notification_title": "Activer les notifications",
|
||||
"install_title": "Installer PairDrop",
|
||||
"pair-device_title": "Associez vos appareils de manière permanente",
|
||||
"edit-paired-devices_title": "Gérer les appareils couplés",
|
||||
"join-public-room_title": "Rejoindre temporairement la salle publique",
|
||||
"cancel-paste-mode": "Terminé"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Déposer pour choisir le destinataire",
|
||||
"no-peers-title": "Ouvrez PairDrop sur d'autres appareils pour envoyer des fichiers",
|
||||
"no-peers-subtitle": "Associez des appareils ou entrez dans une salle publique pour être visible sur d'autres réseaux",
|
||||
"x-instructions_desktop": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
|
||||
"x-instructions_mobile": "Appuyez pour envoyer des fichiers ou appuyez longuement pour envoyer un message",
|
||||
"x-instructions_data-drop-peer": "Déposer pour envoyer au destinataire",
|
||||
"x-instructions_data-drop-bg": "Lâcher pour choisir le destinataire",
|
||||
"click-to-send": "Cliquez pour envoyer",
|
||||
"tap-to-send": "Appuyez pour envoyer",
|
||||
"activate-paste-mode-base": "Ouvrez PairDrop sur d'autres appareils pour envoyer",
|
||||
"activate-paste-mode-and-other-files": "et {{count}} autres fichiers",
|
||||
"activate-paste-mode-shared-text": "texte partagé"
|
||||
},
|
||||
"footer": {
|
||||
"known-as": "Vous êtes connu sous le nom de:",
|
||||
"display-name_data-placeholder": "Chargement…",
|
||||
"display-name_title": "Modifiez le nom de votre appareil de manière permanente",
|
||||
"discovery": "Vous pouvez être découvert:",
|
||||
"on-this-network": "sur ce réseau",
|
||||
"on-this-network_title": "Vous pouvez être découvert par tout le monde sur ce réseau.",
|
||||
"paired-devices": "par les appareils couplés",
|
||||
"paired-devices_title": "Vous pouvez être découvert par les appareils couplés à tout moment, indépendamment du réseau.",
|
||||
"public-room-devices": "dans la salle {{roomId}}",
|
||||
"public-room-devices_title": "Vous pouvez être découvert par les appareils de cette salle publique indépendamment du réseau.",
|
||||
"traffic": "Le trafic est",
|
||||
"routed": "routé via le serveur",
|
||||
"webrtc": "si WebRTC n'est pas disponible.",
|
||||
"display-name_placeholder": "Chargement…"
|
||||
},
|
||||
"dialogs": {
|
||||
"pair-devices-title": "Associer les appareils de manière permanente",
|
||||
"input-key-on-this-device": "Saisissez cette clé sur un autre appareil",
|
||||
"scan-qr-code": "ou scannez le QR-code.",
|
||||
"enter-key-from-another-device": "Entrez ici la clé d'un autre appareil.",
|
||||
"temporary-public-room-title": "Salle publique temporaire",
|
||||
"input-room-id-on-another-device": "Saisissez cet ID de salle sur un autre appareil",
|
||||
"enter-room-id-from-another-device": "Entrez l'ID de la salle depuis un autre appareil pour rejoindre la salle.",
|
||||
"hr-or": "OU",
|
||||
"pair": "associer",
|
||||
"cancel": "Annuler",
|
||||
"edit-paired-devices-title": "Modifier les appareils couplés",
|
||||
"unpair": "Dissocier",
|
||||
"paired-devices-wrapper_data-empty": "Aucun appareil couplé.",
|
||||
"auto-accept-instructions-1": "Activer",
|
||||
"auto-accept": "auto-accepter",
|
||||
"auto-accept-instructions-2": "pour accepter automatiquement tous les fichiers envoyés depuis cet appareil.",
|
||||
"close": "Fermer",
|
||||
"join": "Rejoindre",
|
||||
"leave": "Partir",
|
||||
"would-like-to-share": "aimerait partager",
|
||||
"accept": "Accepter",
|
||||
"decline": "Refuser",
|
||||
"has-sent": "a envoyé:",
|
||||
"share": "Partage",
|
||||
"download": "Télécharger",
|
||||
"send-message-title": "Envoyer un message",
|
||||
"send-message-to": "Envoyer un message à",
|
||||
"send": "Envoyer",
|
||||
"receive-text-title": "Message reçu",
|
||||
"copy": "Copier",
|
||||
"base64-processing": "Traitement…",
|
||||
"base64-tap-to-paste": "Appuyez ici pour coller {{type}}",
|
||||
"base64-paste-to-send": "Coller ici pour envoyer {{type}}",
|
||||
"base64-text": "texte",
|
||||
"base64-files": "fichiers",
|
||||
"file-other-description-image": "et 1 autre image",
|
||||
"file-other-description-file": "et 1 autre fichier",
|
||||
"file-other-description-image-plural": "et {{count}} autres images",
|
||||
"file-other-description-file-plural": "et {{count}} autres fichiers",
|
||||
"title-image": "Image",
|
||||
"title-file": "Fichier",
|
||||
"title-image-plural": "Images",
|
||||
"title-file-plural": "Fichiers",
|
||||
"receive-title": "{{descriptor}} Reçu",
|
||||
"download-again": "Télécharger à nouveau",
|
||||
"language-selector-title": "Sélectionnez la langue",
|
||||
"system-language": "Langue du système"
|
||||
},
|
||||
"about": {
|
||||
"close-about_aria-label": "Fermer à propos de PairDrop",
|
||||
"claim": "Le moyen le plus simple de transférer des fichiers entre appareils",
|
||||
"github_title": "PairDrop sur GitHub",
|
||||
"buy-me-a-coffee_title": "Achete-moi un café!",
|
||||
"tweet_title": "Tweet à propos de PairDrop",
|
||||
"faq_title": "Questions fréquemment posées"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "Le nom d'affichage est modifié de manière permanente.",
|
||||
"display-name-changed-temporarily": "Le nom d'affichage est modifié uniquement pour cette session.",
|
||||
"display-name-random-again": "Le nom d'affichage est à nouveau généré aléatoirement.",
|
||||
"download-successful": "{{descriptor}} téléchargé",
|
||||
"pairing-tabs-error": "Le couplage de deux onglets de navigateur Web est impossible.",
|
||||
"pairing-success": "Appareils couplés.",
|
||||
"pairing-not-persistent": "Les appareils couplés ne sont pas persistants.",
|
||||
"pairing-key-invalid": "Clé invalide",
|
||||
"pairing-key-invalidated": "Clé {{key}} invalidée.",
|
||||
"pairing-cleared": "Tous les appareils ne sont plus appairés.",
|
||||
"public-room-id-invalid": "ID de salle non valide",
|
||||
"public-room-left": "Salle publique {{publicRoomId}} quittée",
|
||||
"copied-to-clipboard": "Copié dans le presse-papier",
|
||||
"copied-to-clipboard-error": "Copie impossible. Copier manuellement.",
|
||||
"text-content-incorrect": "Le contenu du texte est incorrect.",
|
||||
"file-content-incorrect": "Le contenu du fichier est incorrect.",
|
||||
"clipboard-content-incorrect": "Le contenu du presse-papiers est incorrect.",
|
||||
"notifications-enabled": "Notifications activées.",
|
||||
"link-received": "Lien reçu par {{name}} - Cliquez pour ouvrir",
|
||||
"message-received": "Message reçu par {{name}} - Cliquez pour copier",
|
||||
"click-to-download": "Cliquez pour télécharger",
|
||||
"request-title": "{{name}} souhaite transférer {{count}} {{descriptor}}",
|
||||
"click-to-show": "Cliquez pour afficher",
|
||||
"copied-text": "Texte copié dans le presse-papiers",
|
||||
"copied-text-error": "L'écriture dans le presse-papiers a échoué. Copiez manuellement!",
|
||||
"offline": "Vous êtes hors ligne",
|
||||
"online": "Vous êtes de nouveau en ligne",
|
||||
"connected": "Connecté.",
|
||||
"online-requirement-pairing": "Vous devez être en ligne pour coupler des appareils.",
|
||||
"online-requirement-public-room": "Vous devez être en ligne pour créer une salle publique.",
|
||||
"connecting": "Connexion…",
|
||||
"files-incorrect": "Les fichiers sont incorrects.",
|
||||
"file-transfer-completed": "Transfert de fichier terminé.",
|
||||
"ios-memory-limit": "L'envoi de fichiers vers iOS n'est possible que jusqu'à 200 Mo à la fois",
|
||||
"message-transfer-completed": "Transfert de message terminé.",
|
||||
"unfinished-transfers-warning": "Il y a des transferts inachevés. Etes-vous sûr de vouloir fermer PairDrop?",
|
||||
"rate-limit-join-key": "Limite de débit atteinte. Attendez 10 secondes et réessayez.",
|
||||
"selected-peer-left": "Appareils selectionnés restants."
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "Fichier reçu",
|
||||
"file-received-plural": "{{count}} fichiers reçus",
|
||||
"file-transfer-requested": "Transfert de fichier demandé",
|
||||
"image-transfer-requested": "Transfert d'image demandé",
|
||||
"message-received": "Message reçu",
|
||||
"message-received-plural": "{{count}} Messages reçus"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "Cliquez pour envoyer {{descriptor}}",
|
||||
"click-to-send": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
|
||||
"connection-hash": "Pour vérifier la sécurité du chiffrement de bout en bout, comparez ce numéro de sécurité sur les deux appareils",
|
||||
"preparing": "Préparation…",
|
||||
"waiting": "En attente…",
|
||||
"processing": "En cours…",
|
||||
"transferring": "Transfert en cours…"
|
||||
}
|
||||
}
|
||||
158
public/lang/id.json
Normal file
158
public/lang/id.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"footer": {
|
||||
"webrtc": "jika WebRTC tidak tersedia.",
|
||||
"public-room-devices_title": "Anda dapat ditemukan oleh perangkat di ruang publik ini terlepas dari jaringan.",
|
||||
"display-name_data-placeholder": "Memuat…",
|
||||
"display-name_title": "Edit nama perangkat Anda scr. permanen",
|
||||
"traffic": "Lalu lintas",
|
||||
"paired-devices_title": "Anda dapat ditemukan oleh perangkat yang dipasangkan setiap saat tergantung pada jaringan.",
|
||||
"public-room-devices": "dalam room {{roomId}}",
|
||||
"paired-devices": "pada prngkt. yg. dipasangkan",
|
||||
"on-this-network": "pada jaringan ini",
|
||||
"routed": "diarahkan melalui server",
|
||||
"discovery": "Anda dapat ditemukan:",
|
||||
"on-this-network_title": "Anda dapat ditemukan oleh semua orang di jaringan ini.",
|
||||
"known-as": "Anda dikenal sebagai:"
|
||||
},
|
||||
"notifications": {
|
||||
"request-title": "{{name}} ingin mentransfer {{count}} {{descriptor}}",
|
||||
"unfinished-transfers-warning": "Ada transfer yang belum selesai. Apakah Anda yakin ingin menutup PairDrop?",
|
||||
"message-received": "Pesan diterima dari {{name}} - Klik untuk menyalin",
|
||||
"rate-limit-join-key": "Batasan tercapai. Tunggu 10 detik dan coba lagi.",
|
||||
"connecting": "Menghubungkan…",
|
||||
"pairing-key-invalidated": "Kunci {{key}} tidak valid.",
|
||||
"pairing-key-invalid": "Kunci tidak valid",
|
||||
"connected": "Tersambung.",
|
||||
"pairing-not-persistent": "Perangkat dipasangkan tidak akan bertahan lama.",
|
||||
"text-content-incorrect": "Isi teks keliru.",
|
||||
"message-transfer-completed": "Transfer pesan selesai.",
|
||||
"file-transfer-completed": "Transfer file selesai.",
|
||||
"file-content-incorrect": "Isi file keliru.",
|
||||
"files-incorrect": "File tidak benar.",
|
||||
"selected-peer-left": "Rekan terpilih keluar.",
|
||||
"link-received": "Tautan diterima dari {{name}} - Klik untuk membuka",
|
||||
"online": "Anda kembali online",
|
||||
"public-room-left": "Keluar dari ruang publik {{publicRoomId}}",
|
||||
"copied-text": "Teks disalin ke papan klip",
|
||||
"display-name-random-again": "Nama tampilan dibuat secara acak lagi.",
|
||||
"display-name-changed-permanently": "Nama tampilan diubah secara permanen.",
|
||||
"copied-to-clipboard-error": "Penyalinan tak dapat dilakukan. Salinlah secara manual.",
|
||||
"pairing-success": "Perangkat dipasangkan.",
|
||||
"clipboard-content-incorrect": "Isi papan klip keliru.",
|
||||
"display-name-changed-temporarily": "Nama tampilan hanya diubah untuk sesi ini.",
|
||||
"copied-to-clipboard": "Disalin ke papan klip",
|
||||
"offline": "Anda sedang offline",
|
||||
"pairing-tabs-error": "Memasangkan dua tab browser web tidak mungkin dilakukan.",
|
||||
"public-room-id-invalid": "Room ID tidak valid",
|
||||
"click-to-download": "Klik untuk mengunduh",
|
||||
"pairing-cleared": "Semua Perangkat dilepaskan.",
|
||||
"notifications-enabled": "Notifikasi diaktifkan.",
|
||||
"online-requirement-pairing": "Anda harus online untuk memasangkan perangkat.",
|
||||
"ios-memory-limit": "Mengirim file ke iOS hanya dapat dilakukan hingga 200 MB sekaligus",
|
||||
"online-requirement-public-room": "Anda harus online untuk membuat ruang publik.",
|
||||
"copied-text-error": "Menyalin ke papan klip gagal. Salinlah secara manual!",
|
||||
"download-successful": "{{descriptor}} diunduh",
|
||||
"click-to-show": "Klik untuk menampilkan"
|
||||
},
|
||||
"header": {
|
||||
"cancel-paste-mode": "Selesai",
|
||||
"theme-auto_title": "Sesuaikan Tema dengan Sistem",
|
||||
"install_title": "Instal PairDrop",
|
||||
"theme-dark_title": "Selalu Gunakan Tema Gelap",
|
||||
"pair-device_title": "Pasangkan Perangkat Anda Secara Permanen",
|
||||
"join-public-room_title": "Bergabung dgn. Ruang Publik Sementara",
|
||||
"notification_title": "Aktifkan Notifikasi",
|
||||
"edit-paired-devices_title": "Edit Perangkat yg. Dipasangkan",
|
||||
"language-selector_title": "Pilih Bahasa",
|
||||
"about_title": "Tentang PairDrop",
|
||||
"about_aria-label": "Buka Tentang PairDrop",
|
||||
"theme-light_title": "Selalu Gunakan Tema Terang"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_mobile": "Ketuk untuk mengirim file atau ketuk lama untuk mengirim pesan",
|
||||
"click-to-send": "Klik untuk mengirim",
|
||||
"activate-paste-mode-and-other-files": "dan {{count}} file lainnya",
|
||||
"tap-to-send": "Ketuk untuk mengirim",
|
||||
"activate-paste-mode-base": "Buka PairDrop di perangkat lain untuk berkirim",
|
||||
"no-peers-subtitle": "Pasangkan perangkat atau masuk ke ruang publik agar dapat terdeteksi di jaringan lain",
|
||||
"activate-paste-mode-shared-text": "teks bersama",
|
||||
"x-instructions_desktop": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
|
||||
"no-peers-title": "Buka PairDrop di perangkat lain untuk berkirim file",
|
||||
"x-instructions_data-drop-peer": "Lepaskan untuk mengirim ke rekan",
|
||||
"x-instructions_data-drop-bg": "Lepaskan untuk memilih penerima",
|
||||
"no-peers_data-drop-bg": "Lepaskan untuk memilih penerima"
|
||||
},
|
||||
"peer-ui": {
|
||||
"processing": "Memproses…",
|
||||
"click-to-send-paste-mode": "Klik untuk mengirim {{descriptor}}",
|
||||
"click-to-send": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
|
||||
"waiting": "Menunggu…",
|
||||
"connection-hash": "Untuk memverifikasi keamanan enkripsi end-to-end, bandingkan nomor keamanan ini pada kedua perangkat",
|
||||
"preparing": "Menyiapkan…",
|
||||
"transferring": "Mentransfer…"
|
||||
},
|
||||
"dialogs": {
|
||||
"base64-paste-to-send": "Tempel di sini untuk mengirim {{type}}",
|
||||
"auto-accept-instructions-2": "untuk secara otomatis menerima semua file yang dikirim dari perangkat tersebut.",
|
||||
"receive-text-title": "Pesan Diterima",
|
||||
"edit-paired-devices-title": "Edit Perangkat yg. Dipasangkan",
|
||||
"cancel": "Batal",
|
||||
"auto-accept-instructions-1": "Aktifkan",
|
||||
"pair-devices-title": "Pasangkan Perangkat Scr. Permanen",
|
||||
"download": "Unduh",
|
||||
"title-file": "File",
|
||||
"base64-processing": "Memproses…",
|
||||
"decline": "Tolak",
|
||||
"receive-title": "{{descriptor}} Diterima",
|
||||
"leave": "Tinggalkan",
|
||||
"join": "Gabung",
|
||||
"title-image-plural": "Gambar",
|
||||
"send": "Kirim",
|
||||
"base64-tap-to-paste": "Ketuk di sini untuk menempelkan {{type}}",
|
||||
"base64-text": "teks",
|
||||
"copy": "Salin",
|
||||
"file-other-description-image": "dan 1 gambar lainnya",
|
||||
"temporary-public-room-title": "Ruang Publik Sementara",
|
||||
"base64-files": "file",
|
||||
"has-sent": "telah mengirim:",
|
||||
"file-other-description-file": "dan 1 file lainnya",
|
||||
"close": "Tutup",
|
||||
"system-language": "Bahasa Sistem",
|
||||
"unpair": "Lepas",
|
||||
"title-image": "Gambar",
|
||||
"file-other-description-file-plural": "dan {{count}} file lainnya",
|
||||
"would-like-to-share": "ingin berbagi",
|
||||
"send-message-to": "Kirim pesan ke",
|
||||
"language-selector-title": "Pilih Bahasa",
|
||||
"pair": "Pasangkan",
|
||||
"hr-or": "ATAU",
|
||||
"scan-qr-code": "atau pindai kode QR.",
|
||||
"input-key-on-this-device": "Masukkan kunci ini pada perangkat lain",
|
||||
"download-again": "Unduh lagi",
|
||||
"accept": "Terima",
|
||||
"paired-devices-wrapper_data-empty": "Tak ada perangkat yg. dipasangkan.",
|
||||
"enter-key-from-another-device": "Masukkan kunci dari perangkat lain di sini.",
|
||||
"share": "Bagikan",
|
||||
"auto-accept": "terima-otomatis",
|
||||
"title-file-plural": "File",
|
||||
"send-message-title": "Kirim Pesan",
|
||||
"input-room-id-on-another-device": "Masukkan room ID ini pada perangkat lain",
|
||||
"file-other-description-image-plural": "dan {{count}} gambar lainnya",
|
||||
"enter-room-id-from-another-device": "Masukkan room ID dari perangkat lain untuk bergabung dengan room."
|
||||
},
|
||||
"about": {
|
||||
"claim": "Cara termudah untuk mentransfer file lintas perangkat",
|
||||
"tweet_title": "Tweet tentang PairDrop",
|
||||
"close-about_aria-label": "Tutup Tentang PairDrop",
|
||||
"buy-me-a-coffee_title": "Traktir aku kopi!",
|
||||
"github_title": "PairDrop di GitHub",
|
||||
"faq_title": "Pertanyaan yang sering diajukan"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-transfer-requested": "Permintaan Transfer File",
|
||||
"message-received-plural": "{{count}} Pesan Diterima",
|
||||
"message-received": "Pesan Diterima",
|
||||
"file-received": "File Diterima",
|
||||
"file-received-plural": "{{count}} File Diterima"
|
||||
}
|
||||
}
|
||||
138
public/lang/nb.json
Normal file
138
public/lang/nb.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"header": {
|
||||
"edit-paired-devices_title": "Rediger sammenkoblede enheter",
|
||||
"about_title": "Om PairDrop",
|
||||
"about_aria-label": "Åpne «Om PairDrop»",
|
||||
"theme-auto_title": "Juster drakt til system",
|
||||
"theme-light_title": "Alltid bruk lys drakt",
|
||||
"theme-dark_title": "Alltid bruk mørk drakt",
|
||||
"notification_title": "Skru på merknader",
|
||||
"cancel-paste-mode": "Ferdig",
|
||||
"install_title": "Installer PairDrop",
|
||||
"pair-device_title": "Sammenkoble enhet"
|
||||
},
|
||||
"footer": {
|
||||
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
|
||||
"display-name_data-placeholder": "Laster inn…",
|
||||
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
|
||||
"traffic": "Trafikken",
|
||||
"on-this-network": "på dette nettverket",
|
||||
"known-as": "Du er kjent som:",
|
||||
"paired-devices": "sammenkoblede enheter",
|
||||
"routed": "Sendes gjennom tjeneren"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||
"x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding",
|
||||
"x-instructions_data-drop-bg": "Slipp for å velge mottager",
|
||||
"click-to-send": "Klikk for å sende",
|
||||
"no-peers_data-drop-bg": "Slipp for å velge mottager",
|
||||
"no-peers-title": "Åpne PairDrop på andre enheter for å sende filer",
|
||||
"no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk",
|
||||
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
|
||||
"tap-to-send": "Trykk for å sende",
|
||||
"activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende",
|
||||
"activate-paste-mode-and-other-files": "og {{count}} andre filer",
|
||||
"activate-paste-mode-shared-text": "delt tekst"
|
||||
},
|
||||
"dialogs": {
|
||||
"input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet",
|
||||
"pair-devices-title": "Sammenkoble enheter",
|
||||
"would-like-to-share": "ønsker å dele",
|
||||
"auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.",
|
||||
"paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter",
|
||||
"enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.",
|
||||
"edit-paired-devices-title": "Rediger sammenkoblede enheter",
|
||||
"accept": "Godta",
|
||||
"has-sent": "har sendt:",
|
||||
"base64-paste-to-send": "Trykk her for å sende {{type}}",
|
||||
"base64-text": "tekst",
|
||||
"base64-files": "filer",
|
||||
"file-other-description-image-plural": "og {{count}} andre bilder",
|
||||
"receive-title": "{{descriptor}} mottatt",
|
||||
"send-message-title": "Send melding",
|
||||
"base64-processing": "Behandler…",
|
||||
"close": "Lukk",
|
||||
"decline": "Avslå",
|
||||
"download": "Last ned",
|
||||
"copy": "Kopier",
|
||||
"pair": "Sammenkoble",
|
||||
"cancel": "Avbryt",
|
||||
"scan-qr-code": "eller skann QR-koden.",
|
||||
"auto-accept-instructions-1": "Aktiver",
|
||||
"receive-text-title": "Melding mottatt",
|
||||
"auto-accept": "auto-godkjenn",
|
||||
"share": "Del",
|
||||
"send-message-to": "Send en melding til",
|
||||
"send": "Send",
|
||||
"base64-tap-to-paste": "Trykk her for å lime inn {{type}}",
|
||||
"file-other-description-image": "og ett annet bilde",
|
||||
"file-other-description-file-plural": "og {{count}} andre filer",
|
||||
"title-file-plural": "Filer",
|
||||
"download-again": "Last ned igjen",
|
||||
"file-other-description-file": "og én annen fil",
|
||||
"title-image": "Bilde",
|
||||
"title-file": "Fil",
|
||||
"title-image-plural": "Bilder"
|
||||
},
|
||||
"about": {
|
||||
"close-about_aria-label": "Lukk «Om PairDrop»",
|
||||
"faq_title": "Ofte stilte spørsmål",
|
||||
"claim": "Den enkleste måten å overføre filer mellom enheter",
|
||||
"buy-me-a-coffee_title": "Spander drikke.",
|
||||
"tweet_title": "Tvitre om PairDrop",
|
||||
"github_title": "PairDrop på GitHub"
|
||||
},
|
||||
"notifications": {
|
||||
"copied-to-clipboard": "Kopiert til utklippstavlen",
|
||||
"pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.",
|
||||
"notifications-enabled": "Merknader påskrudd.",
|
||||
"click-to-show": "Klikk for å vise",
|
||||
"copied-text": "Tekst kopiert til utklippstavlen",
|
||||
"connected": "Tilkoblet.",
|
||||
"online": "Du er tilbake på nett",
|
||||
"file-transfer-completed": "Filoverføring utført.",
|
||||
"selected-peer-left": "Valgt likemann dro.",
|
||||
"pairing-key-invalid": "Ugyldig nøkkel",
|
||||
"connecting": "Kobler til …",
|
||||
"pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.",
|
||||
"offline": "Du er frakoblet",
|
||||
"online-requirement": "Du må være på nett for å koble sammen enheter.",
|
||||
"display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.",
|
||||
"display-name-changed-permanently": "Visningsnavnet er endret for godt.",
|
||||
"display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.",
|
||||
"text-content-incorrect": "Tekstinnholdet er uriktig.",
|
||||
"file-content-incorrect": "Filinnholdet er uriktig.",
|
||||
"click-to-download": "Klikk for å laste ned",
|
||||
"message-transfer-completed": "Meldingsoverføring utført.",
|
||||
"download-successful": "{{descriptor}} nedlastet",
|
||||
"pairing-success": "Enheter sammenkoblet.",
|
||||
"pairing-cleared": "Sammenkobling av alle enheter opphevet.",
|
||||
"pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.",
|
||||
"copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt!",
|
||||
"clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.",
|
||||
"link-received": "Lenke mottatt av {{name}} - Klikk for å åpne.",
|
||||
"request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}",
|
||||
"message-received": "Melding mottatt av {{name}} - Klikk for å åpne.",
|
||||
"files-incorrect": "Filene er uriktige.",
|
||||
"ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.",
|
||||
"unfinished-transfers-warning": "Lukk med ufullførte overføringer?",
|
||||
"rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen."
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "Fil mottatt",
|
||||
"file-received-plural": "{{count}} filer mottatt",
|
||||
"message-received": "Melding mottatt",
|
||||
"file-transfer-requested": "Filoverføring forespurt",
|
||||
"message-received-plural": "{{count}} meldinger mottatt"
|
||||
},
|
||||
"peer-ui": {
|
||||
"preparing": "Forbereder …",
|
||||
"waiting": "Venter…",
|
||||
"processing": "Behandler …",
|
||||
"transferring": "Overfører …",
|
||||
"click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||
"click-to-send-paste-mode": "Klikk for å sende {{descriptor}}",
|
||||
"connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen."
|
||||
}
|
||||
}
|
||||
158
public/lang/ro.json
Normal file
158
public/lang/ro.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"footer": {
|
||||
"webrtc": "dacă WebRTC nu este disponibil.",
|
||||
"public-room-devices_title": "Poți fi descoperit de dispozitivele din această cameră publică, independent de rețea.",
|
||||
"display-name_data-placeholder": "Se încarcă…",
|
||||
"display-name_title": "Editați permanent numele dispozitivului tău",
|
||||
"traffic": "Traficul este",
|
||||
"paired-devices_title": "Poți fi descoperit în orice moment de dispozitivele cuplate, indiferent de rețea.",
|
||||
"public-room-devices": "în camera {{roomId}}",
|
||||
"paired-devices": "prin dispozitive împerecheate",
|
||||
"on-this-network": "în această rețea",
|
||||
"routed": "rutate prin server",
|
||||
"discovery": "Poți fi descoperit:",
|
||||
"on-this-network_title": "Poți fi descoperit de toată lumea din această rețea.",
|
||||
"known-as": "Ești cunoscut ca:"
|
||||
},
|
||||
"notifications": {
|
||||
"request-title": "{{name}} ar dori să transfere {{count}} {{descriptor}}",
|
||||
"unfinished-transfers-warning": "Există transferuri neterminate. Sigur vrei să închizi PairDrop?",
|
||||
"message-received": "Mesaj primit de {{name}} - Apasă pentru a copia",
|
||||
"rate-limit-join-key": "A fost atinsă limita ratei. Așteptați 10 secunde și încercați din nou.",
|
||||
"connecting": "Conectarea…",
|
||||
"pairing-key-invalidated": "Cheia {{key}} invalidată.",
|
||||
"pairing-key-invalid": "Cheie invalidă",
|
||||
"connected": "Conectat.",
|
||||
"pairing-not-persistent": "Dispozitivele cuplate nu sunt persistente.",
|
||||
"text-content-incorrect": "Conținutul textului este incorect.",
|
||||
"message-transfer-completed": "Transferul mesajului este finalizat.",
|
||||
"file-transfer-completed": "Transfer de fișiere finalizat.",
|
||||
"file-content-incorrect": "Conținutul fișierului este incorect.",
|
||||
"files-incorrect": "Fișierele sunt incorecte.",
|
||||
"selected-peer-left": "Selectat peer a plecat.",
|
||||
"link-received": "Link primit de {{name}} - Apasă pentru a deschide",
|
||||
"online": "Ați revenit online",
|
||||
"public-room-left": "Plecat din camera publică {{publicRoomId}}",
|
||||
"copied-text": "Text copiat în clipboard",
|
||||
"display-name-random-again": "Numele afișat este din nou generat aleatoriu.",
|
||||
"display-name-changed-permanently": "Numele afișat este schimbat permanent.",
|
||||
"copied-to-clipboard-error": "Copierea nu este posibilă. Copiați manual.",
|
||||
"pairing-success": "Dispozitive asociate.",
|
||||
"clipboard-content-incorrect": "Conținutul clipboard-ului este incorect.",
|
||||
"display-name-changed-temporarily": "Numele afișat se modifică numai pentru această sesiune.",
|
||||
"copied-to-clipboard": "Copiat în clipboard",
|
||||
"offline": "Ești offline",
|
||||
"pairing-tabs-error": "Cuplarea între două file de browser web este imposibilă.",
|
||||
"public-room-id-invalid": "ID-ul camerei invalid",
|
||||
"click-to-download": "Apasă pentru a descărca",
|
||||
"pairing-cleared": "Toate dispozitivele sunt decuplate.",
|
||||
"notifications-enabled": "Notificări activate.",
|
||||
"online-requirement-pairing": "Trebuie să fiți online pentru a asocia dispozitivele.",
|
||||
"ios-memory-limit": "Trimiterea de fișiere pe iOS este posibilă doar până la 200 MB simultan",
|
||||
"online-requirement-public-room": "Trebuie să fiți online pentru a crea o cameră publică.",
|
||||
"copied-text-error": "Scrierea în clipboard a eșuat. Copiați manual!",
|
||||
"download-successful": "{{descriptor}} descărcat",
|
||||
"click-to-show": "Apasă pentru a arăta"
|
||||
},
|
||||
"header": {
|
||||
"cancel-paste-mode": "Gata",
|
||||
"theme-auto_title": "Adaptează Tema la Sistem",
|
||||
"install_title": "Instalează PairDrop",
|
||||
"theme-dark_title": "Utilizați mereu tema întunecoasă",
|
||||
"pair-device_title": "Împerechează-ți permanent dispozitivele",
|
||||
"join-public-room_title": "Alătură-te temporar camerei publice",
|
||||
"notification_title": "Activați Notificări",
|
||||
"edit-paired-devices_title": "Editați dispozitivele împerecheate",
|
||||
"language-selector_title": "Selectează Limba",
|
||||
"about_title": "Despre PairDrop",
|
||||
"about_aria-label": "Deschide Despre PairDrop",
|
||||
"theme-light_title": "Utilizați mereu tema luminoasă"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_mobile": "Atingeți pentru a trimite fișiere sau atingeți lung pentru a trimite un mesaj",
|
||||
"click-to-send": "Clic pentru a trimite",
|
||||
"activate-paste-mode-and-other-files": "și {{count}} alte fișiere",
|
||||
"tap-to-send": "Atinge pentru a trimite",
|
||||
"activate-paste-mode-base": "Deschideți PairDrop pe alte dispozitive pentru a trimite",
|
||||
"no-peers-subtitle": "Împerecheați dispozitive sau intrați într-o cameră publică pentru a fi descoperit în alte rețele",
|
||||
"activate-paste-mode-shared-text": "text partajat",
|
||||
"x-instructions_desktop": "Dați clic pentru a trimite fișiere sau dați clic dreapta pentru a trimite un mesaj",
|
||||
"no-peers-title": "Deschideți PairDrop pe alte dispozitive pentru a trimite fișiere",
|
||||
"x-instructions_data-drop-peer": "Eliberare pentru a trimite la peer",
|
||||
"x-instructions_data-drop-bg": "Eliberați pentru a selecta recipientul",
|
||||
"no-peers_data-drop-bg": "Eliberare pentru a selecta recipientul"
|
||||
},
|
||||
"peer-ui": {
|
||||
"processing": "Procesarea…",
|
||||
"click-to-send-paste-mode": "Apasă pentru a trimite {{descriptor}}",
|
||||
"click-to-send": "Apasă pentru a trimite fișiere sau apasă cu butonul din dreapta pentru a trimite un mesaj",
|
||||
"waiting": "Așteptând…",
|
||||
"connection-hash": "Pentru a verifica securitatea criptării end-to-end, comparați acest număr de securitate pe ambele dispozitive",
|
||||
"preparing": "Pregătirea…",
|
||||
"transferring": "Transferul…"
|
||||
},
|
||||
"dialogs": {
|
||||
"base64-paste-to-send": "Lipiți aici pentru a trimite {{type}}",
|
||||
"auto-accept-instructions-2": "pentru a accepta automat toate fișierele trimise de la dispozitivul respectiv.",
|
||||
"receive-text-title": "Mesaj primit",
|
||||
"edit-paired-devices-title": "Editați dispozitivele asociate",
|
||||
"cancel": "Anulează",
|
||||
"auto-accept-instructions-1": "Activează",
|
||||
"pair-devices-title": "Împerecherea permanentă a dispozitivelor",
|
||||
"download": "Descarcă",
|
||||
"title-file": "Fişier",
|
||||
"base64-processing": "Procesarea…",
|
||||
"decline": "Declin",
|
||||
"receive-title": "{{descriptor}} Primit",
|
||||
"leave": "Pleacă",
|
||||
"join": "Alătură-te",
|
||||
"title-image-plural": "Imagini",
|
||||
"send": "Trimite",
|
||||
"base64-tap-to-paste": "Atinge aici pentru a lipi {{type}}",
|
||||
"base64-text": "text",
|
||||
"copy": "Copiază",
|
||||
"file-other-description-image": "și 1 altă imagine",
|
||||
"temporary-public-room-title": "Cameră publică temporară",
|
||||
"base64-files": "fişiere",
|
||||
"has-sent": "a trimis:",
|
||||
"file-other-description-file": "și 1 alt fișier",
|
||||
"close": "Închide",
|
||||
"system-language": "Limba Sistemului",
|
||||
"unpair": "Decuplează",
|
||||
"title-image": "Imagine",
|
||||
"file-other-description-file-plural": "și {{count}} alte fișiere",
|
||||
"would-like-to-share": "ar dori să împărtășească",
|
||||
"send-message-to": "Trimite un mesaj la",
|
||||
"language-selector-title": "Selectaţi Limba",
|
||||
"pair": "Cuplu",
|
||||
"hr-or": "SAU",
|
||||
"scan-qr-code": "sau scanați codul QR.",
|
||||
"input-key-on-this-device": "Introduceți această cheie pe un alt dispozitiv",
|
||||
"download-again": "Descarcă din nou",
|
||||
"accept": "Acceptă",
|
||||
"paired-devices-wrapper_data-empty": "Nu sunt dispozitive asociate.",
|
||||
"enter-key-from-another-device": "Introduceți aici cheia de la un alt dispozitiv.",
|
||||
"share": "Partajați",
|
||||
"auto-accept": "auto-acceptare",
|
||||
"title-file-plural": "Fişiere",
|
||||
"send-message-title": "Trimite un mesaj",
|
||||
"input-room-id-on-another-device": "Introduceți acest ID de cameră pe un alt dispozitiv",
|
||||
"file-other-description-image-plural": "și {{count}} alte imagini",
|
||||
"enter-room-id-from-another-device": "Introdu ID-ul camerei de pe un alt dispozitiv pentru a intra în cameră."
|
||||
},
|
||||
"about": {
|
||||
"claim": "Cel mai simplu mod de a transfera fișiere între dispozitive",
|
||||
"tweet_title": "Tweet despre PairDrop",
|
||||
"close-about_aria-label": "Închide Despre PairDrop",
|
||||
"buy-me-a-coffee_title": "Cumpără-mi o cafea!",
|
||||
"github_title": "PairDrop pe GitHub",
|
||||
"faq_title": "Întrebări frecvente"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-transfer-requested": "Transfer de fișiere cerut",
|
||||
"message-received-plural": "{{count}}} Mesaje primite",
|
||||
"message-received": "Mesaj primit",
|
||||
"file-received": "Fișier Primit",
|
||||
"file-received-plural": "{{count}} Fișiere Primite"
|
||||
}
|
||||
}
|
||||
160
public/lang/ru.json
Normal file
160
public/lang/ru.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"header": {
|
||||
"about_aria-label": "Открыть страницу \"О сервисе\"",
|
||||
"pair-device_title": "Связать ваши устройства навсегда",
|
||||
"install_title": "Установить PairDrop",
|
||||
"cancel-paste-mode": "Выполнено",
|
||||
"edit-paired-devices_title": "Редактировать связанные устройства",
|
||||
"notification_title": "Включить уведомления",
|
||||
"about_title": "О сервисе",
|
||||
"theme-auto_title": "Адаптировать тему к системной",
|
||||
"theme-dark_title": "Всегда использовать темную тему",
|
||||
"theme-light_title": "Всегда использовать светлую тему",
|
||||
"join-public-room_title": "Войти на время в публичную комнату",
|
||||
"language-selector_title": "Выбрать язык"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение",
|
||||
"no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||
"click-to-send": "Нажмите, чтобы отправить",
|
||||
"x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||
"tap-to-send": "Прикоснитесь, чтобы отправить",
|
||||
"x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу",
|
||||
"x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение",
|
||||
"no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы",
|
||||
"no-peers-subtitle": "Свяжите устройства или войдите в публичную комнату, чтобы вас могли обнаружить из других сетей",
|
||||
"activate-paste-mode-and-other-files": "и {{count}} других файлов",
|
||||
"activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить",
|
||||
"activate-paste-mode-shared-text": "общий текст"
|
||||
},
|
||||
"footer": {
|
||||
"display-name_data-placeholder": "Загрузка…",
|
||||
"routed": "направляется через сервер",
|
||||
"webrtc": ", если WebRTC недоступен.",
|
||||
"traffic": "Трафик",
|
||||
"paired-devices": "связанными устройствами",
|
||||
"known-as": "Вы известны под именем:",
|
||||
"on-this-network": "в этой сети",
|
||||
"display-name_title": "Изменить имя вашего устройства навсегда",
|
||||
"public-room-devices_title": "Вы можете быть обнаружены устройствами в этой публичной комнате вне зависимости от сети.",
|
||||
"paired-devices_title": "Вы можете быть обнаружены связанными устройствами в любое время вне зависимости от сети.",
|
||||
"public-room-devices": "в комнате {{roomId}}",
|
||||
"discovery": "Вы можете быть обнаружены:",
|
||||
"on-this-network_title": "Вы можете быть обнаружены кем угодно в этой сети."
|
||||
},
|
||||
"dialogs": {
|
||||
"edit-paired-devices-title": "Редактировать связанные устройства",
|
||||
"auto-accept": "автоприем",
|
||||
"close": "Закрыть",
|
||||
"decline": "Отклонить",
|
||||
"share": "Поделиться",
|
||||
"would-like-to-share": "хотел бы поделиться",
|
||||
"has-sent": "отправил:",
|
||||
"paired-devices-wrapper_data-empty": "Нет связанных устройств.",
|
||||
"download": "Скачать",
|
||||
"receive-text-title": "Сообщение получено",
|
||||
"send": "Отправить",
|
||||
"send-message-to": "Отправить сообщение",
|
||||
"send-message-title": "Отправить сообщение",
|
||||
"copy": "Копировать",
|
||||
"base64-files": "файлы",
|
||||
"base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}",
|
||||
"base64-processing": "Обработка…",
|
||||
"base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}",
|
||||
"base64-text": "текст",
|
||||
"title-file": "Файл",
|
||||
"title-file-plural": "Файлы",
|
||||
"title-image": "Изображение",
|
||||
"title-image-plural": "Изображения",
|
||||
"download-again": "Скачать еще раз",
|
||||
"auto-accept-instructions-2": ", чтобы автоматически принимать все файлы, отправленные с того устройства.",
|
||||
"enter-key-from-another-device": "Введите сюда ключ с другого устройства.",
|
||||
"pair-devices-title": "Соединить устройства навсегда",
|
||||
"input-key-on-this-device": "На другом устройстве введите этот ключ",
|
||||
"scan-qr-code": "или отсканируйте QR-код.",
|
||||
"cancel": "Отменить",
|
||||
"pair": "Подключить",
|
||||
"accept": "Принять",
|
||||
"auto-accept-instructions-1": "Активировать",
|
||||
"file-other-description-file": "и 1 другой файл",
|
||||
"file-other-description-image-plural": "и {{count}} других изображений",
|
||||
"file-other-description-image": "и 1 другое изображение",
|
||||
"file-other-description-file-plural": "и {{count}} других файлов",
|
||||
"receive-title": "{{descriptor}} получен",
|
||||
"system-language": "Язык системы",
|
||||
"unpair": "Отвязать",
|
||||
"language-selector-title": "Выбрать язык",
|
||||
"hr-or": "ИЛИ",
|
||||
"input-room-id-on-another-device": "На другом устройстве введите этот ID комнаты",
|
||||
"leave": "Покинуть",
|
||||
"join": "Войти",
|
||||
"enter-room-id-from-another-device": "Введите ID комнаты с другого устройства, чтобы войти в нее.",
|
||||
"temporary-public-room-title": "Временная публичная комната"
|
||||
},
|
||||
"about": {
|
||||
"close-about-aria-label": "Закрыть страницу \"О сервисе\"",
|
||||
"claim": "Самый простой способ передачи файлов между устройствами",
|
||||
"close-about_aria-label": "Закрыть страницу \"О сервисе\"",
|
||||
"buy-me-a-coffee_title": "Купить мне кофе!",
|
||||
"github_title": "PairDrop на GitHub",
|
||||
"tweet_title": "Твит о PairDrop",
|
||||
"faq_title": "Часто задаваемые вопросы"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "Отображаемое имя было изменено навсегда.",
|
||||
"display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.",
|
||||
"pairing-success": "Устройства связаны.",
|
||||
"pairing-tabs-error": "Связка двух вкладок браузера невозможна.",
|
||||
"copied-to-clipboard": "Скопировано в буфер обмена",
|
||||
"pairing-not-persistent": "Связанные устройства непостоянны.",
|
||||
"link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть",
|
||||
"notifications-enabled": "Уведомления включены.",
|
||||
"text-content-incorrect": "Содержание текста неверно.",
|
||||
"message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать",
|
||||
"connected": "Подключено.",
|
||||
"copied-text": "Текст скопирован в буфер обмена",
|
||||
"online": "Вы снова в сети",
|
||||
"offline": "Вы находитесь вне сети",
|
||||
"online-requirement": "Для сопряжения устройств вам нужно быть в сети.",
|
||||
"files-incorrect": "Файлы неверны.",
|
||||
"message-transfer-completed": "Передача сообщения завершена.",
|
||||
"ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз",
|
||||
"selected-peer-left": "Выбранный узел вышел.",
|
||||
"request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}",
|
||||
"rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.",
|
||||
"unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть PairDrop?",
|
||||
"copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!",
|
||||
"pairing-cleared": "Все устройства отвязаны.",
|
||||
"pairing-key-invalid": "Неверный ключ",
|
||||
"pairing-key-invalidated": "Ключ {{key}} признан недействительным.",
|
||||
"click-to-download": "Нажмите, чтобы скачать",
|
||||
"clipboard-content-incorrect": "Содержание буфера обмена неверно.",
|
||||
"click-to-show": "Нажмите, чтобы показать",
|
||||
"connecting": "Подключение…",
|
||||
"download-successful": "{{descriptor}} загружен",
|
||||
"display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.",
|
||||
"file-content-incorrect": "Содержимое файла неверно.",
|
||||
"file-transfer-completed": "Передача файла завершена.",
|
||||
"public-room-left": "Покинуть публичную комнату {{publicRoomId}}",
|
||||
"copied-to-clipboard-error": "Копирование невозможно. Скопируйте вручную.",
|
||||
"public-room-id-invalid": "Неверный ID комнаты",
|
||||
"online-requirement-pairing": "Для связки устройств необходимо находиться быть онлайн.",
|
||||
"online-requirement-public-room": "Для создания публичной комнаты необходимо быть онлайн."
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}",
|
||||
"preparing": "Подготовка…",
|
||||
"transferring": "Передача…",
|
||||
"processing": "Обработка…",
|
||||
"waiting": "Ожидание…",
|
||||
"connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах",
|
||||
"click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received-plural": "{{count}} файлов получено",
|
||||
"message-received-plural": "{{count}} сообщений получено",
|
||||
"file-received": "Файл получен",
|
||||
"file-transfer-requested": "Запрошена передача файлов",
|
||||
"message-received": "Сообщение получено"
|
||||
}
|
||||
}
|
||||
25
public/lang/tr.json
Normal file
25
public/lang/tr.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "PairDrop Hakkında",
|
||||
"about_aria-label": "PairDrop Hakkında Aç",
|
||||
"theme-auto_title": "Temayı Sisteme Uyarla",
|
||||
"theme-light_title": "Daima Açık Tema Kullan",
|
||||
"theme-dark_title": "Daima Koyu Tema Kullan",
|
||||
"notification_title": "Bildirimleri Etkinleştir",
|
||||
"install_title": "PairDrop'u Yükle",
|
||||
"pair-device_title": "Cihaz Eşleştir",
|
||||
"edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle",
|
||||
"cancel-paste-mode": "Bitti"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
|
||||
},
|
||||
"footer": {
|
||||
"display-name_data-placeholder": "Yükleniyor…",
|
||||
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "İptal",
|
||||
"edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle"
|
||||
}
|
||||
}
|
||||
159
public/lang/zh-CN.json
Normal file
159
public/lang/zh-CN.json
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "关于 PairDrop",
|
||||
"about_aria-label": "打开 关于 PairDrop",
|
||||
"theme-light_title": "总是使用明亮主题",
|
||||
"install_title": "安装 PairDrop",
|
||||
"pair-device_title": "永久配对您的设备",
|
||||
"theme-auto_title": "主题适应系统",
|
||||
"theme-dark_title": "总是使用暗黑主题",
|
||||
"notification_title": "开启通知",
|
||||
"edit-paired-devices_title": "管理已配对设备",
|
||||
"cancel-paste-mode": "完成",
|
||||
"join-public-room_title": "暂时加入公共房间",
|
||||
"language-selector_title": "选择语言"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_data-drop-peer": "释放以发送到此设备",
|
||||
"no-peers_data-drop-bg": "释放来选择接收者",
|
||||
"no-peers-subtitle": "配对新设备 或 加入一个公共房间 以便在其他网络上可见",
|
||||
"no-peers-title": "在其他设备上打开 PairDrop 来发送文件",
|
||||
"x-instructions_desktop": "点击以发送文件 或 右键来发送信息",
|
||||
"x-instructions_mobile": "轻触以发送文件 或 长按来发送信息",
|
||||
"x-instructions_data-drop-bg": "释放来选择接收者",
|
||||
"click-to-send": "点击发送",
|
||||
"tap-to-send": "轻触发送",
|
||||
"activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送",
|
||||
"activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件",
|
||||
"activate-paste-mode-shared-text": "分享文本"
|
||||
},
|
||||
"footer": {
|
||||
"routed": "途径服务器",
|
||||
"webrtc": "如果 WebRTC 不可用。",
|
||||
"known-as": "你的名字是:",
|
||||
"display-name_data-placeholder": "加载中…",
|
||||
"display-name_title": "修改你的默认设备名",
|
||||
"on-this-network": "在此网络上",
|
||||
"paired-devices": "已配对的设备",
|
||||
"traffic": "流量将",
|
||||
"public-room-devices_title": "您可以被这个独立于网络的公共房间中的设备发现。",
|
||||
"paired-devices_title": "您可以在任何时候被已配对的设备发现,而不依赖于网络。",
|
||||
"public-room-devices": "在房间 {{roomId}} 中",
|
||||
"discovery": "您可以被发现:",
|
||||
"on-this-network_title": "您可以被这个网络上的每个人发现。"
|
||||
},
|
||||
"dialogs": {
|
||||
"pair-devices-title": "配对新设备(常驻)",
|
||||
"input-key-on-this-device": "在另一个设备上输入这串数字",
|
||||
"base64-text": "信息",
|
||||
"enter-key-from-another-device": "在此处输入从另一个设备上获得的数字。",
|
||||
"edit-paired-devices-title": "管理已配对的设备",
|
||||
"pair": "配对",
|
||||
"cancel": "取消",
|
||||
"scan-qr-code": "或者 扫描二维码。",
|
||||
"paired-devices-wrapper_data-empty": "无已配对设备。",
|
||||
"auto-accept-instructions-1": "启用",
|
||||
"auto-accept": "自动接收",
|
||||
"decline": "拒绝",
|
||||
"base64-processing": "处理中…",
|
||||
"base64-tap-to-paste": "轻触此处粘贴{{type}}",
|
||||
"base64-paste-to-send": "粘贴到此处以发送 {{type}}",
|
||||
"auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。",
|
||||
"would-like-to-share": "想要分享",
|
||||
"accept": "接收",
|
||||
"close": "关闭",
|
||||
"share": "分享",
|
||||
"download": "保存",
|
||||
"send": "发送",
|
||||
"receive-text-title": "收到信息",
|
||||
"copy": "复制",
|
||||
"send-message-title": "发送信息",
|
||||
"send-message-to": "发了一条信息给",
|
||||
"has-sent": "发送了:",
|
||||
"base64-files": "文件",
|
||||
"file-other-description-file": "和 1 个其他的文件",
|
||||
"file-other-description-image": "和 1 个其他的图片",
|
||||
"file-other-description-image-plural": "和 {{count}} 个其他的图片",
|
||||
"file-other-description-file-plural": "和 {{count}} 个其他的文件",
|
||||
"title-image-plural": "图片",
|
||||
"receive-title": "收到 {{descriptor}}",
|
||||
"title-image": "图片",
|
||||
"title-file": "文件",
|
||||
"title-file-plural": "文件",
|
||||
"download-again": "再次保存",
|
||||
"system-language": "跟随系统语言",
|
||||
"unpair": "取消配对",
|
||||
"language-selector-title": "选择语言",
|
||||
"hr-or": "或者",
|
||||
"input-room-id-on-another-device": "在另一个设备上输入这串房间号",
|
||||
"leave": "离开",
|
||||
"join": "加入",
|
||||
"temporary-public-room-title": "临时公共房间",
|
||||
"enter-room-id-from-another-device": "在另一个设备上输入这串房间号来加入房间。"
|
||||
},
|
||||
"about": {
|
||||
"faq_title": "常见问题",
|
||||
"close-about_aria-label": "关闭 关于 PairDrop",
|
||||
"github_title": "PairDrop 在 GitHub 上开源",
|
||||
"claim": "最简单的跨设备传输方案",
|
||||
"buy-me-a-coffee_title": "帮我买杯咖啡!",
|
||||
"tweet_title": "关于 PairDrop 的推特"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "展示的名字已经永久变更。",
|
||||
"display-name-changed-temporarily": "展示的名字仅在此会话中变更。",
|
||||
"display-name-random-again": "展示的名字已再次随机生成。",
|
||||
"download-successful": "{{descriptor}} 已下载",
|
||||
"pairing-tabs-error": "无法配对两个浏览器标签页。",
|
||||
"pairing-success": "新设备已配对。",
|
||||
"pairing-not-persistent": "配对的设备不是持久的。",
|
||||
"pairing-key-invalid": "无效配对码",
|
||||
"pairing-key-invalidated": "配对码 {{key}} 已失效。",
|
||||
"text-content-incorrect": "文本内容不合法。",
|
||||
"file-content-incorrect": "文件内容不合法。",
|
||||
"clipboard-content-incorrect": "剪贴板内容不合法。",
|
||||
"link-received": "收到来自 {{name}} 的链接 - 点击打开",
|
||||
"message-received": "收到来自 {{name}} 的信息 - 点击复制",
|
||||
"request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}",
|
||||
"click-to-show": "点击展示",
|
||||
"copied-text": "复制到剪贴板",
|
||||
"selected-peer-left": "选择的设备已离开。",
|
||||
"pairing-cleared": "所有设备已解除配对。",
|
||||
"copied-to-clipboard": "已复制到剪贴板",
|
||||
"notifications-enabled": "通知已启用。",
|
||||
"copied-text-error": "写入剪贴板失败。请手动复制!",
|
||||
"click-to-download": "点击以保存",
|
||||
"unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭 PairDrop 吗?",
|
||||
"message-transfer-completed": "信息传输已完成。",
|
||||
"offline": "你未连接到网络",
|
||||
"online": "你已重新连接到网络",
|
||||
"connected": "已连接。",
|
||||
"online-requirement": "你需要连接网络来配对新设备。",
|
||||
"files-incorrect": "文件不合法。",
|
||||
"file-transfer-completed": "文件传输已完成。",
|
||||
"connecting": "连接中…",
|
||||
"ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB",
|
||||
"rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。",
|
||||
"public-room-left": "已退出公共房间 {{publicRoomId}}",
|
||||
"copied-to-clipboard-error": "无法复制。请手动复制。",
|
||||
"public-room-id-invalid": "无效的房间号",
|
||||
"online-requirement-pairing": "您需要连接到互联网来配对新设备。",
|
||||
"online-requirement-public-room": "您需要连接到互联网来创建一个公共房间。"
|
||||
},
|
||||
"document-titles": {
|
||||
"message-received": "收到信息",
|
||||
"message-received-plural": "收到 {{count}} 条信息",
|
||||
"file-transfer-requested": "文件传输请求",
|
||||
"file-received-plural": "收到 {{count}} 个文件",
|
||||
"file-received": "收到文件"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "点击发送 {{descriptor}}",
|
||||
"click-to-send": "点击以发送文件 或 右键来发送信息",
|
||||
"connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号",
|
||||
"preparing": "准备中…",
|
||||
"waiting": "请等待…",
|
||||
"transferring": "传输中…",
|
||||
"processing": "处理中…"
|
||||
}
|
||||
}
|
||||
145
public/scripts/localization.js
Normal file
145
public/scripts/localization.js
Normal file
@@ -0,0 +1,145 @@
|
||||
class Localization {
|
||||
constructor() {
|
||||
Localization.defaultLocale = "en";
|
||||
Localization.supportedLocales = ["en", "nb", "ru", "zh-CN", "de", "ro", "id", "fr"];
|
||||
Localization.translations = {};
|
||||
Localization.defaultTranslations = {};
|
||||
|
||||
Localization.systemLocale = Localization.supportedOrDefault(navigator.languages);
|
||||
|
||||
let storedLanguageCode = localStorage.getItem("language-code");
|
||||
|
||||
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
|
||||
? storedLanguageCode
|
||||
: Localization.systemLocale;
|
||||
|
||||
Localization.setTranslation(Localization.initialLocale)
|
||||
.then(_ => {
|
||||
console.log("Initial translation successful.");
|
||||
Events.fire("initial-translation-loaded");
|
||||
});
|
||||
}
|
||||
|
||||
static isSupported(locale) {
|
||||
return Localization.supportedLocales.indexOf(locale) > -1;
|
||||
}
|
||||
|
||||
static supportedOrDefault(locales) {
|
||||
return locales.find(Localization.isSupported) || Localization.defaultLocale;
|
||||
}
|
||||
|
||||
static async setTranslation(locale) {
|
||||
if (!locale) locale = Localization.systemLocale;
|
||||
|
||||
await Localization.setLocale(locale)
|
||||
await Localization.translatePage();
|
||||
|
||||
console.log("Page successfully translated",
|
||||
`System language: ${Localization.systemLocale}`,
|
||||
`Selected language: ${locale}`
|
||||
);
|
||||
|
||||
Events.fire("translation-loaded");
|
||||
}
|
||||
|
||||
static async setLocale(newLocale) {
|
||||
if (newLocale === Localization.locale) return false;
|
||||
|
||||
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
|
||||
|
||||
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
|
||||
|
||||
if(!newTranslations) return false;
|
||||
|
||||
Localization.locale = newLocale;
|
||||
Localization.translations = newTranslations;
|
||||
}
|
||||
|
||||
static getLocale() {
|
||||
return Localization.locale;
|
||||
}
|
||||
|
||||
static isSystemLocale() {
|
||||
return !localStorage.getItem('language-code');
|
||||
}
|
||||
|
||||
static async fetchTranslationsFor(newLocale) {
|
||||
const response = await fetch(`lang/${newLocale}.json`)
|
||||
|
||||
if (response.redirected === true || response.status !== 200) return false;
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
static async translatePage() {
|
||||
document
|
||||
.querySelectorAll("[data-i18n-key]")
|
||||
.forEach(element => Localization.translateElement(element));
|
||||
}
|
||||
|
||||
static async translateElement(element) {
|
||||
const key = element.getAttribute("data-i18n-key");
|
||||
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
|
||||
|
||||
for (let i in attrs) {
|
||||
let attr = attrs[i];
|
||||
if (attr === "text") {
|
||||
element.innerText = Localization.getTranslation(key);
|
||||
} else {
|
||||
if (attr.startsWith("data-")) {
|
||||
let dataAttr = attr.substring(5);
|
||||
element.dataset.dataAttr = Localization.getTranslation(key, attr);
|
||||
} {
|
||||
element.setAttribute(attr, Localization.getTranslation(key, attr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getTranslation(key, attr=null, data={}, useDefault=false) {
|
||||
const keys = key.split(".");
|
||||
|
||||
let translationCandidates = useDefault
|
||||
? Localization.defaultTranslations
|
||||
: Localization.translations;
|
||||
|
||||
let translation;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
translationCandidates = translationCandidates[keys[i]]
|
||||
}
|
||||
|
||||
let lastKey = keys[keys.length - 1];
|
||||
|
||||
if (attr) lastKey += "_" + attr;
|
||||
|
||||
translation = translationCandidates[lastKey];
|
||||
|
||||
for (let j in data) {
|
||||
translation = translation.replace(`{{${j}}}`, data[j]);
|
||||
}
|
||||
} catch (e) {
|
||||
translation = "";
|
||||
}
|
||||
|
||||
if (!translation) {
|
||||
if (!useDefault) {
|
||||
translation = this.getTranslation(key, attr, data, true);
|
||||
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
|
||||
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`)
|
||||
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
|
||||
} else {
|
||||
console.warn("Missing translation in default language:", key, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return Localization.escapeHTML(translation);
|
||||
}
|
||||
|
||||
static escapeHTML(unsafeText) {
|
||||
let div = document.createElement('div');
|
||||
div.innerText = unsafeText;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,14 @@ class ServerConnection {
|
||||
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
||||
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
||||
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
||||
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
||||
|
||||
Events.on('create-public-room', _ => this._onCreatePublicRoom());
|
||||
Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid));
|
||||
Events.on('leave-public-room', _ => this._onLeavePublicRoom());
|
||||
|
||||
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
@@ -46,23 +50,47 @@ class ServerConnection {
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
Events.fire('ws-connected');
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-pairing"));
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-initiate' })
|
||||
this.send({ type: 'pair-device-initiate' });
|
||||
}
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
_onPairDeviceJoin(pairKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
this.send({ type: 'pair-device-join', pairKey: pairKey });
|
||||
}
|
||||
|
||||
_onCreatePublicRoom() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-public-room"));
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'create-public-room' });
|
||||
}
|
||||
|
||||
_onJoinPublicRoom(roomId, createIfInvalid) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
|
||||
}
|
||||
|
||||
_onLeavePublicRoom() {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onLeavePublicRoom(), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'leave-public-room' });
|
||||
}
|
||||
|
||||
_setRtcConfig(config) {
|
||||
@@ -104,10 +132,10 @@ class ServerConnection {
|
||||
Events.fire('pair-device-join-key-invalid');
|
||||
break;
|
||||
case 'pair-device-canceled':
|
||||
Events.fire('pair-device-canceled', msg.roomKey);
|
||||
Events.fire('pair-device-canceled', msg.pairKey);
|
||||
break;
|
||||
case 'pair-device-join-key-rate-limit':
|
||||
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
|
||||
case 'join-key-rate-limit':
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
|
||||
break;
|
||||
case 'secret-room-deleted':
|
||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||
@@ -115,6 +143,15 @@ class ServerConnection {
|
||||
case 'room-secret-regenerated':
|
||||
Events.fire('room-secret-regenerated', msg);
|
||||
break;
|
||||
case 'public-room-id-invalid':
|
||||
Events.fire('public-room-id-invalid', msg.publicRoomId);
|
||||
break;
|
||||
case 'public-room-created':
|
||||
Events.fire('public-room-created', msg.roomId);
|
||||
break;
|
||||
case 'public-room-left':
|
||||
Events.fire('public-room-left');
|
||||
break;
|
||||
default:
|
||||
console.error('WS receive: unknown message type', msg);
|
||||
}
|
||||
@@ -132,8 +169,8 @@ class ServerConnection {
|
||||
|
||||
_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);
|
||||
sessionStorage.setItem('peer_id', msg.message.peerId);
|
||||
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
|
||||
|
||||
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
||||
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
||||
@@ -155,8 +192,8 @@ class ServerConnection {
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
const peerId = sessionStorage.getItem('peer_id');
|
||||
const peerIdHash = sessionStorage.getItem('peer_id_hash');
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
@@ -167,7 +204,7 @@ class ServerConnection {
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerId = sessionStorage.getItem('peer_id');
|
||||
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
||||
console.log("successfully removed peerId from localStorage");
|
||||
});
|
||||
@@ -183,7 +220,9 @@ class ServerConnection {
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
setTimeout(() => {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.connecting"));
|
||||
}, 100); //delay for 100ms to prevent flickering on page reload
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
@@ -215,12 +254,13 @@ class ServerConnection {
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||
this._server = serverConnection;
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._roomType = roomType;
|
||||
this._updateRoomSecret(roomSecret);
|
||||
|
||||
this._roomIds = {};
|
||||
this._updateRoomIds(roomType, roomId);
|
||||
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
@@ -241,34 +281,58 @@ class Peer {
|
||||
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
||||
}
|
||||
|
||||
_updateRoomSecret(roomSecret) {
|
||||
_isPaired() {
|
||||
return !!this._roomIds['secret'];
|
||||
}
|
||||
|
||||
_getPairSecret() {
|
||||
return this._roomIds['secret'];
|
||||
}
|
||||
|
||||
_getRoomTypes() {
|
||||
return Object.keys(this._roomIds);
|
||||
}
|
||||
|
||||
_updateRoomIds(roomType, roomId) {
|
||||
// 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);
|
||||
})
|
||||
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
|
||||
// multiple roomSecrets with same peer -> delete old roomSecret
|
||||
PersistentStorage.deleteRoomSecret(this._getPairSecret())
|
||||
.then(deletedRoomSecret => {
|
||||
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
||||
});
|
||||
}
|
||||
|
||||
this._roomSecret = roomSecret;
|
||||
this._roomIds[roomType] = roomId;
|
||||
|
||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
|
||||
// increase security by increasing roomSecret length
|
||||
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
|
||||
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
|
||||
console.log('RoomSecret is regenerated to increase security')
|
||||
Events.fire('regenerate-room-secret', this._roomSecret);
|
||||
Events.fire('regenerate-room-secret', this._getPairSecret());
|
||||
}
|
||||
}
|
||||
|
||||
_removeRoomType(roomType) {
|
||||
delete this._roomIds[roomType];
|
||||
|
||||
Events.fire('room-type-removed', {
|
||||
peerId: this._peerId,
|
||||
roomType: roomType
|
||||
});
|
||||
}
|
||||
|
||||
_evaluateAutoAccept() {
|
||||
if (!this._roomSecret) {
|
||||
if (!this._isPaired()) {
|
||||
this._setAutoAccept(false);
|
||||
return;
|
||||
}
|
||||
|
||||
PersistentStorage.getRoomSecretEntry(this._roomSecret)
|
||||
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
|
||||
.then(roomSecretEntry => {
|
||||
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
|
||||
const autoAccept = roomSecretEntry
|
||||
? roomSecretEntry.entry.auto_accept
|
||||
: false;
|
||||
this._setAutoAccept(autoAccept);
|
||||
})
|
||||
.catch(_ => {
|
||||
@@ -277,7 +341,9 @@ class Peer {
|
||||
}
|
||||
|
||||
_setAutoAccept(autoAccept) {
|
||||
this._autoAccept = autoAccept;
|
||||
this._autoAccept = !this._isSameBrowser()
|
||||
? autoAccept
|
||||
: false;
|
||||
}
|
||||
|
||||
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||
@@ -488,7 +554,7 @@ class Peer {
|
||||
|
||||
_abortTransfer() {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||
Events.fire('notify-user', 'Files are incorrect.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
this._digester = null;
|
||||
@@ -536,7 +602,7 @@ class Peer {
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -546,7 +612,7 @@ class Peer {
|
||||
this._chunker = null;
|
||||
if (!this._filesQueue.length) {
|
||||
this._busy = false;
|
||||
Events.fire('notify-user', 'File transfer completed.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
|
||||
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
|
||||
} else {
|
||||
this._dequeueFile();
|
||||
@@ -558,7 +624,7 @@ class Peer {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||
this._filesRequested = null;
|
||||
if (message.reason === 'ios-memory-limit') {
|
||||
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once");
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -568,7 +634,7 @@ class Peer {
|
||||
}
|
||||
|
||||
_onMessageTransferCompleted() {
|
||||
Events.fire('notify-user', 'Message transfer completed.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
|
||||
}
|
||||
|
||||
sendText(text) {
|
||||
@@ -599,8 +665,8 @@ class Peer {
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomSecret);
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomId);
|
||||
this.rtcSupported = true;
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._connect();
|
||||
@@ -626,13 +692,17 @@ class RTCPeer extends Peer {
|
||||
|
||||
_openChannel() {
|
||||
if (!this._conn) return;
|
||||
|
||||
const channel = this._conn.createDataChannel('data-channel', {
|
||||
ordered: true,
|
||||
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
|
||||
});
|
||||
channel.onopen = e => this._onChannelOpened(e);
|
||||
channel.onerror = e => this._onError(e);
|
||||
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
|
||||
|
||||
this._conn.createOffer()
|
||||
.then(d => this._onDescription(d))
|
||||
.catch(e => this._onError(e));
|
||||
}
|
||||
|
||||
_onDescription(description) {
|
||||
@@ -713,7 +783,7 @@ class RTCPeer extends Peer {
|
||||
_onBeforeUnload(e) {
|
||||
if (this._busy) {
|
||||
e.preventDefault();
|
||||
return "There are unfinished transfers. Are you sure you want to close?";
|
||||
return Localization.getTranslation("notifications.unfinished-transfers-warning");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,8 +842,8 @@ class RTCPeer extends Peer {
|
||||
_sendSignal(signal) {
|
||||
signal.type = 'signal';
|
||||
signal.to = this._peerId;
|
||||
signal.roomType = this._roomType;
|
||||
signal.roomSecret = this._roomSecret;
|
||||
signal.roomType = this._getRoomTypes()[0];
|
||||
signal.roomId = this._roomIds[this._getRoomTypes()[0]];
|
||||
this._server.send(signal);
|
||||
}
|
||||
|
||||
@@ -815,7 +885,14 @@ class PeersManager {
|
||||
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));
|
||||
|
||||
// this device closes connection
|
||||
Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));
|
||||
Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));
|
||||
|
||||
// peer closes connection
|
||||
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));
|
||||
@@ -828,47 +905,43 @@ class PeersManager {
|
||||
this.peers[peerId].onServerMessage(message);
|
||||
}
|
||||
|
||||
_refreshPeer(peer, roomType, roomSecret) {
|
||||
_refreshPeer(peer, roomType, roomId) {
|
||||
if (!peer) return false;
|
||||
|
||||
const roomTypeIsSecret = roomType === "secret";
|
||||
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
|
||||
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
|
||||
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
|
||||
|
||||
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
|
||||
if (roomTypeIsSecret && roomSecretsDiffer) {
|
||||
peer._updateRoomSecret(roomSecret);
|
||||
// if roomType or roomId for roomType differs peer is already connected
|
||||
// -> only update roomSecret and reevaluate auto accept
|
||||
if (roomTypesDiffer || roomIdsDiffer) {
|
||||
peer._updateRoomIds(roomType, roomId);
|
||||
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) {
|
||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer) {
|
||||
this._refreshPeer(peer, roomType, roomSecret);
|
||||
this._refreshPeer(peer, roomType, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret);
|
||||
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
|
||||
}
|
||||
|
||||
_onPeerJoined(message) {
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret);
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId);
|
||||
}
|
||||
|
||||
_onPeers(message) {
|
||||
message.peers.forEach(peer => {
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret);
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -899,7 +972,7 @@ class PeersManager {
|
||||
_onPeerLeft(message) {
|
||||
if (message.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', message.peerId);
|
||||
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
|
||||
|
||||
// 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
|
||||
@@ -923,14 +996,42 @@ class PeersManager {
|
||||
if (peer._channel) peer._channel.onclose = null;
|
||||
peer._conn.close();
|
||||
peer._busy = false;
|
||||
peer._roomIds = {};
|
||||
}
|
||||
|
||||
_onRoomSecretsDeleted(roomSecrets) {
|
||||
for (let i=0; i<roomSecrets.length; i++) {
|
||||
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecrets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_onLeavePublicRoom(publicRoomId) {
|
||||
this._disconnectOrRemoveRoomTypeByRoomId('public-id', publicRoomId);
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
|
||||
this._onPeerDisconnected(peerId);
|
||||
}
|
||||
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecret);
|
||||
}
|
||||
|
||||
_disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {
|
||||
const peerIds = this._getPeerIdsFromRoomId(roomId);
|
||||
|
||||
if (!peerIds.length) return;
|
||||
|
||||
for (let i=0; i<peerIds.length; i++) {
|
||||
this._disconnectOrRemoveRoomTypeByPeerId(peerIds[i], roomType);
|
||||
}
|
||||
}
|
||||
|
||||
_disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {
|
||||
const peer = this.peers[peerId];
|
||||
|
||||
if (!peer) return;
|
||||
|
||||
if (peer._getRoomTypes().length > 1) {
|
||||
peer._removeRoomType(roomType);
|
||||
} else {
|
||||
Events.fire('peer-disconnected', peerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,20 +1062,26 @@ class PeersManager {
|
||||
}
|
||||
|
||||
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
||||
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
|
||||
const peerId = this._getPeerIdsFromRoomId(roomSecret)[0];
|
||||
|
||||
if (!peerId) return;
|
||||
|
||||
this.peers[peerId]._setAutoAccept(autoAccept);
|
||||
}
|
||||
|
||||
_getPeerIdFromRoomSecret(roomSecret) {
|
||||
_getPeerIdsFromRoomId(roomId) {
|
||||
if (!roomId) return [];
|
||||
|
||||
let peerIds = []
|
||||
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;
|
||||
|
||||
// peer must have same roomId.
|
||||
if (Object.values(peer._roomIds).includes(roomId)) {
|
||||
peerIds.push(peer._peerId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return peerIds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,7 +1167,7 @@ class FileDigester {
|
||||
}
|
||||
|
||||
class Events {
|
||||
static fire(type, detail) {
|
||||
static fire(type, detail = {}) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
|
||||
1009
public/scripts/ui.js
1009
public/scripts/ui.js
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.7.7';
|
||||
const cacheVersion = 'v1.8.3';
|
||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
--icon-size: 24px;
|
||||
--primary-color: #4285f4;
|
||||
--paired-device-color: #00a69c;
|
||||
--public-room-color: #db8500;
|
||||
--accent-color: var(--primary-color);
|
||||
--peer-width: 120px;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
@@ -23,6 +25,7 @@ body {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
transition: color 300ms;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -40,6 +43,10 @@ html {
|
||||
min-height: fill-available;
|
||||
}
|
||||
|
||||
.fw {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
@@ -51,7 +58,6 @@ html {
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -78,6 +84,10 @@ html {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
align-items: baseline;
|
||||
@@ -215,10 +225,6 @@ a,
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -275,8 +281,6 @@ x-noscript {
|
||||
margin-top: 56px;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 132px;
|
||||
max-height: calc(100vh - 56px - var(--footer-height));
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
@@ -284,11 +288,6 @@ x-noscript {
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 425px) {
|
||||
header:has(#edit-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Peers List */
|
||||
|
||||
@@ -416,8 +415,9 @@ x-no-peers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
height: 137px;
|
||||
text-align: center;
|
||||
animation: fade-in 300ms;
|
||||
animation: fade-in 600ms;
|
||||
animation-fill-mode: backwards;
|
||||
/* prevent flickering on load */
|
||||
animation-iteration-count: 0;
|
||||
@@ -442,7 +442,7 @@ x-no-peers::before {
|
||||
}
|
||||
|
||||
x-no-peers[drop-bg]::before {
|
||||
content: "Release to select recipient";
|
||||
content: attr(data-drop-bg);
|
||||
}
|
||||
|
||||
x-no-peers[drop-bg] * {
|
||||
@@ -461,7 +461,6 @@ x-peer {
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
@@ -490,10 +489,14 @@ x-peer .icon-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||
x-peer.type-secret .icon-wrapper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
|
||||
background: var(--public-room-color);
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
@@ -502,17 +505,29 @@ x-peer x-icon > .highlight-wrapper {
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||
width: 6px;
|
||||
width: 15px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
border-radius: 4px;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||
x-peer.type-ip x-icon > .highlight-wrapper > .highlight.highlight-room-ip {
|
||||
background-color: var(--primary-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight.highlight-room-secret {
|
||||
background-color: var(--paired-device-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer.type-public-id x-icon > .highlight-wrapper > .highlight.highlight-room-public-id {
|
||||
background-color: var(--public-room-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer:not([status]):hover x-icon,
|
||||
x-peer:not([status]):focus x-icon {
|
||||
transform: scale(1.05);
|
||||
@@ -553,22 +568,6 @@ x-peer[status] x-icon {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
x-peer[status=transfer] .status:before {
|
||||
content: 'Transferring...';
|
||||
}
|
||||
|
||||
x-peer[status=prepare] .status:before {
|
||||
content: 'Preparing...';
|
||||
}
|
||||
|
||||
x-peer[status=wait] .status:before {
|
||||
content: 'Waiting...';
|
||||
}
|
||||
|
||||
x-peer[status=process] .status:before {
|
||||
content: 'Processing...';
|
||||
}
|
||||
|
||||
x-peer:not([status]) .status,
|
||||
x-peer[status] .device-name {
|
||||
display: none;
|
||||
@@ -602,13 +601,11 @@ x-peer[drop] x-icon {
|
||||
|
||||
footer {
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
z-index: 2;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
margin: auto 5px 5px;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
@@ -616,45 +613,75 @@ footer .logo {
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
margin-top: -10px;
|
||||
animation: ease-in;
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
color: var(--primary-color);
|
||||
margin: auto 18px;
|
||||
.discovery-wrapper {
|
||||
font-size: 12px;
|
||||
margin: 10px auto auto;
|
||||
border: 3px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 2px;
|
||||
background-color: rgb(var(--bg-color));
|
||||
transition: background-color 0.5s ease;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
#on-this-network {
|
||||
border-bottom: solid 4px var(--primary-color);
|
||||
padding-bottom: 1px;
|
||||
/*You can be discovered wrapper*/
|
||||
.discovery-wrapper > div:first-of-type {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
#paired-devices {
|
||||
border-bottom: solid 4px var(--paired-device-color);
|
||||
padding-bottom: 1px;
|
||||
|
||||
.discovery-wrapper .badge {
|
||||
word-break: keep-all;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: 0.3rem/0.3rem;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
background-color: var(--badge-color);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-room-ip {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.badge-room-secret {
|
||||
background-color: var(--paired-device-color);
|
||||
border-color: var(--paired-device-color);
|
||||
}
|
||||
|
||||
.badge-room-public-id {
|
||||
background-color: var(--public-room-color);
|
||||
border-color: var(--public-room-color);
|
||||
}
|
||||
|
||||
#display-name {
|
||||
position: relative;
|
||||
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;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
@@ -663,7 +690,6 @@ footer .font-body2 {
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
@@ -681,7 +707,6 @@ x-dialog x-paper {
|
||||
z-index: 3;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
@@ -690,14 +715,33 @@ x-dialog x-paper {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper {
|
||||
x-paper > .row:first-of-type {
|
||||
background-color: var(--accent-color);
|
||||
border-bottom: solid 4px var(--border-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
x-paper > .row:first-of-type h2 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#pair-device-dialog,
|
||||
#edit-paired-devices-dialog {
|
||||
--accent-color: var(--paired-device-color);
|
||||
}
|
||||
|
||||
#public-room-dialog {
|
||||
--accent-color: var(--public-room-color);
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper,
|
||||
#public-room-dialog x-paper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
#pair-device-dialog ::-moz-selection,
|
||||
@@ -706,6 +750,12 @@ x-dialog x-paper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
#public-room-dialog ::-moz-selection,
|
||||
#public-room-dialog ::selection {
|
||||
color: black;
|
||||
background: var(--public-room-color);
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -723,24 +773,22 @@ x-dialog a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
x-dialog .font-subheading {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
/* Pair Devices Dialog & Public Room Dialog */
|
||||
|
||||
/* Pair Devices Dialog */
|
||||
|
||||
#key-input-container {
|
||||
.input-key-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#key-input-container > input {
|
||||
.input-key-container > input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
display: -webkit-box !important;
|
||||
display: -webkit-flex !important;
|
||||
display: -moz-flex !important;
|
||||
@@ -751,15 +799,15 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#key-input-container > input + * {
|
||||
.input-key-container > input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#key-input-container > input:nth-of-type(4) {
|
||||
.input-key-container.six-chars > input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#room-key {
|
||||
.key {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
@@ -770,13 +818,48 @@ x-dialog .font-subheading {
|
||||
margin: 15px -15px;
|
||||
}
|
||||
|
||||
#room-key-qr-code {
|
||||
.key-qr-code {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.key-instructions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
x-dialog hr {
|
||||
margin: 40px -24px 30px -24px;
|
||||
border: solid 1.25px var(--border-color);
|
||||
height: 3px;
|
||||
border: none;
|
||||
width: 100%;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.hr-note {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hr-note hr {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.hr-note > div {
|
||||
height: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
|
||||
.hr-note > div > span {
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
color: rgb(var(--text-color));
|
||||
background-color: rgb(var(--bg-color));
|
||||
border: var(--border-color) solid 3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-background {
|
||||
@@ -785,7 +868,7 @@ x-dialog hr {
|
||||
|
||||
/* Edit Paired Devices Dialog */
|
||||
.paired-devices-wrapper:empty:before {
|
||||
content: "No paired devices.";
|
||||
content: attr(data-empty);
|
||||
}
|
||||
|
||||
.paired-devices-wrapper:empty {
|
||||
@@ -870,39 +953,34 @@ x-dialog hr {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.paired-device > .auto-accept {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Receive Dialog */
|
||||
|
||||
x-dialog .row {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
x-paper > .row {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
x-paper > .button-row {
|
||||
border-top: solid 3px var(--border-color);
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button {
|
||||
x-paper > .button-row > .button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
x-paper > .button-row > .button:not(:first-child) {
|
||||
border-right: solid 1.5px var(--border-color);
|
||||
}
|
||||
|
||||
x-paper > .button-row > .button:not(:last-child) {
|
||||
border-left: solid 1.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
margin: 0
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.file-description span {
|
||||
@@ -913,23 +991,29 @@ x-paper > div:last-child > .button:not(:last-child) {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
/* Todo: add pair underline to send / receive dialogs displayName */
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
#send-text-dialog .display-name-wrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#send-text-dialog .textarea {
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
@@ -944,7 +1028,6 @@ x-dialog .dialog-subheader {
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receive-text-dialog #text a {
|
||||
@@ -971,12 +1054,11 @@ x-dialog .dialog-subheader {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64-paste-dialog .textarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
@@ -988,21 +1070,9 @@ x-dialog .dialog-subheader {
|
||||
color: var(--primary-color);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
content: attr(placeholder);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#base64-paste-dialog button {
|
||||
margin: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64-paste-dialog button[close] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#base64-paste-dialog button[close]:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
|
||||
@@ -1019,12 +1089,13 @@ x-dialog .dialog-subheader {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
color: var(--accent-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
color: #5B5B66;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1058,6 +1129,11 @@ x-dialog .dialog-subheader {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.button[selected],
|
||||
.icon-button[selected] {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin: 0;
|
||||
@@ -1100,8 +1176,7 @@ button::-moz-focus-inner {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16px 24px;
|
||||
border-radius: 16px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
background: #f1f3f4;
|
||||
@@ -1128,15 +1203,22 @@ button::-moz-focus-inner {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#about:not(:target) header.fade-in {
|
||||
transition-delay: 400ms;
|
||||
}
|
||||
|
||||
#about:target header.fade-in {
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
|
||||
#about .fade-in {
|
||||
transition: opacity 300ms;
|
||||
transition: opacity 300ms ease 300ms;
|
||||
will-change: opacity;
|
||||
transition-delay: 300ms;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#about:not(:target) .fade-in {
|
||||
opacity: 0;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
@@ -1186,10 +1268,6 @@ button::-moz-focus-inner {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#about header {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
canvas.circles {
|
||||
width: 100vw;
|
||||
position: absolute;
|
||||
@@ -1273,7 +1351,6 @@ x-toast:not([show]):not(:hover) {
|
||||
x-instructions {
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
text-align: center;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
@@ -1288,11 +1365,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
}
|
||||
|
||||
x-instructions[drop-peer]:before {
|
||||
content: "Release to send to peer";
|
||||
content: attr(data-drop-peer);
|
||||
}
|
||||
|
||||
x-instructions[drop-bg]:not([drop-peer]):before {
|
||||
content: "Release to select recipient";
|
||||
content: attr(data-drop-bg);
|
||||
}
|
||||
|
||||
x-instructions p {
|
||||
@@ -1300,7 +1377,7 @@ x-instructions p {
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
@@ -1310,16 +1387,23 @@ x-peers:empty~x-instructions {
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
/* Prevent Cumulative Layout Shift */
|
||||
|
||||
body > header,
|
||||
canvas,
|
||||
#center,
|
||||
x-no-peers,
|
||||
x-peers,
|
||||
x-instructions,
|
||||
footer > .icon.logo,
|
||||
.discovery-wrapper,
|
||||
.known-as-wrapper {
|
||||
transition: opacity 0.5s ease 0.1s;
|
||||
opacity: 0; /* will be set to 1 after initial translation is loaded */
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
|
||||
@media screen and (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
@@ -1341,8 +1425,9 @@ body {
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-test: 18,18,18;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
--bg-color-secondary: #e4e4e4;
|
||||
--border-color: rgb(169, 169, 169);
|
||||
--badge-color: #a5a5a5;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
@@ -1350,7 +1435,8 @@ body.dark-theme {
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
--border-color: rgb(238,238,238);
|
||||
--badge-color: #717171;
|
||||
}
|
||||
|
||||
/* Colored Elements */
|
||||
@@ -1384,7 +1470,7 @@ x-dialog x-paper {
|
||||
|
||||
/* Image/Video/Audio Preview */
|
||||
.file-preview {
|
||||
margin: 10px -24px 40px -24px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.file-preview:empty {
|
||||
@@ -1408,15 +1494,17 @@ x-dialog x-paper {
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
--border-color: rgb(238,238,238);
|
||||
--badge-color: #717171;
|
||||
}
|
||||
|
||||
/* Override dark mode with light mode styles if the user decides to swap */
|
||||
body.light-theme {
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
--bg-color-secondary: #e4e4e4;
|
||||
--border-color: rgb(169, 169, 169);
|
||||
--badge-color: #a5a5a5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,107 +39,175 @@
|
||||
|
||||
<body translate="no">
|
||||
<header class="row-reverse">
|
||||
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
|
||||
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label" title="About PairDrop" aria-label="Open About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title" title="Select Language">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-language-selector" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="theme-wrapper">
|
||||
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
|
||||
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme-auto" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
|
||||
<div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title" 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" >
|
||||
<div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title" 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>
|
||||
<div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" title="Enable Notifications" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="install" class="icon-button" title="Install PairDrop" hidden>
|
||||
<div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" title="Install PairDrop" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title" title="Pair Your Devices Permanently">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
|
||||
<div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" title="Edit Paired Devices" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#edit-pair-devices-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
||||
<div id="join-public-room" class="icon-button" data-i18n-key="header.join-public-room" data-i18n-attrs="title" title="Join Public Room Temporarily">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#public-room-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden>Done</div>
|
||||
</header>
|
||||
<!-- Center -->
|
||||
<div id="center">
|
||||
<!-- Peers -->
|
||||
<div class="x-peers-filler"></div>
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open PairDrop on other devices to send files</h2>
|
||||
<div>Pair devices to be discoverable on other networks</div>
|
||||
<x-no-peers data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg" data-drop-bg="Release to select recipient">
|
||||
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text">Open PairDrop on other devices to send files</h2>
|
||||
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text">Pair devices or enter a public room to be discoverable on other networks</div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
||||
<x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"
|
||||
desktop="Click to send files or right click to send a message"
|
||||
mobile="Tap to send files or long tap to send a message"
|
||||
data-drop-peer="Release to send to peer"
|
||||
data-drop-bg="Release to select recipient">
|
||||
<p id="paste-filename"></p>
|
||||
</x-instructions>
|
||||
<div id="websocket-fallback">
|
||||
<span data-i18n-key="footer.traffic" data-i18n-attrs="text">Traffic is</span>
|
||||
<span data-i18n-key="footer.routed" data-i18n-attrs="text">routed through the server</span>
|
||||
<span data-i18n-key="footer.webrtc" data-i18n-attrs="text">if WebRTC is not available.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="column">
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
</div>
|
||||
<div id="websocket-fallback">
|
||||
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span>
|
||||
<div class="column">
|
||||
<div class="known-as-wrapper">
|
||||
<span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
|
||||
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title"
|
||||
placeholder="Loading..." data-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="discovery-wrapper row">
|
||||
<div class="row center">
|
||||
<span data-i18n-key="footer.discovery" data-i18n-attrs="text">You can be discovered:</span>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title">on this network</span>
|
||||
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden>paired devices</span>
|
||||
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Language Select Dialog -->
|
||||
<x-dialog id="language-select-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<div class="row center">
|
||||
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text">Select Language</h2>
|
||||
</div>
|
||||
<div class="language-buttons">
|
||||
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text">System Language</button>
|
||||
<button class="button fw" value="en">English</button>
|
||||
<button class="button fw" value="de">Deutsch (German)</button>
|
||||
<button class="button fw" value="fr">Français (French)</button>
|
||||
<button class="button fw" value="id">Bahasa Indonesia (Indonesian)</button>
|
||||
<button class="button fw" value="nb">Norsk (Norwegian)</button>
|
||||
<button class="button fw" value="ro">Română (Romanian)</button>
|
||||
<button class="button fw" value="ru">Русский язык (Russian)</button>
|
||||
<button class="button fw" value="zh-CN">中文 (Chinese)</button>
|
||||
</div>
|
||||
<div class="center row-reverse button-row">
|
||||
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Pair Device Dialog -->
|
||||
<x-dialog id="pair-device-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Pair Devices</h2>
|
||||
<div id="room-key-qr-code" class="center"></div>
|
||||
<h1 id="room-key" class="center">000 000</h1>
|
||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
||||
<hr>
|
||||
<div id="key-input-container">
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<div class="row center">
|
||||
<h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text">Pair Devices</h2>
|
||||
</div>
|
||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" disabled>Pair</button>
|
||||
<button class="button" type="button" close>Cancel</button>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="center key-qr-code"></div>
|
||||
<h1 class="center key">000 000</h1>
|
||||
<p class="center text-center key-instructions">
|
||||
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text">Input this key on another device</span>
|
||||
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-note">
|
||||
<hr>
|
||||
<div>
|
||||
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="input-key-container six-chars">
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="tel" class="textarea center" aria-label="pair-key-char-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device here.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row row-reverse">
|
||||
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
|
||||
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -150,13 +218,70 @@
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center">Edit Paired Devices</h2>
|
||||
<div class="paired-devices-wrapper"></div>
|
||||
<div class="font-subheading center">
|
||||
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
|
||||
<div class="row center">
|
||||
<h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text">Edit Paired Devices</h2>
|
||||
</div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="button" close>Close</button>
|
||||
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty" data-empty="No paired devices."></div>
|
||||
<div class="font-subheading center">
|
||||
<p>
|
||||
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text">
|
||||
Activate
|
||||
</span>
|
||||
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text">auto-accept</u>
|
||||
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text">
|
||||
to automatically accept all files sent from that device.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="center row-reverse button-row">
|
||||
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Public Room Dialog -->
|
||||
<x-dialog id="public-room-dialog">
|
||||
<form action="#">
|
||||
<x-background class="full center text-center">
|
||||
<x-paper shadow="2">
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center" data-i18n-key="dialogs.temporary-public-room-title" data-i18n-attrs="text">Temporary Public Room</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="center key-qr-code"></div>
|
||||
<h1 class="center key"></h1>
|
||||
<p class="center text-center key-instructions">
|
||||
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text">Input this room id on another device</span>
|
||||
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-note">
|
||||
<hr>
|
||||
<div>
|
||||
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<div class="input-key-container">
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
<input type="text" class="textarea center" aria-label="room-id-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||
</div>
|
||||
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-room-id-from-another-device" data-i18n-attrs="text">Enter room id from another device to join room.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center row-reverse button-row">
|
||||
<button class="button" type="submit" data-i18n-key="dialogs.join" data-i18n-attrs="text" disabled>Join</button>
|
||||
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
<button class="button leave-room" type="button" data-i18n-key="dialogs.leave" data-i18n-attrs="text">Leave</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -166,24 +291,30 @@
|
||||
<x-dialog id="receive-request-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>would like to share</span>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center"></h2>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column center file-description">
|
||||
<div>
|
||||
<span class="display-name badge"></span>
|
||||
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text">would like to share</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="row file-other">
|
||||
</div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
||||
<div class="row-reverse center button-row">
|
||||
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
|
||||
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -192,24 +323,31 @@
|
||||
<x-dialog id="receive-file-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="center"></h2>
|
||||
<div class="center column file-description">
|
||||
<div>
|
||||
<span class="display-name"></span>
|
||||
<span>has sent</span>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center"></h2>
|
||||
</div>
|
||||
<div class="row file-name" >
|
||||
<span class="file-stem"></span>
|
||||
<span class="file-extension"></span>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column center file-description">
|
||||
<div>
|
||||
<span class="display-name badge"></span>
|
||||
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">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="row file-other"></div>
|
||||
<div class="row font-body2 file-size"></div>
|
||||
</div>
|
||||
<div class="center file-preview"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
||||
<button id="download-btn" class="button" autofocus>Download</button>
|
||||
<button class="button" close>Close</button>
|
||||
<div class="row-reverse center button-row">
|
||||
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden>Share</button>
|
||||
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
|
||||
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -219,16 +357,27 @@
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="text-center">Send Message</h2>
|
||||
<div class="dialog-subheader text-center">
|
||||
<span>Send a Message to</span>
|
||||
<span class="display-name"></span>
|
||||
<div class="row center">
|
||||
<div class="column">
|
||||
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text">Send Message</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||
<div class="center row-reverse">
|
||||
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
|
||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
||||
<div class="row center display-name-wrapper">
|
||||
<div class="column">
|
||||
<div class="text-center">
|
||||
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text">Send a Message to</span>
|
||||
<span class="display-name badge"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column fw">
|
||||
<textarea title="Message" class="textarea" wrap="off" autofocus></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row row-reverse">
|
||||
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
|
||||
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -238,16 +387,23 @@
|
||||
<x-dialog id="receive-text-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h2 class="text-center">Message Received</h2>
|
||||
<div class="text-center dialog-subheader">
|
||||
<span class="display-name"></span>
|
||||
<span>has sent:</span>
|
||||
<div class="row center">
|
||||
<h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text">Message Received</h2>
|
||||
</div>
|
||||
<div class="row-separator"></div>
|
||||
<div id="text"></div>
|
||||
<div class="center row-reverse">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
||||
<div class="row center">
|
||||
<div class="text-center">
|
||||
<span class="display-name badge"></span>
|
||||
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent:</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row center">
|
||||
<div class="column fw">
|
||||
<div id="text" class="textarea fw"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-reverse center button-row">
|
||||
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
|
||||
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
@@ -256,20 +412,22 @@
|
||||
<x-dialog id="base64-paste-dialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
||||
<button class="button center" id="base64-paste-btn" title="Paste"></button>
|
||||
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||
<button class="button center" close>Close</button>
|
||||
<div class="row-reverse center button-row">
|
||||
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
||||
<x-toast id="toast" class="row center" shadow="1"></x-toast>
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
||||
<a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="aria-label" aria-label="Close About PairDrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
@@ -281,26 +439,26 @@
|
||||
</svg>
|
||||
<div class="title-wrapper">
|
||||
<h1>PairDrop</h1>
|
||||
<div class="font-subheading">v1.7.7</div>
|
||||
<div class="font-subheading">v1.8.3</div>
|
||||
</div>
|
||||
<div class="font-subheading">The easiest way to transfer files across devices</div>
|
||||
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text">The easiest way to transfer files across devices</div>
|
||||
<div class="row">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer" data-i18n-key="about.github" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#github" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#monetarization" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#twitter" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#help-outline" />
|
||||
</svg>
|
||||
@@ -374,8 +532,18 @@
|
||||
<!--! 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 id="public-room-icon" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-language-selector" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/localization.js"></script>
|
||||
<script src="scripts/theme.js"></script>
|
||||
<script src="scripts/network.js"></script>
|
||||
<script src="scripts/ui.js"></script>
|
||||
|
||||
160
public_included_ws_fallback/lang/de.json
Normal file
160
public_included_ws_fallback/lang/de.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "Über PairDrop",
|
||||
"notification_title": "Benachrichtigungen aktivieren",
|
||||
"about_aria-label": "Über PairDrop öffnen",
|
||||
"install_title": "PairDrop installieren",
|
||||
"pair-device_title": "Deine Geräte dauerhaft koppeln",
|
||||
"edit-paired-devices_title": "Gekoppelte Geräte bearbeiten",
|
||||
"theme-auto_title": "Systemstil verwenden",
|
||||
"theme-dark_title": "Dunklen Stil verwenden",
|
||||
"theme-light_title": "Hellen Stil verwenden",
|
||||
"cancel-paste-mode": "Fertig",
|
||||
"language-selector_title": "Sprache auswählen",
|
||||
"join-public-room_title": "Öffentlichen Raum temporär betreten"
|
||||
},
|
||||
"dialogs": {
|
||||
"share": "Teilen",
|
||||
"download": "Herunterladen",
|
||||
"pair-devices-title": "Geräte dauerhaft koppeln",
|
||||
"input-key-on-this-device": "Gib diesen Schlüssel auf einem anderen Gerät ein",
|
||||
"enter-key-from-another-device": "Gib den Schlüssel von einem anderen Gerät hier ein.",
|
||||
"pair": "Koppeln",
|
||||
"cancel": "Abbrechen",
|
||||
"edit-paired-devices-title": "Gekoppelte Geräte bearbeiten",
|
||||
"paired-devices-wrapper_data-empty": "Keine gekoppelten Geräte.",
|
||||
"close": "Schließen",
|
||||
"accept": "Akzeptieren",
|
||||
"decline": "Ablehnen",
|
||||
"title-image": "Bild",
|
||||
"title-file": "Datei",
|
||||
"title-image-plural": "Bilder",
|
||||
"title-file-plural": "Dateien",
|
||||
"scan-qr-code": "oder scanne den QR-Code.",
|
||||
"would-like-to-share": "möchte Folgendes teilen",
|
||||
"send": "Senden",
|
||||
"copy": "Kopieren",
|
||||
"receive-text-title": "Textnachricht erhalten",
|
||||
"file-other-description-image-plural": "und {{count}} andere Bilder",
|
||||
"file-other-description-file-plural": "und {{count}} andere Dateien",
|
||||
"auto-accept-instructions-1": "Aktiviere",
|
||||
"auto-accept": "auto-accept",
|
||||
"auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.",
|
||||
"has-sent": "hat Folgendes gesendet:",
|
||||
"send-message-title": "Textnachricht senden",
|
||||
"send-message-to": "Sende eine Textnachricht an",
|
||||
"base64-tap-to-paste": "Hier tippen, um {{type}} einzufügen",
|
||||
"base64-paste-to-send": "Hier einfügen, um {{type}} zu versenden",
|
||||
"base64-text": "Text",
|
||||
"base64-files": "Dateien",
|
||||
"base64-processing": "Bearbeitung läuft…",
|
||||
"file-other-description-image": "und ein anderes Bild",
|
||||
"file-other-description-file": "und eine andere Datei",
|
||||
"receive-title": "{{descriptor}} erhalten",
|
||||
"download-again": "Erneut herunterladen",
|
||||
"system-language": "Systemsprache",
|
||||
"language-selector-title": "Sprache auswählen",
|
||||
"hr-or": "ODER",
|
||||
"input-room-id-on-another-device": "Gib diese Raum-ID auf einem anderen Gerät ein",
|
||||
"unpair": "Entkoppeln",
|
||||
"leave": "Verlassen",
|
||||
"join": "Betreten",
|
||||
"enter-room-id-from-another-device": "Gib die Raum-ID von einem anderen Gerät hier ein.",
|
||||
"temporary-public-room-title": "Temporärer Öffentlicher Raum"
|
||||
},
|
||||
"about": {
|
||||
"tweet_title": "Über PairDrop twittern",
|
||||
"faq_title": "Häufig gestellte Fragen",
|
||||
"close-about_aria-label": "Schließe Über PairDrop",
|
||||
"github_title": "PairDrop auf GitHub",
|
||||
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
|
||||
"claim": "Der einfachste Weg Dateien zwischen Geräten zu teilen"
|
||||
},
|
||||
"footer": {
|
||||
"known-as": "Du wirst angezeigt als:",
|
||||
"display-name_title": "Setze einen permanenten Gerätenamen",
|
||||
"on-this-network": "in diesem Netzwerk",
|
||||
"paired-devices": "für gekoppelte Geräte",
|
||||
"traffic": "Datenverkehr wird",
|
||||
"display-name_placeholder": "Lade…",
|
||||
"routed": "durch den Server geleitet",
|
||||
"webrtc": "wenn WebRTC nicht verfügbar ist.",
|
||||
"display-name_data-placeholder": "Lade…",
|
||||
"public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum unabhängig von deinem Netzwerk gefunden werden.",
|
||||
"paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.",
|
||||
"public-room-devices": "in Raum {{roomId}}",
|
||||
"discovery": "Du bist sichtbar:",
|
||||
"on-this-network_title": "Du kannst von jedem in diesem Netzwerk gefunden werden."
|
||||
},
|
||||
"notifications": {
|
||||
"link-received": "Link von {{name}} empfangen - Klicke um ihn zu öffnen",
|
||||
"message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren",
|
||||
"click-to-download": "Klicken zum Download",
|
||||
"copied-text": "Text in die Zwischenablage kopiert",
|
||||
"connected": "Verbunden.",
|
||||
"pairing-success": "Geräte gekoppelt.",
|
||||
"display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert.",
|
||||
"pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden.",
|
||||
"pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent.",
|
||||
"pairing-key-invalid": "Ungültiger Schlüssel",
|
||||
"pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht.",
|
||||
"copied-to-clipboard": "In die Zwischenablage kopiert",
|
||||
"text-content-incorrect": "Textinhalt ist fehlerhaft.",
|
||||
"clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft.",
|
||||
"copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!",
|
||||
"file-content-incorrect": "Dateiinhalt ist fehlerhaft.",
|
||||
"notifications-enabled": "Benachrichtigungen aktiviert.",
|
||||
"offline": "Du bist offline",
|
||||
"online": "Du bist wieder Online",
|
||||
"unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?",
|
||||
"display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert.",
|
||||
"download-successful": "{{descriptor}} heruntergeladen",
|
||||
"pairing-cleared": "Alle Geräte entkoppelt.",
|
||||
"click-to-show": "Klicken zum Anzeigen",
|
||||
"online-requirement": "Du musst online sein um Geräte zu koppeln.",
|
||||
"display-name-changed-temporarily": "Anzeigename wurde nur für diese Session geändert.",
|
||||
"request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen",
|
||||
"connecting": "Verbindung wird aufgebaut…",
|
||||
"files-incorrect": "Dateien sind fehlerhaft.",
|
||||
"file-transfer-completed": "Dateiübertragung fertiggestellt.",
|
||||
"message-transfer-completed": "Nachrichtenübertragung fertiggestellt.",
|
||||
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
|
||||
"selected-peer-left": "Ausgewählter Peer ist gegangen.",
|
||||
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
|
||||
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
|
||||
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
|
||||
"public-room-id-invalid": "Ungültige Raum-ID",
|
||||
"online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln.",
|
||||
"online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können."
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_desktop": "Klicke, um Dateien zu Senden oder klicke mit der rechten Maustaste, um Textnachrichten zu senden",
|
||||
"no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden",
|
||||
"no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen",
|
||||
"no-peers-subtitle": "Kopple Geräte oder besuche einen öffentlichen Raum, damit du in anderen Netzwerken sichtbar bist",
|
||||
"click-to-send": "Klicke zum Senden von",
|
||||
"tap-to-send": "Tippe zum Senden von",
|
||||
"x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden",
|
||||
"x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen",
|
||||
"x-instructions_mobile": "Tippe zum Senden von Dateien oder tippe lange zum Senden von Nachrichten",
|
||||
"activate-paste-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von",
|
||||
"activate-paste-mode-and-other-files": "und {{count}} anderen Dateien",
|
||||
"activate-paste-mode-shared-text": "freigegebenem Text"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-transfer-requested": "Datenübertagung angefordert",
|
||||
"file-received": "Datei erhalten",
|
||||
"file-received-plural": "{{count}} Dateien erhalten",
|
||||
"message-received": "Nachricht erhalten",
|
||||
"message-received-plural": "{{count}} Nachrichten erhalten"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send": "Klicke um Dateien zu senden oder nutze einen Rechtsklick um eine Textnachricht zu senden",
|
||||
"connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten",
|
||||
"waiting": "Warte…",
|
||||
"click-to-send-paste-mode": "Klicken um {{descriptor}} zu senden",
|
||||
"transferring": "Übertragung läuft…",
|
||||
"processing": "Bearbeitung läuft…",
|
||||
"preparing": "Vorbereitung läuft…"
|
||||
}
|
||||
}
|
||||
159
public_included_ws_fallback/lang/en.json
Normal file
159
public_included_ws_fallback/lang/en.json
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "About PairDrop",
|
||||
"language-selector_title": "Select Language",
|
||||
"about_aria-label": "Open About PairDrop",
|
||||
"theme-auto_title": "Adapt Theme to System",
|
||||
"theme-light_title": "Always Use Light-Theme",
|
||||
"theme-dark_title": "Always Use Dark-Theme",
|
||||
"notification_title": "Enable Notifications",
|
||||
"install_title": "Install PairDrop",
|
||||
"pair-device_title": "Pair Your Devices Permanently",
|
||||
"edit-paired-devices_title": "Edit Paired Devices",
|
||||
"join-public-room_title": "Join Public Room Temporarily",
|
||||
"cancel-paste-mode": "Done"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Release to select recipient",
|
||||
"no-peers-title": "Open PairDrop on other devices to send files",
|
||||
"no-peers-subtitle": "Pair devices or enter a public room to be discoverable on other networks",
|
||||
"x-instructions_desktop": "Click to send files or right click to send a message",
|
||||
"x-instructions_mobile": "Tap to send files or long tap to send a message",
|
||||
"x-instructions_data-drop-peer": "Release to send to peer",
|
||||
"x-instructions_data-drop-bg": "Release to select recipient",
|
||||
"click-to-send": "Click to send",
|
||||
"tap-to-send": "Tap to send",
|
||||
"activate-paste-mode-base": "Open PairDrop on other devices to send",
|
||||
"activate-paste-mode-and-other-files": "and {{count}} other files",
|
||||
"activate-paste-mode-shared-text": "shared text"
|
||||
},
|
||||
"footer": {
|
||||
"known-as": "You are known as:",
|
||||
"display-name_data-placeholder": "Loading…",
|
||||
"display-name_title": "Edit your device name permanently",
|
||||
"discovery": "You can be discovered:",
|
||||
"on-this-network": "on this network",
|
||||
"on-this-network_title": "You can be discovered by everyone on this network.",
|
||||
"paired-devices": "by paired devices",
|
||||
"paired-devices_title": "You can be discovered by paired devices at all times independent of the network.",
|
||||
"public-room-devices": "in room {{roomId}}",
|
||||
"public-room-devices_title": "You can be discovered by devices in this public room independent of the network.",
|
||||
"traffic": "Traffic is",
|
||||
"routed": "routed through the server",
|
||||
"webrtc": "if WebRTC is not available."
|
||||
},
|
||||
"dialogs": {
|
||||
"pair-devices-title": "Pair Devices Permanently",
|
||||
"input-key-on-this-device": "Input this key on another device",
|
||||
"scan-qr-code": "or scan the QR-code.",
|
||||
"enter-key-from-another-device": "Enter key from another device here.",
|
||||
"temporary-public-room-title": "Temporary Public Room",
|
||||
"input-room-id-on-another-device": "Input this room ID on another device",
|
||||
"enter-room-id-from-another-device": "Enter room ID from another device to join room.",
|
||||
"hr-or": "OR",
|
||||
"pair": "Pair",
|
||||
"cancel": "Cancel",
|
||||
"edit-paired-devices-title": "Edit Paired Devices",
|
||||
"unpair": "Unpair",
|
||||
"paired-devices-wrapper_data-empty": "No paired devices.",
|
||||
"auto-accept-instructions-1": "Activate",
|
||||
"auto-accept": "auto-accept",
|
||||
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
|
||||
"close": "Close",
|
||||
"join": "Join",
|
||||
"leave": "Leave",
|
||||
"would-like-to-share": "would like to share",
|
||||
"accept": "Accept",
|
||||
"decline": "Decline",
|
||||
"has-sent": "has sent:",
|
||||
"share": "Share",
|
||||
"download": "Download",
|
||||
"send-message-title": "Send Message",
|
||||
"send-message-to": "Send a Message to",
|
||||
"send": "Send",
|
||||
"receive-text-title": "Message Received",
|
||||
"copy": "Copy",
|
||||
"base64-processing": "Processing…",
|
||||
"base64-tap-to-paste": "Tap here to paste {{type}}",
|
||||
"base64-paste-to-send": "Paste here to send {{type}}",
|
||||
"base64-text": "text",
|
||||
"base64-files": "files",
|
||||
"file-other-description-image": "and 1 other image",
|
||||
"file-other-description-file": "and 1 other file",
|
||||
"file-other-description-image-plural": "and {{count}} other images",
|
||||
"file-other-description-file-plural": "and {{count}} other files",
|
||||
"title-image": "Image",
|
||||
"title-file": "File",
|
||||
"title-image-plural": "Images",
|
||||
"title-file-plural": "Files",
|
||||
"receive-title": "{{descriptor}} Received",
|
||||
"download-again": "Download again",
|
||||
"language-selector-title": "Select Language",
|
||||
"system-language": "System Language"
|
||||
},
|
||||
"about": {
|
||||
"close-about_aria-label": "Close About PairDrop",
|
||||
"claim": "The easiest way to transfer files across devices",
|
||||
"github_title": "PairDrop on GitHub",
|
||||
"buy-me-a-coffee_title": "Buy me a coffee!",
|
||||
"tweet_title": "Tweet about PairDrop",
|
||||
"faq_title": "Frequently asked questions"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "Display name is changed permanently.",
|
||||
"display-name-changed-temporarily": "Display name is changed only for this session.",
|
||||
"display-name-random-again": "Display name is randomly generated again.",
|
||||
"download-successful": "{{descriptor}} downloaded",
|
||||
"pairing-tabs-error": "Pairing two web browser tabs is impossible.",
|
||||
"pairing-success": "Devices paired.",
|
||||
"pairing-not-persistent": "Paired devices are not persistent.",
|
||||
"pairing-key-invalid": "Invalid key",
|
||||
"pairing-key-invalidated": "Key {{key}} invalidated.",
|
||||
"pairing-cleared": "All Devices unpaired.",
|
||||
"public-room-id-invalid": "Invalid room ID",
|
||||
"public-room-left": "Left public room {{publicRoomId}}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"copied-to-clipboard-error": "Copying not possible. Copy manually.",
|
||||
"text-content-incorrect": "Text content is incorrect.",
|
||||
"file-content-incorrect": "File content is incorrect.",
|
||||
"clipboard-content-incorrect": "Clipboard content is incorrect.",
|
||||
"notifications-enabled": "Notifications enabled.",
|
||||
"link-received": "Link received by {{name}} - Click to open",
|
||||
"message-received": "Message received by {{name}} - Click to copy",
|
||||
"click-to-download": "Click to download",
|
||||
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
|
||||
"click-to-show": "Click to show",
|
||||
"copied-text": "Copied text to clipboard",
|
||||
"copied-text-error": "Writing to clipboard failed. Copy manually!",
|
||||
"offline": "You are offline",
|
||||
"online": "You are back online",
|
||||
"connected": "Connected.",
|
||||
"online-requirement-pairing": "You need to be online to pair devices.",
|
||||
"online-requirement-public-room": "You need to be online to create a public room.",
|
||||
"connecting": "Connecting…",
|
||||
"files-incorrect": "Files are incorrect.",
|
||||
"file-transfer-completed": "File transfer completed.",
|
||||
"ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once",
|
||||
"message-transfer-completed": "Message transfer completed.",
|
||||
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
|
||||
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
|
||||
"selected-peer-left": "Selected peer left."
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "File Received",
|
||||
"file-received-plural": "{{count}} Files Received",
|
||||
"file-transfer-requested": "File Transfer Requested",
|
||||
"image-transfer-requested": "Image Transfer Requested",
|
||||
"message-received": "Message Received",
|
||||
"message-received-plural": "{{count}} Messages Received"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "Click to send {{descriptor}}",
|
||||
"click-to-send": "Click to send files or right click to send a message",
|
||||
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
|
||||
"preparing": "Preparing…",
|
||||
"waiting": "Waiting…",
|
||||
"processing": "Processing…",
|
||||
"transferring": "Transferring…"
|
||||
}
|
||||
}
|
||||
160
public_included_ws_fallback/lang/fr.json
Normal file
160
public_included_ws_fallback/lang/fr.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "A propos de",
|
||||
"language-selector_title": "Choix de la langue",
|
||||
"about_aria-label": "Ouvrir à propos de",
|
||||
"theme-auto_title": "Adapter le thème au système",
|
||||
"theme-light_title": "Toujours utiliser le thème clair",
|
||||
"theme-dark_title": "Toujours utiliser le thème sombre",
|
||||
"notification_title": "Activer les notifications",
|
||||
"install_title": "Installer PairDrop",
|
||||
"pair-device_title": "Associez vos appareils de manière permanente",
|
||||
"edit-paired-devices_title": "Gérer les appareils couplés",
|
||||
"join-public-room_title": "Rejoindre temporairement la salle publique",
|
||||
"cancel-paste-mode": "Terminé"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Déposer pour choisir le destinataire",
|
||||
"no-peers-title": "Ouvrez PairDrop sur d'autres appareils pour envoyer des fichiers",
|
||||
"no-peers-subtitle": "Associez des appareils ou entrez dans une salle publique pour être visible sur d'autres réseaux",
|
||||
"x-instructions_desktop": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
|
||||
"x-instructions_mobile": "Appuyez pour envoyer des fichiers ou appuyez longuement pour envoyer un message",
|
||||
"x-instructions_data-drop-peer": "Déposer pour envoyer au destinataire",
|
||||
"x-instructions_data-drop-bg": "Lâcher pour choisir le destinataire",
|
||||
"click-to-send": "Cliquez pour envoyer",
|
||||
"tap-to-send": "Appuyez pour envoyer",
|
||||
"activate-paste-mode-base": "Ouvrez PairDrop sur d'autres appareils pour envoyer",
|
||||
"activate-paste-mode-and-other-files": "et {{count}} autres fichiers",
|
||||
"activate-paste-mode-shared-text": "texte partagé"
|
||||
},
|
||||
"footer": {
|
||||
"known-as": "Vous êtes connu sous le nom de:",
|
||||
"display-name_data-placeholder": "Chargement…",
|
||||
"display-name_title": "Modifiez le nom de votre appareil de manière permanente",
|
||||
"discovery": "Vous pouvez être découvert:",
|
||||
"on-this-network": "sur ce réseau",
|
||||
"on-this-network_title": "Vous pouvez être découvert par tout le monde sur ce réseau.",
|
||||
"paired-devices": "par les appareils couplés",
|
||||
"paired-devices_title": "Vous pouvez être découvert par les appareils couplés à tout moment, indépendamment du réseau.",
|
||||
"public-room-devices": "dans la salle {{roomId}}",
|
||||
"public-room-devices_title": "Vous pouvez être découvert par les appareils de cette salle publique indépendamment du réseau.",
|
||||
"traffic": "Le trafic est",
|
||||
"routed": "routé via le serveur",
|
||||
"webrtc": "si WebRTC n'est pas disponible.",
|
||||
"display-name_placeholder": "Chargement…"
|
||||
},
|
||||
"dialogs": {
|
||||
"pair-devices-title": "Associer les appareils de manière permanente",
|
||||
"input-key-on-this-device": "Saisissez cette clé sur un autre appareil",
|
||||
"scan-qr-code": "ou scannez le QR-code.",
|
||||
"enter-key-from-another-device": "Entrez ici la clé d'un autre appareil.",
|
||||
"temporary-public-room-title": "Salle publique temporaire",
|
||||
"input-room-id-on-another-device": "Saisissez cet ID de salle sur un autre appareil",
|
||||
"enter-room-id-from-another-device": "Entrez l'ID de la salle depuis un autre appareil pour rejoindre la salle.",
|
||||
"hr-or": "OU",
|
||||
"pair": "associer",
|
||||
"cancel": "Annuler",
|
||||
"edit-paired-devices-title": "Modifier les appareils couplés",
|
||||
"unpair": "Dissocier",
|
||||
"paired-devices-wrapper_data-empty": "Aucun appareil couplé.",
|
||||
"auto-accept-instructions-1": "Activer",
|
||||
"auto-accept": "auto-accepter",
|
||||
"auto-accept-instructions-2": "pour accepter automatiquement tous les fichiers envoyés depuis cet appareil.",
|
||||
"close": "Fermer",
|
||||
"join": "Rejoindre",
|
||||
"leave": "Partir",
|
||||
"would-like-to-share": "aimerait partager",
|
||||
"accept": "Accepter",
|
||||
"decline": "Refuser",
|
||||
"has-sent": "a envoyé:",
|
||||
"share": "Partage",
|
||||
"download": "Télécharger",
|
||||
"send-message-title": "Envoyer un message",
|
||||
"send-message-to": "Envoyer un message à",
|
||||
"send": "Envoyer",
|
||||
"receive-text-title": "Message reçu",
|
||||
"copy": "Copier",
|
||||
"base64-processing": "Traitement…",
|
||||
"base64-tap-to-paste": "Appuyez ici pour coller {{type}}",
|
||||
"base64-paste-to-send": "Coller ici pour envoyer {{type}}",
|
||||
"base64-text": "texte",
|
||||
"base64-files": "fichiers",
|
||||
"file-other-description-image": "et 1 autre image",
|
||||
"file-other-description-file": "et 1 autre fichier",
|
||||
"file-other-description-image-plural": "et {{count}} autres images",
|
||||
"file-other-description-file-plural": "et {{count}} autres fichiers",
|
||||
"title-image": "Image",
|
||||
"title-file": "Fichier",
|
||||
"title-image-plural": "Images",
|
||||
"title-file-plural": "Fichiers",
|
||||
"receive-title": "{{descriptor}} Reçu",
|
||||
"download-again": "Télécharger à nouveau",
|
||||
"language-selector-title": "Sélectionnez la langue",
|
||||
"system-language": "Langue du système"
|
||||
},
|
||||
"about": {
|
||||
"close-about_aria-label": "Fermer à propos de PairDrop",
|
||||
"claim": "Le moyen le plus simple de transférer des fichiers entre appareils",
|
||||
"github_title": "PairDrop sur GitHub",
|
||||
"buy-me-a-coffee_title": "Achete-moi un café!",
|
||||
"tweet_title": "Tweet à propos de PairDrop",
|
||||
"faq_title": "Questions fréquemment posées"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "Le nom d'affichage est modifié de manière permanente.",
|
||||
"display-name-changed-temporarily": "Le nom d'affichage est modifié uniquement pour cette session.",
|
||||
"display-name-random-again": "Le nom d'affichage est à nouveau généré aléatoirement.",
|
||||
"download-successful": "{{descriptor}} téléchargé",
|
||||
"pairing-tabs-error": "Le couplage de deux onglets de navigateur Web est impossible.",
|
||||
"pairing-success": "Appareils couplés.",
|
||||
"pairing-not-persistent": "Les appareils couplés ne sont pas persistants.",
|
||||
"pairing-key-invalid": "Clé invalide",
|
||||
"pairing-key-invalidated": "Clé {{key}} invalidée.",
|
||||
"pairing-cleared": "Tous les appareils ne sont plus appairés.",
|
||||
"public-room-id-invalid": "ID de salle non valide",
|
||||
"public-room-left": "Salle publique {{publicRoomId}} quittée",
|
||||
"copied-to-clipboard": "Copié dans le presse-papier",
|
||||
"copied-to-clipboard-error": "Copie impossible. Copier manuellement.",
|
||||
"text-content-incorrect": "Le contenu du texte est incorrect.",
|
||||
"file-content-incorrect": "Le contenu du fichier est incorrect.",
|
||||
"clipboard-content-incorrect": "Le contenu du presse-papiers est incorrect.",
|
||||
"notifications-enabled": "Notifications activées.",
|
||||
"link-received": "Lien reçu par {{name}} - Cliquez pour ouvrir",
|
||||
"message-received": "Message reçu par {{name}} - Cliquez pour copier",
|
||||
"click-to-download": "Cliquez pour télécharger",
|
||||
"request-title": "{{name}} souhaite transférer {{count}} {{descriptor}}",
|
||||
"click-to-show": "Cliquez pour afficher",
|
||||
"copied-text": "Texte copié dans le presse-papiers",
|
||||
"copied-text-error": "L'écriture dans le presse-papiers a échoué. Copiez manuellement!",
|
||||
"offline": "Vous êtes hors ligne",
|
||||
"online": "Vous êtes de nouveau en ligne",
|
||||
"connected": "Connecté.",
|
||||
"online-requirement-pairing": "Vous devez être en ligne pour coupler des appareils.",
|
||||
"online-requirement-public-room": "Vous devez être en ligne pour créer une salle publique.",
|
||||
"connecting": "Connexion…",
|
||||
"files-incorrect": "Les fichiers sont incorrects.",
|
||||
"file-transfer-completed": "Transfert de fichier terminé.",
|
||||
"ios-memory-limit": "L'envoi de fichiers vers iOS n'est possible que jusqu'à 200 Mo à la fois",
|
||||
"message-transfer-completed": "Transfert de message terminé.",
|
||||
"unfinished-transfers-warning": "Il y a des transferts inachevés. Etes-vous sûr de vouloir fermer PairDrop?",
|
||||
"rate-limit-join-key": "Limite de débit atteinte. Attendez 10 secondes et réessayez.",
|
||||
"selected-peer-left": "Appareils selectionnés restants."
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "Fichier reçu",
|
||||
"file-received-plural": "{{count}} fichiers reçus",
|
||||
"file-transfer-requested": "Transfert de fichier demandé",
|
||||
"image-transfer-requested": "Transfert d'image demandé",
|
||||
"message-received": "Message reçu",
|
||||
"message-received-plural": "{{count}} Messages reçus"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "Cliquez pour envoyer {{descriptor}}",
|
||||
"click-to-send": "Cliquez pour envoyer des fichiers ou faites un clic droit pour envoyer un message",
|
||||
"connection-hash": "Pour vérifier la sécurité du chiffrement de bout en bout, comparez ce numéro de sécurité sur les deux appareils",
|
||||
"preparing": "Préparation…",
|
||||
"waiting": "En attente…",
|
||||
"processing": "En cours…",
|
||||
"transferring": "Transfert en cours…"
|
||||
}
|
||||
}
|
||||
158
public_included_ws_fallback/lang/id.json
Normal file
158
public_included_ws_fallback/lang/id.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"footer": {
|
||||
"webrtc": "jika WebRTC tidak tersedia.",
|
||||
"public-room-devices_title": "Anda dapat ditemukan oleh perangkat di ruang publik ini terlepas dari jaringan.",
|
||||
"display-name_data-placeholder": "Memuat…",
|
||||
"display-name_title": "Edit nama perangkat Anda scr. permanen",
|
||||
"traffic": "Lalu lintas",
|
||||
"paired-devices_title": "Anda dapat ditemukan oleh perangkat yang dipasangkan setiap saat tergantung pada jaringan.",
|
||||
"public-room-devices": "dalam room {{roomId}}",
|
||||
"paired-devices": "pada prngkt. yg. dipasangkan",
|
||||
"on-this-network": "pada jaringan ini",
|
||||
"routed": "diarahkan melalui server",
|
||||
"discovery": "Anda dapat ditemukan:",
|
||||
"on-this-network_title": "Anda dapat ditemukan oleh semua orang di jaringan ini.",
|
||||
"known-as": "Anda dikenal sebagai:"
|
||||
},
|
||||
"notifications": {
|
||||
"request-title": "{{name}} ingin mentransfer {{count}} {{descriptor}}",
|
||||
"unfinished-transfers-warning": "Ada transfer yang belum selesai. Apakah Anda yakin ingin menutup PairDrop?",
|
||||
"message-received": "Pesan diterima dari {{name}} - Klik untuk menyalin",
|
||||
"rate-limit-join-key": "Batasan tercapai. Tunggu 10 detik dan coba lagi.",
|
||||
"connecting": "Menghubungkan…",
|
||||
"pairing-key-invalidated": "Kunci {{key}} tidak valid.",
|
||||
"pairing-key-invalid": "Kunci tidak valid",
|
||||
"connected": "Tersambung.",
|
||||
"pairing-not-persistent": "Perangkat dipasangkan tidak akan bertahan lama.",
|
||||
"text-content-incorrect": "Isi teks keliru.",
|
||||
"message-transfer-completed": "Transfer pesan selesai.",
|
||||
"file-transfer-completed": "Transfer file selesai.",
|
||||
"file-content-incorrect": "Isi file keliru.",
|
||||
"files-incorrect": "File tidak benar.",
|
||||
"selected-peer-left": "Rekan terpilih keluar.",
|
||||
"link-received": "Tautan diterima dari {{name}} - Klik untuk membuka",
|
||||
"online": "Anda kembali online",
|
||||
"public-room-left": "Keluar dari ruang publik {{publicRoomId}}",
|
||||
"copied-text": "Teks disalin ke papan klip",
|
||||
"display-name-random-again": "Nama tampilan dibuat secara acak lagi.",
|
||||
"display-name-changed-permanently": "Nama tampilan diubah secara permanen.",
|
||||
"copied-to-clipboard-error": "Penyalinan tak dapat dilakukan. Salinlah secara manual.",
|
||||
"pairing-success": "Perangkat dipasangkan.",
|
||||
"clipboard-content-incorrect": "Isi papan klip keliru.",
|
||||
"display-name-changed-temporarily": "Nama tampilan hanya diubah untuk sesi ini.",
|
||||
"copied-to-clipboard": "Disalin ke papan klip",
|
||||
"offline": "Anda sedang offline",
|
||||
"pairing-tabs-error": "Memasangkan dua tab browser web tidak mungkin dilakukan.",
|
||||
"public-room-id-invalid": "Room ID tidak valid",
|
||||
"click-to-download": "Klik untuk mengunduh",
|
||||
"pairing-cleared": "Semua Perangkat dilepaskan.",
|
||||
"notifications-enabled": "Notifikasi diaktifkan.",
|
||||
"online-requirement-pairing": "Anda harus online untuk memasangkan perangkat.",
|
||||
"ios-memory-limit": "Mengirim file ke iOS hanya dapat dilakukan hingga 200 MB sekaligus",
|
||||
"online-requirement-public-room": "Anda harus online untuk membuat ruang publik.",
|
||||
"copied-text-error": "Menyalin ke papan klip gagal. Salinlah secara manual!",
|
||||
"download-successful": "{{descriptor}} diunduh",
|
||||
"click-to-show": "Klik untuk menampilkan"
|
||||
},
|
||||
"header": {
|
||||
"cancel-paste-mode": "Selesai",
|
||||
"theme-auto_title": "Sesuaikan Tema dengan Sistem",
|
||||
"install_title": "Instal PairDrop",
|
||||
"theme-dark_title": "Selalu Gunakan Tema Gelap",
|
||||
"pair-device_title": "Pasangkan Perangkat Anda Secara Permanen",
|
||||
"join-public-room_title": "Bergabung dgn. Ruang Publik Sementara",
|
||||
"notification_title": "Aktifkan Notifikasi",
|
||||
"edit-paired-devices_title": "Edit Perangkat yg. Dipasangkan",
|
||||
"language-selector_title": "Pilih Bahasa",
|
||||
"about_title": "Tentang PairDrop",
|
||||
"about_aria-label": "Buka Tentang PairDrop",
|
||||
"theme-light_title": "Selalu Gunakan Tema Terang"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_mobile": "Ketuk untuk mengirim file atau ketuk lama untuk mengirim pesan",
|
||||
"click-to-send": "Klik untuk mengirim",
|
||||
"activate-paste-mode-and-other-files": "dan {{count}} file lainnya",
|
||||
"tap-to-send": "Ketuk untuk mengirim",
|
||||
"activate-paste-mode-base": "Buka PairDrop di perangkat lain untuk berkirim",
|
||||
"no-peers-subtitle": "Pasangkan perangkat atau masuk ke ruang publik agar dapat terdeteksi di jaringan lain",
|
||||
"activate-paste-mode-shared-text": "teks bersama",
|
||||
"x-instructions_desktop": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
|
||||
"no-peers-title": "Buka PairDrop di perangkat lain untuk berkirim file",
|
||||
"x-instructions_data-drop-peer": "Lepaskan untuk mengirim ke rekan",
|
||||
"x-instructions_data-drop-bg": "Lepaskan untuk memilih penerima",
|
||||
"no-peers_data-drop-bg": "Lepaskan untuk memilih penerima"
|
||||
},
|
||||
"peer-ui": {
|
||||
"processing": "Memproses…",
|
||||
"click-to-send-paste-mode": "Klik untuk mengirim {{descriptor}}",
|
||||
"click-to-send": "Klik untuk mengirim file atau klik kanan untuk mengirim pesan",
|
||||
"waiting": "Menunggu…",
|
||||
"connection-hash": "Untuk memverifikasi keamanan enkripsi end-to-end, bandingkan nomor keamanan ini pada kedua perangkat",
|
||||
"preparing": "Menyiapkan…",
|
||||
"transferring": "Mentransfer…"
|
||||
},
|
||||
"dialogs": {
|
||||
"base64-paste-to-send": "Tempel di sini untuk mengirim {{type}}",
|
||||
"auto-accept-instructions-2": "untuk secara otomatis menerima semua file yang dikirim dari perangkat tersebut.",
|
||||
"receive-text-title": "Pesan Diterima",
|
||||
"edit-paired-devices-title": "Edit Perangkat yg. Dipasangkan",
|
||||
"cancel": "Batal",
|
||||
"auto-accept-instructions-1": "Aktifkan",
|
||||
"pair-devices-title": "Pasangkan Perangkat Scr. Permanen",
|
||||
"download": "Unduh",
|
||||
"title-file": "File",
|
||||
"base64-processing": "Memproses…",
|
||||
"decline": "Tolak",
|
||||
"receive-title": "{{descriptor}} Diterima",
|
||||
"leave": "Tinggalkan",
|
||||
"join": "Gabung",
|
||||
"title-image-plural": "Gambar",
|
||||
"send": "Kirim",
|
||||
"base64-tap-to-paste": "Ketuk di sini untuk menempelkan {{type}}",
|
||||
"base64-text": "teks",
|
||||
"copy": "Salin",
|
||||
"file-other-description-image": "dan 1 gambar lainnya",
|
||||
"temporary-public-room-title": "Ruang Publik Sementara",
|
||||
"base64-files": "file",
|
||||
"has-sent": "telah mengirim:",
|
||||
"file-other-description-file": "dan 1 file lainnya",
|
||||
"close": "Tutup",
|
||||
"system-language": "Bahasa Sistem",
|
||||
"unpair": "Lepas",
|
||||
"title-image": "Gambar",
|
||||
"file-other-description-file-plural": "dan {{count}} file lainnya",
|
||||
"would-like-to-share": "ingin berbagi",
|
||||
"send-message-to": "Kirim pesan ke",
|
||||
"language-selector-title": "Pilih Bahasa",
|
||||
"pair": "Pasangkan",
|
||||
"hr-or": "ATAU",
|
||||
"scan-qr-code": "atau pindai kode QR.",
|
||||
"input-key-on-this-device": "Masukkan kunci ini pada perangkat lain",
|
||||
"download-again": "Unduh lagi",
|
||||
"accept": "Terima",
|
||||
"paired-devices-wrapper_data-empty": "Tak ada perangkat yg. dipasangkan.",
|
||||
"enter-key-from-another-device": "Masukkan kunci dari perangkat lain di sini.",
|
||||
"share": "Bagikan",
|
||||
"auto-accept": "terima-otomatis",
|
||||
"title-file-plural": "File",
|
||||
"send-message-title": "Kirim Pesan",
|
||||
"input-room-id-on-another-device": "Masukkan room ID ini pada perangkat lain",
|
||||
"file-other-description-image-plural": "dan {{count}} gambar lainnya",
|
||||
"enter-room-id-from-another-device": "Masukkan room ID dari perangkat lain untuk bergabung dengan room."
|
||||
},
|
||||
"about": {
|
||||
"claim": "Cara termudah untuk mentransfer file lintas perangkat",
|
||||
"tweet_title": "Tweet tentang PairDrop",
|
||||
"close-about_aria-label": "Tutup Tentang PairDrop",
|
||||
"buy-me-a-coffee_title": "Traktir aku kopi!",
|
||||
"github_title": "PairDrop di GitHub",
|
||||
"faq_title": "Pertanyaan yang sering diajukan"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-transfer-requested": "Permintaan Transfer File",
|
||||
"message-received-plural": "{{count}} Pesan Diterima",
|
||||
"message-received": "Pesan Diterima",
|
||||
"file-received": "File Diterima",
|
||||
"file-received-plural": "{{count}} File Diterima"
|
||||
}
|
||||
}
|
||||
138
public_included_ws_fallback/lang/nb.json
Normal file
138
public_included_ws_fallback/lang/nb.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"header": {
|
||||
"edit-paired-devices_title": "Rediger sammenkoblede enheter",
|
||||
"about_title": "Om PairDrop",
|
||||
"about_aria-label": "Åpne «Om PairDrop»",
|
||||
"theme-auto_title": "Juster drakt til system",
|
||||
"theme-light_title": "Alltid bruk lys drakt",
|
||||
"theme-dark_title": "Alltid bruk mørk drakt",
|
||||
"notification_title": "Skru på merknader",
|
||||
"cancel-paste-mode": "Ferdig",
|
||||
"install_title": "Installer PairDrop",
|
||||
"pair-device_title": "Sammenkoble enhet"
|
||||
},
|
||||
"footer": {
|
||||
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
|
||||
"display-name_data-placeholder": "Laster inn…",
|
||||
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
|
||||
"traffic": "Trafikken",
|
||||
"on-this-network": "på dette nettverket",
|
||||
"known-as": "Du er kjent som:",
|
||||
"paired-devices": "sammenkoblede enheter",
|
||||
"routed": "Sendes gjennom tjeneren"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||
"x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding",
|
||||
"x-instructions_data-drop-bg": "Slipp for å velge mottager",
|
||||
"click-to-send": "Klikk for å sende",
|
||||
"no-peers_data-drop-bg": "Slipp for å velge mottager",
|
||||
"no-peers-title": "Åpne PairDrop på andre enheter for å sende filer",
|
||||
"no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk",
|
||||
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
|
||||
"tap-to-send": "Trykk for å sende",
|
||||
"activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende",
|
||||
"activate-paste-mode-and-other-files": "og {{count}} andre filer",
|
||||
"activate-paste-mode-shared-text": "delt tekst"
|
||||
},
|
||||
"dialogs": {
|
||||
"input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet",
|
||||
"pair-devices-title": "Sammenkoble enheter",
|
||||
"would-like-to-share": "ønsker å dele",
|
||||
"auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.",
|
||||
"paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter",
|
||||
"enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.",
|
||||
"edit-paired-devices-title": "Rediger sammenkoblede enheter",
|
||||
"accept": "Godta",
|
||||
"has-sent": "har sendt:",
|
||||
"base64-paste-to-send": "Trykk her for å sende {{type}}",
|
||||
"base64-text": "tekst",
|
||||
"base64-files": "filer",
|
||||
"file-other-description-image-plural": "og {{count}} andre bilder",
|
||||
"receive-title": "{{descriptor}} mottatt",
|
||||
"send-message-title": "Send melding",
|
||||
"base64-processing": "Behandler…",
|
||||
"close": "Lukk",
|
||||
"decline": "Avslå",
|
||||
"download": "Last ned",
|
||||
"copy": "Kopier",
|
||||
"pair": "Sammenkoble",
|
||||
"cancel": "Avbryt",
|
||||
"scan-qr-code": "eller skann QR-koden.",
|
||||
"auto-accept-instructions-1": "Aktiver",
|
||||
"receive-text-title": "Melding mottatt",
|
||||
"auto-accept": "auto-godkjenn",
|
||||
"share": "Del",
|
||||
"send-message-to": "Send en melding til",
|
||||
"send": "Send",
|
||||
"base64-tap-to-paste": "Trykk her for å lime inn {{type}}",
|
||||
"file-other-description-image": "og ett annet bilde",
|
||||
"file-other-description-file-plural": "og {{count}} andre filer",
|
||||
"title-file-plural": "Filer",
|
||||
"download-again": "Last ned igjen",
|
||||
"file-other-description-file": "og én annen fil",
|
||||
"title-image": "Bilde",
|
||||
"title-file": "Fil",
|
||||
"title-image-plural": "Bilder"
|
||||
},
|
||||
"about": {
|
||||
"close-about_aria-label": "Lukk «Om PairDrop»",
|
||||
"faq_title": "Ofte stilte spørsmål",
|
||||
"claim": "Den enkleste måten å overføre filer mellom enheter",
|
||||
"buy-me-a-coffee_title": "Spander drikke.",
|
||||
"tweet_title": "Tvitre om PairDrop",
|
||||
"github_title": "PairDrop på GitHub"
|
||||
},
|
||||
"notifications": {
|
||||
"copied-to-clipboard": "Kopiert til utklippstavlen",
|
||||
"pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.",
|
||||
"notifications-enabled": "Merknader påskrudd.",
|
||||
"click-to-show": "Klikk for å vise",
|
||||
"copied-text": "Tekst kopiert til utklippstavlen",
|
||||
"connected": "Tilkoblet.",
|
||||
"online": "Du er tilbake på nett",
|
||||
"file-transfer-completed": "Filoverføring utført.",
|
||||
"selected-peer-left": "Valgt likemann dro.",
|
||||
"pairing-key-invalid": "Ugyldig nøkkel",
|
||||
"connecting": "Kobler til …",
|
||||
"pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.",
|
||||
"offline": "Du er frakoblet",
|
||||
"online-requirement": "Du må være på nett for å koble sammen enheter.",
|
||||
"display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.",
|
||||
"display-name-changed-permanently": "Visningsnavnet er endret for godt.",
|
||||
"display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.",
|
||||
"text-content-incorrect": "Tekstinnholdet er uriktig.",
|
||||
"file-content-incorrect": "Filinnholdet er uriktig.",
|
||||
"click-to-download": "Klikk for å laste ned",
|
||||
"message-transfer-completed": "Meldingsoverføring utført.",
|
||||
"download-successful": "{{descriptor}} nedlastet",
|
||||
"pairing-success": "Enheter sammenkoblet.",
|
||||
"pairing-cleared": "Sammenkobling av alle enheter opphevet.",
|
||||
"pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.",
|
||||
"copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt!",
|
||||
"clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.",
|
||||
"link-received": "Lenke mottatt av {{name}} - Klikk for å åpne.",
|
||||
"request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}",
|
||||
"message-received": "Melding mottatt av {{name}} - Klikk for å åpne.",
|
||||
"files-incorrect": "Filene er uriktige.",
|
||||
"ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.",
|
||||
"unfinished-transfers-warning": "Lukk med ufullførte overføringer?",
|
||||
"rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen."
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "Fil mottatt",
|
||||
"file-received-plural": "{{count}} filer mottatt",
|
||||
"message-received": "Melding mottatt",
|
||||
"file-transfer-requested": "Filoverføring forespurt",
|
||||
"message-received-plural": "{{count}} meldinger mottatt"
|
||||
},
|
||||
"peer-ui": {
|
||||
"preparing": "Forbereder …",
|
||||
"waiting": "Venter…",
|
||||
"processing": "Behandler …",
|
||||
"transferring": "Overfører …",
|
||||
"click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||
"click-to-send-paste-mode": "Klikk for å sende {{descriptor}}",
|
||||
"connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen."
|
||||
}
|
||||
}
|
||||
158
public_included_ws_fallback/lang/ro.json
Normal file
158
public_included_ws_fallback/lang/ro.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"footer": {
|
||||
"webrtc": "dacă WebRTC nu este disponibil.",
|
||||
"public-room-devices_title": "Poți fi descoperit de dispozitivele din această cameră publică, independent de rețea.",
|
||||
"display-name_data-placeholder": "Se încarcă…",
|
||||
"display-name_title": "Editați permanent numele dispozitivului tău",
|
||||
"traffic": "Traficul este",
|
||||
"paired-devices_title": "Poți fi descoperit în orice moment de dispozitivele cuplate, indiferent de rețea.",
|
||||
"public-room-devices": "în camera {{roomId}}",
|
||||
"paired-devices": "prin dispozitive împerecheate",
|
||||
"on-this-network": "în această rețea",
|
||||
"routed": "rutate prin server",
|
||||
"discovery": "Poți fi descoperit:",
|
||||
"on-this-network_title": "Poți fi descoperit de toată lumea din această rețea.",
|
||||
"known-as": "Ești cunoscut ca:"
|
||||
},
|
||||
"notifications": {
|
||||
"request-title": "{{name}} ar dori să transfere {{count}} {{descriptor}}",
|
||||
"unfinished-transfers-warning": "Există transferuri neterminate. Sigur vrei să închizi PairDrop?",
|
||||
"message-received": "Mesaj primit de {{name}} - Apasă pentru a copia",
|
||||
"rate-limit-join-key": "A fost atinsă limita ratei. Așteptați 10 secunde și încercați din nou.",
|
||||
"connecting": "Conectarea…",
|
||||
"pairing-key-invalidated": "Cheia {{key}} invalidată.",
|
||||
"pairing-key-invalid": "Cheie invalidă",
|
||||
"connected": "Conectat.",
|
||||
"pairing-not-persistent": "Dispozitivele cuplate nu sunt persistente.",
|
||||
"text-content-incorrect": "Conținutul textului este incorect.",
|
||||
"message-transfer-completed": "Transferul mesajului este finalizat.",
|
||||
"file-transfer-completed": "Transfer de fișiere finalizat.",
|
||||
"file-content-incorrect": "Conținutul fișierului este incorect.",
|
||||
"files-incorrect": "Fișierele sunt incorecte.",
|
||||
"selected-peer-left": "Selectat peer a plecat.",
|
||||
"link-received": "Link primit de {{name}} - Apasă pentru a deschide",
|
||||
"online": "Ați revenit online",
|
||||
"public-room-left": "Plecat din camera publică {{publicRoomId}}",
|
||||
"copied-text": "Text copiat în clipboard",
|
||||
"display-name-random-again": "Numele afișat este din nou generat aleatoriu.",
|
||||
"display-name-changed-permanently": "Numele afișat este schimbat permanent.",
|
||||
"copied-to-clipboard-error": "Copierea nu este posibilă. Copiați manual.",
|
||||
"pairing-success": "Dispozitive asociate.",
|
||||
"clipboard-content-incorrect": "Conținutul clipboard-ului este incorect.",
|
||||
"display-name-changed-temporarily": "Numele afișat se modifică numai pentru această sesiune.",
|
||||
"copied-to-clipboard": "Copiat în clipboard",
|
||||
"offline": "Ești offline",
|
||||
"pairing-tabs-error": "Cuplarea între două file de browser web este imposibilă.",
|
||||
"public-room-id-invalid": "ID-ul camerei invalid",
|
||||
"click-to-download": "Apasă pentru a descărca",
|
||||
"pairing-cleared": "Toate dispozitivele sunt decuplate.",
|
||||
"notifications-enabled": "Notificări activate.",
|
||||
"online-requirement-pairing": "Trebuie să fiți online pentru a asocia dispozitivele.",
|
||||
"ios-memory-limit": "Trimiterea de fișiere pe iOS este posibilă doar până la 200 MB simultan",
|
||||
"online-requirement-public-room": "Trebuie să fiți online pentru a crea o cameră publică.",
|
||||
"copied-text-error": "Scrierea în clipboard a eșuat. Copiați manual!",
|
||||
"download-successful": "{{descriptor}} descărcat",
|
||||
"click-to-show": "Apasă pentru a arăta"
|
||||
},
|
||||
"header": {
|
||||
"cancel-paste-mode": "Gata",
|
||||
"theme-auto_title": "Adaptează Tema la Sistem",
|
||||
"install_title": "Instalează PairDrop",
|
||||
"theme-dark_title": "Utilizați mereu tema întunecoasă",
|
||||
"pair-device_title": "Împerechează-ți permanent dispozitivele",
|
||||
"join-public-room_title": "Alătură-te temporar camerei publice",
|
||||
"notification_title": "Activați Notificări",
|
||||
"edit-paired-devices_title": "Editați dispozitivele împerecheate",
|
||||
"language-selector_title": "Selectează Limba",
|
||||
"about_title": "Despre PairDrop",
|
||||
"about_aria-label": "Deschide Despre PairDrop",
|
||||
"theme-light_title": "Utilizați mereu tema luminoasă"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_mobile": "Atingeți pentru a trimite fișiere sau atingeți lung pentru a trimite un mesaj",
|
||||
"click-to-send": "Clic pentru a trimite",
|
||||
"activate-paste-mode-and-other-files": "și {{count}} alte fișiere",
|
||||
"tap-to-send": "Atinge pentru a trimite",
|
||||
"activate-paste-mode-base": "Deschideți PairDrop pe alte dispozitive pentru a trimite",
|
||||
"no-peers-subtitle": "Împerecheați dispozitive sau intrați într-o cameră publică pentru a fi descoperit în alte rețele",
|
||||
"activate-paste-mode-shared-text": "text partajat",
|
||||
"x-instructions_desktop": "Dați clic pentru a trimite fișiere sau dați clic dreapta pentru a trimite un mesaj",
|
||||
"no-peers-title": "Deschideți PairDrop pe alte dispozitive pentru a trimite fișiere",
|
||||
"x-instructions_data-drop-peer": "Eliberare pentru a trimite la peer",
|
||||
"x-instructions_data-drop-bg": "Eliberați pentru a selecta recipientul",
|
||||
"no-peers_data-drop-bg": "Eliberare pentru a selecta recipientul"
|
||||
},
|
||||
"peer-ui": {
|
||||
"processing": "Procesarea…",
|
||||
"click-to-send-paste-mode": "Apasă pentru a trimite {{descriptor}}",
|
||||
"click-to-send": "Apasă pentru a trimite fișiere sau apasă cu butonul din dreapta pentru a trimite un mesaj",
|
||||
"waiting": "Așteptând…",
|
||||
"connection-hash": "Pentru a verifica securitatea criptării end-to-end, comparați acest număr de securitate pe ambele dispozitive",
|
||||
"preparing": "Pregătirea…",
|
||||
"transferring": "Transferul…"
|
||||
},
|
||||
"dialogs": {
|
||||
"base64-paste-to-send": "Lipiți aici pentru a trimite {{type}}",
|
||||
"auto-accept-instructions-2": "pentru a accepta automat toate fișierele trimise de la dispozitivul respectiv.",
|
||||
"receive-text-title": "Mesaj primit",
|
||||
"edit-paired-devices-title": "Editați dispozitivele asociate",
|
||||
"cancel": "Anulează",
|
||||
"auto-accept-instructions-1": "Activează",
|
||||
"pair-devices-title": "Împerecherea permanentă a dispozitivelor",
|
||||
"download": "Descarcă",
|
||||
"title-file": "Fişier",
|
||||
"base64-processing": "Procesarea…",
|
||||
"decline": "Declin",
|
||||
"receive-title": "{{descriptor}} Primit",
|
||||
"leave": "Pleacă",
|
||||
"join": "Alătură-te",
|
||||
"title-image-plural": "Imagini",
|
||||
"send": "Trimite",
|
||||
"base64-tap-to-paste": "Atinge aici pentru a lipi {{type}}",
|
||||
"base64-text": "text",
|
||||
"copy": "Copiază",
|
||||
"file-other-description-image": "și 1 altă imagine",
|
||||
"temporary-public-room-title": "Cameră publică temporară",
|
||||
"base64-files": "fişiere",
|
||||
"has-sent": "a trimis:",
|
||||
"file-other-description-file": "și 1 alt fișier",
|
||||
"close": "Închide",
|
||||
"system-language": "Limba Sistemului",
|
||||
"unpair": "Decuplează",
|
||||
"title-image": "Imagine",
|
||||
"file-other-description-file-plural": "și {{count}} alte fișiere",
|
||||
"would-like-to-share": "ar dori să împărtășească",
|
||||
"send-message-to": "Trimite un mesaj la",
|
||||
"language-selector-title": "Selectaţi Limba",
|
||||
"pair": "Cuplu",
|
||||
"hr-or": "SAU",
|
||||
"scan-qr-code": "sau scanați codul QR.",
|
||||
"input-key-on-this-device": "Introduceți această cheie pe un alt dispozitiv",
|
||||
"download-again": "Descarcă din nou",
|
||||
"accept": "Acceptă",
|
||||
"paired-devices-wrapper_data-empty": "Nu sunt dispozitive asociate.",
|
||||
"enter-key-from-another-device": "Introduceți aici cheia de la un alt dispozitiv.",
|
||||
"share": "Partajați",
|
||||
"auto-accept": "auto-acceptare",
|
||||
"title-file-plural": "Fişiere",
|
||||
"send-message-title": "Trimite un mesaj",
|
||||
"input-room-id-on-another-device": "Introduceți acest ID de cameră pe un alt dispozitiv",
|
||||
"file-other-description-image-plural": "și {{count}} alte imagini",
|
||||
"enter-room-id-from-another-device": "Introdu ID-ul camerei de pe un alt dispozitiv pentru a intra în cameră."
|
||||
},
|
||||
"about": {
|
||||
"claim": "Cel mai simplu mod de a transfera fișiere între dispozitive",
|
||||
"tweet_title": "Tweet despre PairDrop",
|
||||
"close-about_aria-label": "Închide Despre PairDrop",
|
||||
"buy-me-a-coffee_title": "Cumpără-mi o cafea!",
|
||||
"github_title": "PairDrop pe GitHub",
|
||||
"faq_title": "Întrebări frecvente"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-transfer-requested": "Transfer de fișiere cerut",
|
||||
"message-received-plural": "{{count}}} Mesaje primite",
|
||||
"message-received": "Mesaj primit",
|
||||
"file-received": "Fișier Primit",
|
||||
"file-received-plural": "{{count}} Fișiere Primite"
|
||||
}
|
||||
}
|
||||
160
public_included_ws_fallback/lang/ru.json
Normal file
160
public_included_ws_fallback/lang/ru.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"header": {
|
||||
"about_aria-label": "Открыть страницу \"О сервисе\"",
|
||||
"pair-device_title": "Связать ваши устройства навсегда",
|
||||
"install_title": "Установить PairDrop",
|
||||
"cancel-paste-mode": "Выполнено",
|
||||
"edit-paired-devices_title": "Редактировать связанные устройства",
|
||||
"notification_title": "Включить уведомления",
|
||||
"about_title": "О сервисе",
|
||||
"theme-auto_title": "Адаптировать тему к системной",
|
||||
"theme-dark_title": "Всегда использовать темную тему",
|
||||
"theme-light_title": "Всегда использовать светлую тему",
|
||||
"join-public-room_title": "Войти на время в публичную комнату",
|
||||
"language-selector_title": "Выбрать язык"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение",
|
||||
"no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||
"click-to-send": "Нажмите, чтобы отправить",
|
||||
"x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||
"tap-to-send": "Прикоснитесь, чтобы отправить",
|
||||
"x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу",
|
||||
"x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение",
|
||||
"no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы",
|
||||
"no-peers-subtitle": "Свяжите устройства или войдите в публичную комнату, чтобы вас могли обнаружить из других сетей",
|
||||
"activate-paste-mode-and-other-files": "и {{count}} других файлов",
|
||||
"activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить",
|
||||
"activate-paste-mode-shared-text": "общий текст"
|
||||
},
|
||||
"footer": {
|
||||
"display-name_data-placeholder": "Загрузка…",
|
||||
"routed": "направляется через сервер",
|
||||
"webrtc": ", если WebRTC недоступен.",
|
||||
"traffic": "Трафик",
|
||||
"paired-devices": "связанными устройствами",
|
||||
"known-as": "Вы известны под именем:",
|
||||
"on-this-network": "в этой сети",
|
||||
"display-name_title": "Изменить имя вашего устройства навсегда",
|
||||
"public-room-devices_title": "Вы можете быть обнаружены устройствами в этой публичной комнате вне зависимости от сети.",
|
||||
"paired-devices_title": "Вы можете быть обнаружены связанными устройствами в любое время вне зависимости от сети.",
|
||||
"public-room-devices": "в комнате {{roomId}}",
|
||||
"discovery": "Вы можете быть обнаружены:",
|
||||
"on-this-network_title": "Вы можете быть обнаружены кем угодно в этой сети."
|
||||
},
|
||||
"dialogs": {
|
||||
"edit-paired-devices-title": "Редактировать связанные устройства",
|
||||
"auto-accept": "автоприем",
|
||||
"close": "Закрыть",
|
||||
"decline": "Отклонить",
|
||||
"share": "Поделиться",
|
||||
"would-like-to-share": "хотел бы поделиться",
|
||||
"has-sent": "отправил:",
|
||||
"paired-devices-wrapper_data-empty": "Нет связанных устройств.",
|
||||
"download": "Скачать",
|
||||
"receive-text-title": "Сообщение получено",
|
||||
"send": "Отправить",
|
||||
"send-message-to": "Отправить сообщение",
|
||||
"send-message-title": "Отправить сообщение",
|
||||
"copy": "Копировать",
|
||||
"base64-files": "файлы",
|
||||
"base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}",
|
||||
"base64-processing": "Обработка…",
|
||||
"base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}",
|
||||
"base64-text": "текст",
|
||||
"title-file": "Файл",
|
||||
"title-file-plural": "Файлы",
|
||||
"title-image": "Изображение",
|
||||
"title-image-plural": "Изображения",
|
||||
"download-again": "Скачать еще раз",
|
||||
"auto-accept-instructions-2": ", чтобы автоматически принимать все файлы, отправленные с того устройства.",
|
||||
"enter-key-from-another-device": "Введите сюда ключ с другого устройства.",
|
||||
"pair-devices-title": "Соединить устройства навсегда",
|
||||
"input-key-on-this-device": "На другом устройстве введите этот ключ",
|
||||
"scan-qr-code": "или отсканируйте QR-код.",
|
||||
"cancel": "Отменить",
|
||||
"pair": "Подключить",
|
||||
"accept": "Принять",
|
||||
"auto-accept-instructions-1": "Активировать",
|
||||
"file-other-description-file": "и 1 другой файл",
|
||||
"file-other-description-image-plural": "и {{count}} других изображений",
|
||||
"file-other-description-image": "и 1 другое изображение",
|
||||
"file-other-description-file-plural": "и {{count}} других файлов",
|
||||
"receive-title": "{{descriptor}} получен",
|
||||
"system-language": "Язык системы",
|
||||
"unpair": "Отвязать",
|
||||
"language-selector-title": "Выбрать язык",
|
||||
"hr-or": "ИЛИ",
|
||||
"input-room-id-on-another-device": "На другом устройстве введите этот ID комнаты",
|
||||
"leave": "Покинуть",
|
||||
"join": "Войти",
|
||||
"enter-room-id-from-another-device": "Введите ID комнаты с другого устройства, чтобы войти в нее.",
|
||||
"temporary-public-room-title": "Временная публичная комната"
|
||||
},
|
||||
"about": {
|
||||
"close-about-aria-label": "Закрыть страницу \"О сервисе\"",
|
||||
"claim": "Самый простой способ передачи файлов между устройствами",
|
||||
"close-about_aria-label": "Закрыть страницу \"О сервисе\"",
|
||||
"buy-me-a-coffee_title": "Купить мне кофе!",
|
||||
"github_title": "PairDrop на GitHub",
|
||||
"tweet_title": "Твит о PairDrop",
|
||||
"faq_title": "Часто задаваемые вопросы"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "Отображаемое имя было изменено навсегда.",
|
||||
"display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.",
|
||||
"pairing-success": "Устройства связаны.",
|
||||
"pairing-tabs-error": "Связка двух вкладок браузера невозможна.",
|
||||
"copied-to-clipboard": "Скопировано в буфер обмена",
|
||||
"pairing-not-persistent": "Связанные устройства непостоянны.",
|
||||
"link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть",
|
||||
"notifications-enabled": "Уведомления включены.",
|
||||
"text-content-incorrect": "Содержание текста неверно.",
|
||||
"message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать",
|
||||
"connected": "Подключено.",
|
||||
"copied-text": "Текст скопирован в буфер обмена",
|
||||
"online": "Вы снова в сети",
|
||||
"offline": "Вы находитесь вне сети",
|
||||
"online-requirement": "Для сопряжения устройств вам нужно быть в сети.",
|
||||
"files-incorrect": "Файлы неверны.",
|
||||
"message-transfer-completed": "Передача сообщения завершена.",
|
||||
"ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз",
|
||||
"selected-peer-left": "Выбранный узел вышел.",
|
||||
"request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}",
|
||||
"rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.",
|
||||
"unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть PairDrop?",
|
||||
"copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!",
|
||||
"pairing-cleared": "Все устройства отвязаны.",
|
||||
"pairing-key-invalid": "Неверный ключ",
|
||||
"pairing-key-invalidated": "Ключ {{key}} признан недействительным.",
|
||||
"click-to-download": "Нажмите, чтобы скачать",
|
||||
"clipboard-content-incorrect": "Содержание буфера обмена неверно.",
|
||||
"click-to-show": "Нажмите, чтобы показать",
|
||||
"connecting": "Подключение…",
|
||||
"download-successful": "{{descriptor}} загружен",
|
||||
"display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.",
|
||||
"file-content-incorrect": "Содержимое файла неверно.",
|
||||
"file-transfer-completed": "Передача файла завершена.",
|
||||
"public-room-left": "Покинуть публичную комнату {{publicRoomId}}",
|
||||
"copied-to-clipboard-error": "Копирование невозможно. Скопируйте вручную.",
|
||||
"public-room-id-invalid": "Неверный ID комнаты",
|
||||
"online-requirement-pairing": "Для связки устройств необходимо находиться быть онлайн.",
|
||||
"online-requirement-public-room": "Для создания публичной комнаты необходимо быть онлайн."
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}",
|
||||
"preparing": "Подготовка…",
|
||||
"transferring": "Передача…",
|
||||
"processing": "Обработка…",
|
||||
"waiting": "Ожидание…",
|
||||
"connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах",
|
||||
"click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received-plural": "{{count}} файлов получено",
|
||||
"message-received-plural": "{{count}} сообщений получено",
|
||||
"file-received": "Файл получен",
|
||||
"file-transfer-requested": "Запрошена передача файлов",
|
||||
"message-received": "Сообщение получено"
|
||||
}
|
||||
}
|
||||
25
public_included_ws_fallback/lang/tr.json
Normal file
25
public_included_ws_fallback/lang/tr.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "PairDrop Hakkında",
|
||||
"about_aria-label": "PairDrop Hakkında Aç",
|
||||
"theme-auto_title": "Temayı Sisteme Uyarla",
|
||||
"theme-light_title": "Daima Açık Tema Kullan",
|
||||
"theme-dark_title": "Daima Koyu Tema Kullan",
|
||||
"notification_title": "Bildirimleri Etkinleştir",
|
||||
"install_title": "PairDrop'u Yükle",
|
||||
"pair-device_title": "Cihaz Eşleştir",
|
||||
"edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle",
|
||||
"cancel-paste-mode": "Bitti"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
|
||||
},
|
||||
"footer": {
|
||||
"display-name_data-placeholder": "Yükleniyor…",
|
||||
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
|
||||
},
|
||||
"dialogs": {
|
||||
"cancel": "İptal",
|
||||
"edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle"
|
||||
}
|
||||
}
|
||||
159
public_included_ws_fallback/lang/zh-CN.json
Normal file
159
public_included_ws_fallback/lang/zh-CN.json
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"header": {
|
||||
"about_title": "关于 PairDrop",
|
||||
"about_aria-label": "打开 关于 PairDrop",
|
||||
"theme-light_title": "总是使用明亮主题",
|
||||
"install_title": "安装 PairDrop",
|
||||
"pair-device_title": "永久配对您的设备",
|
||||
"theme-auto_title": "主题适应系统",
|
||||
"theme-dark_title": "总是使用暗黑主题",
|
||||
"notification_title": "开启通知",
|
||||
"edit-paired-devices_title": "管理已配对设备",
|
||||
"cancel-paste-mode": "完成",
|
||||
"join-public-room_title": "暂时加入公共房间",
|
||||
"language-selector_title": "选择语言"
|
||||
},
|
||||
"instructions": {
|
||||
"x-instructions_data-drop-peer": "释放以发送到此设备",
|
||||
"no-peers_data-drop-bg": "释放来选择接收者",
|
||||
"no-peers-subtitle": "配对新设备 或 加入一个公共房间 以便在其他网络上可见",
|
||||
"no-peers-title": "在其他设备上打开 PairDrop 来发送文件",
|
||||
"x-instructions_desktop": "点击以发送文件 或 右键来发送信息",
|
||||
"x-instructions_mobile": "轻触以发送文件 或 长按来发送信息",
|
||||
"x-instructions_data-drop-bg": "释放来选择接收者",
|
||||
"click-to-send": "点击发送",
|
||||
"tap-to-send": "轻触发送",
|
||||
"activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送",
|
||||
"activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件",
|
||||
"activate-paste-mode-shared-text": "分享文本"
|
||||
},
|
||||
"footer": {
|
||||
"routed": "途径服务器",
|
||||
"webrtc": "如果 WebRTC 不可用。",
|
||||
"known-as": "你的名字是:",
|
||||
"display-name_data-placeholder": "加载中…",
|
||||
"display-name_title": "修改你的默认设备名",
|
||||
"on-this-network": "在此网络上",
|
||||
"paired-devices": "已配对的设备",
|
||||
"traffic": "流量将",
|
||||
"public-room-devices_title": "您可以被这个独立于网络的公共房间中的设备发现。",
|
||||
"paired-devices_title": "您可以在任何时候被已配对的设备发现,而不依赖于网络。",
|
||||
"public-room-devices": "在房间 {{roomId}} 中",
|
||||
"discovery": "您可以被发现:",
|
||||
"on-this-network_title": "您可以被这个网络上的每个人发现。"
|
||||
},
|
||||
"dialogs": {
|
||||
"pair-devices-title": "配对新设备(常驻)",
|
||||
"input-key-on-this-device": "在另一个设备上输入这串数字",
|
||||
"base64-text": "信息",
|
||||
"enter-key-from-another-device": "在此处输入从另一个设备上获得的数字。",
|
||||
"edit-paired-devices-title": "管理已配对的设备",
|
||||
"pair": "配对",
|
||||
"cancel": "取消",
|
||||
"scan-qr-code": "或者 扫描二维码。",
|
||||
"paired-devices-wrapper_data-empty": "无已配对设备。",
|
||||
"auto-accept-instructions-1": "启用",
|
||||
"auto-accept": "自动接收",
|
||||
"decline": "拒绝",
|
||||
"base64-processing": "处理中…",
|
||||
"base64-tap-to-paste": "轻触此处粘贴{{type}}",
|
||||
"base64-paste-to-send": "粘贴到此处以发送 {{type}}",
|
||||
"auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。",
|
||||
"would-like-to-share": "想要分享",
|
||||
"accept": "接收",
|
||||
"close": "关闭",
|
||||
"share": "分享",
|
||||
"download": "保存",
|
||||
"send": "发送",
|
||||
"receive-text-title": "收到信息",
|
||||
"copy": "复制",
|
||||
"send-message-title": "发送信息",
|
||||
"send-message-to": "发了一条信息给",
|
||||
"has-sent": "发送了:",
|
||||
"base64-files": "文件",
|
||||
"file-other-description-file": "和 1 个其他的文件",
|
||||
"file-other-description-image": "和 1 个其他的图片",
|
||||
"file-other-description-image-plural": "和 {{count}} 个其他的图片",
|
||||
"file-other-description-file-plural": "和 {{count}} 个其他的文件",
|
||||
"title-image-plural": "图片",
|
||||
"receive-title": "收到 {{descriptor}}",
|
||||
"title-image": "图片",
|
||||
"title-file": "文件",
|
||||
"title-file-plural": "文件",
|
||||
"download-again": "再次保存",
|
||||
"system-language": "跟随系统语言",
|
||||
"unpair": "取消配对",
|
||||
"language-selector-title": "选择语言",
|
||||
"hr-or": "或者",
|
||||
"input-room-id-on-another-device": "在另一个设备上输入这串房间号",
|
||||
"leave": "离开",
|
||||
"join": "加入",
|
||||
"temporary-public-room-title": "临时公共房间",
|
||||
"enter-room-id-from-another-device": "在另一个设备上输入这串房间号来加入房间。"
|
||||
},
|
||||
"about": {
|
||||
"faq_title": "常见问题",
|
||||
"close-about_aria-label": "关闭 关于 PairDrop",
|
||||
"github_title": "PairDrop 在 GitHub 上开源",
|
||||
"claim": "最简单的跨设备传输方案",
|
||||
"buy-me-a-coffee_title": "帮我买杯咖啡!",
|
||||
"tweet_title": "关于 PairDrop 的推特"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "展示的名字已经永久变更。",
|
||||
"display-name-changed-temporarily": "展示的名字仅在此会话中变更。",
|
||||
"display-name-random-again": "展示的名字已再次随机生成。",
|
||||
"download-successful": "{{descriptor}} 已下载",
|
||||
"pairing-tabs-error": "无法配对两个浏览器标签页。",
|
||||
"pairing-success": "新设备已配对。",
|
||||
"pairing-not-persistent": "配对的设备不是持久的。",
|
||||
"pairing-key-invalid": "无效配对码",
|
||||
"pairing-key-invalidated": "配对码 {{key}} 已失效。",
|
||||
"text-content-incorrect": "文本内容不合法。",
|
||||
"file-content-incorrect": "文件内容不合法。",
|
||||
"clipboard-content-incorrect": "剪贴板内容不合法。",
|
||||
"link-received": "收到来自 {{name}} 的链接 - 点击打开",
|
||||
"message-received": "收到来自 {{name}} 的信息 - 点击复制",
|
||||
"request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}",
|
||||
"click-to-show": "点击展示",
|
||||
"copied-text": "复制到剪贴板",
|
||||
"selected-peer-left": "选择的设备已离开。",
|
||||
"pairing-cleared": "所有设备已解除配对。",
|
||||
"copied-to-clipboard": "已复制到剪贴板",
|
||||
"notifications-enabled": "通知已启用。",
|
||||
"copied-text-error": "写入剪贴板失败。请手动复制!",
|
||||
"click-to-download": "点击以保存",
|
||||
"unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭 PairDrop 吗?",
|
||||
"message-transfer-completed": "信息传输已完成。",
|
||||
"offline": "你未连接到网络",
|
||||
"online": "你已重新连接到网络",
|
||||
"connected": "已连接。",
|
||||
"online-requirement": "你需要连接网络来配对新设备。",
|
||||
"files-incorrect": "文件不合法。",
|
||||
"file-transfer-completed": "文件传输已完成。",
|
||||
"connecting": "连接中…",
|
||||
"ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB",
|
||||
"rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。",
|
||||
"public-room-left": "已退出公共房间 {{publicRoomId}}",
|
||||
"copied-to-clipboard-error": "无法复制。请手动复制。",
|
||||
"public-room-id-invalid": "无效的房间号",
|
||||
"online-requirement-pairing": "您需要连接到互联网来配对新设备。",
|
||||
"online-requirement-public-room": "您需要连接到互联网来创建一个公共房间。"
|
||||
},
|
||||
"document-titles": {
|
||||
"message-received": "收到信息",
|
||||
"message-received-plural": "收到 {{count}} 条信息",
|
||||
"file-transfer-requested": "文件传输请求",
|
||||
"file-received-plural": "收到 {{count}} 个文件",
|
||||
"file-received": "收到文件"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-paste-mode": "点击发送 {{descriptor}}",
|
||||
"click-to-send": "点击以发送文件 或 右键来发送信息",
|
||||
"connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号",
|
||||
"preparing": "准备中…",
|
||||
"waiting": "请等待…",
|
||||
"transferring": "传输中…",
|
||||
"processing": "处理中…"
|
||||
}
|
||||
}
|
||||
145
public_included_ws_fallback/scripts/localization.js
Normal file
145
public_included_ws_fallback/scripts/localization.js
Normal file
@@ -0,0 +1,145 @@
|
||||
class Localization {
|
||||
constructor() {
|
||||
Localization.defaultLocale = "en";
|
||||
Localization.supportedLocales = ["en", "nb", "ru", "zh-CN", "de", "ro", "id", "fr"];
|
||||
Localization.translations = {};
|
||||
Localization.defaultTranslations = {};
|
||||
|
||||
Localization.systemLocale = Localization.supportedOrDefault(navigator.languages);
|
||||
|
||||
let storedLanguageCode = localStorage.getItem("language-code");
|
||||
|
||||
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
|
||||
? storedLanguageCode
|
||||
: Localization.systemLocale;
|
||||
|
||||
Localization.setTranslation(Localization.initialLocale)
|
||||
.then(_ => {
|
||||
console.log("Initial translation successful.");
|
||||
Events.fire("initial-translation-loaded");
|
||||
});
|
||||
}
|
||||
|
||||
static isSupported(locale) {
|
||||
return Localization.supportedLocales.indexOf(locale) > -1;
|
||||
}
|
||||
|
||||
static supportedOrDefault(locales) {
|
||||
return locales.find(Localization.isSupported) || Localization.defaultLocale;
|
||||
}
|
||||
|
||||
static async setTranslation(locale) {
|
||||
if (!locale) locale = Localization.systemLocale;
|
||||
|
||||
await Localization.setLocale(locale)
|
||||
await Localization.translatePage();
|
||||
|
||||
console.log("Page successfully translated",
|
||||
`System language: ${Localization.systemLocale}`,
|
||||
`Selected language: ${locale}`
|
||||
);
|
||||
|
||||
Events.fire("translation-loaded");
|
||||
}
|
||||
|
||||
static async setLocale(newLocale) {
|
||||
if (newLocale === Localization.locale) return false;
|
||||
|
||||
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
|
||||
|
||||
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
|
||||
|
||||
if(!newTranslations) return false;
|
||||
|
||||
Localization.locale = newLocale;
|
||||
Localization.translations = newTranslations;
|
||||
}
|
||||
|
||||
static getLocale() {
|
||||
return Localization.locale;
|
||||
}
|
||||
|
||||
static isSystemLocale() {
|
||||
return !localStorage.getItem('language-code');
|
||||
}
|
||||
|
||||
static async fetchTranslationsFor(newLocale) {
|
||||
const response = await fetch(`lang/${newLocale}.json`)
|
||||
|
||||
if (response.redirected === true || response.status !== 200) return false;
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
static async translatePage() {
|
||||
document
|
||||
.querySelectorAll("[data-i18n-key]")
|
||||
.forEach(element => Localization.translateElement(element));
|
||||
}
|
||||
|
||||
static async translateElement(element) {
|
||||
const key = element.getAttribute("data-i18n-key");
|
||||
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
|
||||
|
||||
for (let i in attrs) {
|
||||
let attr = attrs[i];
|
||||
if (attr === "text") {
|
||||
element.innerText = Localization.getTranslation(key);
|
||||
} else {
|
||||
if (attr.startsWith("data-")) {
|
||||
let dataAttr = attr.substring(5);
|
||||
element.dataset.dataAttr = Localization.getTranslation(key, attr);
|
||||
} {
|
||||
element.setAttribute(attr, Localization.getTranslation(key, attr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getTranslation(key, attr=null, data={}, useDefault=false) {
|
||||
const keys = key.split(".");
|
||||
|
||||
let translationCandidates = useDefault
|
||||
? Localization.defaultTranslations
|
||||
: Localization.translations;
|
||||
|
||||
let translation;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
translationCandidates = translationCandidates[keys[i]]
|
||||
}
|
||||
|
||||
let lastKey = keys[keys.length - 1];
|
||||
|
||||
if (attr) lastKey += "_" + attr;
|
||||
|
||||
translation = translationCandidates[lastKey];
|
||||
|
||||
for (let j in data) {
|
||||
translation = translation.replace(`{{${j}}}`, data[j]);
|
||||
}
|
||||
} catch (e) {
|
||||
translation = "";
|
||||
}
|
||||
|
||||
if (!translation) {
|
||||
if (!useDefault) {
|
||||
translation = this.getTranslation(key, attr, data, true);
|
||||
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
|
||||
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`)
|
||||
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
|
||||
} else {
|
||||
console.warn("Missing translation in default language:", key, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return Localization.escapeHTML(translation);
|
||||
}
|
||||
|
||||
static escapeHTML(unsafeText) {
|
||||
let div = document.createElement('div');
|
||||
div.innerText = unsafeText;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
window.URL = window.URL || window.webkitURL;
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
window.isRtcSupported = false; //!!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
window.hiddenProperty = 'hidden' in document ? 'hidden' :
|
||||
'webkitHidden' in document ? 'webkitHidden' :
|
||||
@@ -21,10 +21,14 @@ class ServerConnection {
|
||||
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
||||
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
||||
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
||||
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
||||
|
||||
Events.on('create-public-room', _ => this._onCreatePublicRoom());
|
||||
Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid));
|
||||
Events.on('leave-public-room', _ => this._onLeavePublicRoom());
|
||||
|
||||
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
@@ -44,23 +48,47 @@ class ServerConnection {
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
Events.fire('ws-connected');
|
||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
||||
if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-pairing"));
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-initiate' })
|
||||
this.send({ type: 'pair-device-initiate' });
|
||||
}
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
_onPairDeviceJoin(pairKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
this.send({ type: 'pair-device-join', pairKey: pairKey });
|
||||
}
|
||||
|
||||
_onCreatePublicRoom() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-public-room"));
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'create-public-room' });
|
||||
}
|
||||
|
||||
_onJoinPublicRoom(roomId, createIfInvalid) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
|
||||
}
|
||||
|
||||
_onLeavePublicRoom() {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onLeavePublicRoom(), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'leave-public-room' });
|
||||
}
|
||||
|
||||
_setRtcConfig(config) {
|
||||
@@ -102,10 +130,10 @@ class ServerConnection {
|
||||
Events.fire('pair-device-join-key-invalid');
|
||||
break;
|
||||
case 'pair-device-canceled':
|
||||
Events.fire('pair-device-canceled', msg.roomKey);
|
||||
Events.fire('pair-device-canceled', msg.pairKey);
|
||||
break;
|
||||
case 'pair-device-join-key-rate-limit':
|
||||
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
|
||||
case 'join-key-rate-limit':
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
|
||||
break;
|
||||
case 'secret-room-deleted':
|
||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||
@@ -113,6 +141,15 @@ class ServerConnection {
|
||||
case 'room-secret-regenerated':
|
||||
Events.fire('room-secret-regenerated', msg);
|
||||
break;
|
||||
case 'public-room-id-invalid':
|
||||
Events.fire('public-room-id-invalid', msg.publicRoomId);
|
||||
break;
|
||||
case 'public-room-created':
|
||||
Events.fire('public-room-created', msg.roomId);
|
||||
break;
|
||||
case 'public-room-left':
|
||||
Events.fire('public-room-left');
|
||||
break;
|
||||
case 'request':
|
||||
case 'header':
|
||||
case 'partition':
|
||||
@@ -139,18 +176,12 @@ class ServerConnection {
|
||||
|
||||
_onPeers(msg) {
|
||||
Events.fire('peers', msg);
|
||||
if (msg.roomType === "ip" && msg.peers.length === 0) {
|
||||
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => {
|
||||
if (!peerId) return;
|
||||
console.log("successfully removed other peerIds from localStorage");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onDisplayName(msg) {
|
||||
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
sessionStorage.setItem('peer_id', msg.message.peerId);
|
||||
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
|
||||
|
||||
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
||||
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
||||
@@ -172,8 +203,8 @@ class ServerConnection {
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
const peerId = sessionStorage.getItem('peer_id');
|
||||
const peerIdHash = sessionStorage.getItem('peer_id_hash');
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
@@ -184,7 +215,7 @@ class ServerConnection {
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerId = sessionStorage.getItem('peer_id');
|
||||
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
||||
console.log("successfully removed peerId from localStorage");
|
||||
});
|
||||
@@ -200,7 +231,9 @@ class ServerConnection {
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
setTimeout(() => {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.connecting"));
|
||||
}, 100); //delay for 100ms to prevent flickering on page reload
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
@@ -232,12 +265,13 @@ class ServerConnection {
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||
this._server = serverConnection;
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._roomType = roomType;
|
||||
this._updateRoomSecret(roomSecret);
|
||||
|
||||
this._roomIds = {};
|
||||
this._updateRoomIds(roomType, roomId);
|
||||
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
@@ -258,34 +292,58 @@ class Peer {
|
||||
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
||||
}
|
||||
|
||||
_updateRoomSecret(roomSecret) {
|
||||
_isPaired() {
|
||||
return !!this._roomIds['secret'];
|
||||
}
|
||||
|
||||
_getPairSecret() {
|
||||
return this._roomIds['secret'];
|
||||
}
|
||||
|
||||
_getRoomTypes() {
|
||||
return Object.keys(this._roomIds);
|
||||
}
|
||||
|
||||
_updateRoomIds(roomType, roomId) {
|
||||
// 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);
|
||||
})
|
||||
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
|
||||
// multiple roomSecrets with same peer -> delete old roomSecret
|
||||
PersistentStorage.deleteRoomSecret(this._getPairSecret())
|
||||
.then(deletedRoomSecret => {
|
||||
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
||||
});
|
||||
}
|
||||
|
||||
this._roomSecret = roomSecret;
|
||||
this._roomIds[roomType] = roomId;
|
||||
|
||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
|
||||
// increase security by increasing roomSecret length
|
||||
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
|
||||
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
|
||||
console.log('RoomSecret is regenerated to increase security')
|
||||
Events.fire('regenerate-room-secret', this._roomSecret);
|
||||
Events.fire('regenerate-room-secret', this._getPairSecret());
|
||||
}
|
||||
}
|
||||
|
||||
_removeRoomType(roomType) {
|
||||
delete this._roomIds[roomType];
|
||||
|
||||
Events.fire('room-type-removed', {
|
||||
peerId: this._peerId,
|
||||
roomType: roomType
|
||||
});
|
||||
}
|
||||
|
||||
_evaluateAutoAccept() {
|
||||
if (!this._roomSecret) {
|
||||
if (!this._isPaired()) {
|
||||
this._setAutoAccept(false);
|
||||
return;
|
||||
}
|
||||
|
||||
PersistentStorage.getRoomSecretEntry(this._roomSecret)
|
||||
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
|
||||
.then(roomSecretEntry => {
|
||||
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
|
||||
const autoAccept = roomSecretEntry
|
||||
? roomSecretEntry.entry.auto_accept
|
||||
: false;
|
||||
this._setAutoAccept(autoAccept);
|
||||
})
|
||||
.catch(_ => {
|
||||
@@ -294,7 +352,9 @@ class Peer {
|
||||
}
|
||||
|
||||
_setAutoAccept(autoAccept) {
|
||||
this._autoAccept = autoAccept;
|
||||
this._autoAccept = !this._isSameBrowser()
|
||||
? autoAccept
|
||||
: false;
|
||||
}
|
||||
|
||||
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||
@@ -505,7 +565,7 @@ class Peer {
|
||||
|
||||
_abortTransfer() {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||
Events.fire('notify-user', 'Files are incorrect.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
this._digester = null;
|
||||
@@ -546,14 +606,14 @@ class Peer {
|
||||
this._abortTransfer();
|
||||
}
|
||||
|
||||
// include for compatibility with Snapdrop for Android app
|
||||
// include for compatibility with 'Snapdrop & PairDrop for Android' app
|
||||
Events.fire('file-received', fileBlob);
|
||||
|
||||
this._filesReceived.push(fileBlob);
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
@@ -563,7 +623,8 @@ class Peer {
|
||||
this._chunker = null;
|
||||
if (!this._filesQueue.length) {
|
||||
this._busy = false;
|
||||
Events.fire('notify-user', 'File transfer completed.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
|
||||
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
|
||||
} else {
|
||||
this._dequeueFile();
|
||||
}
|
||||
@@ -574,7 +635,7 @@ class Peer {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||
this._filesRequested = null;
|
||||
if (message.reason === 'ios-memory-limit') {
|
||||
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once");
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -584,7 +645,7 @@ class Peer {
|
||||
}
|
||||
|
||||
_onMessageTransferCompleted() {
|
||||
Events.fire('notify-user', 'Message transfer completed.');
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
|
||||
}
|
||||
|
||||
sendText(text) {
|
||||
@@ -615,8 +676,8 @@ class Peer {
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomSecret);
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomId);
|
||||
this.rtcSupported = true;
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._connect();
|
||||
@@ -642,13 +703,17 @@ class RTCPeer extends Peer {
|
||||
|
||||
_openChannel() {
|
||||
if (!this._conn) return;
|
||||
|
||||
const channel = this._conn.createDataChannel('data-channel', {
|
||||
ordered: true,
|
||||
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
|
||||
});
|
||||
channel.onopen = e => this._onChannelOpened(e);
|
||||
channel.onerror = e => this._onError(e);
|
||||
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
|
||||
|
||||
this._conn.createOffer()
|
||||
.then(d => this._onDescription(d))
|
||||
.catch(e => this._onError(e));
|
||||
}
|
||||
|
||||
_onDescription(description) {
|
||||
@@ -729,7 +794,7 @@ class RTCPeer extends Peer {
|
||||
_onBeforeUnload(e) {
|
||||
if (this._busy) {
|
||||
e.preventDefault();
|
||||
return "There are unfinished transfers. Are you sure you want to close?";
|
||||
return Localization.getTranslation("notifications.unfinished-transfers-warning");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,8 +853,8 @@ class RTCPeer extends Peer {
|
||||
_sendSignal(signal) {
|
||||
signal.type = 'signal';
|
||||
signal.to = this._peerId;
|
||||
signal.roomType = this._roomType;
|
||||
signal.roomSecret = this._roomSecret;
|
||||
signal.roomType = this._getRoomTypes()[0];
|
||||
signal.roomId = this._roomIds[this._getRoomTypes()[0]];
|
||||
this._server.send(signal);
|
||||
}
|
||||
|
||||
@@ -835,8 +900,8 @@ class WSPeer extends Peer {
|
||||
|
||||
sendJSON(message) {
|
||||
message.to = this._peerId;
|
||||
message.roomType = this._roomType;
|
||||
message.roomSecret = this._roomSecret;
|
||||
message.roomType = this._getRoomTypes()[0];
|
||||
message.roomId = this._roomIds[this._getRoomTypes()[0]];
|
||||
this._server.send(message);
|
||||
}
|
||||
|
||||
@@ -871,7 +936,14 @@ class PeersManager {
|
||||
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));
|
||||
|
||||
// this device closes connection
|
||||
Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));
|
||||
Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));
|
||||
|
||||
// peer closes connection
|
||||
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));
|
||||
@@ -886,51 +958,45 @@ class PeersManager {
|
||||
this.peers[peerId].onServerMessage(message);
|
||||
}
|
||||
|
||||
_refreshPeer(peer, roomType, roomSecret) {
|
||||
_refreshPeer(peer, roomType, roomId) {
|
||||
if (!peer) return false;
|
||||
|
||||
const roomTypeIsSecret = roomType === "secret";
|
||||
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
|
||||
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
|
||||
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
|
||||
|
||||
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
|
||||
if (roomTypeIsSecret && roomSecretsDiffer) {
|
||||
peer._updateRoomSecret(roomSecret);
|
||||
// if roomType or roomId for roomType differs peer is already connected
|
||||
// -> only update roomSecret and reevaluate auto accept
|
||||
if (roomTypesDiffer || roomIdsDiffer) {
|
||||
peer._updateRoomIds(roomType, roomId);
|
||||
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, rtcSupported) {
|
||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer) {
|
||||
this._refreshPeer(peer, roomType, roomSecret);
|
||||
this._refreshPeer(peer, roomType, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.isRtcSupported && rtcSupported) {
|
||||
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret);
|
||||
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomId);
|
||||
} else {
|
||||
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret);
|
||||
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerJoined(message) {
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported);
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported);
|
||||
}
|
||||
|
||||
_onPeers(message) {
|
||||
message.peers.forEach(peer => {
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported);
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -970,7 +1036,7 @@ class PeersManager {
|
||||
}
|
||||
if (message.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
Events.fire('peer-disconnected', message.peerId);
|
||||
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
|
||||
|
||||
// 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
|
||||
@@ -1002,14 +1068,42 @@ class PeersManager {
|
||||
if (peer._channel) peer._channel.onclose = null;
|
||||
peer._conn.close();
|
||||
peer._busy = false;
|
||||
peer._roomIds = {};
|
||||
}
|
||||
|
||||
_onRoomSecretsDeleted(roomSecrets) {
|
||||
for (let i=0; i<roomSecrets.length; i++) {
|
||||
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecrets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_onLeavePublicRoom(publicRoomId) {
|
||||
this._disconnectOrRemoveRoomTypeByRoomId('public-id', publicRoomId);
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
|
||||
this._onPeerDisconnected(peerId);
|
||||
}
|
||||
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecret);
|
||||
}
|
||||
|
||||
_disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {
|
||||
const peerIds = this._getPeerIdsFromRoomId(roomId);
|
||||
|
||||
if (!peerIds.length) return;
|
||||
|
||||
for (let i=0; i<peerIds.length; i++) {
|
||||
this._disconnectOrRemoveRoomTypeByPeerId(peerIds[i], roomType);
|
||||
}
|
||||
}
|
||||
|
||||
_disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {
|
||||
const peer = this.peers[peerId];
|
||||
|
||||
if (!peer) return;
|
||||
|
||||
if (peer._getRoomTypes().length > 1) {
|
||||
peer._removeRoomType(roomType);
|
||||
} else {
|
||||
Events.fire('peer-disconnected', peerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1040,20 +1134,26 @@ class PeersManager {
|
||||
}
|
||||
|
||||
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
||||
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
|
||||
const peerId = this._getPeerIdsFromRoomId(roomSecret)[0];
|
||||
|
||||
if (!peerId) return;
|
||||
|
||||
this.peers[peerId]._setAutoAccept(autoAccept);
|
||||
}
|
||||
|
||||
_getPeerIdFromRoomSecret(roomSecret) {
|
||||
_getPeerIdsFromRoomId(roomId) {
|
||||
if (!roomId) return [];
|
||||
|
||||
let peerIds = []
|
||||
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;
|
||||
|
||||
// peer must have same roomId.
|
||||
if (Object.values(peer._roomIds).includes(roomId)) {
|
||||
peerIds.push(peer._peerId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return peerIds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,7 +1239,7 @@ class FileDigester {
|
||||
}
|
||||
|
||||
class Events {
|
||||
static fire(type, detail) {
|
||||
static fire(type, detail = {}) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.7.7';
|
||||
const cacheVersion = 'v1.8.3';
|
||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
--icon-size: 24px;
|
||||
--primary-color: #4285f4;
|
||||
--paired-device-color: #00a69c;
|
||||
--public-room-color: #db8500;
|
||||
--accent-color: var(--primary-color);
|
||||
--peer-width: 120px;
|
||||
--ws-peer-color: #ff6b6b;
|
||||
color-scheme: light dark;
|
||||
@@ -24,6 +26,7 @@ body {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
transition: color 300ms;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -41,6 +44,10 @@ html {
|
||||
min-height: fill-available;
|
||||
}
|
||||
|
||||
.fw {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
@@ -52,7 +59,6 @@ html {
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -79,6 +85,10 @@ html {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
align-items: baseline;
|
||||
@@ -216,10 +226,6 @@ a,
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -276,8 +282,6 @@ x-noscript {
|
||||
margin-top: 56px;
|
||||
flex-direction: column-reverse;
|
||||
flex-grow: 1;
|
||||
--footer-height: 146px;
|
||||
max-height: calc(100vh - 56px - var(--footer-height));
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
@@ -285,17 +289,7 @@ x-noscript {
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 402px) and (max-width: 425px) {
|
||||
header:has(#edit-pair-devices:not([hidden]))~#center {
|
||||
--footer-height: 164px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 402px) {
|
||||
#center {
|
||||
--footer-height: 184px;
|
||||
}
|
||||
}
|
||||
/* Peers List */
|
||||
|
||||
#x-peers-filler {
|
||||
@@ -426,8 +420,9 @@ x-no-peers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
height: 137px;
|
||||
text-align: center;
|
||||
animation: fade-in 300ms;
|
||||
animation: fade-in 600ms;
|
||||
animation-fill-mode: backwards;
|
||||
/* prevent flickering on load */
|
||||
animation-iteration-count: 0;
|
||||
@@ -452,7 +447,7 @@ x-no-peers::before {
|
||||
}
|
||||
|
||||
x-no-peers[drop-bg]::before {
|
||||
content: "Release to select recipient";
|
||||
content: attr(data-drop-bg);
|
||||
}
|
||||
|
||||
x-no-peers[drop-bg] * {
|
||||
@@ -471,7 +466,6 @@ x-peer {
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
@@ -500,10 +494,14 @@ x-peer .icon-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
||||
x-peer.type-secret .icon-wrapper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
|
||||
background: var(--public-room-color);
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
@@ -512,17 +510,29 @@ x-peer x-icon > .highlight-wrapper {
|
||||
}
|
||||
|
||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||
width: 6px;
|
||||
width: 15px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
border-radius: 4px;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
||||
x-peer.type-ip x-icon > .highlight-wrapper > .highlight.highlight-room-ip {
|
||||
background-color: var(--primary-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight.highlight-room-secret {
|
||||
background-color: var(--paired-device-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer.type-public-id x-icon > .highlight-wrapper > .highlight.highlight-room-public-id {
|
||||
background-color: var(--public-room-color);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
x-peer:not([status]):hover x-icon,
|
||||
x-peer:not([status]):focus x-icon {
|
||||
transform: scale(1.05);
|
||||
@@ -551,6 +561,14 @@ x-peer.ws-peer .highlight-wrapper {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
#websocket-fallback {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#websocket-fallback > span:nth-of-type(2) {
|
||||
border-bottom: solid 2px var(--ws-peer-color);
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@@ -580,22 +598,6 @@ x-peer.ws-peer .highlight-wrapper {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
x-peer[status=transfer] .status:before {
|
||||
content: 'Transferring...';
|
||||
}
|
||||
|
||||
x-peer[status=prepare] .status:before {
|
||||
content: 'Preparing...';
|
||||
}
|
||||
|
||||
x-peer[status=wait] .status:before {
|
||||
content: 'Waiting...';
|
||||
}
|
||||
|
||||
x-peer[status=process] .status:before {
|
||||
content: 'Processing...';
|
||||
}
|
||||
|
||||
x-peer:not([status]) .status,
|
||||
x-peer[status] .device-name {
|
||||
display: none;
|
||||
@@ -629,12 +631,11 @@ x-peer[drop] x-icon {
|
||||
|
||||
footer {
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
z-index: 2;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
margin: auto 5px 5px;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
@@ -642,45 +643,75 @@ footer .logo {
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
margin-top: -10px;
|
||||
animation: ease-in;
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
color: var(--primary-color);
|
||||
margin: auto 18px;
|
||||
.discovery-wrapper {
|
||||
font-size: 12px;
|
||||
margin: 10px auto auto;
|
||||
border: 3px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 2px;
|
||||
background-color: rgb(var(--bg-color));
|
||||
transition: background-color 0.5s ease;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
#on-this-network {
|
||||
border-bottom: solid 4px var(--primary-color);
|
||||
padding-bottom: 1px;
|
||||
/*You can be discovered wrapper*/
|
||||
.discovery-wrapper > div:first-of-type {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
#paired-devices {
|
||||
border-bottom: solid 4px var(--paired-device-color);
|
||||
padding-bottom: 1px;
|
||||
|
||||
.discovery-wrapper .badge {
|
||||
word-break: keep-all;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: 0.3rem/0.3rem;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
background-color: var(--badge-color);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-room-ip {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.badge-room-secret {
|
||||
background-color: var(--paired-device-color);
|
||||
border-color: var(--paired-device-color);
|
||||
}
|
||||
|
||||
.badge-room-public-id {
|
||||
background-color: var(--public-room-color);
|
||||
border-color: var(--public-room-color);
|
||||
}
|
||||
|
||||
#display-name {
|
||||
position: relative;
|
||||
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;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
@@ -689,7 +720,6 @@ footer .font-body2 {
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
@@ -707,7 +737,6 @@ x-dialog x-paper {
|
||||
z-index: 3;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
@@ -716,14 +745,33 @@ x-dialog x-paper {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper {
|
||||
x-paper > .row:first-of-type {
|
||||
background-color: var(--accent-color);
|
||||
border-bottom: solid 4px var(--border-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
x-paper > .row:first-of-type h2 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#pair-device-dialog,
|
||||
#edit-paired-devices-dialog {
|
||||
--accent-color: var(--paired-device-color);
|
||||
}
|
||||
|
||||
#public-room-dialog {
|
||||
--accent-color: var(--public-room-color);
|
||||
}
|
||||
|
||||
#pair-device-dialog x-paper,
|
||||
#public-room-dialog x-paper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: max(50%, 350px);
|
||||
margin-top: -328.5px;
|
||||
width: calc(100vw - 20px);
|
||||
height: 625px;
|
||||
}
|
||||
|
||||
#pair-device-dialog ::-moz-selection,
|
||||
@@ -732,6 +780,12 @@ x-dialog x-paper {
|
||||
background: var(--paired-device-color);
|
||||
}
|
||||
|
||||
#public-room-dialog ::-moz-selection,
|
||||
#public-room-dialog ::selection {
|
||||
color: black;
|
||||
background: var(--public-room-color);
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -749,24 +803,22 @@ x-dialog a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
x-dialog .font-subheading {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
/* Pair Devices Dialog & Public Room Dialog */
|
||||
|
||||
/* Pair Devices Dialog */
|
||||
|
||||
#key-input-container {
|
||||
.input-key-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#key-input-container > input {
|
||||
.input-key-container > input {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 30px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
display: -webkit-box !important;
|
||||
display: -webkit-flex !important;
|
||||
display: -moz-flex !important;
|
||||
@@ -777,15 +829,15 @@ x-dialog .font-subheading {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#key-input-container > input + * {
|
||||
.input-key-container > input + * {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
#key-input-container > input:nth-of-type(4) {
|
||||
.input-key-container.six-chars > input:nth-of-type(4) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
#room-key {
|
||||
.key {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
@@ -796,13 +848,48 @@ x-dialog .font-subheading {
|
||||
margin: 15px -15px;
|
||||
}
|
||||
|
||||
#room-key-qr-code {
|
||||
.key-qr-code {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.key-instructions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
x-dialog h2 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
x-dialog hr {
|
||||
margin: 40px -24px 30px -24px;
|
||||
border: solid 1.25px var(--border-color);
|
||||
height: 3px;
|
||||
border: none;
|
||||
width: 100%;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.hr-note {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hr-note hr {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.hr-note > div {
|
||||
height: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
|
||||
.hr-note > div > span {
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
color: rgb(var(--text-color));
|
||||
background-color: rgb(var(--bg-color));
|
||||
border: var(--border-color) solid 3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#pair-device-dialog x-background {
|
||||
@@ -811,7 +898,7 @@ x-dialog hr {
|
||||
|
||||
/* Edit Paired Devices Dialog */
|
||||
.paired-devices-wrapper:empty:before {
|
||||
content: "No paired devices.";
|
||||
content: attr(data-empty);
|
||||
}
|
||||
|
||||
.paired-devices-wrapper:empty {
|
||||
@@ -896,39 +983,34 @@ x-dialog hr {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.paired-device > .auto-accept {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Receive Dialog */
|
||||
|
||||
x-dialog .row {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
x-paper > .row {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* button row*/
|
||||
x-paper > div:last-child {
|
||||
margin: auto -24px -15px;
|
||||
border-top: solid 2.5px var(--border-color);
|
||||
x-paper > .button-row {
|
||||
border-top: solid 3px var(--border-color);
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button {
|
||||
x-paper > .button-row > .button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
x-paper > div:last-child > .button:not(:last-child) {
|
||||
border-left: solid 2.5px var(--border-color);
|
||||
x-paper > .button-row > .button:not(:first-child) {
|
||||
border-right: solid 1.5px var(--border-color);
|
||||
}
|
||||
|
||||
x-paper > .button-row > .button:not(:last-child) {
|
||||
border-left: solid 1.5px var(--border-color);
|
||||
}
|
||||
|
||||
.file-description {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.file-description .row {
|
||||
margin: 0
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.file-description span {
|
||||
@@ -939,23 +1021,29 @@ x-paper > div:last-child > .button:not(:last-child) {
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
max-width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.file-stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
/* Send Text Dialog */
|
||||
/* Todo: add pair underline to send / receive dialogs displayName */
|
||||
x-dialog .dialog-subheader {
|
||||
margin-bottom: 25px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
#send-text-dialog .display-name-wrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#send-text-dialog .textarea {
|
||||
min-height: 200px;
|
||||
margin: 14px auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
@@ -970,7 +1058,6 @@ x-dialog .dialog-subheader {
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#receive-text-dialog #text a {
|
||||
@@ -997,12 +1084,11 @@ x-dialog .dialog-subheader {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
border: solid 12px #438cff;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64-paste-dialog .textarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
@@ -1014,21 +1100,9 @@ x-dialog .dialog-subheader {
|
||||
color: var(--primary-color);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
content: attr(placeholder);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#base64-paste-dialog button {
|
||||
margin: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#base64-paste-dialog button[close] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#base64-paste-dialog button[close]:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
|
||||
@@ -1045,12 +1119,13 @@ x-dialog .dialog-subheader {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
color: var(--accent-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button[disabled] {
|
||||
color: #5B5B66;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1084,6 +1159,11 @@ x-dialog .dialog-subheader {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.button[selected],
|
||||
.icon-button[selected] {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#cancel-paste-mode {
|
||||
z-index: 2;
|
||||
margin: 0;
|
||||
@@ -1126,8 +1206,7 @@ button::-moz-focus-inner {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16px 24px;
|
||||
border-radius: 16px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
background: #f1f3f4;
|
||||
@@ -1154,15 +1233,22 @@ button::-moz-focus-inner {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#about:not(:target) header.fade-in {
|
||||
transition-delay: 400ms;
|
||||
}
|
||||
|
||||
#about:target header.fade-in {
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
|
||||
#about .fade-in {
|
||||
transition: opacity 300ms;
|
||||
transition: opacity 300ms ease 300ms;
|
||||
will-change: opacity;
|
||||
transition-delay: 300ms;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#about:not(:target) .fade-in {
|
||||
opacity: 0;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
@@ -1212,10 +1298,6 @@ button::-moz-focus-inner {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#about header {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
canvas.circles {
|
||||
width: 100vw;
|
||||
position: absolute;
|
||||
@@ -1299,7 +1381,6 @@ x-toast:not([show]):not(:hover) {
|
||||
x-instructions {
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
text-align: center;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
@@ -1314,11 +1395,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||
}
|
||||
|
||||
x-instructions[drop-peer]:before {
|
||||
content: "Release to send to peer";
|
||||
content: attr(data-drop-peer);
|
||||
}
|
||||
|
||||
x-instructions[drop-bg]:not([drop-peer]):before {
|
||||
content: "Release to select recipient";
|
||||
content: attr(data-drop-bg);
|
||||
}
|
||||
|
||||
x-instructions p {
|
||||
@@ -1326,7 +1407,7 @@ x-instructions p {
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
@@ -1336,36 +1417,26 @@ x-peers:empty~x-instructions {
|
||||
}
|
||||
}
|
||||
|
||||
#websocket-fallback {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
/* Prevent Cumulative Layout Shift */
|
||||
|
||||
#websocket-fallback > span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#websocket-fallback > span > span {
|
||||
border-bottom: solid 4px var(--ws-peer-color);
|
||||
body > header,
|
||||
canvas,
|
||||
#center,
|
||||
x-no-peers,
|
||||
x-peers,
|
||||
x-instructions,
|
||||
footer > .icon.logo,
|
||||
.discovery-wrapper,
|
||||
.known-as-wrapper {
|
||||
transition: opacity 0.5s ease 0.1s;
|
||||
opacity: 0; /* will be set to 1 after initial translation is loaded */
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media screen and (max-width: 360px) {
|
||||
x-dialog x-paper {
|
||||
padding: 15px;
|
||||
}
|
||||
x-paper > div:last-child {
|
||||
margin: auto -15px -15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px) {
|
||||
#websocket-fallback {
|
||||
padding-bottom: 16px;
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1384,8 +1455,9 @@ body {
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-test: 18,18,18;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
--bg-color-secondary: #e4e4e4;
|
||||
--border-color: rgb(169, 169, 169);
|
||||
--badge-color: #a5a5a5;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
@@ -1393,7 +1465,8 @@ body.dark-theme {
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
--border-color: rgb(238,238,238);
|
||||
--badge-color: #717171;
|
||||
}
|
||||
|
||||
/* Colored Elements */
|
||||
@@ -1427,7 +1500,7 @@ x-dialog x-paper {
|
||||
|
||||
/* Image/Video/Audio Preview */
|
||||
.file-preview {
|
||||
margin: 10px -24px 40px -24px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.file-preview:empty {
|
||||
@@ -1451,15 +1524,17 @@ x-dialog x-paper {
|
||||
--text-color: 238,238,238;
|
||||
--bg-color: 18,18,18; /*rgb code*/
|
||||
--bg-color-secondary: #333;
|
||||
--border-color: #252525;
|
||||
--border-color: rgb(238,238,238);
|
||||
--badge-color: #717171;
|
||||
}
|
||||
|
||||
/* Override dark mode with light mode styles if the user decides to swap */
|
||||
body.light-theme {
|
||||
--text-color: 51,51,51;
|
||||
--bg-color: 250,250,250; /*rgb code*/
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
--border-color: #e7e8e8;
|
||||
--bg-color-secondary: #e4e4e4;
|
||||
--border-color: rgb(169, 169, 169);
|
||||
--badge-color: #a5a5a5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user