Compare commits

..

61 Commits

Author SHA1 Message Date
schlagmichdoch
50539ed485 Delete files from OPFS when user closes the ReceiveFileDialog 2024-08-21 15:05:48 +02:00
schlagmichdoch
490bad5734 Fix wrong file name with navigator.share 2024-08-21 13:34:40 +02:00
schlagmichdoch
1d5f2e8023 Round kB/s as well 2024-07-17 17:11:21 +02:00
schlagmichdoch
b261cb73ba Merge branch 'master' into sw_digester 2024-07-17 15:39:18 +02:00
schlagmichdoch
db8d5f97ea Merge branch 'add_transfer_notes' into sw_digester 2024-07-15 00:06:03 +02:00
schlagmichdoch
3b31472c52 Merge branch 'sw_digester' of https://github.com/schlagmichdoch/PairDrop into sw_digester 2024-07-14 23:32:44 +02:00
schlagmichdoch
f7ea519106 Do not stop trying to reconnect to server if offline (private networks and on same machine might still work) 2024-07-14 18:05:51 +02:00
schlagmichdoch
76c47c9623 Rewrite FileDigester to tidy up code, be able to delete files in OPFS onPageHide and on abort of file transfer 2024-07-14 18:04:03 +02:00
schlagmichdoch
fa86212139 Merge branch 'master' into sw_digester 2024-07-14 14:32:35 +02:00
schlagmichdoch
51299bcf73 Refactor static variable names 2024-05-16 20:48:02 +02:00
schlagmichdoch
5f6d330386 Fix translations in default locale 2024-05-16 20:47:10 +02:00
schlagmichdoch
f103f37e16 Merge branch 'sw_digester' into add_transfer_notes 2024-05-16 20:19:42 +02:00
schlagmichdoch
4b99ff46da Merge branch 'fix_public_room_switching' into sw_digester 2024-05-16 20:00:56 +02:00
schlagmichdoch
be381ea438 When switching public rooms disconnect from devices in old room (fixes #298) 2024-05-16 19:54:59 +02:00
schlagmichdoch
9b3571feac Refactor BrowserTabsConnector and PeersManager 2024-05-16 19:37:32 +02:00
schlagmichdoch
8a56a271bc Make PWA run standalone (fixes #264) 2024-02-23 13:02:40 +01:00
schlagmichdoch
07e46e472e Prevent flickering of text on load by adding defer="true" to deferred style sheets 2024-02-22 15:30:09 +01:00
schlagmichdoch
3b772d0619 Tidy up code of progress animation and make it linear; Tidy up code of setting statusText for transfer notes 2024-02-17 21:01:17 +01:00
schlagmichdoch
d70f9d762e Remove redundant 'receive-progress' and move setting of progress to receive confirmation methods 2024-02-17 15:39:33 +01:00
schlagmichdoch
8592499d22 Replace status: null with status: idle; Set status to processing immediately after receiving is done 2024-02-17 14:17:43 +01:00
schlagmichdoch
e29ea44025 Add transfer notes: Speed + Time left 2024-02-17 14:05:54 +01:00
schlagmichdoch
00f1a20177 Round progress to 4th digit to prevent weird progress bar behavior on reconnect 2024-02-17 12:27:29 +01:00
schlagmichdoch
3c8848d406 Add STATE_TRANSFER_REQUEST_RECEIVED and close transfer request dialog if requesting peer reloads 2024-02-17 12:27:29 +01:00
schlagmichdoch
0d17ada58b NoSleep: Move evaluation if any peer is still busy to the PeerManager 2024-02-17 12:27:28 +01:00
schlagmichdoch
74bd7dd406 Check if RAM would be exceeded before using navigator.share() 2024-02-17 12:27:28 +01:00
schlagmichdoch
f4a947527d Move service worker digestion into separate class and add static function to check if it is supported by the browser. Change ram-exceed-ios waring accordingly. 2024-02-17 12:26:21 +01:00
schlagmichdoch
90f10910aa Fix _fileReceived getting called twice 2024-02-15 18:02:23 +01:00
schlagmichdoch
aacf24c31f Fix reconnecting by always accepting new ice candidates 2024-02-15 18:02:23 +01:00
schlagmichdoch
c0e5b66d41 Fix share menu error detection on iOS 2024-02-15 18:02:23 +01:00
schlagmichdoch
42bd71a3dc Add error status and check if too many bytes are received 2024-02-15 18:02:23 +01:00
schlagmichdoch
a98499ea5a Move header comparison to _onTransferHeader function as there is no benefit in doing it after file is received 2024-02-15 18:02:23 +01:00
schlagmichdoch
7c471910ef Tidy up Peer classes 2024-02-15 18:02:15 +01:00
schlagmichdoch
da558ddceb Move beforeunload event to Peer class to include it to the WSPeer; Add reset method to Peer class to prevent returning the "unfinished-transfers" warning when closing the page after a peer has left during transfer 2024-02-15 15:18:25 +01:00
schlagmichdoch
1df8fe258e Tidy up zipper functions 2024-02-15 15:18:25 +01:00
schlagmichdoch
65936a4d7d Truncate file used by the sw-file-digester.js after processing 2024-02-15 15:18:25 +01:00
schlagmichdoch
7c6062e1e0 Solve "transfer-complete" and "receive-complete" status detection via css instead of adding a new class 2024-02-13 18:24:08 +01:00
schlagmichdoch
902b5c6b8f Refactor file transfer 2024-02-09 04:11:36 +01:00
schlagmichdoch
19d33e11d8 Implement fallback to download if navigator.share() fails. Refactor ReceiveFileDialog 2024-02-09 04:11:36 +01:00
schlagmichdoch
d8908e01ea Add alert for iOS when receiving big files using a private tab 2024-02-09 04:11:36 +01:00
schlagmichdoch
2d2cfec5f0 Add missing checks for transfer states 2024-02-09 04:11:36 +01:00
schlagmichdoch
40a12b5501 Fix progress animation 2024-02-09 04:11:36 +01:00
schlagmichdoch
5ee8bb871e Move file creation to serviceworker to prevent loading everything into RAM 2024-02-09 04:11:36 +01:00
schlagmichdoch
ef3c338dad Activate NoSleep on file transfers instead of on click and deactivate when transfer is finished 2024-02-09 04:11:32 +01:00
schlagmichdoch
6d95f3f4e2 Fix canceling file selector on Windows Edge sometimes blocks UI (#257) 2024-02-09 01:59:30 +01:00
schlagmichdoch
c33d49702e Animate progress circle and show complete status in blue for 10s 2024-02-09 01:56:23 +01:00
schlagmichdoch
1d62a9ff49 Add state management to network peers 2024-02-09 01:55:37 +01:00
schlagmichdoch
3dd40e238a Refactor _downloadNotification function 2024-02-09 01:55:37 +01:00
schlagmichdoch
417d5421a6 Refactor _displayFiles function; Only show ReceiveFileDialog if share menu is used OR if automatic download has not worked 2024-02-09 01:55:37 +01:00
schlagmichdoch
7af51bbd5f Tidy up chunker code 2024-02-09 01:55:37 +01:00
schlagmichdoch
88739107e4 Remove limit for sending to iOS devices (fixes #211) 2024-02-09 01:55:37 +01:00
schlagmichdoch
6de97e7ff1 Put all log prompts into new Logger class; Only log debugging logs to console if PairDrop is in debug mode; Implement activation of debug mode via URL argument (?debug=true) 2024-02-09 01:55:37 +01:00
schlagmichdoch
b61de4eb87 send transfer abortion in some cases; Clarify variable names 2024-02-09 01:55:37 +01:00
schlagmichdoch
cfe5b4afda Prevent full datachannel buffer on file end 2024-02-09 01:55:37 +01:00
schlagmichdoch
91fc2b7bf5 Add speed log to browser console 2024-02-09 01:55:37 +01:00
schlagmichdoch
c670b39732 Fix byte size conversion 2024-02-09 01:55:37 +01:00
schlagmichdoch
e5a09b6be1 Include label in x-peer size transformation; Prevent use of transparency for status & displayName; 2024-02-09 01:55:27 +01:00
schlagmichdoch
1d81b744ea Put blop sound into separate function and only play if on desktop 2024-02-05 21:08:10 +01:00
schlagmichdoch
c37412cfd3 Fix variable should be static 2024-02-05 21:08:10 +01:00
schlagmichdoch
a5dc8b6da2 Split transfer into message and data transfer and rewrite FileChunkerLogic completely. Condense all Relaying of the Websocket Fallback into one message type 'ws-relay' 2024-02-05 21:08:06 +01:00
schlagmichdoch
d81c03a560 Prefill room secrets entry with displayName given by server to prevent displayName undefined in EditPairedDevices Dialog (fixes #221) 2024-02-05 02:16:25 +01:00
schlagmichdoch
f22abca783 Implement new status 'connecting', automatic reconnect on disconnect and auto resume of transfer + sending of queued messages. (fixes #260 and #247) 2024-02-05 02:16:17 +01:00
48 changed files with 2785 additions and 1693 deletions

View File

@@ -1,13 +1,5 @@
node_modules
.github
.git*
.idea
dev
docs
licenses
node_modules
pairdrop-cli
*.md
*.yml
Dockerfile
rtc_config_example.json
turnserver_example.conf
*.md

View File

@@ -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.10
Version: v1.10.9
**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.10
Version: v1.10.9
**Additional context**
Add any other context about the problem here.

View File

@@ -1,13 +1,11 @@
FROM alpine:latest
FROM node:lts-alpine
WORKDIR /home/node/app
COPY package*.json ./
RUN apk add --no-cache nodejs npm
RUN NODE_ENV="production" npm ci --omit=dev
RUN npm ci
# Directories and files excluded via .dockerignore
COPY . .
# environment settings

View File

@@ -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>View all differences</summary>
<details><summary>List view</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))
* [libheif](https://github.com/strukturag/libheif) library to handle HEIC/HEIF files (GPLv3)
* [heic2any](https://github.com/alexcorvi/heic2any) HEIC/HEIF to PNG/GIF/JPEG ([MIT](licenses/MIT-heic2any))
* [Weblate](https://weblate.org/) web-based localization tool
[FAQ](docs/faq.md)

View File

@@ -19,8 +19,7 @@ server {
}
server {
listen 443 ssl;
http2 on;
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/pairdrop-dev.crt;
ssl_certificate_key /etc/ssl/certs/pairdrop-dev.key;
@@ -37,6 +36,6 @@ server {
alias /etc/ssl/certs/pairdropCA.crt;
}
# To allow POST on static pages
error_page 405 =200 $uri;
error_page 405 =200 $uri;
}

View File

@@ -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://en.m.wikipedia.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://wikiless.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>

View File

@@ -556,6 +556,10 @@ a2enmod proxy
a2enmod proxy_http
```
```bash
a2enmod proxy_wstunnel
```
<br>
Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
@@ -566,10 +570,18 @@ Create a new configuration file under `/etc/apache2/sites-available` (on Debian)
```apacheconf
<VirtualHost *:80>
ProxyPass / http://127.0.0.1:3000/ upgrade=websocket
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]
</VirtualHost>
<VirtualHost *:443>
ProxyPass / https://127.0.0.1:3000/ upgrade=websocket
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]
</VirtualHost>
```
@@ -577,10 +589,14 @@ 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 / http://127.0.0.1:3000/ upgrade=websocket
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]
</VirtualHost>
```

View File

@@ -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.10/pairdrop-cli.zip"
wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.9/pairdrop-cli.zip"
```
or
```shell
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.10/pairdrop-cli.zip"
curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.9/pairdrop-cli.zip"
```
2. Unzip the archive to a folder of your choice e.g. `/usr/share/pairdrop-cli/`
```shell

22
licenses/MIT-heic2any Normal file
View File

@@ -0,0 +1,22 @@
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
View File

@@ -1,12 +1,12 @@
{
"name": "pairdrop",
"version": "1.10.10",
"version": "1.10.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.10.10",
"version": "1.10.9",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.10.10",
"version": "1.10.9",
"type": "module",
"description": "",
"main": "server/index.js",

View File

@@ -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" 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" placeholder="Loading..." autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg class="icon edit-pen">
<use xlink:href="#edit-pen-icon"></use>
</svg>
@@ -202,11 +202,6 @@
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Catalan)</span>
</button>
<button class="btn fw wrap" value="cs">
<span>Čeština</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Czech)</span>
</button>
<button class="btn fw wrap" value="da">
<span>Dansk</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
@@ -290,11 +285,6 @@
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Turkish)</span>
</button>
<button class="btn fw wrap" value="uk">
<span>Українська</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Ukrainian)</span>
</button>
<button class="btn fw wrap" value="zh-CN">
<span>中文</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
@@ -487,7 +477,7 @@
<div class="center file-preview"></div>
<div class="row-reverse center btn-row wrap">
<button id="share-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button>
<button id="download-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus disabled></button>
<button id="download-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus disabled hidden></button>
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div>
</x-paper>
@@ -622,7 +612,7 @@
</svg>
<div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1>
<div class="font-subheading">v1.10.10</div>
<div class="font-subheading">v1.10.9</div>
</div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row">
@@ -784,7 +774,7 @@
<script src="scripts/ui-main.js" defer></script>
<script src="scripts/main.js" defer></script>
<!-- Sounds -->
<audio id="blop" autobuffer="true">
<audio id="blop" preload="metadata" disableremoteplayback="true" x-webkit-airplay="deny" muted>
<source src="sounds/blop.mp3" type="audio/mpeg">
<source src="sounds/blop.ogg" type="audio/ogg">
</audio>

View File

@@ -48,7 +48,6 @@
"pairing-cleared": "جميع الأجهزة غير مقترنة",
"notifications-enabled": "تم تمكين الإشعارات",
"online-requirement-pairing": "يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة",
"ios-memory-limit": "لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة",
"online-requirement-public-room": "يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة",
"copied-text-error": "فشلت الكتابة من الحافظة. انسخ يدويًا!",
"download-successful": "تم تحميل {{descriptor}}",

View File

@@ -145,7 +145,6 @@
"pairing-cleared": "Tots els dispositius desvinculats",
"notifications-enabled": "Notificacions habilitades",
"online-requirement-pairing": "Has d'estar en línia per vincular dispositius",
"ios-memory-limit": "Tan sols és possible enviar fitxers de fins a 200 MB a iOS",
"online-requirement-public-room": "Cal que estiguis en línia per poder crear una sala pública",
"room-url-copied-to-clipboard": "Enllaç a la sala pública copiat al porta-retalls",
"copied-text-error": "L'escriptura al porta-retalls ha fallat. Copiar manualment!",

View File

@@ -4,181 +4,6 @@
"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ě",
"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…"
"pair-device_title": "Spárovat zařízení permanentně"
}
}

