mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-07 18:32:15 +00:00
Compare commits
28 Commits
v1.10.9
...
fix-thumbn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb6fe7ae61 | ||
|
|
00d2757fdc | ||
|
|
ab67c5858d | ||
|
|
b34b3f7b39 | ||
|
|
3e502a76d0 | ||
|
|
08af4670ee | ||
|
|
a458c00213 | ||
|
|
2e088b8434 | ||
|
|
31e3b4304c | ||
|
|
d73e5666b9 | ||
|
|
96a055b7d0 | ||
|
|
703894e309 | ||
|
|
13f7d36da0 | ||
|
|
1549ff48c9 | ||
|
|
9f4d99c8db | ||
|
|
563c8dd8a8 | ||
|
|
43c346894d | ||
|
|
a68cd75b71 | ||
|
|
e85a2e6293 | ||
|
|
794e6304fe | ||
|
|
f9b8b0fadf | ||
|
|
10f648b7cd | ||
|
|
331c61fec8 | ||
|
|
fa24e77d3b | ||
|
|
6ca039910a | ||
|
|
9f02f7b3ca | ||
|
|
04d65da779 | ||
|
|
e6f2c776dc |
@@ -1,5 +1,13 @@
|
||||
node_modules
|
||||
.github
|
||||
.git*
|
||||
|
||||
*.md
|
||||
.idea
|
||||
dev
|
||||
docs
|
||||
licenses
|
||||
node_modules
|
||||
pairdrop-cli
|
||||
*.md
|
||||
*.yml
|
||||
Dockerfile
|
||||
rtc_config_example.json
|
||||
turnserver_example.conf
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
4
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -36,7 +36,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Bug occurs on official PairDrop instance https://pairdrop.net/**
|
||||
No | Yes
|
||||
Version: v1.10.9
|
||||
Version: v1.10.10
|
||||
|
||||
**Bug occurs on self-hosted PairDrop instance**
|
||||
No | Yes
|
||||
@@ -44,7 +44,7 @@ No | Yes
|
||||
**Self-Hosted Setup**
|
||||
Proxy: Nginx | Apache2
|
||||
Deployment: docker run | docker compose | npm run start:prod
|
||||
Version: v1.10.9
|
||||
Version: v1.10.10
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
fqdn.env
|
||||
/docker/certs
|
||||
/dev/certs
|
||||
qrcode-svg/
|
||||
turnserver.conf
|
||||
rtc_config.json
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
FROM node:lts-alpine
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci
|
||||
RUN apk add --no-cache nodejs npm
|
||||
RUN NODE_ENV="production" npm ci --omit=dev
|
||||
|
||||
# Directories and files excluded via .dockerignore
|
||||
COPY . .
|
||||
|
||||
# environment settings
|
||||
|
||||
@@ -32,7 +32,7 @@ Send a file from your phone to your laptop?
|
||||
<img src="docs/pairdrop_screenshot_mobile.gif" alt="Screenshot GIF showing PairDrop in use" style="width: 300px">
|
||||
|
||||
## Differences to the [Snapdrop](https://github.com/RobinLinus/snapdrop) it is based on
|
||||
<details><summary>List view</summary>
|
||||
<details><summary>View all differences</summary>
|
||||
|
||||
### Paired Devices and Public Rooms — Internet Transfer
|
||||
* Transfer files over the Internet between paired devices or by entering temporary public rooms.
|
||||
@@ -103,7 +103,7 @@ Connect to others in complex network situations, or over the Internet.
|
||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/) library
|
||||
* [cyrb53](https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js) super-fast hash function
|
||||
* [NoSleep](https://github.com/richtr/NoSleep.js) display sleep, add wake lock ([MIT](licenses/MIT-NoSleep))
|
||||
* [heic2any](https://github.com/alexcorvi/heic2any) HEIC/HEIF to PNG/GIF/JPEG ([MIT](licenses/MIT-heic2any))
|
||||
* [libheif](https://github.com/strukturag/libheif) library to handle HEIC/HEIF files (GPLv3)
|
||||
* [Weblate](https://weblate.org/) web-based localization tool
|
||||
|
||||
[FAQ](docs/faq.md)
|
||||
|
||||
3
dev/nginx-with-openssl.Dockerfile
Normal file
3
dev/nginx-with-openssl.Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
42
dev/nginx/default.conf
Normal file
42
dev/nginx/default.conf
Normal file
@@ -0,0 +1,42 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
expires epoch;
|
||||
|
||||
location / {
|
||||
proxy_connect_timeout 300;
|
||||
proxy_pass http://pairdrop:3000;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
}
|
||||
|
||||
location /ca.crt {
|
||||
alias /etc/ssl/certs/pairdropCA.crt;
|
||||
}
|
||||
|
||||
# To allow POST on static pages
|
||||
error_page 405 =200 $uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
ssl_certificate /etc/ssl/certs/pairdrop-dev.crt;
|
||||
ssl_certificate_key /etc/ssl/certs/pairdrop-dev.key;
|
||||
|
||||
expires epoch;
|
||||
|
||||
location / {
|
||||
proxy_connect_timeout 300;
|
||||
proxy_pass http://pairdrop:3000;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
}
|
||||
|
||||
location /ca.crt {
|
||||
alias /etc/ssl/certs/pairdropCA.crt;
|
||||
}
|
||||
# To allow POST on static pages
|
||||
error_page 405 =200 $uri;
|
||||
}
|
||||
|
||||
9
dev/openssl/create.sh
Normal file
9
dev/openssl/create.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
cnf_dir='/mnt/openssl/'
|
||||
certs_dir='/etc/ssl/certs/'
|
||||
openssl req -config ${cnf_dir}pairdropCA.cnf -new -x509 -days 1 -keyout ${certs_dir}pairdropCA.key -out ${certs_dir}pairdropCA.crt
|
||||
openssl req -config ${cnf_dir}pairdropCert.cnf -new -out /tmp/pairdrop-dev.csr -keyout ${certs_dir}pairdrop-dev.key
|
||||
openssl x509 -req -in /tmp/pairdrop-dev.csr -CA ${certs_dir}pairdropCA.crt -CAkey ${certs_dir}pairdropCA.key -CAcreateserial -extensions req_ext -extfile ${cnf_dir}pairdropCert.cnf -sha512 -days 1 -out ${certs_dir}pairdrop-dev.crt
|
||||
|
||||
exec "$@"
|
||||
26
dev/openssl/pairdropCA.cnf
Normal file
26
dev/openssl/pairdropCA.cnf
Normal file
@@ -0,0 +1,26 @@
|
||||
[ req ]
|
||||
default_bits = 2048
|
||||
default_md = sha256
|
||||
default_days = 1
|
||||
encrypt_key = no
|
||||
distinguished_name = subject
|
||||
x509_extensions = x509_ext
|
||||
string_mask = utf8only
|
||||
prompt = no
|
||||
|
||||
[ subject ]
|
||||
organizationName = PairDrop
|
||||
OU = CA
|
||||
commonName = pairdrop-CA
|
||||
|
||||
[ x509_ext ]
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer
|
||||
|
||||
# You only need digitalSignature below. *If* you don't allow
|
||||
# RSA Key transport (i.e., you use ephemeral cipher suites), then
|
||||
# omit keyEncipherment because that's key transport.
|
||||
|
||||
basicConstraints = critical, CA:TRUE, pathlen:0
|
||||
keyUsage = critical, digitalSignature, keyEncipherment, cRLSign, keyCertSign
|
||||
|
||||
29
dev/openssl/pairdropCert.cnf
Normal file
29
dev/openssl/pairdropCert.cnf
Normal file
@@ -0,0 +1,29 @@
|
||||
[ req ]
|
||||
default_bits = 2048
|
||||
default_md = sha256
|
||||
default_days = 1
|
||||
encrypt_key = no
|
||||
distinguished_name = subject
|
||||
req_extensions = req_ext
|
||||
string_mask = utf8only
|
||||
prompt = no
|
||||
|
||||
[ subject ]
|
||||
organizationName = PairDrop
|
||||
OU = Development
|
||||
|
||||
# Use a friendly name here because it's presented to the user. The server's DNS
|
||||
# names are placed in Subject Alternate Names. Plus, DNS names here is deprecated
|
||||
# by both IETF and CA/Browser Forums. If you place a DNS name here, then you
|
||||
# must include the DNS name in the SAN too (otherwise, Chrome and others that
|
||||
# strictly follow the CA/Browser Baseline Requirements will fail).
|
||||
|
||||
commonName = ${ENV::FQDN}
|
||||
|
||||
[ req_ext ]
|
||||
subjectKeyIdentifier = hash
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
subjectAltName = DNS:${ENV::FQDN}
|
||||
nsComment = "OpenSSL Generated Certificate"
|
||||
extendedKeyUsage = serverAuth
|
||||
34
docker-compose-dev.yml
Normal file
34
docker-compose-dev.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
version: "3"
|
||||
services:
|
||||
pairdrop:
|
||||
build: .
|
||||
container_name: pairdrop
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000 # UID to run the application as
|
||||
- PGID=1000 # GID to run the application as
|
||||
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
|
||||
- RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.
|
||||
- DEBUG_MODE=false # Set to true to debug container and peer connections.
|
||||
- TZ=Etc/UTC # Time Zone
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`
|
||||
nginx:
|
||||
build:
|
||||
context: dev/
|
||||
dockerfile: nginx-with-openssl.Dockerfile
|
||||
image: "nginx-with-openssl"
|
||||
volumes:
|
||||
- ./public:/usr/share/nginx/html
|
||||
- ./dev/certs:/etc/ssl/certs
|
||||
- ./dev/openssl:/mnt/openssl
|
||||
- ./dev/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "8443:443"
|
||||
environment:
|
||||
- FQDN=localhost
|
||||
entrypoint: /mnt/openssl/create.sh
|
||||
command: ["nginx", "-g", "daemon off;"]
|
||||
restart: unless-stopped
|
||||
@@ -188,7 +188,7 @@ to learn more about STUN, TURN and WebRTC.
|
||||
<br>
|
||||
|
||||
Yes. Your files are sent using WebRTC, encrypting them in transit.
|
||||
Still you have to trust the PairDrop server. To ensure the connection is secure and there is no [MITM](https://wikiless.org/wiki/Man-in-the-middle_attack) there is a plan to make PairDrop
|
||||
Still you have to trust the PairDrop server. To ensure the connection is secure and there is no [MITM](https://en.m.wikipedia.org/wiki/Man-in-the-middle_attack) there is a plan to make PairDrop
|
||||
zero trust by encrypting the signaling and implementing a verification process. See [issue #180](https://github.com/schlagmichdoch/PairDrop/issues/180) to keep updated.
|
||||
|
||||
<br>
|
||||
|
||||
@@ -556,10 +556,6 @@ a2enmod proxy
|
||||
a2enmod proxy_http
|
||||
```
|
||||
|
||||
```bash
|
||||
a2enmod proxy_wstunnel
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
|
||||
@@ -570,18 +566,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
|
||||
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
ProxyPass / http://127.0.0.1:3000/
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/?(.*) "ws://127.0.0.1:3000/$1" [P,L]
|
||||
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket
|
||||
</VirtualHost>
|
||||
<VirtualHost *:443>
|
||||
ProxyPass / https://127.0.0.1:3000/
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
|
||||
ProxyPass / https://127.0.0.1:3000/ upgrade=websocket
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
@@ -589,14 +577,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
|
||||
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
Redirect permanent / https://127.0.0.1:3000/
|
||||
Redirect permanent / https://127.0.0.1:3000/
|
||||
</VirtualHost>
|
||||
<VirtualHost *:443>
|
||||
ProxyPass / https://127.0.0.1:3000/
|
||||
RewriteEngine on
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L]
|
||||
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
@@ -652,68 +636,74 @@ To run PairDrop including its own coturn-server you need to punch holes in the f
|
||||
|
||||
<br>
|
||||
|
||||
### Firewall
|
||||
To run PairDrop including its own coturn-server you need to punch holes in the firewall. These ports must be opened additionally:
|
||||
- 3478 tcp/udp
|
||||
- 5349 tcp/udp
|
||||
- 10000:20000 tcp/udp
|
||||
|
||||
<br>
|
||||
|
||||
## Local Development
|
||||
|
||||
### Install
|
||||
|
||||
All files needed for developing are available on the branch `dev`.
|
||||
All files needed for developing are available in the folder `./dev`.
|
||||
|
||||
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
|
||||
For convenience, there is also a docker compose file for developing:
|
||||
|
||||
Then, clone the repository and run docker-compose:
|
||||
#### Developing with docker compose
|
||||
First, [Install docker with docker compose.](https://docs.docker.com/compose/install/)
|
||||
|
||||
Then, clone the repository and run docker compose:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop
|
||||
```
|
||||
```bash
|
||||
git checkout dev
|
||||
```
|
||||
```bash
|
||||
docker compose -f docker-compose-dev.yml up -d
|
||||
docker compose -f docker-compose-dev.yml up --no-deps --build
|
||||
```
|
||||
|
||||
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 Node.js server, run `docker logs pairdrop`.
|
||||
|
||||
- After changes to the code you have to rerun the `docker compose` command
|
||||
|
||||
<br>
|
||||
|
||||
### Testing PWA related features
|
||||
#### Testing PWA related features
|
||||
|
||||
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.
|
||||
you need to change the FQDN environment variable in `docker-compose-dev.yml`
|
||||
to the fully qualified domain name of your workstation. (Default: localhost)
|
||||
|
||||
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`,
|
||||
##### Windows
|
||||
- Make sure to install it to the `Trusted Root Certification Authorities` store.
|
||||
|
||||
##### 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`.
|
||||
|
||||
##### Firefox
|
||||
Firefox uses its own trust store. To install the CA:
|
||||
- point Firefox at `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)
|
||||
- When prompted, select `Trust this CA to identify websites` and click _OK_.
|
||||
|
||||
Alternatively:
|
||||
1. Download `ca.crt` from `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)
|
||||
2. Go to `about:preferences#privacy` scroll down to `Security` and `Certificates` and click `View Certificates`
|
||||
3. Import the downloaded certificate file (step 1)
|
||||
|
||||
##### Chrome
|
||||
- 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).
|
||||
|
||||
##### Google Chrome
|
||||
- To skip the installation of the certificate, you can also open `chrome://flags/#unsafely-treat-insecure-origin-as-secure`
|
||||
- The feature `Insecure origins treated as secure` must be enabled and the list must include your PairDrop test instance. E.g.: `http://127.0.0.1:3000,https://127.0.0.1:8443`
|
||||
|
||||
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.
|
||||
|
||||
The site is served on `https://<Your FQDN>:8443`.
|
||||
The site is served on `https://<Your FQDN>:8443` (Default: `https://localhost:8443`).
|
||||
|
||||
[< Back](/README.md)
|
||||
|
||||
@@ -45,11 +45,11 @@ This pairdrop-cli version was released alongside v1.10.4
|
||||
#### Linux / Mac
|
||||
1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases)
|
||||
```shell
|
||||
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.9/pairdrop-cli.zip"
|
||||
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.10/pairdrop-cli.zip"
|
||||
```
|
||||
or
|
||||
```shell
|
||||
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.9/pairdrop-cli.zip"
|
||||
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.10/pairdrop-cli.zip"
|
||||
```
|
||||
2. Unzip the archive to a folder of your choice e.g. `/usr/share/pairdrop-cli/`
|
||||
```shell
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Alex Corvi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.10.9",
|
||||
"version": "1.10.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pairdrop",
|
||||
"version": "1.10.9",
|
||||
"version": "1.10.10",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.10.9",
|
||||
"version": "1.10.10",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"main": "server/index.js",
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
<div class="column">
|
||||
<div class="known-as-wrapper">
|
||||
<span data-i18n-key="footer.known-as" data-i18n-attrs="text"></span>
|
||||
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title" placeholder="Loading..." autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg class="icon edit-pen">
|
||||
<use xlink:href="#edit-pen-icon"></use>
|
||||
</svg>
|
||||
@@ -202,6 +202,11 @@
|
||||
<span> - </span>
|
||||
<span>(Catalan)</span>
|
||||
</button>
|
||||
<button class="btn fw wrap" value="cs">
|
||||
<span>Čeština</span>
|
||||
<span> - </span>
|
||||
<span>(Czech)</span>
|
||||
</button>
|
||||
<button class="btn fw wrap" value="da">
|
||||
<span>Dansk</span>
|
||||
<span> - </span>
|
||||
@@ -285,6 +290,11 @@
|
||||
<span> - </span>
|
||||
<span>(Turkish)</span>
|
||||
</button>
|
||||
<button class="btn fw wrap" value="uk">
|
||||
<span>Українська</span>
|
||||
<span> - </span>
|
||||
<span>(Ukrainian)</span>
|
||||
</button>
|
||||
<button class="btn fw wrap" value="zh-CN">
|
||||
<span>中文</span>
|
||||
<span> - </span>
|
||||
@@ -612,7 +622,7 @@
|
||||
</svg>
|
||||
<div class="title-wrapper" dir="ltr">
|
||||
<h1>PairDrop</h1>
|
||||
<div class="font-subheading">v1.10.9</div>
|
||||
<div class="font-subheading">v1.10.10</div>
|
||||
</div>
|
||||
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
|
||||
<div class="row">
|
||||
|
||||
@@ -4,6 +4,181 @@
|
||||
"about_title": "O službě PairDrop",
|
||||
"language-selector_title": "Nastavit jazyk",
|
||||
"theme-auto_title": "Automatické přizpůsobení tématu systému",
|
||||
"pair-device_title": "Spárovat zařízení permanentně"
|
||||
"pair-device_title": "Spárovat zařízení permanentně",
|
||||
"theme-light_title": "Vždy používat světlé téma",
|
||||
"theme-dark_title": "Vždy používat tmavé téma",
|
||||
"notification_title": "Povolit upozornění",
|
||||
"install_title": "Nainstalovat PairDrop",
|
||||
"edit-paired-devices_title": "Upravit spárovaná zařízení",
|
||||
"join-public-room_title": "Připojte se dočasně k veřejné místnosti",
|
||||
"cancel-share-mode": "Zrušit",
|
||||
"edit-share-mode": "Upravit",
|
||||
"expand_title": "Rozbalit řádek tlačítka záhlaví"
|
||||
},
|
||||
"about": {
|
||||
"buy-me-a-coffee_title": "Kupte mi kávu!",
|
||||
"close-about_aria-label": "Zavřít O PairDrop",
|
||||
"claim": "Nejjednodušší způsob přenosu souborů mezi zařízeními",
|
||||
"github_title": "PairDrop na GitHubu",
|
||||
"tweet_title": "Tweet o PairDrop",
|
||||
"mastodon_title": "Napište o PairDrop na Mastodon",
|
||||
"custom_title": "Sledujte nás",
|
||||
"privacypolicy_title": "Otevřete naše zásady ochrany osobních údajů",
|
||||
"bluesky_title": "Sledujte nás na BlueSky",
|
||||
"faq_title": "Často kladené otázky"
|
||||
},
|
||||
"footer": {
|
||||
"webrtc": "pokud WebRTC není k dispozici.",
|
||||
"known-as": "Jste známí jako:",
|
||||
"display-name_data-placeholder": "Načítání…",
|
||||
"display-name_title": "Trvale upravit název zařízení",
|
||||
"discovery": "Můžete být objeveni:",
|
||||
"on-this-network": "na této síti",
|
||||
"on-this-network_title": "V této síti vás může objevit každý.",
|
||||
"paired-devices": "pomocí spárovaných zařízení",
|
||||
"paired-devices_title": "Spárovaná zařízení vás mohou kdykoli objevit nezávisle na síti.",
|
||||
"public-room-devices": "v místnosti {{roomId}}",
|
||||
"public-room-devices_title": "Zařízení v této veřejné místnosti vás mohou objevit nezávisle na síti.",
|
||||
"traffic": "Provoz je",
|
||||
"routed": "směrovány přes server"
|
||||
},
|
||||
"dialogs": {
|
||||
"auto-accept": "auto-accept",
|
||||
"pair-devices-title": "Spárujte zařízení trvale",
|
||||
"input-key-on-this-device": "Zadejte tento klíč na jiném zařízení",
|
||||
"scan-qr-code": "nebo naskenujte QR kód.",
|
||||
"enter-key-from-another-device": "Zde zadejte klíč z jiného zařízení.",
|
||||
"temporary-public-room-title": "Dočasná veřejná místnost",
|
||||
"input-room-id-on-another-device": "Zadejte toto ID místnosti na jiném zařízení",
|
||||
"enter-room-id-from-another-device": "Chcete-li se připojit k místnosti, zadejte ID místnosti z jiného zařízení.",
|
||||
"hr-or": "NEBO",
|
||||
"pair": "Párovat",
|
||||
"cancel": "Zrušit",
|
||||
"edit-paired-devices-title": "Upravit spárovaná zařízení",
|
||||
"unpair": "Zrušit spárování",
|
||||
"paired-device-removed": "Spárované zařízení bylo odstraněno.",
|
||||
"paired-devices-wrapper_data-empty": "Žádná spárovaná zařízení.",
|
||||
"auto-accept-instructions-1": "Aktivovat",
|
||||
"auto-accept-instructions-2": "automaticky přijímat všechny soubory odeslané z tohoto zařízení.",
|
||||
"close": "Zavřít",
|
||||
"join": "Připojit",
|
||||
"leave": "Odejít",
|
||||
"accept": "Přijmout",
|
||||
"decline": "Odmítnout",
|
||||
"would-like-to-share": "by se rád podělil",
|
||||
"has-sent": "odeslal:",
|
||||
"share": "Sdílet",
|
||||
"download": "Stáhnout",
|
||||
"send-message-title": "Poslat zprávu",
|
||||
"send-message-to": "Komu:",
|
||||
"message_title": "Vložte zprávu k odeslání",
|
||||
"message_placeholder": "Text",
|
||||
"send": "Odeslat",
|
||||
"receive-text-title": "Zpráva přijata",
|
||||
"copy": "Kopírovat",
|
||||
"base64-title-files": "Sdílet soubory",
|
||||
"base64-title-text": "Sdílet text",
|
||||
"base64-processing": "Zpracovává se…",
|
||||
"base64-tap-to-paste": "Klepnutím sem sdílejte {{type}}",
|
||||
"base64-files": "soubory",
|
||||
"file-other-description-image": "a 1 další obrázek",
|
||||
"base64-paste-to-send": "Sem vložte schránku pro sdílení {{type}}",
|
||||
"base64-text": "text",
|
||||
"file-other-description-file": "a 1 další soubor",
|
||||
"file-other-description-image-plural": "a další obrázky ({{count}})",
|
||||
"file-other-description-file-plural": "a {{count}} dalších souborů",
|
||||
"title-image": "Obrázek",
|
||||
"title-file": "Soubor",
|
||||
"title-image-plural": "Obrázky",
|
||||
"title-file-plural": "Soubory",
|
||||
"receive-title": "{{descriptor}} Přijato",
|
||||
"download-again": "Stáhnout znovu",
|
||||
"language-selector-title": "Nastavit jazyk",
|
||||
"system-language": "Jazyk systému",
|
||||
"public-room-qr-code_title": "Kliknutím zkopírujete odkaz do veřejné místnosti",
|
||||
"pair-devices-qr-code_title": "Kliknutím zkopírujete odkaz pro spárování tohoto zařízení",
|
||||
"approve": "schválit",
|
||||
"share-text-title": "Sdílet textovou zprávu",
|
||||
"share-text-subtitle": "Upravit zprávu před odesláním:",
|
||||
"share-text-checkbox": "Při sdílení textu vždy zobrazit tento dialog",
|
||||
"close-toast_title": "Zavřít oznámení"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Uvolněním vyberte příjemce",
|
||||
"no-peers-title": "Otevřete PairDrop na jiných zařízeních a posílejte soubory",
|
||||
"no-peers-subtitle": "Spárujte zařízení nebo vstupte do veřejné místnosti, abyste byli zjistitelní v jiných sítích",
|
||||
"x-instructions_desktop": "Kliknutím odešlete soubory nebo kliknutím pravým tlačítkem odešlete zprávu",
|
||||
"x-instructions_mobile": "Klepnutím odešlete soubory nebo dlouhým klepnutím odešlete zprávu",
|
||||
"x-instructions_data-drop-peer": "Uvolněním odešlete",
|
||||
"x-instructions_data-drop-bg": "Uvolněním vyberte příjemce",
|
||||
"x-instructions-share-mode_desktop": "Kliknutím odešlete {{descriptor}}",
|
||||
"x-instructions-share-mode_mobile": "Klepnutím odešlete {{descriptor}}",
|
||||
"activate-share-mode-base": "Pro odeslání otevřete PairDrop na jiných zařízeních",
|
||||
"activate-share-mode-and-other-file": "a 1 další soubor",
|
||||
"activate-share-mode-and-other-files-plural": "a {{count}} dalších souborů",
|
||||
"activate-share-mode-shared-text": "sdílený text",
|
||||
"activate-share-mode-shared-file": "sdílený soubor",
|
||||
"activate-share-mode-shared-files-plural": "{{count}} sdílených souborů",
|
||||
"webrtc-requirement": "Chcete-li použít PairDrop, musí být povoleno WebRTC!"
|
||||
},
|
||||
"notifications": {
|
||||
"display-name-changed-permanently": "Zobrazované jméno je trvale změněno",
|
||||
"display-name-changed-temporarily": "Zobrazované jméno je změněno pouze pro tuto relaci",
|
||||
"display-name-random-again": "Zobrazované jméno je opět náhodně generováno",
|
||||
"download-successful": "{{descriptor}} staženo",
|
||||
"pairing-tabs-error": "Spárování dvou záložek webového prohlížeče není možné",
|
||||
"pairing-success": "Zařízení spárována",
|
||||
"pairing-not-persistent": "Spárovaná zařízení nejsou trvalá",
|
||||
"pairing-key-invalid": "Neplatný klíč",
|
||||
"pairing-key-invalidated": "Klíč {{key}} byl neplatný",
|
||||
"public-room-id-invalid": "Neplatné ID místnosti",
|
||||
"public-room-left": "Opustit veřejnou místnost {{publicRoomId}}",
|
||||
"copied-to-clipboard": "Zkopírováno do schránky",
|
||||
"pair-url-copied-to-clipboard": "Odkaz pro spárování tohoto zařízení byl zkopírován do schránky",
|
||||
"room-url-copied-to-clipboard": "Odkaz do veřejné místnosti zkopírován do schránky",
|
||||
"pairing-cleared": "Všechna nespárovaná zařízení",
|
||||
"copied-to-clipboard-error": "Kopírování není možné. Kopírovat ručně.",
|
||||
"text-content-incorrect": "Textový obsah je nesprávný",
|
||||
"file-content-incorrect": "Obsah souboru je nesprávný",
|
||||
"clipboard-content-incorrect": "Obsah schránky je nesprávný",
|
||||
"notifications-enabled": "Oznámení povolena",
|
||||
"notifications-permissions-error": "Oprávnění k oznámení bylo zablokováno, protože uživatel několikrát odmítl výzvu k povolení. Toto lze resetovat v části Informace o stránce, ke které se dostanete kliknutím na ikonu zámku vedle řádku adresy URL.",
|
||||
"link-received": "Odkaz obdržel {{name}} – kliknutím otevřete",
|
||||
"message-received": "Zpráva přijatá uživatelem {{name}} – kliknutím zkopírujte",
|
||||
"click-to-download": "Kliknutím stáhnete",
|
||||
"request-title": "{{name}} chce přenést {{count}} {{descriptor}}",
|
||||
"copied-text": "Text byl zkopírován do schránky",
|
||||
"click-to-show": "Kliknutím zobrazíte",
|
||||
"copied-text-error": "Zápis do schránky se nezdařil. Zkopírujte ručně!",
|
||||
"offline": "Jste offline",
|
||||
"online": "Jste zpět online",
|
||||
"connected": "Připojeno",
|
||||
"online-requirement-public-room": "Chcete-li vytvořit veřejnou místnost, musíte být online",
|
||||
"online-requirement-pairing": "Chcete-li spárovat zařízení, musíte být online",
|
||||
"connecting": "Připojování…",
|
||||
"files-incorrect": "Soubory jsou nesprávné",
|
||||
"file-transfer-completed": "Přenos souborů byl dokončen",
|
||||
"message-transfer-completed": "Přenos zprávy byl dokončen",
|
||||
"ios-memory-limit": "Odesílání souborů do iOS je možné pouze do velikosti 200 MB najednou",
|
||||
"unfinished-transfers-warning": "Existují nedokončené přenosy Opravdu chcete zavřít PairDrop?",
|
||||
"rate-limit-join-key": "Bylo dosaženo limitu. Počkejte 10 sekund a zkuste to znovu.",
|
||||
"selected-peer-left": "Vybraný partner odešel"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "Soubor byl přijat",
|
||||
"file-received-plural": "Počet přijatých souborů: {{count}}",
|
||||
"file-transfer-requested": "Požadován přenos souboru",
|
||||
"message-received": "Zpráva přijata",
|
||||
"image-transfer-requested": "Požadován přenos obrázku",
|
||||
"message-received-plural": "Počet přijatých zpráv: {{count}}"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-share-mode": "Kliknutím odešlete {{descriptor}}",
|
||||
"click-to-send": "Kliknutím odešlete soubory nebo kliknutím pravým tlačítkem odešlete zprávu",
|
||||
"transferring": "Přenáší se…",
|
||||
"connection-hash": "Chcete-li ověřit bezpečnost šifrování typu end-to-end, porovnejte toto číslo zabezpečení na obou zařízeních",
|
||||
"preparing": "Připravuje se…",
|
||||
"waiting": "Čekání…",
|
||||
"processing": "Zpracovává se…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"transferring": "Mentransfer…"
|
||||
},
|
||||
"dialogs": {
|
||||
"base64-paste-to-send": "Tempel di sini untuk mengirim {{type}}",
|
||||
"base64-paste-to-send": "Tempel salinan 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",
|
||||
@@ -154,7 +154,12 @@
|
||||
"base64-title-files": "Bagikan File",
|
||||
"base64-title-text": "Bagikan Teks",
|
||||
"message_placeholder": "Teks",
|
||||
"paired-device-removed": "Perangkat yang dipasangkan telah dihapus."
|
||||
"paired-device-removed": "Perangkat yang dipasangkan telah dihapus.",
|
||||
"approve": "menyetujui",
|
||||
"share-text-title": "Kirim Pesan Teks",
|
||||
"share-text-subtitle": "Edit pesan sebelum mengirim:",
|
||||
"share-text-checkbox": "Selalu tampilkan dialog ini ketika mengirimkan teks",
|
||||
"close-toast_title": "Tutup notifikasi"
|
||||
},
|
||||
"about": {
|
||||
"claim": "Cara termudah untuk mentransfer file lintas perangkat",
|
||||
@@ -162,7 +167,11 @@
|
||||
"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"
|
||||
"faq_title": "Pertanyaan yang sering diajukan",
|
||||
"mastodon_title": "Tulis tentang PairDrop di Mastodon",
|
||||
"bluesky_title": "Ikuti kami di BlueSky",
|
||||
"custom_title": "Ikuti kami",
|
||||
"privacypolicy_title": "Buka kebijakan privasi kami"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-transfer-requested": "Permintaan Transfer File",
|
||||
|
||||
184
public/lang/uk.json
Normal file
184
public/lang/uk.json
Normal file
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"header": {
|
||||
"about_aria-label": "Відкрити \"Про PairDrop\"",
|
||||
"theme-auto_title": "Автоматично адаптувати тему до системної",
|
||||
"theme-light_title": "Завжди використовувати світлу тему",
|
||||
"install_title": "Встановити PairDrop",
|
||||
"join-public-room_title": "Приєднатися до публічної кімнати тимчасово",
|
||||
"cancel-share-mode": "Скасувати",
|
||||
"edit-share-mode": "Редагувати",
|
||||
"about_title": "Про PairDrop",
|
||||
"language-selector_title": "Встановити мову",
|
||||
"theme-dark_title": "Завжди використовувати темну тему",
|
||||
"pair-device_title": "Зв'язати ваші пристрої назавжди",
|
||||
"notification_title": "Увімкнути сповіщення",
|
||||
"edit-paired-devices_title": "Редагувати зв'язані пристрої",
|
||||
"expand_title": "Розгорнути рядок кнопок заголовка"
|
||||
},
|
||||
"instructions": {
|
||||
"no-peers_data-drop-bg": "Відпустіть, щоб вибрати одержувача",
|
||||
"x-instructions_desktop": "Натисніть, щоб надіслати файли, або клацніть правою кнопкою миші, щоб надіслати повідомлення",
|
||||
"x-instructions_data-drop-peer": "Відпустіть, щоб надіслати партнеру",
|
||||
"x-instructions-share-mode_desktop": "Натисніть, щоб надіслати {{descriptor}}",
|
||||
"x-instructions-share-mode_mobile": "Торкніться, щоб надіслати {{descriptor}}",
|
||||
"activate-share-mode-and-other-file": "та 1 інший файл",
|
||||
"activate-share-mode-shared-file": "спільний файл",
|
||||
"webrtc-requirement": "Щоб використовувати цей екземпляр PairDrop, WebRTC має бути увімкнено!",
|
||||
"no-peers-title": "Відкрийте PairDrop на інших пристроях, щоб надіслати файли",
|
||||
"no-peers-subtitle": "Зв’яжіть пристрої або введіть публічну кімнату, щоб бути помітним в інших мережах",
|
||||
"x-instructions_mobile": "Торкніться, щоб надіслати файли, або довго натисніть, щоб надіслати повідомлення",
|
||||
"x-instructions_data-drop-bg": "Відпустіть, щоб вибрати одержувача",
|
||||
"activate-share-mode-base": "Відкрийте PairDrop на інших пристроях, щоб надіслати",
|
||||
"activate-share-mode-and-other-files-plural": "та {{count}} інших файлів",
|
||||
"activate-share-mode-shared-text": "спільний текст",
|
||||
"activate-share-mode-shared-files-plural": "{{count}} спільних файлів"
|
||||
},
|
||||
"footer": {
|
||||
"known-as": "Вам відомо як:",
|
||||
"discovery": "Вас можна знайти:",
|
||||
"public-room-devices": "у кімнаті {{roomId}}",
|
||||
"public-room-devices_title": "Вас можуть знайти пристрої в цій публічній кімнаті, незалежно від мережі.",
|
||||
"traffic": "Трафік",
|
||||
"webrtc": "якщо WebRTC недоступний.",
|
||||
"display-name_data-placeholder": "Завантаження…",
|
||||
"display-name_title": "Редагувати назву вашого пристрою назавжди",
|
||||
"on-this-network_title": "Вас можуть знайти всі на цій мережі.",
|
||||
"routed": "маршрутизований через сервер",
|
||||
"on-this-network": "в цій мережі",
|
||||
"paired-devices": "через зв'язані пристрої",
|
||||
"paired-devices_title": "Вас можуть знайти зв'язані пристрої в будь-який час, незалежно від мережі."
|
||||
},
|
||||
"dialogs": {
|
||||
"input-key-on-this-device": "Введіть цей ключ на іншому пристрої",
|
||||
"scan-qr-code": "або відскануйте QR-код.",
|
||||
"enter-key-from-another-device": "Введіть ключ з іншого пристрою тут.",
|
||||
"temporary-public-room-title": "Тимчасова публічна кімната",
|
||||
"input-room-id-on-another-device": "Введіть цей ID кімнати на іншому пристрої",
|
||||
"enter-room-id-from-another-device": "Введіть ID кімнати з іншого пристрою, щоб приєднатися до кімнати.",
|
||||
"hr-or": "АБО",
|
||||
"cancel": "Скасувати",
|
||||
"edit-paired-devices-title": "Редагувати Зв'язані пристрої",
|
||||
"unpair": "Від'єднати",
|
||||
"paired-device-removed": "Зв'язаний пристрій був видалений.",
|
||||
"paired-devices-wrapper_data-empty": "Немає зв'язаних пристроїв.",
|
||||
"auto-accept-instructions-1": "Активувати",
|
||||
"auto-accept": "автоматичне прийняття",
|
||||
"auto-accept-instructions-2": "щоб автоматично приймати всі файли, надіслані з цього пристрою.",
|
||||
"join": "Приєднатися",
|
||||
"leave": "Покинути",
|
||||
"accept": "Прийняти",
|
||||
"decline": "Відхилити",
|
||||
"has-sent": "відправив:",
|
||||
"share": "Поділитися",
|
||||
"download": "Завантажити",
|
||||
"send-message-title": "Надіслати повідомлення",
|
||||
"send-message-to": "Кому:",
|
||||
"message_title": "Введіть повідомлення для надсилання",
|
||||
"base64-title-text": "Поділитися текстом",
|
||||
"base64-processing": "Обробка…",
|
||||
"base64-text": "текст",
|
||||
"file-other-description-image": "та ще 1 зображення",
|
||||
"file-other-description-file": "та ще 1 файл",
|
||||
"file-other-description-image-plural": "та ще {{count}} зображень",
|
||||
"title-file": "Файл",
|
||||
"title-image-plural": "Зображення",
|
||||
"title-file-plural": "Файли",
|
||||
"receive-title": "{{descriptor}} отримано",
|
||||
"system-language": "Системна мова",
|
||||
"public-room-qr-code_title": "Натисніть, щоб скопіювати посилання на публічну кімнату",
|
||||
"share-text-title": "Поділитися текстовим повідомленням",
|
||||
"share-text-subtitle": "Редагувати повідомлення перед відправкою:",
|
||||
"share-text-checkbox": "Завжди показувати цей діалог при поділі тексту",
|
||||
"close-toast_title": "Закрити сповіщення",
|
||||
"pair-devices-title": "Зв’язати пристрої назавжди",
|
||||
"pair": "Приєднати",
|
||||
"close": "Закрити",
|
||||
"would-like-to-share": "хоче поділитися",
|
||||
"copy": "Копіювати",
|
||||
"message_placeholder": "Текст",
|
||||
"send": "Надіслати",
|
||||
"base64-title-files": "Поділитися файлами",
|
||||
"receive-text-title": "Повідомлення отримано",
|
||||
"base64-tap-to-paste": "Натисніть тут, щоб поділитися {{type}}",
|
||||
"base64-paste-to-send": "Вставте буфер обміну тут, щоб поділитися {{type}}",
|
||||
"file-other-description-file-plural": "та ще {{count}} файлів",
|
||||
"base64-files": "файли",
|
||||
"title-image": "Зображення",
|
||||
"language-selector-title": "Встановити мову",
|
||||
"approve": "схвалити",
|
||||
"download-again": "Завантажити знову",
|
||||
"pair-devices-qr-code_title": "Натисніть, щоб скопіювати посилання для зв'язування цього пристрою"
|
||||
},
|
||||
"about": {
|
||||
"close-about_aria-label": "Закрити \"Про PairDrop\"",
|
||||
"github_title": "PairDrop на GitHub",
|
||||
"buy-me-a-coffee_title": "Купи мені каву!",
|
||||
"tweet_title": "Твіт про PairDrop",
|
||||
"bluesky_title": "Підписуйтесь на нас у BlueSky",
|
||||
"privacypolicy_title": "Відкрити нашу політику конфіденційності",
|
||||
"faq_title": "Часто задавані питання",
|
||||
"mastodon_title": "Напишіть про PairDrop на Mastodon",
|
||||
"custom_title": "Підписуйтесь на нас",
|
||||
"claim": "Найпростіший спосіб передачі файлів між пристроями"
|
||||
},
|
||||
"notifications": {
|
||||
"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}} недійсний",
|
||||
"pairing-cleared": "Всі пристрої роз'єднані",
|
||||
"public-room-id-invalid": "Недійсний ID кімнати",
|
||||
"public-room-left": "Покинув публічну кімнату {{publicRoomId}}",
|
||||
"copied-to-clipboard-error": "Копіювання неможливе. Скопіюйте вручну.",
|
||||
"clipboard-content-incorrect": "Вміст буфера обміну неправильний",
|
||||
"link-received": "Посилання отримано від {{name}} - Натисніть, щоб відкрити",
|
||||
"message-received": "Повідомлення отримано від {{name}} - Натисніть, щоб скопіювати",
|
||||
"click-to-download": "Натисніть, щоб завантажити",
|
||||
"request-title": "{{name}} хоче передати {{count}} {{descriptor}}",
|
||||
"click-to-show": "Натисніть, щоб показати",
|
||||
"copied-text": "Текст скопійовано в буфер обміну",
|
||||
"copied-text-error": "Запис у буфер обміну не вдався. Скопіюйте вручну!",
|
||||
"offline": "Ви офлайн",
|
||||
"online-requirement-pairing": "Вам потрібно бути онлайн, щоб зв'язати пристрої",
|
||||
"online-requirement-public-room": "Вам потрібно бути онлайн, щоб створити публічну кімнату",
|
||||
"connecting": "Підключення…",
|
||||
"ios-memory-limit": "Відправка файлів на iOS можлива лише до 200 МБ за один раз",
|
||||
"message-transfer-completed": "Передача повідомлення завершена",
|
||||
"rate-limit-join-key": "Досягнуто ліміт швидкості. Зачекайте 10 секунд і спробуйте знову.",
|
||||
"selected-peer-left": "Обраний пір залишив",
|
||||
"files-incorrect": "Файли неправильні",
|
||||
"display-name-changed-permanently": "Відображуване ім'я було змінено назавжди",
|
||||
"notifications-permissions-error": "Дозвіл на сповіщення було заблоковано, оскільки користувач кілька разів відхилив запит на дозвіл. Це можна скинути в інформації про сторінку, до якої можна отримати доступ, натиснувши значок замка поруч з рядком URL.",
|
||||
"copied-to-clipboard": "Скопійовано в буфер обміну",
|
||||
"pair-url-copied-to-clipboard": "Посилання для зв'язування цього пристрою скопійовано в буфер обміну",
|
||||
"room-url-copied-to-clipboard": "Посилання на публічну кімнату скопійовано в буфер обміну",
|
||||
"text-content-incorrect": "Текстовий вміст неправильний",
|
||||
"file-content-incorrect": "Вміст файлу неправильний",
|
||||
"notifications-enabled": "Сповіщення увімкнені",
|
||||
"connected": "Підключено",
|
||||
"online": "Ви знову онлайн",
|
||||
"file-transfer-completed": "Передача файлу завершена",
|
||||
"unfinished-transfers-warning": "Є незавершені передачі. Ви впевнені, що хочете закрити PairDrop?"
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "Файл отримано",
|
||||
"file-received-plural": "Отримано {{count}} файлів",
|
||||
"image-transfer-requested": "Запит на передачу зображення",
|
||||
"message-received": "Повідомлення отримано",
|
||||
"message-received-plural": "Отримано {{count}} повідомлень",
|
||||
"file-transfer-requested": "Запит на передачу файлу"
|
||||
},
|
||||
"peer-ui": {
|
||||
"click-to-send-share-mode": "Натисніть, щоб відправити {{descriptor}}",
|
||||
"connection-hash": "Щоб перевірити безпеку кінцевого шифрування, порівняйте цей номер безпеки на обох пристроях",
|
||||
"processing": "Обробка…",
|
||||
"click-to-send": "Натисніть, щоб відправити файли, або клацніть правою кнопкою миші, щоб відправити повідомлення",
|
||||
"preparing": "Підготовка…",
|
||||
"waiting": "Чекаю…",
|
||||
"transferring": "Переводимо…"
|
||||
}
|
||||
}
|
||||
1
public/scripts/heic2any.min.js
vendored
1
public/scripts/heic2any.min.js
vendored
File diff suppressed because one or more lines are too long
36
public/scripts/heif-convert.js
Normal file
36
public/scripts/heif-convert.js
Normal file
@@ -0,0 +1,36 @@
|
||||
function HeifConvert(libheif) {
|
||||
this.libheif = libheif;
|
||||
this.decoder = new libheif.HeifDecoder();
|
||||
}
|
||||
|
||||
|
||||
HeifConvert.prototype.convert = async function (buffer) {
|
||||
const decodeResult = this.decoder.decode(buffer);
|
||||
const image = decodeResult[0];
|
||||
|
||||
let w = image.get_width();
|
||||
let h = image.get_height();
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.createImageData(w, h);
|
||||
|
||||
await copyData(imageData, image);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
image.free();
|
||||
return canvas;
|
||||
};
|
||||
|
||||
function copyData(dataContainer, image) {
|
||||
return new Promise((resolve, reject) => {
|
||||
image.display(
|
||||
dataContainer,
|
||||
function () {
|
||||
resolve()
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
41
public/scripts/libheif.js
Normal file
41
public/scripts/libheif.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/scripts/libheif.wasm
Normal file
BIN
public/scripts/libheif.wasm
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@ class Localization {
|
||||
Localization.$htmlRoot = document.querySelector('html');
|
||||
|
||||
Localization.defaultLocale = "en";
|
||||
Localization.supportedLocales = ["ar", "be", "ca", "da", "de", "en", "es", "fr", "he", "hu", "id", "it", "ja", "kn", "nb", "nl", "pl", "pt-BR", "ro", "ru", "tr", "zh-CN", "zh-TW"];
|
||||
Localization.supportedLocales = ["ar", "be", "ca", "cs", "da", "de", "en", "es", "fr", "he", "hu", "id", "it", "ja", "kn", "nb", "nl", "pl", "pt-BR", "ro", "ru", "tr", "uk", "zh-CN", "zh-TW"];
|
||||
Localization.supportedLocalesRtl = ["ar", "he"];
|
||||
|
||||
Localization.translations = {};
|
||||
|
||||
@@ -17,7 +17,8 @@ class PairDrop {
|
||||
"scripts/qr-code.min.js",
|
||||
"scripts/zip.min.js",
|
||||
"scripts/no-sleep.min.js",
|
||||
"scripts/heic2any.min.js"
|
||||
"scripts/heif-convert.js",
|
||||
"scripts/libheif.js"
|
||||
];
|
||||
|
||||
this.registerServiceWorker();
|
||||
|
||||
@@ -332,6 +332,8 @@ class Peer {
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
|
||||
this.maxMessageSize = 65536; // 64 KB
|
||||
|
||||
// evaluate auto accept
|
||||
this._evaluateAutoAccept();
|
||||
}
|
||||
@@ -450,14 +452,7 @@ class Peer {
|
||||
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8, status: 'prepare'})
|
||||
|
||||
let dataUrl = '';
|
||||
if (files[0].type.split('/')[0] === 'image') {
|
||||
try {
|
||||
dataUrl = await getThumbnailAsDataUrl(files[0], 400, null, 0.9);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
let dataUrl = await this.getFileTransferThumbnail(files[0]);
|
||||
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'prepare'})
|
||||
|
||||
@@ -472,6 +467,30 @@ class Peer {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'})
|
||||
}
|
||||
|
||||
async getFileTransferThumbnail(image) {
|
||||
if (image.type.split('/')[0] !== 'image') {
|
||||
// file is not of type image -> abort!
|
||||
return '';
|
||||
}
|
||||
|
||||
let dataUrl = '';
|
||||
try {
|
||||
// Iteratively lower thumbnail quality until its size is less than maxMessageSize - 2 kB
|
||||
let quality = 1;
|
||||
do {
|
||||
quality -= 0.1;
|
||||
if (quality <= 0) {
|
||||
console.error("Could not create thumbnail that fits into one message.");
|
||||
return '';
|
||||
}
|
||||
dataUrl = await getThumbnailAsDataUrl(image, 450, 450, quality);
|
||||
} while (new Blob([dataUrl]).size + 2_000 > this.maxMessageSize);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
async sendFiles() {
|
||||
for (let i=0; i<this._filesRequested.length; i++) {
|
||||
this._filesQueue.push(this._filesRequested[i]);
|
||||
@@ -725,7 +744,8 @@ class RTCPeer extends Peer {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomId);
|
||||
|
||||
this.rtcSupported = true;
|
||||
this.rtcConfig = rtcConfig
|
||||
this.rtcConfig = rtcConfig;
|
||||
this.maxMessageSize = 262144; // 256 KB
|
||||
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._connect();
|
||||
@@ -811,6 +831,13 @@ class RTCPeer extends Peer {
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._onPageHide());
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
this._setMaxMessageSize();
|
||||
}
|
||||
|
||||
_setMaxMessageSize() {
|
||||
this.maxMessageSize = this._conn && this._conn.sctp
|
||||
? Math.min(this._conn.sctp.maxMessageSize, 1048576) // 1 MB max
|
||||
: 262144; // 256 KB
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
|
||||
@@ -201,15 +201,11 @@ class FooterUI {
|
||||
this.$discoveryWrapper = $$('footer .discovery-wrapper');
|
||||
|
||||
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
this.$displayName.addEventListener('focus', e => this._onFocusDisplayName(e));
|
||||
this.$displayName.addEventListener('blur', e => this._onBlurDisplayName(e));
|
||||
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
|
||||
// Load saved display name on page load
|
||||
Events.on('ws-connected', _ => this._loadSavedDisplayName());
|
||||
|
||||
Events.on('evaluate-footer-badges', _ => this._evaluateFooterBadges());
|
||||
}
|
||||
|
||||
@@ -234,17 +230,20 @@ class FooterUI {
|
||||
}
|
||||
|
||||
async _loadSavedDisplayName() {
|
||||
const displayName = await this._getSavedDisplayName()
|
||||
const displayNameSaved = await this._getSavedDisplayName()
|
||||
|
||||
if (!displayName) return;
|
||||
if (!displayNameSaved) return;
|
||||
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
Events.fire('self-display-name-changed', displayName);
|
||||
console.log("Retrieved edited display name:", displayNameSaved)
|
||||
Events.fire('self-display-name-changed', displayNameSaved);
|
||||
}
|
||||
|
||||
_onDisplayName(displayName){
|
||||
// set display name
|
||||
this.$displayName.setAttribute('placeholder', displayName);
|
||||
async _onDisplayName(displayNameServer){
|
||||
// load saved displayname first to prevent flickering
|
||||
await this._loadSavedDisplayName();
|
||||
|
||||
// set original display name as placeholder
|
||||
this.$displayName.setAttribute('placeholder', displayNameServer);
|
||||
}
|
||||
|
||||
|
||||
@@ -259,9 +258,27 @@ class FooterUI {
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUpDisplayName(e) {
|
||||
_onFocusDisplayName(e) {
|
||||
if (!e.target.innerText) {
|
||||
// Fix z-position of cursor when div is completely empty (Firefox only)
|
||||
e.target.innerText = "\n";
|
||||
|
||||
// On Chromium based browsers the cursor position is lost when adding sth. to the focused node. This adds it back.
|
||||
let sel = window.getSelection();
|
||||
sel.collapse(e.target.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
async _onBlurDisplayName(e) {
|
||||
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) {
|
||||
e.target.innerText = '';
|
||||
}
|
||||
|
||||
// Remove selection from text
|
||||
window.getSelection().removeAllRanges();
|
||||
|
||||
await this._saveDisplayName(e.target.innerText)
|
||||
}
|
||||
|
||||
async _saveDisplayName(newDisplayName) {
|
||||
|
||||
@@ -281,10 +281,8 @@ class PeersUI {
|
||||
|
||||
if (files[0].type.split('/')[0] === 'image') {
|
||||
try {
|
||||
let imageUrl = await getThumbnailAsDataUrl(files[0], 80, null, 0.9);
|
||||
|
||||
let imageUrl = await getThumbnailAsDataUrl(files[0], 80, 80, 0.9);
|
||||
this.$shareModeImageThumb.style.backgroundImage = `url(${imageUrl})`;
|
||||
|
||||
this.$shareModeImageThumb.removeAttribute('hidden');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -478,12 +478,7 @@ function getThumbnailAsDataUrl(file, width = undefined, height = undefined, qual
|
||||
try {
|
||||
if (file.type === "image/heif" || file.type === "image/heic") {
|
||||
// browsers can't show heic files --> convert to jpeg before creating thumbnail
|
||||
let blob = await fileToBlob(file);
|
||||
file = await heic2any({
|
||||
blob,
|
||||
toType: "image/jpeg",
|
||||
quality: quality
|
||||
});
|
||||
file = await heicToJpeg(file, 0.5);
|
||||
}
|
||||
|
||||
let imageUrl = URL.createObjectURL(file);
|
||||
@@ -493,26 +488,40 @@ function getThumbnailAsDataUrl(file, width = undefined, height = undefined, qual
|
||||
|
||||
await waitUntilImageIsLoaded(imageUrl);
|
||||
|
||||
let imageWidth = image.width;
|
||||
let imageHeight = image.height;
|
||||
let canvas = document.createElement('canvas');
|
||||
let heightForSpecifiedWidth;
|
||||
let widthForSpecifiedHeight;
|
||||
|
||||
// resize the canvas and draw the image data into it
|
||||
if (width) {
|
||||
heightForSpecifiedWidth = Math.floor(image.height * width / image.width);
|
||||
}
|
||||
if (height) {
|
||||
widthForSpecifiedHeight = Math.floor(image.width * height / image.height);
|
||||
}
|
||||
|
||||
// resize the canvas and draw the image on it
|
||||
if (width && height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
// mode "contain": preserve aspect ratio and use arguments as boundaries
|
||||
if (height > heightForSpecifiedWidth) {
|
||||
canvas.width = width;
|
||||
canvas.height = heightForSpecifiedWidth;
|
||||
}
|
||||
else {
|
||||
canvas.width = widthForSpecifiedHeight;
|
||||
canvas.height = height;
|
||||
}
|
||||
}
|
||||
else if (width) {
|
||||
canvas.width = width;
|
||||
canvas.height = Math.floor(imageHeight * width / imageWidth)
|
||||
canvas.height = heightForSpecifiedWidth;
|
||||
}
|
||||
else if (height) {
|
||||
canvas.width = Math.floor(imageWidth * height / imageHeight);
|
||||
canvas.width = widthForSpecifiedHeight;
|
||||
canvas.height = height;
|
||||
}
|
||||
else {
|
||||
canvas.width = imageWidth;
|
||||
canvas.height = imageHeight
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height
|
||||
}
|
||||
|
||||
let ctx = canvas.getContext("2d");
|
||||
@@ -527,6 +536,32 @@ function getThumbnailAsDataUrl(file, width = undefined, height = undefined, qual
|
||||
})
|
||||
}
|
||||
|
||||
function initHeicConverter() {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch("libheif.wasm")
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then(async (wasmBinary) => {
|
||||
resolve(new HeifConvert(libheif({ wasmBinary: wasmBinary })));
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function heicToJpeg(file, quality) {
|
||||
const heicConverter = await initHeicConverter();
|
||||
console.log("Using libheif", heicConverter.libheif.heif_get_version());
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const canvas = await heicConverter.convert(buffer);
|
||||
|
||||
return new Promise(resolve => {
|
||||
canvas.toBlob(blob => resolve(blob),
|
||||
'image/jpeg',
|
||||
quality
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Resolves returned promise when image is loaded and throws error if image cannot be shown
|
||||
function waitUntilImageIsLoaded(imageUrl, timeout = 10000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.10.9';
|
||||
const cacheVersion = 'v1.10.10';
|
||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||
const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions
|
||||
const relativePathsToCache = [
|
||||
@@ -7,6 +7,9 @@ const relativePathsToCache = [
|
||||
'manifest.json',
|
||||
'styles/styles-main.css',
|
||||
'styles/styles-deferred.css',
|
||||
'scripts/heif-convert.js',
|
||||
'scripts/libheif.js',
|
||||
'scripts/libheif.wasm',
|
||||
'scripts/localization.js',
|
||||
'scripts/main.js',
|
||||
'scripts/network.js',
|
||||
@@ -29,6 +32,7 @@ const relativePathsToCache = [
|
||||
'lang/ar.json',
|
||||
'lang/be.json',
|
||||
'lang/ca.json',
|
||||
'lang/cs.json',
|
||||
'lang/da.json',
|
||||
'lang/de.json',
|
||||
'lang/en.json',
|
||||
@@ -47,6 +51,7 @@ const relativePathsToCache = [
|
||||
'lang/ro.json',
|
||||
'lang/ru.json',
|
||||
'lang/tr.json',
|
||||
'lang/uk.json',
|
||||
'lang/zh-CN.json',
|
||||
'lang/zh-TW.json'
|
||||
];
|
||||
|
||||
@@ -557,6 +557,10 @@ footer .logo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#display-name:focus::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html:not([dir="rtl"]) #display-name,
|
||||
html:not([dir="rtl"]) .edit-pen {
|
||||
margin-left: -1rem;
|
||||
@@ -939,8 +943,8 @@ body {
|
||||
--lt-dialog-bg-color: #fff;
|
||||
--lt-bg-color: 255,255,255;
|
||||
--lt-bg-color-secondary: #f2f2f2;
|
||||
--lt-border-color: #a9a9a9;
|
||||
--lt-badge-color: #a5a5a5;
|
||||
--lt-border-color: #757575;
|
||||
--lt-badge-color: #757575;
|
||||
--lt-lang-hr-color: #DDD;
|
||||
|
||||
--lt-shadow-color-secondary-rgb: 0,0,0;
|
||||
@@ -953,8 +957,8 @@ body {
|
||||
--dt-dialog-bg-color: #141414;
|
||||
--dt-bg-color: 0,0,0;
|
||||
--dt-bg-color-secondary: #262628;
|
||||
--dt-border-color: #919191;
|
||||
--dt-badge-color: #717171;
|
||||
--dt-border-color: #757575;
|
||||
--dt-badge-color: #757575;
|
||||
--dt-lang-hr-color: #404040;
|
||||
|
||||
--dt-shadow-color-secondary-rgb: 255,255,255;
|
||||
|
||||
@@ -44,15 +44,18 @@ export default class Peer {
|
||||
_setIP(request) {
|
||||
if (request.headers['cf-connecting-ip']) {
|
||||
this.ip = request.headers['cf-connecting-ip'].split(/\s*,\s*/)[0];
|
||||
} else if (request.headers['x-forwarded-for']) {
|
||||
}
|
||||
else if (request.headers['x-forwarded-for']) {
|
||||
this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
|
||||
} else {
|
||||
this.ip = request.connection.remoteAddress;
|
||||
}
|
||||
else {
|
||||
this.ip = request.socket.remoteAddress ?? '';
|
||||
}
|
||||
|
||||
// remove the prefix used for IPv4-translated addresses
|
||||
if (this.ip.substring(0,7) === "::ffff:")
|
||||
if (this.ip.substring(0,7) === "::ffff:") {
|
||||
this.ip = this.ip.substring(7);
|
||||
}
|
||||
|
||||
let ipv6_was_localized = false;
|
||||
if (this.conf.ipv6Localize && this.ip.includes(':')) {
|
||||
|
||||
Reference in New Issue
Block a user