View File

@@ -138,7 +138,6 @@
"message-transfer-completed": "Nachricht übertragen",
"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",

View File

@@ -158,11 +158,13 @@
"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"
"selected-peer-left": "Selected peer left",
"error-sharing-size": "Files too big to be shared. They can be downloaded instead.",
"error-sharing-default": "Error while sharing. It can be downloaded instead.",
"ram-exceed-ios": "One of the files is bigger than 250 MB and will crash the page on iOS. Use https and do not use private tabs on the iOS device to prevent this."
},
"document-titles": {
"file-received": "File Received",
@@ -176,9 +178,14 @@
"click-to-send-share-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",
"connecting": "Connecting…",
"preparing": "Preparing…",
"waiting": "Waiting…",
"processing": "Processing…",
"transferring": "Transferring…"
"transferring": "Sending…",
"receiving": "Receiving…",
"transfer-complete": "Sent",
"receive-complete": "Received",
"error": "Error"
}
}

View File

@@ -64,7 +64,6 @@
"pairing-cleared": "Todos los dispositivos han sido desemparejados",
"notifications-enabled": "Notificaciones habilitadas",
"online-requirement-pairing": "Debes estar en línea para emparejar dispositivos",
"ios-memory-limit": "Enviar archivos a iOS sólo admite hasta 200 MB a la vez",
"online-requirement-public-room": "Debes estar en línea para crear una sala pública",
"copied-text-error": "Error al escribir en el portapapeles. ¡Cópielo manualmente!",
"download-successful": "{{descriptor}} descargado",

View File

@@ -156,7 +156,6 @@
"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. Êtes-vous sûr de vouloir fermer PairDrop ?",
"rate-limit-join-key": "Limite de débit atteinte. Attendez 10 secondes et réessayez.",

View File

@@ -48,7 +48,6 @@
"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",
@@ -101,7 +100,7 @@
"transferring": "Mentransfer…"
},
"dialogs": {
"base64-paste-to-send": "Tempel salinan di sini untuk mengirim {{type}}",
"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",
@@ -154,12 +153,7 @@
"base64-title-files": "Bagikan File",
"base64-title-text": "Bagikan Teks",
"message_placeholder": "Teks",
"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"
"paired-device-removed": "Perangkat yang dipasangkan telah dihapus."
},
"about": {
"claim": "Cara termudah untuk mentransfer file lintas perangkat",
@@ -167,11 +161,7 @@
"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",
"mastodon_title": "Tulis tentang PairDrop di Mastodon",
"bluesky_title": "Ikuti kami di BlueSky",
"custom_title": "Ikuti kami",
"privacypolicy_title": "Buka kebijakan privasi kami"
"faq_title": "Pertanyaan yang sering diajukan"
},
"document-titles": {
"file-transfer-requested": "Permintaan Transfer File",

View File

@@ -143,7 +143,6 @@
"pairing-cleared": "Tutti i dispositivi sono stati dissociati",
"notifications-enabled": "Notifiche attivate",
"online-requirement-pairing": "Devi essere online per associare dispositivi",
"ios-memory-limit": "L'invio di file a dispositivi iOS è possibile solo 200 MB alla volta",
"online-requirement-public-room": "Devi essere online per creare una stanza pubblica",
"copied-text-error": "Scrittura negli appunti fallita. Copia manualmente!",
"download-successful": "{{descriptor}} scaricato",

View File

@@ -48,7 +48,6 @@
"pairing-cleared": "全てのデバイスとのペアリングを解除しました",
"notifications-enabled": "通知が有効になりました",
"online-requirement-pairing": "ペアリングするにはオンラインである必要があります",
"ios-memory-limit": "iOSへのファイル送信は一度に200MBまでしかできません",
"online-requirement-public-room": "公開ルームを作成するにはオンラインである必要があります",
"copied-text-error": "クリップボードにコピーできませんでした。手動でコピーしてください。",
"download-successful": "{{descriptor}}をダウンロードしました",

View File

@@ -128,7 +128,6 @@
"pairing-cleared": "ಎಲ್ಲಾ ಸಾಧನಗಳನ್ನು ಜೋಡಿಯಾಗಿ ತೆಗೆಯಲಾಗಿದೆ",
"notifications-enabled": "ಸೂಚನೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ",
"online-requirement-pairing": "ಸಾಧನಗಳನ್ನು ಜೋಡಿಸಲು ನೀವು ಆನ್‌ಲೈನ್‌ ಇರಬೇಕು",
"ios-memory-limit": "iOSಗೆ ಫೈಲ್‌ಗಳನ್ನು ಕಳುಹಿಸುವುದು ಒಂದೇ ಬಾರಿಗೆ 200 MB ವರೆಗೆ ಮಾತ್ರ ಸಾಧ್ಯವಾಗಿದೆ",
"online-requirement-public-room": "ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯನ್ನು ರಚಿಸಲು ನೀವು ಆನ್‌ಲೈನ್‌ ಇರಬೇಕು",
"room-url-copied-to-clipboard": "ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯ ಲಿಂಕ್ ಅನ್ನು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಲಾಗಿದೆ",
"copied-text-error": "ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ಬರೆಯುವುದು ವಿಫಲವಾಗಿದೆ. ಕೈಯಾರೆ ನಕಲಿಸಿ!",

View File

@@ -116,7 +116,6 @@
"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."
},

View File

@@ -48,7 +48,6 @@
"pairing-cleared": "Alle apparaten ontkoppeld",
"notifications-enabled": "Meldingen geactiveerd",
"online-requirement-pairing": "U moet online zijn om apparaten te koppelen",
"ios-memory-limit": "Bestandsoverdrachten naar iOS kunnen slechts met 200 MB per keer",
"online-requirement-public-room": "U moet online zijn om een openbare kamer te maken",
"copied-text-error": "Schrijven naar klembord mislukt. Kopieer handmatig!",
"download-successful": "{{descriptor}} downloaden",

View File

@@ -153,7 +153,6 @@
"connecting": "Conectando…",
"files-incorrect": "Os arquivos estão incorretos",
"file-transfer-completed": "Transferência de arquivo concluída",
"ios-memory-limit": "Enviar arquivos para iOS só é possível até 200 MB de uma vez",
"message-transfer-completed": "Transferência de mensagem concluída",
"unfinished-transfers-warning": "Há transferências inacabadas. Tem certeza de que deseja fechar o PairDrop?",
"rate-limit-join-key": "Limite de taxa atingido. Aguarde 10 segundos e tente novamente.",

View File

@@ -48,7 +48,6 @@
"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",

View File

@@ -124,7 +124,6 @@
"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 секунд и повторите попытку.",

View File

@@ -1,184 +0,0 @@
{
"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": "Переводимо…"
}
}

View File

@@ -154,7 +154,6 @@
"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": "无法复制。请手动复制。",

View File

@@ -28,7 +28,7 @@
"background_color": "#efefef",
"start_url": "/",
"scope": "/",
"display": "minimal-ui",
"display": "standalone",
"theme_color": "#3367d6",
"screenshots" : [
{

View File

@@ -2,18 +2,27 @@ class BrowserTabsConnector {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
Events.on('broadcast-send', e => this._broadcastSend(e.detail.type, e.detail.data));
Events.on('broadcast-self-display-name-changed', e => this._onBroadcastSelfDisplayNameChanged(e.detail.displayName));
}
_broadcastSend(message) {
this.bc.postMessage(message);
_broadcastSend(type, data) {
this.bc.postMessage({ type, data });
}
_onBroadcastSelfDisplayNameChanged(displayName) {
this._broadcastSend('self-display-name-changed', { displayName: displayName });
}
_onMessage(e) {
console.log('Broadcast:', e.data)
switch (e.data.type) {
const type = e.data.type;
const data = e.data.data;
Logger.debug('Broadcast:', type, data);
switch (type) {
case 'self-display-name-changed':
Events.fire('self-display-name-changed', e.data.detail);
Events.fire('self-display-name-changed', data.displayName);
break;
}
}
@@ -25,6 +34,11 @@ class BrowserTabsConnector {
: false;
}
static isOnlyTab() {
let peerIdsBrowser = JSON.parse(localStorage.getItem('peer_ids_browser'));
return peerIdsBrowser.length <= 1;
}
static async addPeerIdToLocalStorage() {
const peerId = sessionStorage.getItem('peer_id');
if (!peerId) return false;

1
public/scripts/heic2any.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +0,0 @@
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()
}
);
})
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -2,28 +2,28 @@ class Localization {
constructor() {
Localization.$htmlRoot = document.querySelector('html');
Localization.defaultLocale = "en";
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.localeDefault = "en";
Localization.localesSupported = ["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.localesRtl = ["ar", "he"];
Localization.translations = {};
Localization.translationsDefaultLocale = {};
Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages);
Localization.localeSystem = Localization.getSupportedOrDefaultLocales(navigator.languages);
let storedLanguageCode = localStorage.getItem('language_code');
Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)
Localization.localeInitial = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)
? storedLanguageCode
: Localization.systemLocale;
: Localization.localeSystem;
}
static localeIsSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1;
return Localization.localesSupported.indexOf(locale) > -1;
}
static localeIsRtl(locale) {
return Localization.supportedLocalesRtl.indexOf(locale) > -1;
return Localization.localesRtl.indexOf(locale) > -1;
}
static currentLocaleIsRtl() {
@@ -31,7 +31,7 @@ class Localization {
}
static currentLocaleIsDefault() {
return Localization.locale === Localization.defaultLocale
return Localization.locale === Localization.localeDefault
}
static getSupportedOrDefaultLocales(locales) {
@@ -44,15 +44,15 @@ class Localization {
// If there is no perfect match for browser locales, try generic locales first before resorting to the default locale
return locales.find(Localization.localeIsSupported)
|| localesGeneric.find(Localization.localeIsSupported)
|| Localization.defaultLocale;
|| Localization.localeDefault;
}
async setInitialTranslation() {
await Localization.setTranslation(Localization.initialLocale)
await Localization.setTranslation(Localization.localeInitial)
}
static async setTranslation(locale) {
if (!locale) locale = Localization.systemLocale;
if (!locale) locale = Localization.localeSystem;
await Localization.setLocale(locale)
await Localization.translatePage();
@@ -67,8 +67,8 @@ class Localization {
Localization.$htmlRoot.setAttribute('lang', locale);
console.log("Page successfully translated",
`System language: ${Localization.systemLocale}`,
Logger.debug("Page successfully translated",
`System language: ${Localization.localeSystem}`,
`Selected language: ${locale}`
);
@@ -78,7 +78,7 @@ class Localization {
static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false;
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
Localization.translationsDefaultLocale = await Localization.fetchTranslationsFor(Localization.localeDefault);
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
@@ -145,7 +145,7 @@ class Localization {
translation = translationObj[lastKey];
} catch (e) {
console.error(e);
Logger.error(e);
}
if (!translation) {
@@ -179,7 +179,7 @@ class Localization {
}
catch (e) {
// Log warnings and help calls
console.warn(e);
Logger.warn(e);
Localization.logTranslationMissingOrBroken(key, attr, data, useDefault);
Localization.logHelpCallKey(key, attr);
Localization.logHelpCall();
@@ -192,7 +192,7 @@ class Localization {
else {
// Is not default locale yet
// Get translation for default language with same arguments
console.log(`Using default language ${Localization.defaultLocale.toUpperCase()} instead.`);
Logger.debug(`Using default language ${Localization.localeDefault.toUpperCase()} instead.`);
translation = this.getTranslation(key, attr, data, true);
}
}
@@ -202,14 +202,14 @@ class Localization {
static logTranslationMissingOrBroken(key, attr, data, useDefault) {
let usedLocale = useDefault
? Localization.defaultLocale.toUpperCase()
? Localization.localeDefault.toUpperCase()
: Localization.locale.toUpperCase();
console.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data);
Logger.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data);
}
static logHelpCall() {
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
Logger.warn("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
}
static logHelpCallKey(key, attr) {
@@ -219,7 +219,7 @@ class Localization {
? key
: `${key}_${attr}`;
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`);
Logger.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`);
}
static escapeHTML(unsafeText) {

View File

@@ -1,3 +1,23 @@
class Logger {
static debug(message, ...optionalParams) {
if (window.debugMode) {
console.debug("DEBUG:", message, ...optionalParams);
}
}
static log(message, ...optionalParams) {
console.log("LOG:", message, ...optionalParams);
}
static warn(message, ...optionalParams) {
console.warn("WARN:", message, ...optionalParams);
}
static error(message, ...optionalParams) {
console.error("ERROR:", message, ...optionalParams);
}
}
class PairDrop {
constructor() {
@@ -17,8 +37,7 @@ class PairDrop {
"scripts/qr-code.min.js",
"scripts/zip.min.js",
"scripts/no-sleep.min.js",
"scripts/heif-convert.js",
"scripts/libheif.js"
"scripts/heic2any.min.js"
];
this.registerServiceWorker();
@@ -35,14 +54,14 @@ class PairDrop {
this.initialize()
.then(_ => {
console.log("Initialization completed.");
Logger.log("Initialization completed.");
});
}
async initialize() {
// Translate page before fading in
await this.localization.setInitialTranslation()
console.log("Initial translation successful.");
Logger.log("Initial translation successful.");
// Show "Loading..." until connected to WsServer
await this.footerUI.showLoading();
@@ -57,16 +76,16 @@ class PairDrop {
await this.backgroundCanvas.fadeIn();
// Load deferred assets
console.log("Load deferred assets...");
Logger.log("Load deferred assets...");
await this.loadDeferredAssets();
console.log("Loading of deferred assets completed.");
Logger.log("Loading of deferred assets completed.");
console.log("Hydrate UI...");
Logger.log("Hydrate UI...");
await this.hydrate();
console.log("UI hydrated.");
Logger.log("UI hydrated.");
// Evaluate url params as soon as ws is connected
console.log("Evaluate URL params as soon as websocket connection is established.");
Logger.log("Evaluate URL params as soon as websocket connection is established.");
Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true});
}
@@ -75,14 +94,14 @@ class PairDrop {
navigator.serviceWorker
.register('service-worker.js')
.then(serviceWorker => {
console.log('Service Worker registered');
Logger.log('Service Worker registered');
window.serviceWorker = serviceWorker
});
}
}
onPwaInstallable(e) {
if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
if (!window.matchMedia('(display-mode: standalone)').matches) {
// only display install btn when not installed
this.$headerInstallBtn.removeAttribute('hidden');
this.$headerInstallBtn.addEventListener('click', () => {
@@ -118,6 +137,7 @@ class PairDrop {
let stylesheet = document.createElement('link');
stylesheet.rel = 'preload';
stylesheet.as = 'style';
stylesheet.defer = true;
stylesheet.href = url;
stylesheet.onload = _ => {
stylesheet.onload = null;
@@ -134,10 +154,10 @@ class PairDrop {
return new Promise( async (resolve) => {
try {
await this.loadStyleSheet(url);
console.log(`Stylesheet loaded successfully: ${url}`);
Logger.log(`Stylesheet loaded successfully: ${url}`);
resolve();
} catch (error) {
console.error('Error loading stylesheet:', error);
Logger.error('Error loading stylesheet:', error);
}
});
}
@@ -157,10 +177,10 @@ class PairDrop {
return new Promise( async (resolve) => {
try {
await this.loadScript(url);
console.log(`Script loaded successfully: ${url}`);
Logger.log(`Script loaded successfully: ${url}`);
resolve();
} catch (error) {
console.error('Error loading script:', error);
Logger.error('Error loading script:', error);
}
});
}
@@ -230,12 +250,17 @@ class PairDrop {
this.publicRoomDialog._createPublicRoom();
}
}
else if (urlParams.has("debug") && urlParams.get("debug") === "true") {
window.debugMode = true;
}
// remove url params from url
const urlWithoutParams = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", urlWithoutParams);
if (!window.debugMode) {
// remove url params from url
const urlWithoutParams = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", urlWithoutParams);
}
console.log("URL params evaluated.");
Logger.log("URL params evaluated.");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,19 +7,18 @@ class PersistentStorage {
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 5);
DBOpenRequest.onerror = e => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
console.log(e)
Logger.error('Error initializing database:', e);
};
DBOpenRequest.onsuccess = _ => {
console.log('Database initialised.');
Logger.debug('Database initialised.');
};
DBOpenRequest.onupgradeneeded = async e => {
const db = e.target.result;
const txn = e.target.transaction;
db.onerror = e => console.log('Error loading database: ' + e);
db.onerror = e => Logger.error('Error loading database:', e);
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
Logger.debug(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
if (e.oldVersion === 0) {
// initiate v1
@@ -54,7 +53,7 @@ class PersistentStorage {
}
static logBrowserNotCapable() {
console.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed.");
Logger.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed.");
}
static set(key, value) {
@@ -66,7 +65,7 @@ class PersistentStorage {
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.put(value, key);
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. Added key-pair: ${key} - ${value}`);
Logger.debug(`Request successful. Added key-pair: ${key} - ${value}`);
resolve(value);
};
}
@@ -85,7 +84,7 @@ class PersistentStorage {
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.get(key);
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
Logger.debug(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
resolve(objectStoreRequest.result);
}
}
@@ -104,7 +103,7 @@ class PersistentStorage {
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.delete(key);
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. Deleted key: ${key}`);
Logger.debug(`Request successful. Deleted key: ${key}`);
resolve();
};
}
@@ -128,7 +127,7 @@ class PersistentStorage {
'auto_accept': false
});
objectStoreRequest.onsuccess = e => {
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
Logger.debug(`Request successful. RoomSecret added: ${e.target.result}`);
resolve();
}
}
@@ -145,7 +144,7 @@ class PersistentStorage {
for (let i = 0; i < roomSecrets.length; i++) {
secrets.push(roomSecrets[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
Logger.debug(`Request successful. Retrieved ${secrets.length} room_secrets`);
return(secrets);
} catch (e) {
this.logBrowserNotCapable();
@@ -182,13 +181,13 @@ class PersistentStorage {
objectStoreRequestKey.onsuccess = e => {
const key = e.target.result;
if (!key) {
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
Logger.debug(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestRetrieval = objectStore.get(key);
objectStoreRequestRetrieval.onsuccess = e => {
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
Logger.debug(`Request successful. Retrieved entry for room_secret: ${key}`);
resolve({
"entry": e.target.result,
"key": key
@@ -215,14 +214,14 @@ class PersistentStorage {
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
if (!e.target.result) {
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
Logger.debug(`Nothing to delete. room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const key = e.target.result;
const objectStoreRequestDeletion = objectStore.delete(key);
objectStoreRequestDeletion.onsuccess = _ => {
console.log(`Request successful. Deleted room_secret: ${key}`);
Logger.debug(`Request successful. Deleted room_secret: ${key}`);
resolve(roomSecret);
}
objectStoreRequestDeletion.onerror = e => {
@@ -245,7 +244,7 @@ class PersistentStorage {
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.clear();
objectStoreRequest.onsuccess = _ => {
console.log('Request successful. All room_secrets cleared');
Logger.debug('Request successful. All room_secrets cleared');
resolve();
};
}
@@ -255,15 +254,15 @@ class PersistentStorage {
})
}
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
static updateRoomSecretDisplayName(roomSecret, displayName) {
return this.updateRoomSecret(roomSecret, null, displayName, null);
}
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
return this.updateRoomSecret(roomSecret, null, null, null, autoAccept);
}
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
static updateRoomSecret(roomSecret, updatedRoomSecret = null, updatedDisplayName = null, updatedDeviceName = null, updatedAutoAccept = null) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = e => {
@@ -278,16 +277,16 @@ class PersistentStorage {
const objectStore = transaction.objectStore('room_secrets');
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
const updatedRoomSecretEntry = {
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
'secret': updatedRoomSecret !== null ? updatedRoomSecret : roomSecretEntry.entry.secret,
'display_name': updatedDisplayName !== null ? updatedDisplayName : roomSecretEntry.entry.display_name,
'device_name': updatedDeviceName !== null ? updatedDeviceName : roomSecretEntry.entry.device_name,
'auto_accept': updatedAutoAccept !== null ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
};
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
objectStoreRequestUpdate.onsuccess = e => {
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
Logger.debug(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
resolve({
"entry": updatedRoomSecretEntry,
"key": roomSecretEntry.key

View File

@@ -0,0 +1,131 @@
self.accessHandle = undefined;
self.messageQueue = [];
self.busy = false;
self.addEventListener('message', async e => {
// Put message into queue if busy
if (self.busy) {
self.messageQueue.push(e.data);
return;
}
await digestMessage(e.data);
});
async function digestMessage(message) {
self.busy = true;
try {
switch (message.type) {
case "check-support":
await checkSupport();
break;
case "chunk":
await onChunk(message.id, message.chunk, message.offset);
break;
case "get-file":
await onGetFile(message.id);
break;
case "delete-file":
await onDeleteFile(message.id);
break;
case "clear-directory":
await onClearDirectory();
break;
}
}
catch (e) {
self.postMessage({type: "error", error: e});
}
// message is digested. Digest next message.
await messageDigested();
}
async function messageDigested() {
if (!self.messageQueue.length) {
// no chunk in queue -> set flag to false and stop
this.busy = false;
return;
}
// Digest next message in queue
await this.digestMessage(self.messageQueue.pop());
}
async function checkSupport() {
try {
const accessHandle = await getAccessHandle("test");
self.postMessage({type: "support", supported: true});
accessHandle.close();
}
catch (e) {
self.postMessage({type: "support", supported: false});
}
}
async function getFileHandle(id) {
const dirHandle = await navigator.storage.getDirectory();
return await dirHandle.getFileHandle(id, {create: true});
}
async function getAccessHandle(id) {
const fileHandle = await getFileHandle(id);
if (!self.accessHandle) {
// Create FileSystemSyncAccessHandle on the file.
self.accessHandle = await fileHandle.createSyncAccessHandle();
}
return self.accessHandle;
}
async function onChunk(id, chunk, offset) {
const accessHandle = await getAccessHandle(id);
// Write the message to the end of the file.
let encodedMessage = new DataView(chunk);
accessHandle.write(encodedMessage, { at: offset });
self.postMessage({type: "chunk-written", offset: offset});
}
async function onGetFile(id) {
const fileHandle = await getFileHandle(id);
let file = await fileHandle.getFile();
self.postMessage({type: "file", file: file});
}
async function onDeleteFile(id) {
const accessHandle = await getAccessHandle(id);
// Truncate the file to 0 bytes
accessHandle.truncate(0);
// Persist changes to disk.
accessHandle.flush();
// Always close FileSystemSyncAccessHandle if done.
accessHandle.close();
self.postMessage({type: "file-deleted", id: id});
}
async function onClearDirectory() {
const dirHandle = await navigator.storage.getDirectory();
// Iterate through directory entries and truncate all entries to 0
for await (const [id, fileHandle] of dirHandle.entries()) {
const accessHandle = await fileHandle.createSyncAccessHandle();
// Truncate the file to 0 bytes
accessHandle.truncate(0);
// Persist changes to disk.
accessHandle.flush();
// Always close FileSystemSyncAccessHandle if done.
accessHandle.close();
}
}

View File

@@ -201,11 +201,15 @@ class FooterUI {
this.$discoveryWrapper = $$('footer .discovery-wrapper');
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('focus', e => this._onFocusDisplayName(e));
this.$displayName.addEventListener('blur', e => this._onBlurDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail.displayName));
// Load saved display name on page load
Events.on('ws-connected', _ => this._loadSavedDisplayName());
Events.on('evaluate-footer-badges', _ => this._evaluateFooterBadges());
}
@@ -230,20 +234,17 @@ class FooterUI {
}
async _loadSavedDisplayName() {
const displayNameSaved = await this._getSavedDisplayName()
const displayName = await this._getSavedDisplayName()
if (!displayNameSaved) return;
if (!displayName) return;
console.log("Retrieved edited display name:", displayNameSaved)
Events.fire('self-display-name-changed', displayNameSaved);
Logger.debug("Retrieved edited display name:", displayName)
Events.fire('self-display-name-changed', { displayName: 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);
_onDisplayName(displayName){
// set display name
this.$displayName.setAttribute('placeholder', displayName);
}
@@ -258,27 +259,9 @@ class FooterUI {
}
}
_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) {
_onKeyUpDisplayName(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 = '';
}
// Remove selection from text
window.getSelection().removeAllRanges();
await this._saveDisplayName(e.target.innerText)
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
}
async _saveDisplayName(newDisplayName) {
@@ -292,25 +275,25 @@ class FooterUI {
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-permanently"));
})
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
Logger.debug("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('edited_display_name', newDisplayName);
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily"));
})
.finally(() => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
Events.fire('self-display-name-changed', { displayName: newDisplayName });
Events.fire('broadcast-self-display-name-changed', { displayName: newDisplayName });
});
}
else {
PersistentStorage.delete('edited_display_name')
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
Logger.debug("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('edited_display_name');
})
.finally(() => {
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-random-again"));
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
Events.fire('self-display-name-changed', { displayName: '' });
Events.fire('broadcast-self-display-name-changed', { displayName: '' });
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -62,41 +62,67 @@ window.isMobile = window.iOS || window.android;
// Helper functions
const audioPlayer = (() => {
const blop = document.getElementById('blop');
blop.addEventListener('ended', _ => {
blop.muted = true
});
return {
playBlop() {
if (window.isMobile) return;
blop.muted = false;
blop.play();
}
}
})();
const zipper = (() => {
let zipWriter;
return {
createNewZipWriter() {
zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, level: 0 });
},
addFile(file, options) {
return zipWriter.add(file.name, new zip.BlobReader(file), options);
},
async getBlobURL() {
if (zipWriter) {
const blobURL = URL.createObjectURL(await zipWriter.close());
zipWriter = null;
return blobURL;
async getObjectUrlOfZipFile(files, onZipProgressCallback){
try {
const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"));
let bytesProcessed = 0;
for (let i = 0; i < files.length; i++) {
await zipWriter.add(
files[i].name,
new zip.BlobReader(files[i]),
{
onprogress: (progress) => onZipProgressCallback(bytesProcessed + progress)
}
);
bytesProcessed += files[i].size;
}
return URL.createObjectURL(await zipWriter.close());
}
else {
throw new Error("Zip file closed");
}
},
async getZipFile(filename = "archive.zip") {
if (zipWriter) {
const file = new File([await zipWriter.close()], filename, {type: "application/zip"});
zipWriter = null;
return file;
}
else {
throw new Error("Zip file closed");
catch (e) {
Logger.error(e);
return false;
}
},
async getEntries(file, options) {
return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);
try {
return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);
}
catch (e) {
Logger.error(e);
return false;
}
},
async getData(entry, options) {
return await entry.getData(new zip.BlobWriter(), options);
try {
return await entry.getData(new zip.BlobWriter(), options);
}
catch (e) {
Logger.error(e);
return false;
}
},
};
@@ -478,7 +504,12 @@ 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
file = await heicToJpeg(file, 0.5);
let blob = await fileToBlob(file);
file = await heic2any({
blob,
toType: "image/jpeg",
quality: quality
});
}
let imageUrl = URL.createObjectURL(file);
@@ -488,40 +519,26 @@ 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;
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
// resize the canvas and draw the image data into it
if (width && 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;
}
canvas.width = width;
canvas.height = height;
}
else if (width) {
canvas.width = width;
canvas.height = heightForSpecifiedWidth;
canvas.height = Math.floor(imageHeight * width / imageWidth)
}
else if (height) {
canvas.width = widthForSpecifiedHeight;
canvas.width = Math.floor(imageWidth * height / imageHeight);
canvas.height = height;
}
else {
canvas.width = image.width;
canvas.height = image.height
canvas.width = imageWidth;
canvas.height = imageHeight
}
let ctx = canvas.getContext("2d");
@@ -530,38 +547,12 @@ function getThumbnailAsDataUrl(file, width = undefined, height = undefined, qual
let dataUrl = canvas.toDataURL("image/jpeg", quality);
resolve(dataUrl);
} catch (e) {
console.error(e);
Logger.error(e);
reject(new Error(`Could not create an image thumbnail from type ${file.type}`));
}
})
}
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) => {
@@ -628,4 +619,27 @@ function isUrlValid(url) {
catch (e) {
return false;
}
}
// polyfill for crypto.randomUUID()
// Credits: @Briguy37 - https://stackoverflow.com/a/8809472/14678591
function generateUUID() {
return crypto && crypto.randomUUID()
? crypto.randomUUID()
: () => {
let
d = new Date().getTime(),
d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
let r = Math.random() * 16;
if (d > 0) {
r = (d + r) % 16 | 0;
d = Math.floor(d / 16);
} else {
r = (d2 + r) % 16 | 0;
d2 = Math.floor(d2 / 16);
}
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16);
});
};
}

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.10.10';
const cacheVersion = 'v1.10.9';
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,9 +7,6 @@ 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',
@@ -32,7 +29,6 @@ const relativePathsToCache = [
'lang/ar.json',
'lang/be.json',
'lang/ca.json',
'lang/cs.json',
'lang/da.json',
'lang/de.json',
'lang/en.json',
@@ -51,7 +47,6 @@ const relativePathsToCache = [
'lang/ro.json',
'lang/ru.json',
'lang/tr.json',
'lang/uk.json',
'lang/zh-CN.json',
'lang/zh-TW.json'
];

View File

@@ -104,6 +104,8 @@ x-peer {
padding: 8px;
align-content: start;
flex-wrap: wrap;
transition: transform 150ms;
will-change: transform;
}
x-peer input[type="file"] {
@@ -121,8 +123,6 @@ x-peer label {
x-peer x-icon {
--icon-size: 40px;
margin-bottom: 4px;
transition: transform 150ms;
will-change: transform;
display: flex;
flex-direction: column;
}
@@ -148,7 +148,7 @@ x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
.highlight-wrapper {
align-self: center;
align-items: center;
margin: 7px auto 0;
margin: 10px auto 0;
height: 6px;
}
@@ -187,14 +187,16 @@ x-peer:not(.type-public-id) .highlight-room-public-id {
display: none;
}
x-peer:not([status]):hover x-icon,
x-peer:not([status]):focus x-icon {
x-peer:is(:not([status]), [status$=-complete], [status=error]):hover,
x-peer:is(:not([status]), [status$=-complete], [status=error]):focus {
transform: scale(1.05);
}
x-peer[status] x-icon {
x-peer[status]:not([status$=-complete]) x-icon {
box-shadow: none;
opacity: 0.8;
}
x-peer[status] {
transform: scale(1);
}
@@ -237,7 +239,7 @@ x-peer.ws-peer .highlight-wrapper {
.status,
.device-name {
opacity: 0.7;
color: color-mix(in srgb, rgb(var(--text-color)) 30%, grey);
white-space: nowrap;
}
@@ -246,14 +248,22 @@ x-peer[status] .device-name {
display: none;
}
x-peer[status] {
x-peer[status]:not([status$=-complete]):not([status=error]) {
pointer-events: none;
}
x-peer x-icon {
x-peer {
animation: pop 600ms ease-out 1;
}
x-peer[status$=-complete] .status {
color: var(--primary-color);
}
x-peer[status=error] .status {
color: var(--error-color);
}
@keyframes pop {
0% {
transform: scale(0.7);
@@ -744,7 +754,6 @@ x-dialog .dialog-subheader {
top: -8px;
clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg);
transition: transform 200ms;
}
.circle {
@@ -758,6 +767,10 @@ x-dialog .dialog-subheader {
transform: var(--progress);
}
.animate .circle {
transition: transform 200ms linear;
}
.over50 {
clip: rect(auto, auto, auto, auto);
}

View File

@@ -557,10 +557,6 @@ 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;
@@ -928,6 +924,7 @@ x-peers:empty~x-instructions {
body {
/* Constant colors */
--primary-color: #4285f4;
--error-color: #ff6b6b;
--paired-device-color: #00a69c;
--public-room-color: #ed9d01;
--accent-color: var(--primary-color);
@@ -943,8 +940,8 @@ body {
--lt-dialog-bg-color: #fff;
--lt-bg-color: 255,255,255;
--lt-bg-color-secondary: #f2f2f2;
--lt-border-color: #757575;
--lt-badge-color: #757575;
--lt-border-color: #a9a9a9;
--lt-badge-color: #a5a5a5;
--lt-lang-hr-color: #DDD;
--lt-shadow-color-secondary-rgb: 0,0,0;
@@ -957,8 +954,8 @@ body {
--dt-dialog-bg-color: #141414;
--dt-bg-color: 0,0,0;
--dt-bg-color-secondary: #262628;
--dt-border-color: #757575;
--dt-badge-color: #757575;
--dt-border-color: #919191;
--dt-badge-color: #717171;
--dt-lang-hr-color: #404040;
--dt-shadow-color-secondary-rgb: 255,255,255;

View File

@@ -89,22 +89,12 @@ export default class PairDropWsServer {
this._onLeavePublicRoom(sender);
break;
case 'signal':
this._signalAndRelay(sender, message);
this._signalAndWsRelay(sender, message);
break;
case 'request':
case 'header':
case 'partition':
case 'partition-received':
case 'progress':
case 'files-transfer-response':
case 'file-transfer-complete':
case 'message-transfer-complete':
case 'text':
case 'display-name-changed':
case 'ws-chunk':
case 'ws-relay':
// relay ws-fallback
if (this._conf.wsFallback) {
this._signalAndRelay(sender, message);
this._signalAndWsRelay(sender, message);
}
else {
console.log("Websocket fallback is not activated on this instance.")
@@ -112,7 +102,7 @@ export default class PairDropWsServer {
}
}
_signalAndRelay(sender, message) {
_signalAndWsRelay(sender, message) {
const room = message.roomType === 'ip'
? sender.ip
: message.roomId;
@@ -261,7 +251,6 @@ export default class PairDropWsServer {
return;
}
this._leavePublicRoom(sender);
this._joinPublicRoom(sender, message.publicRoomId);
}
@@ -322,7 +311,7 @@ export default class PairDropWsServer {
_joinPublicRoom(peer, publicRoomId) {
// prevent joining of 2 public rooms simultaneously
this._leavePublicRoom(peer);
this._leavePublicRoom(peer, true);
this._joinRoom(peer, 'public-id', publicRoomId);